// KAR-840 PR1: Rehydrate a QafExportInput from persisted qaf_* rows.
//
// The 8-sheet Excel export (buildQafExportWorkbook) was written to consume the
// in-memory QafComparisonResult produced during analyze. To offer the export as
// an on-demand download from the detail page, we reconstruct the engine inputs
// from what is already stored — no re-upload, no re-parse of the raw workbook:
//   - qaf_manufacturing_step.raw_values holds the full QAFRow per step
//   - qaf_part holds the identity fields (part number, supplier, dates, …)
// The reconstructed QafFileParsed is fed back through compareQafPair, so the
// downloaded workbook reproduces the on-screen result (deterministic engine,
// same version). Identity-based plausibility (part name / variant / quotation
// date) can't be re-derived from the single last-write-wins qaf_part row, so the
// caller passes the persisted analyze-time issues via plausibilityOverride.
//
// Provenance note: cell addresses (SummaryField.cell) are not persisted, so the
// rehydrated summary carries cell:null. That only affects the "source cell"
// hint; all values and every derived diff/plausibility result are identical.
//
// KAR-886: qaf_manufacturing_step.raw_values is the *entire* persisted QAFRow,
// including its optional sourceCells/normalized provenance — rehydrateFile
// passes r.steps straight through with no field-by-field reconstruction, so
// that provenance survives a rehydrate round-trip for free. Rows persisted
// before KAR-886 simply lack the keys (undefined), which the engine already
// tolerates since nothing in compareQafPair/matcher/differ reads them.

import type { QAFRow } from '@/lib/qaf-parser'
import { compareQafPair, type QafFileParsed, type QafComparisonResult } from './compare'
import type { QafFileRef } from './baseline'
import type { QafSummary } from './types'
import type { PlausibilityIssue, MakeOrBuyIndicationConfig } from './plausibility'
import type { RuleEnforcement, RuleEngineConfig } from './rule-engine'
import { buildQafExportWorkbook, type QafExportInput } from './export'
import { buildFormulaProvenance } from './formula-engine'
import { VALUE_STATES, type ValueState } from './cell-state'
import { SUMMARY_METRIC_KEYS, type SummaryMetricsParse } from './summary-metrics'
import {
  DEFAULT_ENGINE_CONFIG,
  isEngineConfigVersionAtLeast,
  type EngineConfig,
  type EngineConfigSectionKey,
} from './engine-config'
import type { ReconciliationConfig } from './reconciliation'
import type { G60StructureGuardConfig } from './g60/structure-guard'
import type { DifferBandsConfig } from './differ'
import type { MatchConfig } from './types'
import type { FormulaEngineConfig } from './formula-engine'
import type { BusinessRulesConfig } from './business-rules'
import type { MultiQafDetectionConfig } from './qaf-type-detector'

/** A persisted qaf_part row (identity fields), as loaded from Supabase. */
export interface PartRow {
  part_number: string | null
  part_name: string | null
  variant: string | null
  project_code: string | null
  supplier: string | null
  quotation_date: string | null
  version: string | null
}

/** A persisted qaf_file row (subset used for the file ref). */
export interface FileRow {
  id: string
  original_file_name: string
  template_type: string | null
}

/** One rehydrated file side: its ref, identity summary and step rows. */
export interface RehydratedFile {
  file: FileRow
  part: PartRow | null
  steps: QAFRow[]
}

function field(value: string | null | undefined) {
  return { value: value ?? null, cell: null }
}

/** Reconstruct the 9-field engine summary from a persisted qaf_part row. */
export function partToSummary(part: PartRow | null): QafSummary {
  const p = part ?? ({} as PartRow)
  return {
    partNumber: field(p.part_number),
    quotationDate: field(p.quotation_date),
    supplier: field(p.supplier),
    partName: field(p.part_name),
    variant: field(p.variant),
    project: field(p.project_code),
    requestVersion: field(p.version),
    // Not persisted distinctly — left null (only used for identity plausibility,
    // which compares alt vs neu; null == null yields no spurious issue).
    changeIndex: field(null),
    supplierNo: field(null),
    // KAR-910: the 4 new SUMMARY premise fields (peakVolumeYear/
    // productionStartSop/deliverySite/shiftsPerWeek) have no qaf_part column
    // — PartRow only persists the original 9-field identity block. Same
    // "not persisted distinctly, left null" reasoning as changeIndex/
    // supplierNo above; only affects a rehydrated (export/re-download) file,
    // never the live analyze path (which reads these straight off the parsed
    // workbook via summary-parser.ts).
    peakVolumeYear: field(null),
    productionStartSop: field(null),
    deliverySite: field(null),
    shiftsPerWeek: field(null),
    // QVS-P4 (KAR-973): same "not persisted distinctly on qaf_part, left
    // null" limitation as the 4 KAR-910 fields directly above — a rehydrated
    // (export/re-download) file's QafSummary genuinely has no source for
    // these two either. QVS itself does NOT depend on this path (it reads a
    // durable, per-file copy from qaf_summary_metric instead — see
    // lib/qaf-value-stream/internal/qaf-source.ts — precisely because this
    // rehydrate path cannot carry them).
    plannedCapacity: field(null),
    lotSize: field(null),
  }
}

