// Loop 6 Etappe 2a: Entity/Fact-Graph des kanonischen Exports — der Teil,
// der heute wahr sein kann.
//
// Modellierungs-Entscheidungen (Design-Memo 10.08. „Graph-Vollausbau", hier
// bewusst auf den belegbaren Kern geschnitten):
//
// - NUR die SUM-Schicht liefert Facts: `qaf_summary_metric` führt seit der
//   Provenance-Migration (07.08.) sheet/formula/value_state je Zelle —
//   `fact.source.sheet` ist Schema-PFLICHT, und die SUM-Provenienz ist die
//   einzige persistierte Quelle, die sie erfüllen kann. Die MFG-/MAT-
//   Diff-Tabellen haben KEINE sheet-Spalte (DB-Lücke, nicht Code-Lücke);
//   ein Fact mit erfundenem Blattnamen wäre ein Weg-4-Bruch. MFG/MAT
//   bleiben deshalb in dieser Etappe ohne Facts — Etappe 2b nach
//   DB-Erweiterung.
// - Eine Metrik-Seite OHNE sheet-Provenienz (Bestandszeilen vor der
//   Migration) bekommt KEINEN Fact und verschwindet NICHT still: sie wird
//   als withheld gezählt und im extensions-Block des Dokuments ausgewiesen.
// - `raw_value` trägt die Engine-Zahl EXAKT (String(value), kein
//   Rundungsverlust, kein Locale); `normalized_value` ist der einheiten-
//   bewusste decimal-String und wird NUR gesetzt, wo die Einheit aus den
//   bestehenden Differenz-Records ableitbar ist — keine zweite
//   Einheiten-Registry, keine geratene Präzision.
// - Namenskonvention: alles hier heißt Canonical* — `ai-ready.ts` führt ein
//   STRUKTURELL ANDERES `Fact` (Aussage+Werte-Bündel); die Kollision ist
//   bekannt und gewollt getrennt gehalten.
// - mapping_groups bleiben leer: Cross-Dokument-Identität ist Territorium
//   der Multi-QAF-Identitätskonzepte (deferred_by_product_owner) bzw.
//   braucht eine eigene, begründete mapping_type/method-Entscheidung.

import type { SummaryMetricDiff } from './summary-metrics'
import type { ValueState } from './cell-state'

// ── Geschlossenes Graph-Vokabular (die EINE Stelle — Schema lässt die Typen
//    als freie Strings, deshalb wird das Vokabular hier zentral verwaltet) ──

export const CANONICAL_ENTITY_TYPES = {
  part: 'part',
} as const

export const CANONICAL_RELATIONSHIP_TYPES = {
  samePartAs: 'same_part_as',
} as const

/** Interner Zell-Wertzustand (cell-state.ts, 5 Werte) → Schema-Enum
 * `fact.value_state` (7 Werte). Exhaustiv über Record<ValueState, …> —
 * ein neuer interner Wert bricht den Build statt still durchzurutschen.
 * `missing`/`not_applicable` haben bewusst KEINE interne Entsprechung:
 * eine fehlende Erhebung führt zu withheld, nicht zu einem Fact. */
export const CANONICAL_VALUE_STATE: Record<ValueState, string> = {
  formula_and_cached: 'formula',
  constant: 'hardcoded',
  empty: 'blank',
  error: 'formula_error',
  external_link: 'external_link',
}

// ── Eingaben/Ausgaben ────────────────────────────────────────────────────────

export interface CanonicalGraphDocumentInput {
  id: string
  partNumber: string | null
  partName: string | null
  variant: string | null
  supplier: string | null
}

