// Pure view-model builders for the Summary-comparison "Produktionssicht /
// Raten" (§6) and "Anomalien & Treiber (Master-Sätze)" (§7) sections
// (KAR-960/P3, Master-Prompt §22/§23 — Gate-Audit Top-5 #5).
//
// Both sections used to be UNCONDITIONAL PlaceholderSections
// (qaf-comparison-detail.tsx, qaf-section-registry.ts SUMMARY_PLACEHOLDERS)
// with a blanket "Summary-QAFs enthalten keine Prozessdaten"/"kein
// INPUT-Blatt mit Master-Sätzen" text — factually wrong for the ~346-file
// LEGACY/V9 majority once Hebel A (manufacturing-field-candidates.ts) and
// Hebel B (summary-field-candidates.ts) actually extract this data. This
// module computes, from already-available per-comparison data, whether each
// section has anything real to show and — per §23 — a PRECISE,
// evidence-based reason when it does not ("no source field was identified
// after scanning all relevant sheets", not a per-QAF-TYPE blanket claim).
//
// Pure, no I/O, no React — exercised directly by __tests__/production-view.test.ts;
// qaf-comparison-detail.tsx only renders what these functions return.

import type { QAFRow } from '../../../qaf-parser'
import type { FieldCandidate, FieldConflictStatus } from './types'
import { resolveAllFieldCandidates } from './field-candidates'
import { HIGHER_IS_BETTER_FIELDS } from '../../../qaf/comparison'

// ── §6 Produktionssicht / Raten (Hebel A) ──────────────────────────────────

export interface ProductionStepRates {
  zykluszeit: number | null
  teileProZyklus: number | null
  anzahlMA: number | null
  lohnkosten: number | null
  mss: number | null
  ausschuss: number | null
}

export interface ProductionRateRow {
  /** position_number when present, else the process name, else a synthetic
   * row key — never blank (Master-Prompt §17: every row must stay
   * addressable). */
  key: string
  label: string
  alt: ProductionStepRates | null
  neu: ProductionStepRates | null
}

export interface StepRowLike {
  file_id: string
  position_number: string | null
  process_name: string | null
  /** Optional (older rows may lack raw_values, same tolerance
   * qaf-comparison-detail.tsx's own StepRow prop type already carries). */
  raw_values?: QAFRow | null
}

function ratesFromRow(row: QAFRow | null | undefined): ProductionStepRates | null {
  if (!row) return null
  const { zykluszeit, teileProZyklus, anzahlMA, lohnkosten, mss, ausschuss } = row
  if (zykluszeit === null && teileProZyklus === null && anzahlMA === null && lohnkosten === null && mss === null && ausschuss === null) {
    return null
  }
  return { zykluszeit, teileProZyklus, anzahlMA, lohnkosten, mss, ausschuss }
}

/**
 * Build one row per DISTINCT manufacturing-step key across BOTH files'
 * already-loaded `qaf_manufacturing_step` rows (the same `steps` prop
 * qaf-comparison-detail.tsx already receives — no new query). Keys on
 * `position_number` (the row-identity field used everywhere else in this
 * codebase, qaf-parser.ts's own OVERRIDABLE_MANUFACTURING_FIELD_KEYS doc) so
 * an ALT/NEU pair with the same position number lands on the same row even
 * without a resolved step-match (stepMatches is a SEPARATE, confidence-scored
 * matching result this simple rates view does not need — showing raw
 * captured values, not a matched delta).
 */
export function buildProductionRateRows(
  steps: readonly StepRowLike[],
  baselineFileId: string | null,
  comparisonFileId: string | null,
): ProductionRateRow[] {
  const rows = new Map<string, ProductionRateRow>()
  for (const step of steps) {
    const side: 'alt' | 'neu' | null = step.file_id === baselineFileId ? 'alt' : step.file_id === comparisonFileId ? 'neu' : null
    if (!side) continue
    const rates = ratesFromRow(step.raw_values)
    if (!rates) continue
    const key = step.position_number?.trim() || step.process_name?.trim() || `row-${step.file_id}-${rows.size}`
    const label = step.process_name?.trim() || key
    const existing = rows.get(key) ?? { key, label, alt: null, neu: null }
    existing[side] = rates
    rows.set(key, existing)
  }
  return [...rows.values()].sort((a, b) => a.key.localeCompare(b.key, 'de'))
}

export interface SectionAvailability {
  available: boolean
  /** §23: precise, evidence-based — never the old blanket "Summary-QAFs
   * enthalten keine Prozessdaten" claim. */
  reason: string
}

