// Consolidated Zelle→Ergebnis Explain-Panel provenance (KAR-922/P6.2).
//
// Problem this closes: Master-Prompt §17 requires every comparison result to
// stay traceable to source file/sheet/cell/label/canonical field/raw+
// normalized value/formula/comparison rule/mapping method/confidence/unit+
// currency conversion/delta/validation result/QAF-guide requirement/engine
// version/template profile version/timestamp. KAR-886/891/893/894/895/896/
// 900/902/903/904/910/911/912 already persist (or, for a documented subset,
// only compute at ingest-time — see the formula/confidence notes below) every
// one of these attributes across the MANUFACTURING/MATERIAL/SBM/RMR/
// LOGISTICS/SUMMARY modules, but each one lives in a different shape
// (QAFRow.sourceCells vs. SummaryMetricValue.cell vs. MaterialRow.rawText,
// ...) and no single place assembles them into one "explain this result"
// view — KAR-911's ProvenanceTooltip only ever shows a handful of fields
// (source cell, module confidence, engine version, manual override), not the
// full §17 set.
//
// This module is that single place: one shared output shape
// (ExplainAttributes/ExplainSideAttributes) plus one thin adapter function
// per module (buildManufacturingExplainAttributes/buildMaterialExplain-
// Attributes/...), all delegating to the same generic core
// (buildRowModuleExplainAttributes/buildDetailSideAttributes) so there is
// exactly one place that decides what "present"/"not applicable"/"not
// captured" means for a given attribute — no per-module duplicate logic
// (task instruction: "kein Ad-hoc-Duplikat je Modul — ein Interface,
// Modul-Adapter").
//
// Reine Präsentationsschicht: every value read here was already computed and
// persisted (or, where documented below, only ever computed at ingest-time
// and intentionally not persisted) by an earlier PR — no new engine
// computation, no new persistence, no new confidence scoring happens in this
// file.
//
// Tri-State-Disziplin (task instruction: "NIE fabrizieren"): every attribute
// is one of
//   - 'present'        — a real value the engine actually computed/read.
//   - 'not_applicable' — this attribute structurally does not exist for this
//                         field/row (e.g. a text field has no formula; the
//                         engine performs no unit/currency conversion at
//                         all, ever, for any field).
//   - 'not_captured'   — the attribute conceptually exists but was not
//                         persisted for THIS value (predates the feature
//                         that introduced it, or was only ever computed
//                         in-memory at ingest-time and not persisted — e.g.
//                         SUMMARY per-metric confidence/formula). The UI
//                         renders this as "nicht erfasst (älterer Lauf)".
// Never a bare null with no explanation of which of the two it is.
//
// tdd-guard:skip — type declarations + a `note` constant at the top; every
// function below is exercised by __tests__/explain-provenance.test.ts.

import type { QAFFieldKey } from '@/lib/qaf-parser'
import { byCanonicalId } from './canonical-model'
import {
  QAF_FIELD_KEY_TO_CANONICAL,
  MATERIAL_FIELD_KEY_TO_CANONICAL,
  SBM_FIELD_KEY_TO_CANONICAL,
  RMR_FIELD_KEY_TO_CANONICAL,
  LOG_FIELD_KEY_TO_CANONICAL,
  SUMMARY_METRIC_KEY_TO_CANONICAL,
} from './canonical-fields'
import type { MaterialFieldKey } from './material-parser'
import type { SbmFieldKey } from './sbm-parser'
import type { RmrFieldKey } from './rmr-parser'
import type { LogisticsFieldKey } from './logistics-parser'
import type { SummaryMetricKey } from './summary-metrics'
import type { FormulaProvenance } from './formula-engine'
import type { CanonicalRequirement } from './canonical-fields.types'
import type { DecodedMessage } from './bilingual-message'

// ── Shared output shape ─────────────────────────────────────────────────────

export type ExplainStatus = 'present' | 'not_applicable' | 'not_captured'

