// Vergleichs-scoped Autosave-Namespace (KAR-985) — additives JSONB-Bag auf
// qaf_comparison.user_inputs (Migration supabase-migration-qaf-comparison-
// user-inputs.sql), gleiches Muster wie qaf_file.g60_meta (siehe z. B.
// field-mapping-override.ts's Doc-Kommentar: "misc JSONB bag"): ein
// Top-Level-Namespace je Editor-Fläche, geladen/geschrieben als Ganzes,
// andere Namespaces bleiben beim Schreiben unangetastet.
//
// Zwei Editor-Flächen speisen dieses Bag:
//   - "12. Hochrechnung & Potenzial in €" (qaf-projection.tsx): Jahres-/
//     Volumen-Tabelle, Δ-Override, Abwehrquote.
//   - Preis-Kalkulator (qaf-price-calculator.tsx, KAR-847/848): die
//     manuellen UnitPriceParams-Felder.
//
// Pure + unit-tested; keine React-/Supabase-Abhängigkeiten hier — die
// Server Action (app/qaf-differences/qaf-comparison-user-inputs-actions.ts)
// und der Client-Hook (components/qaf-differences/qaf-autosave.tsx) bauen
// auf diesen Funktionen auf, ohne sie zu duplizieren.

import type { ProjectionYearInput } from './projection'
import type { UnitPriceParams } from './g60/calculator'

/** Section 12 "Hochrechnung & Potenzial in €" (qaf-projection.tsx) editable state. */
export interface QafProjectionUserInputs {
  years: ProjectionYearInput[]
  overrideDelta: number | null
  defendPct: number
}

/** Preis-Kalkulator (qaf-price-calculator.tsx) editable state — the fixed-mode
 * comparison's manual field values (KAR-848: 1:1 per comparison). */
export interface QafCalculatorUserInputs {
  params: UnitPriceParams
}

/** Whole additive JSONB bag persisted on qaf_comparison.user_inputs. Each
 * namespace key is owned by exactly one editor surface and replaced WHOLESALE
 * on save (that editor always sends its full current state) — see
 * mergeQafComparisonUserInputs for why a shallow top-level merge is enough. */
export interface QafComparisonUserInputs {
  v: 1
  projection?: QafProjectionUserInputs
  calculator?: QafCalculatorUserInputs
}

/** A save patches exactly one namespace at a time (the editor that changed) —
 * the server action's whole contract. Never both in one call from either
 * editor today, but the type does not forbid it (a future combined save
 * would still merge correctly). */
export type QafComparisonUserInputsPatch = Partial<Pick<QafComparisonUserInputs, 'projection' | 'calculator'>>

const MAX_PROJECTION_YEARS = 50

function isPlainObject(value: unknown): value is Record<string, unknown> {
  return typeof value === 'object' && value !== null && !Array.isArray(value)
}

function isFiniteNumber(value: unknown): value is number {
  return typeof value === 'number' && Number.isFinite(value)
}

const UNIT_PRICE_PARAM_KEYS: (keyof UnitPriceParams)[] = [
  'residualPerUnit',
  'cycleSec',
  'partsPerCycle',
  'employees',
  'labourRatePerHour',
  'inefficiency',
  'machineRatePerHour',
  'materialPerUnit',
  'scrapRate',
  'sgaFkRate',
  'sgaMatRate',
  'profitFkRate',
  'profitMatRate',
]

function validateProjectionUserInputs(value: unknown): string | null {
  if (!isPlainObject(value)) return 'invalid projection payload'
  if (!Array.isArray(value.years) || value.years.length > MAX_PROJECTION_YEARS) return 'invalid projection years'
  for (const row of value.years) {
    if (!isPlainObject(row) || !isFiniteNumber(row.year) || !isFiniteNumber(row.volume) || typeof row.included !== 'boolean') {
      return 'invalid projection year row'
    }
  }
  if (value.overrideDelta !== null && !isFiniteNumber(value.overrideDelta)) return 'invalid projection overrideDelta'
  if (!isFiniteNumber(value.defendPct) || value.defendPct < 0 || value.defendPct > 1) return 'invalid projection defendPct'
  return null
}

function validateCalculatorUserInputs(value: unknown): string | null {
  if (!isPlainObject(value) || !isPlainObject(value.params)) return 'invalid calculator payload'
  for (const key of UNIT_PRICE_PARAM_KEYS) {
    if (!isFiniteNumber(value.params[key])) return `invalid calculator param: ${key}`
  }
  return null
}