export interface CanonicalGraphInput {
  award: CanonicalGraphDocumentInput
  current: CanonicalGraphDocumentInput
  summaryDiffs: readonly SummaryMetricDiff[]
  /** metric_id (`summary.<key>`) → Einheit, abgeleitet aus den bestehenden
   * Differenz-Records (eine Quelle). Metriken ohne Eintrag bekommen kein
   * normalized_value. */
  unitByMetricId: ReadonlyMap<string, string>
  /** decimalString aus canonical-export.ts — injiziert statt importiert,
   * damit dieses Modul unterhalb des Exports bleibt (kein Import-Zirkel). */
  decimal: (n: number, unit?: string) => string
}

export interface CanonicalWithheldFact {
  metric_id: string
  document_role: 'award' | 'current'
  reason: 'missing_sheet_provenance'
}

export interface CanonicalGraph {
  entities: Record<string, unknown>[]
  facts: Record<string, unknown>[]
  relationships: Record<string, unknown>[]
  /** metric_id → fact_ids je Seite, für difference.base/comparison_fact_ids. */
  factIdsByMetricId: ReadonlyMap<string, { base: string[]; comparison: string[] }>
  withheldFacts: readonly CanonicalWithheldFact[]
}

// ── Bau ──────────────────────────────────────────────────────────────────────

function partLabels(doc: CanonicalGraphDocumentInput): Record<string, string> {
  const labels: Record<string, string> = {}
  if (doc.partNumber !== null) labels.part_number = doc.partNumber
  if (doc.partName !== null) labels.part_designation = doc.partName
  if (doc.variant !== null) labels.variant = doc.variant
  if (doc.supplier !== null) labels.supplier = doc.supplier
  return labels
}

interface FactSide {
  role: 'award' | 'current'
  documentId: string
  entityId: string
  value: number | null
  cell: string | null
  provenance: { sheet: string | null; formula: string | null; valueState: ValueState } | undefined
}

/**
 * Baut Entities, Facts und Relationships deterministisch: Facts sortiert
 * nach metricKey (ordinal), je Metrik award vor current; IDs fortlaufend
 * nach dem assignDifferenceIds-Muster (erst ordnen, dann zählen — keine
 * zweite Vergabelogik, dasselbe Prinzip).
 */
export function buildCanonicalGraph(input: CanonicalGraphInput): CanonicalGraph {
  const awardEntityId = 'E-PART-001'
  const currentEntityId = 'E-PART-002'

  const entities: Record<string, unknown>[] = [
    {
      entity_id: awardEntityId,
      document_id: input.award.id,
      entity_type: CANONICAL_ENTITY_TYPES.part,
      labels: partLabels(input.award),
    },
    {
      entity_id: currentEntityId,
      document_id: input.current.id,
      entity_type: CANONICAL_ENTITY_TYPES.part,
      labels: partLabels(input.current),
    },
  ]

  const relationships: Record<string, unknown>[] = [
    {
      relationship_id: 'R-001',
      relationship_type: CANONICAL_RELATIONSHIP_TYPES.samePartAs,
      from_entity_id: awardEntityId,
      to_entity_id: currentEntityId,
    },
  ]

  const sorted = [...input.summaryDiffs].sort((a, b) =>
    a.metricKey < b.metricKey ? -1 : a.metricKey > b.metricKey ? 1 : 0,
  )

  const facts: Record<string, unknown>[] = []
  const factIdsByMetricId = new Map<string, { base: string[]; comparison: string[] }>()
  const withheldFacts: CanonicalWithheldFact[] = []
  let factCounter = 0

  for (const diff of sorted) {
    const metricId = `summary.${diff.metricKey}`
    const sides: FactSide[] = [
      {
        role: 'award',
        documentId: input.award.id,
        entityId: awardEntityId,
        value: diff.altValue,
        cell: diff.sourceAlt,
        provenance: diff.provenanceAlt,
      },
      {
        role: 'current',
        documentId: input.current.id,
        entityId: currentEntityId,
        value: diff.neuValue,
        cell: diff.sourceNeu,
        provenance: diff.provenanceNeu,
      },
    ]
    for (const side of sides) {
      const sheet = side.provenance?.sheet ?? null
      if (side.provenance === undefined || sheet === null || sheet === '') {
        // Kein Blattname erhoben → Fact wäre nicht schema-valide
        // (source.sheet ist Pflicht). Zurückhalten, sichtbar zählen.
        withheldFacts.push({ metric_id: metricId, document_role: side.role, reason: 'missing_sheet_provenance' })
        continue
      }
      factCounter += 1
      const factId = `F-SUM-${String(factCounter).padStart(3, '0')}`
      const unit = input.unitByMetricId.get(metricId)
      const valueState = side.provenance.valueState
      const dataType =
        side.value !== null ? 'decimal' : valueState === 'error' ? 'error' : 'blank'
      facts.push({
        fact_id: factId,
        document_id: side.documentId,
        entity_id: side.entityId,
        metric_id: metricId,
        data_type: dataType,
        value_state: CANONICAL_VALUE_STATE[valueState],
        source: { sheet, cell: side.cell },
        ...(side.value !== null ? { raw_value: String(side.value) } : {}),
        ...(side.value !== null && unit !== undefined
          ? { normalized_value: input.decimal(side.value, unit) }
          : {}),
        unit: unit ?? null,
        currency: diff.currency,
        formula_raw: side.provenance.formula,
      })
      const entry = factIdsByMetricId.get(metricId) ?? { base: [], comparison: [] }
      if (side.role === 'award') entry.base.push(factId)
      else entry.comparison.push(factId)
      factIdsByMetricId.set(metricId, entry)
    }
  }

  return { entities, facts, relationships, factIdsByMetricId, withheldFacts }
}