/** One §17 attribute. `note` is a factual, non-fabricated explanation of WHY
 * the attribute is unavailable — never invented data, only documentation of
 * an actual system fact (e.g. "engine performs no currency conversion").
 *
 * KAR-924/Teil 1: `note` is bilingual (DE+EN, same {de,en} shape
 * bilingual-message.ts already uses for KAR-906) instead of a single German
 * string — the Explain-Panel's EN view otherwise showed a translated status
 * phrase ("not captured (older run)") followed by an untranslated German
 * `note` sentence, which is exactly the leftover-German bug this task
 * closes. */
export interface ExplainValue<T> {
  status: ExplainStatus
  value: T | null
  note?: DecodedMessage
}

function present<T>(value: T): ExplainValue<T> {
  return { status: 'present', value }
}
function notApplicable<T>(note?: DecodedMessage): ExplainValue<T> {
  return { status: 'not_applicable', value: null, note }
}
function notCaptured<T>(note?: DecodedMessage): ExplainValue<T> {
  return { status: 'not_captured', value: null, note }
}

/** Machine-mapping-method key (KAR-924/Teil 1: the server emits this enum
 * key instead of a ready-made German sentence — qaf-explain-panel.tsx
 * resolves it to a DE/EN label via its own small dictionary, mirroring how
 * `originalLabelDe`/`originalLabelEn` are already resolved client-side by
 * locale). */
export type MappingMethodKind = 'manual_override' | 'automatic_header_mapping'

/** Per-side (ALT/NEU) §17 attributes — everything that is intrinsically tied
 * to ONE specific source file. */
export interface ExplainSideAttributes {
  sourceFile: ExplainValue<string>
  sourceSheet: ExplainValue<string>
  sourceCell: ExplainValue<string>
  rawValue: ExplainValue<string>
  normalizedValue: ExplainValue<string>
  formula: ExplainValue<{ raw: string; normalized: string }>
  mappingMethod: ExplainValue<MappingMethodKind>
  confidence: ExplainValue<number>
  unitConversion: ExplainValue<string>
  currencyConversion: ExplainValue<string>
  templateProfileVersion: ExplainValue<string>
}

/** "QAF guide requirement" value (KAR-924/Teil 1: enum key + non-translatable
 * evidence citation, instead of a ready-made German sentence like "Pflichtfeld
 * (Quelle: leitfaden-teil1:9)") — qaf-explain-panel.tsx resolves `requirement`
 * to a DE/EN label and wraps `evidence` in a localized "Quelle"/"Source"
 * prefix. `evidence` itself (a Leitfaden citation, e.g. "leitfaden-teil1:9")
 * is a reference, not prose, so it is never translated. */
export interface QafGuideRequirementValue {
  requirement: CanonicalRequirement
  evidence?: string
}

/** Full §17 attribute set for one result value (one field on one row/metric,
 * comparing its ALT and NEU side). */
export interface ExplainAttributes {
  canonicalFieldId: ExplainValue<string>
  originalLabelDe: ExplainValue<string>
  originalLabelEn: ExplainValue<string>
  qafGuideRequirement: ExplainValue<QafGuideRequirementValue>
  alt: ExplainSideAttributes
  neu: ExplainSideAttributes
  calculatedDelta: ExplainValue<string>
  comparisonRule: ExplainValue<DecodedMessage>
  validationResult: ExplainValue<string>
  engineVersion: ExplainValue<Record<string, unknown>>
  timestamp: ExplainValue<string>
}