/** KAR-966 (Kais' UI feedback, item 3): diff-highlight classification for
 * one Produktionssicht ALT/NEU field pair — reuses the SAME favourability
 * semantics qaf-comparison-detail.tsx's `costDeltaColor`/`diffStatusClass`
 * already establish elsewhere on this page (an increase is unfavorable/red
 * unless the field is in `HIGHER_IS_BETTER_FIELDS`, a decrease favorable/
 * green, no change neutral) — no new colour vocabulary invented for this
 * section. Pure/testable here; qaf-comparison-detail.tsx only maps the
 * returned kind to the existing `bg-status-*`/`text-*` tokens and an
 * accessible ▲/▼ marker (colour is never the only signal, per task
 * instruction — WCAG use-of-color).
 *
 * `field` is typed as `keyof ProductionStepRates` (PR #330 review fix — was
 * a plain `string`, so a typo in a call site's field name would silently
 * fall through `HIGHER_IS_BETTER_FIELDS.has(field)` as `false` — i.e. "higher
 * is worse" — with no compiler check against the actual field set). Any
 * future field addition (e.g. `teileProZyklus`, already in
 * HIGHER_IS_BETTER_FIELDS) is classified correctly without touching this
 * function, and still gets compile-time checking at the call site.
 */
export type ProductionFieldDeltaKind = 'unfavorable' | 'favorable' | 'unchanged' | 'unknown'

export function productionFieldDelta(
  altVal: number | null,
  neuVal: number | null,
  field: keyof ProductionStepRates,
): ProductionFieldDeltaKind {
  if (altVal === null || neuVal === null) return 'unknown'
  if (altVal === neuVal) return 'unchanged'
  const increased = neuVal > altVal
  const higherIsBetter = HIGHER_IS_BETTER_FIELDS.has(field)
  const unfavorable = increased !== higherIsBetter
  return unfavorable ? 'unfavorable' : 'favorable'
}

/** §22 Partial Module Population: available whenever EITHER side contributed
 * at least one row with rates — a one-sided upload (e.g. only NEU parsed so
 * far) still renders what it has instead of hiding the whole section. */
export function productionViewAvailability(rows: readonly ProductionRateRow[]): SectionAvailability {
  const altCount = rows.filter((r) => r.alt).length
  const neuCount = rows.filter((r) => r.neu).length
  if (altCount === 0 && neuCount === 0) {
    return {
      available: false,
      reason:
        'Kein Fertigungskosten-Blatt mit auswertbaren Prozesszeilen in ALT oder NEU gefunden — die Produktionssicht bleibt leer, bis eine Datei mit Fertigungskosten-Sheet vorliegt.',
    }
  }
  return {
    available: true,
    reason: `Prozess-Parameter aus dem Fertigungskosten-Blatt verfügbar: ${altCount} ALT- und ${neuCount} NEU-Fertigungsschritt(e) mit Zykluszeit/MSS/Personal/Lohnsatz/Ausschuss.`,
  }
}

// ── §7 Anomalien & Treiber (Master-Sätze) (Hebel B) ────────────────────────

const MASTER_RATE_ROWS: readonly { key: string; fieldId: string; label: string }[] = [
  { key: 'laborRate', fieldId: 'cap_sum_master_labor_rate', label: 'Lohnsatz' },
  { key: 'sgaRate', fieldId: 'cap_sum_master_sga_rate', label: 'SG&A-Satz' },
  { key: 'profitRate', fieldId: 'cap_sum_master_profit_rate', label: 'Gewinnsatz' },
  { key: 'scrapRate', fieldId: 'cap_sum_master_scrap_rate', label: 'Scrap-Satz' },
]

export interface MasterRateSideValue {
  value: number
  status: FieldConflictStatus
  cell: string | null
}

export interface MasterRateRow {
  key: string
  label: string
  fieldId: string
  alt: MasterRateSideValue | null
  neu: MasterRateSideValue | null
}

function resolvedSideValue(candidates: readonly FieldCandidate<number>[], fieldId: string): MasterRateSideValue | null {
  const own = candidates.filter((c) => c.fieldId === fieldId)
  if (own.length === 0) return null
  const [resolution] = resolveAllFieldCandidates(own)
  if (!resolution.selected) return null
  return { value: resolution.selected.value, status: resolution.status, cell: resolution.selected.source.cell ?? null }
}

/** Build the 4 master-rate rows from both sides' already-persisted
 * (g60_meta.field_candidates.summary) candidate lists — resolves EACH SIDE
 * independently (an ALT-side label collision must not contaminate NEU's
 * resolution, and vice versa). */
export function buildMasterRateRows(
  altCandidates: readonly FieldCandidate<number>[],
  neuCandidates: readonly FieldCandidate<number>[],
): MasterRateRow[] {
  return MASTER_RATE_ROWS.map(({ key, fieldId, label }) => ({
    key,
    label,
    fieldId,
    alt: resolvedSideValue(altCandidates, fieldId),
    neu: resolvedSideValue(neuCandidates, fieldId),
  }))
}