/** Reconstruct a QafFileParsed from persisted rows. */
/** Persisted qaf_summary_metric row subset (reverse of buildSummaryMetricRows). */
export interface SummaryMetricDbRow {
  metric_key: string
  value: number | null
  currency: string | null
  source_cell: string | null
  /** Kriterium-2-Provenienz (Provenance-Migration 2026-08-07) — optional,
   * damit Aufrufer mit alten Selects weiter kompilieren; fehlend = nicht
   * erhoben (Bestandszeilen vor der Migration). */
  sheet?: string | null
  formula?: string | null
  value_state?: string | null
}

/**
 * Rebuild a SummaryMetricsParse from persisted qaf_summary_metric rows so a
 * re-compare (KAR-845 manual matching / swap) reproduces the original
 * summary-level diffs instead of silently dropping them. Absent metrics come
 * back as value null (same as an unparsed metric); locate metadata is
 * conservative (fixedRow, confidence 0.6; absent metrics: labelCollision/0) — it is display-only downstream.
 */
export function summaryMetricsFromRows(
  rows: SummaryMetricDbRow[],
  templateType: string | null,
): SummaryMetricsParse {
  const byKey = new Map(rows.map((r) => [r.metric_key, r]))
  const metrics = {} as SummaryMetricsParse['metrics']
  // Kriterium 2: die Provenance-Migration persistiert sheet/formula/value_state
  // je Zeile — daraus wird die volle Provenienz rekonstruiert.
  // buildFormulaProvenance ist deterministisch über dem Rohtext (normalized +
  // hash werden neu gerechnet), womit auch die Formel-Engine auf dem DB-Pfad
  // arbeitsfähig ist. Bestandszeilen ohne die Spalten bleiben ohne Zustand —
  // ehrlich „nicht erhoben", exakt wie vor der Migration.
  let sheet: string | undefined
  let formulas: SummaryMetricsParse['formulas']
  let valueStates: SummaryMetricsParse['valueStates']
  for (const key of SUMMARY_METRIC_KEYS) {
    const row = byKey.get(key)
    metrics[key] = {
      value: row?.value ?? null,
      cell: row?.source_cell ?? null,
      howLocated: row ? 'fixedRow' : 'labelCollision',
      confidence: row ? 0.6 : 0,
      // Quelle ist hier die Datenbank, nicht die Arbeitsmappe: das Datei-Label
      // liegt nicht vor, also ist es weder bestätigt noch widerlegt.
      labelFile: null,
      labelVerified: null,
    }
    sheet ??= row?.sheet ?? undefined
    if (row?.formula) {
      ;(formulas ??= {})[key] = buildFormulaProvenance(row.formula)
    }
    // Validation an der System-Boundary: nur die fünf Spezifikations-Zustände
    // passieren — DB-Fremdwerte werden verworfen statt weitergereicht.
    if (row?.value_state && (VALUE_STATES as readonly string[]).includes(row.value_state)) {
      ;(valueStates ??= {})[key] = row.value_state as ValueState
    }
  }
  const currency = rows.find((r) => r.currency)?.currency ?? null
  return {
    template: (templateType === 'BMW_SUMMARY_V9' ? 'BMW_SUMMARY_V9' : 'BMW_SUMMARY_LEGACY') as SummaryMetricsParse['template'],
    currency,
    metrics,
    ...(sheet !== undefined ? { sheet } : {}),
    ...(formulas ? { formulas } : {}),
    ...(valueStates ? { valueStates } : {}),
  }
}