const NO_CONVERSION_NOTE: DecodedMessage = {
  de: 'Die Engine führt aktuell keine Einheiten-/Währungsumrechnung durch — nur Rohwerte werden verglichen.',
  en: 'The engine currently performs no unit/currency conversion — only raw values are compared.',
}
const NO_VALUE_ON_SIDE_NOTE: DecodedMessage = { de: 'Kein Wert auf dieser Seite.', en: 'No value on this side.' }
const NO_FILE_ON_SIDE_NOTE: DecodedMessage = {
  de: 'Keine Datei auf dieser Seite (einseitiger Entwurf).',
  en: 'No file on this side (one-sided draft).',
}
const TEMPLATE_PROFILE_NOT_CAPTURED_NOTE: DecodedMessage = {
  de: 'Template-Profil nicht erfasst (älterer Lauf, vor KAR-895).',
  en: 'Template profile not captured (older run, predates KAR-895).',
}
const NO_COMPARISON_RESULT_NOTE: DecodedMessage = {
  de: 'Kein Vergleichsergebnis für diese Zeile.',
  en: 'No comparison result for this row.',
}
const LABEL_NOT_FOUND_NOTE: DecodedMessage = {
  de: 'Standardbezeichnung nicht in der Registry gefunden.',
  en: 'Standard label not found in the registry.',
}

/** Shared by every adapter below — resolves the "QAF guide requirement"
 * attribute from the canonical field registry (mandatory/conditional/
 * optional + Leitfaden-Fundstelle), the same registry rule-engine.ts already
 * uses for R2/R3. KAR-924/Teil 1: returns the enum key + evidence citation,
 * not a pre-rendered German sentence — see QafGuideRequirementValue doc. */
export function buildQafGuideRequirement(canonicalId: string | undefined): ExplainValue<QafGuideRequirementValue> {
  if (!canonicalId) return notApplicable({ de: 'Kein kanonisches Feld zugeordnet.', en: 'No canonical field mapped.' })
  const field = byCanonicalId(canonicalId)
  if (!field) return notCaptured({ de: 'Kanonisches Feld nicht in der Registry gefunden.', en: 'Canonical field not found in the registry.' })
  const evidence = field.evidence[0]?.source
  return present({ requirement: field.requirement, evidence })
}

function fmtDeltaText(d: { deltaAbsolute: number | null; deltaPercent: number | null } | null | undefined): ExplainValue<string> {
  if (!d || (d.deltaAbsolute === null && d.deltaPercent === null)) {
    return notApplicable({
      de: 'Kein Delta berechenbar (strukturelle Abweichung oder fehlender Wert auf mindestens einer Seite).',
      en: 'No delta can be calculated (structural difference or missing value on at least one side).',
    })
  }
  const parts: string[] = []
  if (d.deltaAbsolute !== null) parts.push(`Δ abs ${d.deltaAbsolute}`)
  if (d.deltaPercent !== null) parts.push(`Δ % ${(d.deltaPercent * 100).toFixed(1)}%`)
  return present(parts.join(' · '))
}

/** One rule-engine/plausibility finding tied to this specific field/row —
 * the same shape compare.ts's plausibility issues already carry (issue_type +
 * explanation), narrowed to what this module needs to render the
 * "comparison rule" attribute. `explanationEn` (KAR-924/Teil 1) is the
 * English counterpart the caller decodes via bilingual-message.ts's
 * `decodeBilingual` — optional so existing DE-only fixtures/callers keep
 * working (falls back to `explanation` below, same tolerance rule
 * decodeBilingual itself already applies to legacy DE-only issues). */
export interface RuleFindingLike {
  rule: string
  explanation: string
  explanationEn?: string
}

function buildComparisonRuleAndValidation(
  diffStatus: string | null | undefined,
  ruleFinding: RuleFindingLike | null | undefined,
): { comparisonRule: ExplainValue<DecodedMessage>; validationResult: ExplainValue<string> } {
  if (ruleFinding) {
    const explanationEn = ruleFinding.explanationEn ?? ruleFinding.explanation
    return {
      comparisonRule: present({
        de: `${ruleFinding.rule}: ${ruleFinding.explanation}`,
        en: `${ruleFinding.rule}: ${explanationEn}`,
      }),
      validationResult: diffStatus ? present(diffStatus) : notApplicable(NO_COMPARISON_RESULT_NOTE),
    }
  }
  if (diffStatus) {
    return {
      comparisonRule: present({
        de: `Status-Band-Vergleich (${diffStatus})`,
        en: `Status-band comparison (${diffStatus})`,
      }),
      validationResult: present(diffStatus),
    }
  }
  return {
    comparisonRule: notApplicable(NO_COMPARISON_RESULT_NOTE),
    validationResult: notApplicable(NO_COMPARISON_RESULT_NOTE),
  }
}