export function anomaliesAvailability(rows: readonly MasterRateRow[]): SectionAvailability {
  const populated = rows.filter((r) => r.alt || r.neu)
  if (populated.length === 0) {
    return {
      available: false,
      reason:
        'Keine Master-Sätze (Lohnsatz, SG&A, Gewinn, Scrap) im Zusammenfassungs-Blatt gefunden — nach allen bekannten DE/EN-Label-Varianten gescannt, kein eindeutiger Treffer.',
    }
  }
  const inconsistent = rows.filter((r) => r.alt?.status === 'INCONSISTENT' || r.neu?.status === 'INCONSISTENT')
  const foundLabels = populated.map((r) => r.label).join(', ')
  const inconsistentNote =
    inconsistent.length > 0
      ? ` ${inconsistent.length} Satz/Sätze mit widersprüchlichen Quellzellen im Blatt (${inconsistent.map((r) => r.label).join(', ')}) — geprüft, nicht geraten.`
      : ''
  return {
    available: true,
    reason: `Master-Sätze im Zusammenfassungs-Blatt gefunden: ${foundLabels}.${inconsistentNote}`,
  }
}

// ── §11 Szenario-Editor (Summary-Vergleiche) (KAR-966 item 2) ──────────────
//
// TECHNICAL DECISION (documented per task instruction — see KAR-966 report
// for the full write-up): the G60 scenario-recompute engine (scenario.ts —
// `buildScenarioModel`/`recomputeScenario`, fed by `G60Rates` +
// `G60ComponentRow[]` per Kostenreiter) is NOT activatable for Summary
// comparisons with reasonable effort, because it structurally needs data
// Hebel A/B's label-based Summary extraction does not — and cannot,
// short of a second parser — produce:
//
//   1. Per-row SUMIF/aggregation columns from the FIXED G60 workbook layout:
//      Losgröße (BA), Jahresvolumen-/Fertigungs-SUMIF-Flags (AZ/AY/L),
//      Sourcing-Flag directed/sourced (E), Materialschrott-Rate (X),
//      Rüst-/Logistikanteile (DB/AD), and the residual anchor (rem_pu —
//      the constant recomputeTab holds fixed while scenario overrides move
//      cycle/labour/machine/scrap). A Fertigungskosten-Blatt row
//      (qaf_manufacturing_step.raw_values, a QAFRow) carries only the six
//      process-parameter scalars (zykluszeit/teileProZyklus/anzahlMA/
//      lohnkosten/mss/ausschuss) — none of the above.
//   2. A per-Kostenreiter CACHED, exact Angebotspreis to delta-anchor
//      overrides against (G60TabAggregate, row 41 of the G60 sheet).
//      recomputeScenario's whole "no overrides → reproduces the persisted
//      number exactly" guarantee depends on that cache; Summary comparisons
//      have no equivalent per-step total (the SAME gap
//      calculatorParamsFromSummaryStep already documents by always setting
//      `residualPerUnit: 0` — "no exact-tab anchor concept" — one layer up,
//      in the simpler Preis-Kalkulator).
//
// Building an analogous scenario engine for the Summary shape would mean
// inventing values for BA/AZ/AY/L/E/X/DB/AD/rem_pu that are not in the
// source data (a Master-Prompt §23 violation) or writing and validating a
// SECOND recompute engine against a non-column-fixed, label-resolved sheet
// layout — a new engine, not a UI fix, out of scope for this task.
//
// The pre-KAR-966 hint text ("Der Szenario-Editor braucht die Prozesszeilen
// der G60-Detail-QAFs") was therefore not wrong about the CONCLUSION but
// framed it as a QAF-TYPE fact ("G60-Detail-QAFs") — exactly the blanket-
// claim pattern §23 forbids elsewhere (see this module's own header comment
// re: the old prod/anomalies placeholders). This function replaces that
// static string with a real, evidence-based SectionAvailability call, same
// principle as productionViewAvailability/anomaliesAvailability above — but,
// unlike those two, the result is a STRUCTURAL constant for THIS component
// (qaf-comparison-detail.tsx only ever renders for non-'g60' comparison
// modes, app/qaf-differences/[id]/page.tsx), not a per-instance capability
// gap: no Summary comparison can ever satisfy the engine's inputs, so
// `available` is always `false` here. `processRowCount` (the same
// productionRows count the caller already computed) only makes the reason
// text precise/evidence-based per §23 — it does not change the outcome.
export function scenarioAvailability(processRowCount: number): SectionAvailability {
  return {
    available: false,
    reason:
      processRowCount > 0
        ? `${processRowCount} Fertigungsschritt(e) mit Prozess-Parametern aus dem Fertigungskosten-Blatt vorhanden, aber ohne die G60-Rohformat-Spalten (Losgröße, SUMIF-/Sourcing-Flags, Materialschrott-Rate, Rüst-/Logistikanteile) und den je-Kostenreiter gecachten Angebotspreis, die der Szenario-Editor zum Delta-Anchoring braucht — die liefert nur ein G60-Detail-QAF. Für dieses Format rechnet die Hochrechnung (Sektion 12) live mit den erfassten Werten.`
        : 'Keine Fertigungsschritte mit Prozess-Parametern gefunden — und selbst mit welchen fehlen die G60-Rohformat-Spalten (Losgröße, SUMIF-/Sourcing-Flags, Materialschrott-Rate, Rüst-/Logistikanteile) sowie der je-Kostenreiter gecachte Angebotspreis, die der Szenario-Editor zum Delta-Anchoring braucht — die liefert nur ein G60-Detail-QAF. Nutze die Hochrechnung (Sektion 12).',
  }
}