/** Validates a save patch before it touches the DB — same "small pure
 * function, unit-testable without a Supabase client" precedent as actions.ts's
 * validateFieldMappingOverrideInput. Rejects an empty patch (nothing to
 * save) and unknown top-level keys (only `projection`/`calculator` exist). */
export function validateQafComparisonUserInputsPatch(patch: unknown): string | null {
  if (!isPlainObject(patch)) return 'invalid patch'
  const keys = Object.keys(patch)
  if (keys.length === 0) return 'empty patch'
  if (keys.some((k) => k !== 'projection' && k !== 'calculator')) return 'invalid patch keys'
  if ('projection' in patch) {
    const err = validateProjectionUserInputs(patch.projection)
    if (err) return err
  }
  if ('calculator' in patch) {
    const err = validateCalculatorUserInputs(patch.calculator)
    if (err) return err
  }
  return null
}

/** Strips everything but the known fields from a VALIDATED patch (PR #349
 * review fix): validation checks the required shapes, but unknown EXTRA
 * properties inside the namespaces (and arbitrarily large payloads smuggled
 * in them) would otherwise be persisted verbatim into the JSONB bag. Only
 * schema-known fields ever reach the DB. */
export function normalizeQafComparisonUserInputsPatch(patch: QafComparisonUserInputsPatch): QafComparisonUserInputsPatch {
  const next: QafComparisonUserInputsPatch = {}
  if (patch.projection) {
    next.projection = {
      years: patch.projection.years.map((row) => ({ year: row.year, volume: row.volume, included: row.included })),
      overrideDelta: patch.projection.overrideDelta,
      defendPct: patch.projection.defendPct,
    }
  }
  if (patch.calculator) {
    const source = patch.calculator.params
    // Explicit literal instead of a keyed copy loop: the compiler now proves
    // completeness — a new UnitPriceParams field fails HERE instead of being
    // silently dropped from persisted saves.
    const params: UnitPriceParams = {
      residualPerUnit: source.residualPerUnit,
      cycleSec: source.cycleSec,
      partsPerCycle: source.partsPerCycle,
      employees: source.employees,
      labourRatePerHour: source.labourRatePerHour,
      inefficiency: source.inefficiency,
      machineRatePerHour: source.machineRatePerHour,
      materialPerUnit: source.materialPerUnit,
      scrapRate: source.scrapRate,
      sgaFkRate: source.sgaFkRate,
      sgaMatRate: source.sgaMatRate,
      profitFkRate: source.profitFkRate,
      profitMatRate: source.profitMatRate,
    }
    next.calculator = { params }
  }
  return next
}

function hasKnownShape(value: unknown): value is Partial<QafComparisonUserInputs> {
  return isPlainObject(value)
}

/** Read-modify-write merge (the server action applies this to the value it
 * just read) — same shallow-spread discipline as actions.ts's
 * `{ ...meta, fieldMappingOverrides: nextOverrides }` for qaf_file.g60_meta.
 * `current` may be `null`/`{}`/anything the column happens to hold (a
 * pre-migration read, or a row nobody has ever saved autosave data for) —
 * always returns a valid v1 bag, and a `projection` patch never touches an
 * already-saved `calculator` key (or vice versa). */
export function mergeQafComparisonUserInputs(current: unknown, patch: QafComparisonUserInputsPatch): QafComparisonUserInputs {
  const base = hasKnownShape(current) ? current : {}
  return { ...base, ...patch, v: 1 }
}

/** Extracts the projection namespace from a persisted (or absent/malformed)
 * bag — the load-side counterpart of the merge above. Anything that isn't a
 * plain object, or carries no `projection` key, reads back as `null` (never
 * fabricated) — exactly the "no saved inputs" state a pre-autosave or
 * pre-migration comparison is in. */
export function readQafProjectionUserInputs(bag: unknown): QafProjectionUserInputs | null {
  if (!hasKnownShape(bag)) return null
  return bag.projection ?? null
}

/** Calculator-namespace counterpart of readQafProjectionUserInputs above. */
export function readQafCalculatorUserInputs(bag: unknown): QafCalculatorUserInputs | null {
  if (!hasKnownShape(bag)) return null
  return bag.calculator ?? null
}