function buildEngineVersionAttribute(engineVersion: Record<string, unknown> | null | undefined): ExplainValue<Record<string, unknown>> {
  return engineVersion
    ? present(engineVersion)
    : notCaptured({
        de: 'Engine-Version nicht erfasst (älterer Lauf, vor der Versionierung in qaf_comparison.engine_version).',
        en: 'Engine version not captured (older run, predates versioning in qaf_comparison.engine_version).',
      })
}

function buildTimestampAttribute(timestamp: string | null | undefined): ExplainValue<string> {
  return timestamp ? present(timestamp) : notCaptured({ de: 'Zeitstempel nicht erfasst.', en: 'Timestamp not captured.' })
}

// ── Row modules (MANUFACTURING/MATERIAL/SBM/RMR/LOGISTICS) ─────────────────
//
// All five share the identical row shape (RowValues & { sourceCells,
// normalized, rawText, formulas?, manualOverride? }, see qaf-parser.ts/
// material-parser.ts/sbm-parser.ts/rmr-parser.ts/logistics-parser.ts module
// headers — all four detail parsers were built "sourceCells/normalized/
// rawText present from day one", mirroring qaf-parser.ts's KAR-886/891
// additive fields) — so ONE generic core (buildDetailSideAttributes /
// buildRowModuleExplainAttributes) serves all five; each module gets a thin
// adapter that only supplies its own field-key→canonical-id map and sheet
// name.

/** Structural subset every row-module's row shape satisfies — deliberately
 * duck-typed (not importing QAFRow/MaterialRow/... directly) so this core
 * stays agnostic of which module called it. */
export interface DetailRowLike<K extends string> {
  sourceCells?: Partial<Record<K, string>>
  normalized?: Partial<Record<K, string | number | null>>
  rawText?: Partial<Record<K, string>>
  formulas?: Partial<Record<K, FormulaProvenance>>
  manualOverride?: Partial<Record<K, { sourceDescription: string; setBy: string; setAt: string }>>
}

export interface DetailSideInput<K extends string> {
  fileName: string | null
  sheetName: string
  row: DetailRowLike<K> | null
  fieldKey: K
  /** Module-wide parseConfidence (0..1) — KAR-893/897/898/902/903's
   * per-module average, the finest confidence grain the engine currently
   * persists (no per-field mapping-method/confidence exists yet). */
  moduleConfidence: number | null
  /** KAR-895/P1.4 matchedProfile id + classification, e.g.
   * "QAF_V9_SUMMARY (known)" — file-level, but rendered per side since ALT
   * and NEU are two different files that can carry different profiles. */
  templateProfile: string | null
}

/** One side's (ALT or NEU) full set of §17 side-attributes for one field on
 * one row — the generic core every row-module adapter below delegates to. */