// ── Selbstprüfung (ajv prüft Form, keine Querverweise) ───────────────────────

/**
 * Referenz-Integrität des Graphen: eindeutige IDs, existierende
 * document_id/entity_id-Ziele, Relationship-Enden, sheet-Pflicht.
 * Vorbild validateTraceability — Rückgabe ist eine Issue-Liste, der
 * Aufrufer macht daraus einen benannten validation-Check.
 */
export function validateCanonicalGraph(
  graph: CanonicalGraph,
  documentIds: readonly string[],
): string[] {
  const issues: string[] = []
  const docIds = new Set(documentIds)
  const entityIds = new Set<string>()

  for (const e of graph.entities) {
    const id = e.entity_id as string
    if (entityIds.has(id)) issues.push(`entity_id doppelt: ${id}`)
    entityIds.add(id)
    if (!docIds.has(e.document_id as string)) issues.push(`entity ${id}: unbekannte document_id`)
  }
  const factIds = new Set<string>()
  for (const f of graph.facts) {
    const id = f.fact_id as string
    if (factIds.has(id)) issues.push(`fact_id doppelt: ${id}`)
    factIds.add(id)
    if (!docIds.has(f.document_id as string)) issues.push(`fact ${id}: unbekannte document_id`)
    if (!entityIds.has(f.entity_id as string)) issues.push(`fact ${id}: unbekannte entity_id`)
    const source = f.source as { sheet?: unknown } | undefined
    if (typeof source?.sheet !== 'string' || source.sheet === '') {
      issues.push(`fact ${id}: source.sheet fehlt oder leer`)
    }
  }
  const relationshipIds = new Set<string>()
  for (const r of graph.relationships) {
    const id = r.relationship_id as string
    if (relationshipIds.has(id)) issues.push(`relationship_id doppelt: ${id}`)
    relationshipIds.add(id)
    if (!entityIds.has(r.from_entity_id as string)) issues.push(`relationship ${id}: from_entity_id unbekannt`)
    if (!entityIds.has(r.to_entity_id as string)) issues.push(`relationship ${id}: to_entity_id unbekannt`)
  }
  for (const [metricId, entry] of graph.factIdsByMetricId) {
    for (const fid of [...entry.base, ...entry.comparison]) {
      if (!factIds.has(fid)) issues.push(`factIdsByMetricId ${metricId}: fact ${fid} existiert nicht`)
    }
  }
  return issues
}