export function rehydrateFile(r: RehydratedFile): QafFileParsed {
  const ref: QafFileRef = {
    id: r.file.id,
    fileName: r.file.original_file_name,
    quotationDate: r.part?.quotation_date ?? null,
  }
  return { ref, summary: partToSummary(r.part), steps: r.steps }
}

export interface RehydrateExportArgs {
  /** Caller-supplied label (engine code stays Date-free). */
  generatedAtLabel: string
  baseline: RehydratedFile
  comparison: RehydratedFile
  /**
   * Persisted plausibility issues (qaf_plausibility_issue) to use verbatim
   * instead of the re-run's output. REQUIRED for fidelity: qaf_part is a single
   * last-write-wins row per (project, part number), so a re-run sees identical
   * alt/neu identity fields and the identity checks (part_name_changed,
   * variant_changed, quotation_date_order) can never fire. Passing the rows that
   * were computed per-file at analyze time keeps the export's Plausibilitäts-
   * Sheet identical to the on-screen result.
   */
  plausibilityOverride?: PlausibilityIssue[]
  /**
   * Persisted qaf_comparison.engine_version JSONB (KAR-889 reviewer finding,
   * export/rehydrate reproducibility gap; generalized to the full engine
   * config by P1.5/KAR-896). Used to resolve the engine config that produced
   * this comparison, so the re-run reproduces the same FieldDiffs/bands/
   * matching decisions as the on-screen result even if the live
   * DEFAULT_ENGINE_CONFIG has since changed — without this, a later change to
   * any default section would silently change an OLD comparison's exported
   * deltas while the detail page (reads persisted rows, no re-run) stays
   * correct. Optional; absent/unrecognized resolves to DEFAULT_ENGINE_CONFIG
   * (see resolvePersistedEngineConfig) — a missing key must never silently
   * upgrade an old comparison to a stricter mode.
   */
  engineVersion?: unknown
}

/**
 * Reconstruct the full effective EngineConfig a comparison was persisted
 * under, from its qaf_comparison.engine_version JSONB (P1.5/KAR-896,
 * generalizes the KAR-889 resolvePersistedRuleEnforcement pattern to every
 * config section). Sections not present in the persisted
 * `engineConfigOverrides` fall back to DEFAULT_ENGINE_CONFIG's CURRENT value
 * for that section — same tradeoff already accepted for ruleEnforcement pre-
 * KAR-896 (there is no DB-backed historical-default store in this PR, see
 * engine-config.ts module header; a future DB-backed config item would need
 * to archive versioned defaults keyed by configVersion to close this gap for
 * comparisons whose overrides didn't happen to cover every section that
 * later changed).
 *
 * Backward compat: a comparison persisted before KAR-896 (or by any
 * KAR-889-shaped caller) carries a bare `ruleEnforcement` key instead of
 * `engineConfigOverrides.ruleEngine` — that legacy key is folded in so old
 * comparisons keep resolving to their original mode without re-persisting
 * anything.
 */