export function buildDetailSideAttributes<K extends string>(input: DetailSideInput<K>): ExplainSideAttributes {
  const { fileName, sheetName, row, fieldKey, moduleConfidence, templateProfile } = input

  if (!row) {
    return {
      sourceFile: fileName ? present(fileName) : notApplicable(NO_FILE_ON_SIDE_NOTE),
      sourceSheet: notApplicable(NO_VALUE_ON_SIDE_NOTE),
      sourceCell: notApplicable(NO_VALUE_ON_SIDE_NOTE),
      rawValue: notApplicable(NO_VALUE_ON_SIDE_NOTE),
      normalizedValue: notApplicable(NO_VALUE_ON_SIDE_NOTE),
      formula: notApplicable(NO_VALUE_ON_SIDE_NOTE),
      mappingMethod: notApplicable(NO_VALUE_ON_SIDE_NOTE),
      confidence: notApplicable(NO_VALUE_ON_SIDE_NOTE),
      unitConversion: notApplicable(NO_CONVERSION_NOTE),
      currencyConversion: notApplicable(NO_CONVERSION_NOTE),
      templateProfileVersion: notApplicable(NO_VALUE_ON_SIDE_NOTE),
    }
  }

  const cell = row.sourceCells?.[fieldKey]
  const normalized = row.normalized?.[fieldKey]
  const rawText = row.rawText?.[fieldKey]
  const formula = row.formulas?.[fieldKey]
  const override = row.manualOverride?.[fieldKey]
  const hasNormalized = normalized !== undefined && normalized !== null

  return {
    sourceFile: fileName ? present(fileName) : notCaptured({ de: 'Dateiname nicht verfügbar.', en: 'File name not available.' }),
    sourceSheet: present(sheetName),
    sourceCell: cell
      ? present(cell)
      : notCaptured({
          de: 'Quellzelle nicht erfasst (älterer Lauf oder Feld nicht im Header gefunden).',
          en: 'Source cell not captured (older run or field not found in the header).',
        }),
    rawValue: rawText
      ? present(rawText)
      : hasNormalized
        ? notCaptured({
            de: 'Engine speichert nur bei nicht-numerischem Text einen separaten Rohwert — bei erfolgreichem Zahlen-Parse ist nur der normalisierte Wert gespeichert.',
            en: 'The engine only stores a separate raw value for non-numeric text — once a value parses successfully as a number, only the normalized value is stored.',
          })
        : notCaptured({
            de: 'Kein Rohwert erfasst (älterer Lauf oder Feld nicht gefunden).',
            en: 'No raw value captured (older run or field not found).',
          }),
    normalizedValue: hasNormalized
      ? present(String(normalized))
      : notCaptured({
          de: 'Kein normalisierter Wert erfasst (älterer Lauf oder Feld nicht gefunden).',
          en: 'No normalized value captured (older run or field not found).',
        }),
    formula: formula
      ? formula.unresolved
        ? notCaptured({
            de: 'Formel erkannt, aber von der Engine nicht auflösbar (Shared-Formula ohne lesbaren Ausdruck).',
            en: 'Formula detected but not resolvable by the engine (shared formula with no readable expression).',
          })
        : present({ raw: formula.raw, normalized: formula.normalized })
      : notApplicable({
          de: 'Keine Formel in dieser Zelle (fester Wert), oder Formel-Erfassung deckt dieses Feld nicht ab.',
          en: 'No formula in this cell (fixed value), or formula capture does not cover this field.',
        }),
    mappingMethod: override
      ? present('manual_override')
      : cell
        ? present('automatic_header_mapping')
        : notCaptured({
            de: 'Zuordnungsmethode je Einzelfeld nicht erfasst (nur modulweite Confidence gespeichert).',
            en: 'Per-field mapping method not captured (only module-wide confidence is stored).',
          }),
    confidence:
      moduleConfidence !== null
        ? present(moduleConfidence)
        : notCaptured({ de: 'Modul-Confidence nicht erfasst (älterer Lauf).', en: 'Module confidence not captured (older run).' }),
    unitConversion: notApplicable(NO_CONVERSION_NOTE),
    currencyConversion: notApplicable(NO_CONVERSION_NOTE),
    templateProfileVersion: templateProfile ? present(templateProfile) : notCaptured(TEMPLATE_PROFILE_NOT_CAPTURED_NOTE),
  }
}

export interface RowModuleSideInput<K extends string> {
  fileName: string | null
  row: DetailRowLike<K> | null
  moduleConfidence: number | null
  templateProfile: string | null
}

