// G60 persistence mapper (KAR-840 G60 stage G2) — pure transformation of a
// parsed G60 workbook into rows matching migration #105 (qaf_g60_tab,
// qaf_input_card, qaf_file.g60_meta). The server action inserts them; keeping
// this pure means the whole persistence shape is unit-tested without a DB.

import { parseLocaleNumber } from '../normalizer'
import type { G60ComponentRow, G60ParseResult, G60Rates, G60Volumes } from './parser'
import type { G60GuardedParseResult, G60StructureFinding } from './structure-guard'
import type { FacetDegradation } from '../types'

export interface G60PersistenceContext {
  projectId: string
  fileId: string
}

export interface G60TabRow {
  project_id: string
  file_id: string
  tab_name: string
  tab_index: number
  part: string | null
  aggregate: unknown
  component_rows: unknown
}

export interface G60InputCardRow {
  project_id: string
  file_id: string
  code: string
  label: string
  value_numeric: number | null
  value_text: string | null
}

const OK_STRUCTURE_FINDING: G60StructureFinding = { ok: true, mismatches: [], confidence: 1 }

export interface G60FileMeta {
  rates: G60Rates
  volumes: G60Volumes
  /** KAR-888: INPUT rate-card label finding, persisted once at ingest time so
   * a later change to the guard's anchor list/thresholds cannot silently
   * reinterpret an already-reviewed file (same reproducibility reasoning as
   * ENGINE_VERSION.g60Parser). Defaults to "ok" for a plain (unguarded)
   * G60ParseResult, so pre-KAR-888 call sites keep working unchanged. */
  inputStructure: G60StructureFinding
  /** Tabs dropped entirely by the structure guard (hard header mismatch) —
   * absent from qaf_g60_tab; kept here as the audit trail for "why". */
  excludedTabs: Record<string, G60StructureFinding>
  /**
   * PR #328 review fix (finding [2]/[3]): structure-guard.ts's
   * `rateDegradation` — set whenever the INPUT rate-card is hard-broken
   * (every surviving tab's SGA/Profit withheld, see G60TabAggregate.SGA doc
   * comment), undefined for a plain (unguarded) G60ParseResult or an intact
   * rate-card, same default-safe contract as `inputStructure`/`excludedTabs`
   * above.
   */
  rateDegradation?: FacetDegradation
}

export interface G60Rowset {
  meta: G60FileMeta
  tabRows: G60TabRow[]
  cardRows: G60InputCardRow[]
}

function isGuardedParseResult(parsed: G60ParseResult | G60GuardedParseResult): parsed is G60GuardedParseResult {
  return 'inputStructure' in parsed
}

export function buildG60Rows(
  ctx: G60PersistenceContext,
  parsed: G60ParseResult | G60GuardedParseResult,
  fullTabs: Record<string, G60ComponentRow[]>,
): G60Rowset {
  const base = { project_id: ctx.projectId, file_id: ctx.fileId }
  const guarded = isGuardedParseResult(parsed)
  const tabStructure = guarded ? parsed.tabStructure : {}

  const tabRows: G60TabRow[] = Object.entries(parsed.tabs).map(([name, aggregate], index) => ({
    ...base,
    tab_name: name,
    tab_index: index,
    part: aggregate.part || null,
    // KAR-888: the per-tab structure finding rides along inside the existing
    // JSONB `aggregate` blob (no schema change) — ok findings are recorded
    // explicitly too, so a reader never has to guess "no field = unchecked"
    // vs. "field present = checked and fine".
    aggregate: { ...aggregate, structure: tabStructure[name] ?? OK_STRUCTURE_FINDING },
    component_rows: fullTabs[name] ?? [],
  }))

  const cardRows: G60InputCardRow[] = Object.entries(parsed.card).map(([code, entry]) => {
    const numeric = parseLocaleNumber(entry.value)
    return {
      ...base,
      code,
      label: entry.label,
      value_numeric: numeric,
      value_text: numeric === null ? (entry.value === null || entry.value === undefined ? null : String(entry.value)) : null,
    }
  })

  return {
    meta: {
      rates: parsed.rates,
      volumes: parsed.volumes,
      inputStructure: guarded ? parsed.inputStructure : OK_STRUCTURE_FINDING,
      excludedTabs: guarded ? parsed.excludedTabs : {},
      rateDegradation: guarded ? parsed.rateDegradation : undefined,
    },
    tabRows,
    cardRows,
  }
}