export function resolvePersistedEngineConfig(engineVersion: unknown): EngineConfig {
  if (engineVersion === null || typeof engineVersion !== 'object') return DEFAULT_ENGINE_CONFIG

  const ev = engineVersion as Record<string, unknown>
  const configVersion = typeof ev.configVersion === 'string' ? ev.configVersion : DEFAULT_ENGINE_CONFIG.configVersion
  const overrides =
    ev.engineConfigOverrides !== null && typeof ev.engineConfigOverrides === 'object'
      ? (ev.engineConfigOverrides as Partial<Record<EngineConfigSectionKey, unknown>>)
      : undefined
  const legacyRuleEnforcement: RuleEnforcement | undefined = ev.ruleEnforcement === 'block' ? 'block' : undefined

  return {
    configVersion,
    ruleEngine: {
      ...DEFAULT_ENGINE_CONFIG.ruleEngine,
      ...(overrides?.ruleEngine as Partial<RuleEngineConfig> | undefined),
      ...(legacyRuleEnforcement ? { ruleEnforcement: legacyRuleEnforcement } : {}),
    },
    reconciliation: {
      ...DEFAULT_ENGINE_CONFIG.reconciliation,
      ...(overrides?.reconciliation as Partial<ReconciliationConfig> | undefined),
    },
    g60StructureGuard: {
      ...DEFAULT_ENGINE_CONFIG.g60StructureGuard,
      ...(overrides?.g60StructureGuard as Partial<G60StructureGuardConfig> | undefined),
    },
    differBands: {
      ...DEFAULT_ENGINE_CONFIG.differBands,
      ...(overrides?.differBands as Partial<DifferBandsConfig> | undefined),
    },
    matching: {
      ...DEFAULT_ENGINE_CONFIG.matching,
      ...(overrides?.matching as Partial<MatchConfig> | undefined),
    },
    formulaEngine: {
      ...DEFAULT_ENGINE_CONFIG.formulaEngine,
      ...(overrides?.formulaEngine as Partial<FormulaEngineConfig> | undefined),
    },
    businessRules: {
      ...DEFAULT_ENGINE_CONFIG.businessRules,
      ...(overrides?.businessRules as Partial<BusinessRulesConfig> | undefined),
    },
    // KAR-926/Multi-QAF-Programm P0.1: no historical comparison ever
    // persisted an override for this section (it did not exist before this
    // PR) — falls back to DEFAULT_ENGINE_CONFIG.multiQafDetection for every
    // comparison, same "sections not present fall back to the CURRENT
    // default" tradeoff this function's own doc comment already documents
    // for every other section. Since KAR-925 (13.07.2026) that CURRENT
    // default is `enabled: true` — for most callers (export/rehydrate
    // reproduction, live recompute) that is the intended "adopt today's
    // rules" behavior this function exists for. The ONE exception is
    // replaceComparisonFile's ingest-time gate (file-replace-actions.ts): that call site
    // uses resolveReplaceMultiQafDetectionConfig below instead of this
    // function directly, because for that specific call a bare
    // configVersion-less fallback would silently activate the detector for
    // pre-flip comparisons on file replace (KAR-925 adversarial-review F1
    // fix) — see that function's doc for why the general fallback here is
    // safe for every other caller/section but not for that one.
    multiQafDetection: {
      ...DEFAULT_ENGINE_CONFIG.multiQafDetection,
      ...(overrides?.multiQafDetection as Partial<MultiQafDetectionConfig> | undefined),
    },
    // D17 (Spec-Erhebung 08.08.2026): section did not exist before configVersion
    // 1.5.0 — historical comparisons fall back to the CURRENT default, the same
    // documented tradeoff as every section above. A recompute of an old
    // comparison can therefore newly produce a make_or_buy_indication finding —
    // intended "adopt today's rules" behavior; the run is re-stamped with its
    // engine_version, so provenance stays honest.
    makeOrBuyIndication: {
      ...DEFAULT_ENGINE_CONFIG.makeOrBuyIndication,
      ...(overrides?.makeOrBuyIndication as Partial<MakeOrBuyIndicationConfig> | undefined),
    },
  }
}

/**
 * KAR-925 adversarial-review F1 fix (13.07.2026) — dedicated resolver for
 * `replaceComparisonFile`'s (file-replace-actions.ts) multiQafDetection ingest-time gate.
 * NOT a substitute for `resolvePersistedEngineConfig` in general: every OTHER
 * section's "fall back to the CURRENT default when no override is
 * persisted" contract (see that function's doc) is exactly right for a live
 * recompute — recompareComparison (actions.ts) already deliberately
 * re-runs compareQafPair with NO engineConfig override at all, so a live
 * edit always reflects today's rules; only export/rehydrate reproduces the
 * historical config verbatim. `multiQafDetection.enabled`, however, is not a
 * threshold tweak — flipping it (KAR-925) turns "detector never runs" into
 * "detector runs and can ingest the file as a Multi-QAF container instead of
 * summary/g60, which the role-kind guard then rejects+deletes if the role
 * expected something else". Adopting that switch retroactively, for a
 * comparison that has never been computed under it, on the very first file
 * replace after the flip is a materially different (and wrong) kind of
 * "adopt today's rules" than any other section's fallback — see
 * engine-config.ts's 1.4.0 changelog note.
 *
 * Resolution:
 *  1. An explicit per-comparison `engineConfigOverrides.multiQafDetection`
 *     always wins (same precedence resolvePersistedEngineConfig itself
 *     uses) — no comparison persists one today, but a future DB-backed
 *     config might.
 *  2. Otherwise, read the comparison's OWN raw `configVersion` stamp (not
 *     resolvePersistedEngineConfig's resolved one — THAT already defaults a
 *     missing stamp to DEFAULT_ENGINE_CONFIG.configVersion, i.e. today's
 *     '1.4.0', which would defeat this check entirely). `configVersion >=
 *     '1.4.0'` means the comparison was already created/last recomputed
 *     under the flipped default (see persistence-mapper.ts's
 *     buildEngineConfigDelta call, which stamps every fresh compute with the
 *     CURRENT configVersion) — inheriting today's multiQafDetection is then
 *     a genuine no-op, not an upgrade, so this resolves the same as
 *     resolvePersistedEngineConfig would.
 *  3. A stamp below '1.4.0' (KAR-926's own '1.3.0', anything earlier, or no
 *     recognizable stamp at all — including a G60 comparison's
 *     ENGINE_VERSION-literal-only insert, whose `configVersion` is a fixed
 *     '1.0.0' regardless of when the comparison was created, see types.ts)
 *     means this comparison has never been computed with the detector
 *     enabled — this resolver pins `enabled: false` for that one replace,
 *     which is what was actually in effect when the comparison was created.
 *     Every OTHER field on the section (thresholds) still comes from the
 *     current default; only `enabled` has ever changed shape since 1.3.0.
 *
 * A comparison recomputed via replaceComparisonFile picks up the CURRENT
 * configVersion stamp afterward (same recompute-always-adopts-today's-rules
 * path every other section already follows) — so this is a one-time grace
 * for the FIRST replace after the flip, not a permanent lock; that is
 * intentional, not a bug: once a comparison has actually been computed with
 * detection on, a later replace correctly keeps using it.
 */