export interface RowModuleDiffLike {
  deltaAbsolute: number | null
  deltaPercent: number | null
  status: string | null
}

export interface RowModuleExplainInput<K extends string> {
  fieldKey: K
  fieldKeyToCanonical: Partial<Record<K, string>>
  sheetName: string
  alt: RowModuleSideInput<K>
  neu: RowModuleSideInput<K>
  diff: RowModuleDiffLike | null
  ruleFinding?: RuleFindingLike | null
  engineVersion: Record<string, unknown> | null
  timestamp: string | null
}

/** Generic core every row-module adapter (MANUFACTURING/MATERIAL/SBM/RMR/
 * LOGISTICS) delegates to — the single place that assembles the shared
 * (canonicalFieldId/label/requirement/delta/rule/engineVersion/timestamp)
 * attributes on top of the two buildDetailSideAttributes calls. */
export function buildRowModuleExplainAttributes<K extends string>(input: RowModuleExplainInput<K>): ExplainAttributes {
  const canonicalId = input.fieldKeyToCanonical[input.fieldKey]
  const canonical = canonicalId ? byCanonicalId(canonicalId) : undefined

  const alt = buildDetailSideAttributes({
    fileName: input.alt.fileName,
    sheetName: input.sheetName,
    row: input.alt.row,
    fieldKey: input.fieldKey,
    moduleConfidence: input.alt.moduleConfidence,
    templateProfile: input.alt.templateProfile,
  })
  const neu = buildDetailSideAttributes({
    fileName: input.neu.fileName,
    sheetName: input.sheetName,
    row: input.neu.row,
    fieldKey: input.fieldKey,
    moduleConfidence: input.neu.moduleConfidence,
    templateProfile: input.neu.templateProfile,
  })

  const { comparisonRule, validationResult } = buildComparisonRuleAndValidation(input.diff?.status, input.ruleFinding)

  return {
    canonicalFieldId: canonicalId
      ? present(canonicalId)
      : notCaptured({ de: 'Feld ist keinem kanonischen Feld zugeordnet.', en: 'Field is not mapped to a canonical field.' }),
    originalLabelDe: canonical ? present(canonical.labelDe) : notCaptured(LABEL_NOT_FOUND_NOTE),
    originalLabelEn: canonical
      ? present(canonical.labelEn)
      : notCaptured({ de: 'Standardbezeichnung (EN) nicht in der Registry gefunden.', en: 'Standard label (EN) not found in the registry.' }),
    qafGuideRequirement: buildQafGuideRequirement(canonicalId),
    alt,
    neu,
    calculatedDelta: fmtDeltaText(input.diff),
    comparisonRule,
    validationResult,
    engineVersion: buildEngineVersionAttribute(input.engineVersion),
    timestamp: buildTimestampAttribute(input.timestamp),
  }
}

// ── Per-module adapters (row modules) ───────────────────────────────────────

export type RowModuleAdapterInput<K extends string> = Omit<RowModuleExplainInput<K>, 'fieldKeyToCanonical' | 'sheetName'>

/** MANUFACTURING (Fertigungskosten) — the module the live UI already renders
 * per-row deltas for (A1. Fertigungskosten-Deltas). */
export function buildManufacturingExplainAttributes(input: RowModuleAdapterInput<QAFFieldKey>): ExplainAttributes {
  return buildRowModuleExplainAttributes({
    ...input,
    fieldKeyToCanonical: QAF_FIELD_KEY_TO_CANONICAL,
    sheetName: 'Fertigungskosten',
  })
}

/** MATERIAL (KAR-897/P1.6) — adapter ready; qaf-comparison-detail.tsx has no
 * rendered value-diff section for MATERIAL yet (only the degradation notice,
 * module-degradation.ts), so this is not yet wired into the live UI. See PR
 * body / CHANGELOG for the documented follow-up (same carve-out pattern
 * KAR-911 used for G60). */
export function buildMaterialExplainAttributes(input: RowModuleAdapterInput<MaterialFieldKey>): ExplainAttributes {
  return buildRowModuleExplainAttributes({
    ...input,
    fieldKeyToCanonical: MATERIAL_FIELD_KEY_TO_CANONICAL,
    sheetName: 'MATERIAL',
  })
}

/** SBM-DEVICES-FWZ (KAR-898/P1.7) — same UI-wiring status as MATERIAL above. */
export function buildSbmExplainAttributes(input: RowModuleAdapterInput<SbmFieldKey>): ExplainAttributes {
  return buildRowModuleExplainAttributes({
    ...input,
    fieldKeyToCanonical: SBM_FIELD_KEY_TO_CANONICAL,
    sheetName: 'SBM-DEVICES-FWZ',
  })
}

/** RAW MATERIAL RISKS (KAR-902/P2.3) — same UI-wiring status as MATERIAL above. */
export function buildRmrExplainAttributes(input: RowModuleAdapterInput<RmrFieldKey>): ExplainAttributes {
  return buildRowModuleExplainAttributes({
    ...input,
    fieldKeyToCanonical: RMR_FIELD_KEY_TO_CANONICAL,
    sheetName: 'RAW MATERIAL RISKS',
  })
}

/** LOGISTICS & CUSTOM (KAR-903/P2.4) — same UI-wiring status as MATERIAL above. */
export function buildLogisticsExplainAttributes(input: RowModuleAdapterInput<LogisticsFieldKey>): ExplainAttributes {
  return buildRowModuleExplainAttributes({
    ...input,
    fieldKeyToCanonical: LOG_FIELD_KEY_TO_CANONICAL,
    sheetName: 'LOGISTICS & CUSTOM',
  })
}

// ── SUMMARY (Zusammenfassung) ───────────────────────────────────────────────
//
// Distinct row shape (SummaryMetricValue: single {value, cell} per metric,
// not a sourceCells/normalized/rawText map) — its own adapter rather than a
// buildDetailSideAttributes call, but it still fills the exact same
// ExplainAttributes/ExplainSideAttributes shape as every row-module adapter
// above (the "ein Interface" half of the task instruction).
//
// Two attributes are structurally weaker than the row modules', and this
// adapter is explicit rather than silent about why (Tri-State-Disziplin):
//   - formula: SummaryMetricsParse.formulas (KAR-900/P2.1) is an in-memory-
//     only field, never round-tripped into qaf_summary_diff/qaf_summary_metric
//     (see summary-metrics.ts's SummaryMetricsParse doc comment and
//     types.ts's FieldDiff.formulaFinding doc — "does not survive a DB
//     round-trip"). A detected formula CHANGE still surfaces separately as
//     its own plausibility finding (A4) — this attribute just cannot show
//     the formula text itself for an already-persisted comparison.
//   - confidence / mappingMethod: SummaryMetricValue.confidence/howLocated
//     (KAR-907) are computed at parse time but qaf_summary_metric/
//     qaf_summary_diff carry no column for them (persistence-mapper.ts's
//     SummaryMetricRow/SummaryDiffRow shape) — same "not captured for THIS
//     persisted value" situation as formula above.

export interface SummarySideInput {
  fileName: string | null
  cell: string | null
  value: number | null
  currency: string | null
  templateProfile: string | null
}