export function resolveReplaceMultiQafDetectionConfig(engineVersion: unknown): MultiQafDetectionConfig {
  const resolved = resolvePersistedEngineConfig(engineVersion)
  const ev = engineVersion !== null && typeof engineVersion === 'object' ? (engineVersion as Record<string, unknown>) : undefined
  const overrides = ev?.engineConfigOverrides
  const hasExplicitOverride =
    overrides !== null && typeof overrides === 'object' && 'multiQafDetection' in (overrides as Record<string, unknown>)
  if (hasExplicitOverride) return resolved.multiQafDetection

  const rawConfigVersion = typeof ev?.configVersion === 'string' ? ev.configVersion : undefined
  if (isEngineConfigVersionAtLeast(rawConfigVersion, '1.4.0')) return resolved.multiQafDetection

  return { ...resolved.multiQafDetection, enabled: false }
}

/**
 * Resolve the ruleEnforcement mode a comparison was persisted under (KAR-889
 * reviewer finding). Thin wrapper around resolvePersistedEngineConfig
 * (P1.5/KAR-896) — kept for existing callers/tests that only need the
 * rule-engine mode, not the full config. Comparisons persisted before
 * KAR-889 — or any shape that doesn't carry the key — fall back to 'warn',
 * the engine's original non-blocking default: a missing key must never
 * silently upgrade old data to the stricter 'block' mode.
 */
export function resolvePersistedRuleEnforcement(engineVersion: unknown): RuleEnforcement {
  return resolvePersistedEngineConfig(engineVersion).ruleEngine.ruleEnforcement
}

/**
 * Rehydrate the full QafExportInput for one comparison: reconstruct both file
 * sides, re-run the deterministic comparison under the mode it was originally
 * persisted with, and assemble the export payload.
 */
export function rehydrateExportInput(args: RehydrateExportArgs): QafExportInput {
  const alt = rehydrateFile(args.baseline)
  const neu = rehydrateFile(args.comparison)
  const engineConfig = resolvePersistedEngineConfig(args.engineVersion)
  const result: QafComparisonResult = compareQafPair(alt, neu, { engineConfig })
  // Prefer the analyze-time plausibility (computed from each file's own summary)
  // over the lossy re-run — see plausibilityOverride doc above.
  if (args.plausibilityOverride) result.plausibility = args.plausibilityOverride
  return {
    generatedAtLabel: args.generatedAtLabel,
    files: [
      { fileName: alt.ref.fileName, partNumber: alt.summary.partNumber.value, status: 'parsed' },
      { fileName: neu.ref.fileName, partNumber: neu.summary.partNumber.value, status: 'parsed' },
    ],
    comparisons: [result],
  }
}

/** Convenience: rehydrate + build the workbook in one call. */
export async function buildExportWorkbookFromRows(args: RehydrateExportArgs): Promise<ArrayBuffer> {
  return buildQafExportWorkbook(rehydrateExportInput(args))
}