function buildSummarySideAttributes(input: SummarySideInput): ExplainSideAttributes {
  return {
    sourceFile: input.fileName ? present(input.fileName) : notApplicable(NO_FILE_ON_SIDE_NOTE),
    sourceSheet: present('Zusammenfassung'),
    sourceCell: input.cell
      ? present(input.cell)
      : notCaptured({
          de: 'Quellzelle nicht erfasst (älterer Lauf oder Kennzahl nicht gefunden).',
          en: 'Source cell not captured (older run or metric not found).',
        }),
    rawValue: notCaptured({
      de: 'Rohtext vor Normalisierung wird für Summary-Kennzahlen nicht persistiert (nur der normalisierte Wert).',
      en: 'Raw text prior to normalization is not persisted for summary metrics (only the normalized value).',
    }),
    normalizedValue:
      input.value !== null
        ? present(`${input.value}${input.currency ? ` ${input.currency}` : ''}`)
        : notCaptured({
            de: 'Kein Wert erfasst (älterer Lauf oder Kennzahl nicht gefunden).',
            en: 'No value captured (older run or metric not found).',
          }),
    // KAR-924/Teil 1: mappingMethod is a MappingMethodKind-typed value, so
    // "not persisted for SUMMARY" can only ever be notCaptured here (unlike
    // the row-module core above, SUMMARY has no 'manual_override'/
    // 'automatic_header_mapping' distinction to resolve to a present value).
    formula: notCaptured({
      de: 'Formel-Provenance ist nur zur Ingest-/Vergleichszeit verfügbar und wird nicht persistiert — eine erkannte Formeländerung erscheint stattdessen als eigener Befund in Plausibilität (A4).',
      en: 'Formula provenance is only available at ingest/comparison time and is not persisted — a detected formula change instead appears as its own finding in Plausibility (A4).',
    }),
    mappingMethod: notCaptured({
      de: 'Zuordnungsmethode (Label-Anker/Fixzeile/Aggregat) wird für Summary-Kennzahlen nicht persistiert.',
      en: 'Mapping method (label anchor/fixed row/aggregate) is not persisted for summary metrics.',
    }),
    confidence: notCaptured({
      de: 'Confidence wird für Summary-Kennzahlen nicht persistiert.',
      en: 'Confidence is not persisted for summary metrics.',
    }),
    unitConversion: notApplicable(NO_CONVERSION_NOTE),
    currencyConversion: notApplicable(NO_CONVERSION_NOTE),
    templateProfileVersion: input.templateProfile ? present(input.templateProfile) : notCaptured(TEMPLATE_PROFILE_NOT_CAPTURED_NOTE),
  }
}

export interface SummaryExplainInput {
  metricKey: SummaryMetricKey
  alt: SummarySideInput
  neu: SummarySideInput
  deltaAbsolute: number | null
  deltaPercent: number | null
  status: string | null
  ruleFinding?: RuleFindingLike | null
  engineVersion: Record<string, unknown> | null
  timestamp: string | null
}

export function buildSummaryExplainAttributes(input: SummaryExplainInput): ExplainAttributes {
  const canonicalId = SUMMARY_METRIC_KEY_TO_CANONICAL[input.metricKey]
  const canonical = canonicalId ? byCanonicalId(canonicalId) : undefined
  const { comparisonRule, validationResult } = buildComparisonRuleAndValidation(input.status, input.ruleFinding)

  return {
    canonicalFieldId: canonicalId
      ? present(canonicalId)
      : notCaptured({ de: 'Kennzahl ist keinem kanonischen Feld zugeordnet.', en: 'Metric is not mapped to a canonical field.' }),
    originalLabelDe: canonical ? present(canonical.labelDe) : notCaptured(LABEL_NOT_FOUND_NOTE),
    originalLabelEn: canonical
      ? present(canonical.labelEn)
      : notCaptured({ de: 'Standardbezeichnung (EN) nicht in der Registry gefunden.', en: 'Standard label (EN) not found in the registry.' }),
    qafGuideRequirement: buildQafGuideRequirement(canonicalId),
    alt: buildSummarySideAttributes(input.alt),
    neu: buildSummarySideAttributes(input.neu),
    calculatedDelta: fmtDeltaText({ deltaAbsolute: input.deltaAbsolute, deltaPercent: input.deltaPercent }),
    comparisonRule,
    validationResult,
    engineVersion: buildEngineVersionAttribute(input.engineVersion),
    timestamp: buildTimestampAttribute(input.timestamp),
  }
}
