// SemanticSheetResolver (KAR-959/P2, Master-Prompt §15).
//
// Classifies every worksheet in a workbook into a SheetRole, so the rest of
// the capability-Kern (capability-detector.ts) never has to re-derive "which
// sheet carries MATERIAL data" from a raw sheet-name string itself.
//
// Reuses the EXISTING module-sheet-names.ts DE/EN alias registry
// (KAR-905/P3.1, matchesModuleSheetName) as the PRIMARY signal — task
// instruction explicitly calls for building "über die BESTEHENDE
// module-sheet-names.ts-Registry" rather than a parallel one. That registry
// is untouched by this PR: it drives the PRODUCTION parsers'
// (material-parser.ts, sbm-parser.ts, ...) own sheet routing, and changing
// it would be a real behavior change to already-shipped extraction — outside
// this PR's "capability-Kern foundation module, minimal integration" scope
// (see PR body). Instead, corpus-observed sheet names it does NOT cover
// (Master-Prompt §15's own examples: "Fertigungskosten"/"Manufactering
// costs" [sic]/"Manufacturing Cost" are ALREADY covered by
// module-sheet-names.ts MANUFACTURING; "LV Detail EU" and "Production Cost"
// are NOT) are recognized through a second, purely additive,
// capability-layer-local alias table (EXTRA_ROLE_ALIASES below) that never
// touches production parser routing.
//
// ASSUMPTION (documented per Master-Prompt §15 "ASSUMPTIONS explizit"):
// classification here uses NAME-SIGNAL evidence only (sheet tab name
// substring match) — not the full multi-signal evidence list §15 asks for
// ("Labels. Data types. Formula structure. References from other sheets.
// Variant structures. Cost structures. Units. Currency fields. Position
// inside the workbook. Hidden status. Summary dependencies."). Widening to
// multi-signal evidence is explicit future work (P3+, once
// FormulaLineageAnalyzer/DataQualityAnalyzer exist per Master-Prompt §31) —
// flagged honestly in every SheetRoleAssignment's `confidence`, which never
// exceeds NAME_SIGNAL_MAX_CONFIDENCE for that reason.
//
// A second ASSUMPTION: MATERIAL and RMR aliases genuinely collide on the
// substring "material" (module-sheet-names.ts RMR alias "raw material
// risk"/"rohstoffrisiken" both contain "material"; material-parser.ts's own
// isMaterialSheetName resolves this with an inline `if
// (n.includes('raw material')) return false` guard). This resolver
// generalizes that same precedent into a fixed MODULE_PRIORITY check order
// (RMR before MATERIAL) rather than re-deriving parser-specific exclusion
// logic — first alias match in priority order wins, one role per sheet.

import type { QafSheetModule } from '../module-sheet-names'
import { matchesModuleSheetName } from '../module-sheet-names'
import type { SheetRole, SheetRoleAssignment } from './types'

/** Sheet-name signal confidence ceiling for this P2, name-only classifier
 * (see module header ASSUMPTION). */
export const NAME_SIGNAL_MAX_CONFIDENCE = 0.9

const MODULE_TO_ROLE: Record<QafSheetModule, SheetRole> = {
  SUMMARY: 'summary',
  MANUFACTURING: 'manufacturing',
  MATERIAL: 'material',
  SBM: 'sbm',
  LOGISTICS: 'logistics',
  RMR: 'rmr',
  LC_CN: 'lccn',
  CO2E: 'co2e',
}

/** Fixed check order for module-sheet-names.ts's registry — RMR before
 * MATERIAL resolves the "raw material risk"/"material" substring collision
 * (see module header). The remaining order is otherwise arbitrary (no other
 * pair of module-sheet-names.ts alias lists collides today) but kept
 * explicit/stable rather than relying on object key iteration order. */
const MODULE_PRIORITY: readonly QafSheetModule[] = ['RMR', 'SBM', 'LOGISTICS', 'LC_CN', 'CO2E', 'SUMMARY', 'MANUFACTURING', 'MATERIAL']

interface ExtraRoleAlias {
  /** Substring matched case-insensitively against the worksheet tab name. */
  name: string
  role: SheetRole
  confidence: number
  /** Short evidence pointer for SheetRoleAssignment.signal. */
  evidence: string
}

/**
 * Capability-layer-local additions beyond module-sheet-names.ts — every
 * entry here is corpus-analysis evidence cited in the KAR-959 task
 * instruction / Master-Prompt §15 / gate-audit.md, deliberately kept OUT of
 * module-sheet-names.ts (see module header). Checked in array order, after
 * every MODULE_PRIORITY module-sheet-names.ts check has failed — a sheet
 * that already matched a module alias never reaches this table.
 */
const EXTRA_ROLE_ALIASES: readonly ExtraRoleAlias[] = [
  // Master-Prompt §15 example 2: "LV Detail EU may be MANUFACTURING_COST." —
  // capability-matrix.md's real-corpus finding for the BMW G70/I20 family // allow-customer-string
  // (Umlageschema-Kalkulation, "Lohn-/Verrechnungs-Detail" style sheet name),
  // not covered by module-sheet-names.ts MANUFACTURING today.
  { name: 'lv detail', role: 'manufacturing', confidence: 0.75, evidence: 'corpus-alias:lv-detail' },
  // Master-Prompt §18: "Support structures such as ... Production Cost ...
  // Labor Value Detail" (both listed alongside Fertigungskosten/LV Detail
  // EU/Manufacturing Cost as MANUFACTURING-equivalent structures).
  { name: 'production cost', role: 'manufacturing', confidence: 0.75, evidence: 'corpus-alias:production-cost' },
  { name: 'labor value detail', role: 'manufacturing', confidence: 0.75, evidence: 'corpus-alias:labor-value-detail' },
  { name: 'labour value detail', role: 'manufacturing', confidence: 0.75, evidence: 'corpus-alias:labour-value-detail' },
  // Master-Prompt §15 example 8: "Rüstkosten EU may be SETUP_COST." — no // allow-customer-string
  // dedicated SETUP_COST module/parser exists yet (gate-audit.md notes SBM's
  // own tool/device costs cover a different scope) — `setup` is carried as
  // its own SheetRole per the task instruction's own role list, ready for a
  // future dedicated extractor.
  { name: 'rüstkosten', role: 'setup', confidence: 0.8, evidence: 'corpus-alias:ruestkosten' },
  { name: 'ruestkosten', role: 'setup', confidence: 0.8, evidence: 'corpus-alias:ruestkosten-ascii' },
  { name: 'setup cost', role: 'setup', confidence: 0.8, evidence: 'corpus-alias:setup-cost' },
  // G60's INPUT rate-card sheet (internal/g60/parser.ts detectG60/
  // detailTabs) — structurally distinct from the 8 standard-QAF modules but
  // named in the task's own role list; recognized here so a capability
  // matrix computed on a G60-shaped workbook still surfaces the sheet
  // instead of collapsing it to `unknown`.
  { name: 'input', role: 'input', confidence: 0.7, evidence: 'corpus-alias:g60-input' },
  // ── KAR-962/P5 §27 label-synonym-discovery (DuckDB query over the 227-file
  // dev split, profiles/corpus.duckdb `sheets` table joined against
  // splits/dev.json — holdout/validation untouched) ──────────────────────
  //
  // 'prämissenblatt'/'assumptions sheet' — 172+45 dev-split workbooks carry
  // one of these tab names (plus 166 more carrying the RMR-specific
  // "Prämissenblatt_Rohstoff" DE variant, matched by the SAME substring check
  // below — kept as one shared `premise` role rather than splitting into a
  // second RMR-premise role: both are "workbook-wide assumptions",
  // Master-Prompt §27 point 25 "unknown but recurring structures" discipline
  // says do not over-fragment a role without a concrete second consumer).
  // This is the single biggest previously-`unknown` sheet-role gap this scan
  // found — see premise-field-candidates.ts for the P3-follow-up consumer
  // (Master-Sätze Lohnsatz/SGA/Gewinn/Scrap, task instruction: "prüfe das
  // Prämissenblatt als Quelle"). "Prämissenblatt_Rohstoff" reaches this alias
  // safely (contains neither "material" nor "raw material risk"/
  // "rohstoffrisiken", so MODULE_PRIORITY's earlier MATERIAL/RMR checks don't
  // intercept it) — its EN counterpart "Assumptions sheet raw material" (43
  // dev-split hits) does NOT: it contains the substring "material", so
  // MODULE_PRIORITY's MATERIAL check (checked before EXTRA_ROLE_ALIASES)
  // classifies it `material` instead — a documented, accepted imprecision
  // (see sheet-resolver.test.ts), not reordering MODULE_PRIORITY (that risks
  // new collisions for every OTHER alias, outside this PR's evidence scope).
  { name: 'prämissenblatt', role: 'premise', confidence: 0.8, evidence: 'corpus-alias:praemissenblatt' },
  { name: 'assumptions sheet', role: 'premise', confidence: 0.8, evidence: 'corpus-alias:assumptions-sheet' },
  // 'lohnsatz' as its OWN tab (not the "Fertigungskosten"-embedded per-row
  // labor cost, and distinct from "Prämissenblatt" — capability-matrix.md
  // itself names a dedicated "Lohnsatz"-Tab as one of the two real-corpus
  // locations for the Master-Sätze) — 3 dev-split hits, low-frequency but a
  // direct, unambiguous tab-name match for the same master-rate concept
  // `premise` already covers, so folded into the same role rather than a
  // fourth ad hoc one.
  { name: 'lohnsatz', role: 'premise', confidence: 0.75, evidence: 'corpus-alias:lohnsatz-tab' },
] as const

/** Sheet-name substring-collision guards for EXTRA_ROLE_ALIASES, same
 * discipline as MODULE_PRIORITY above — 'input' is a common enough substring
 * (e.g. a hypothetical "Cost Input Detail" sheet) that it is checked LAST
 * among the extra aliases, after the more specific manufacturing/setup
 * aliases have had a chance to match first. */

/**
 * Classify one worksheet tab name into a SheetRole with confidence +
 * evidence. Never returns a role without a non-empty `reasoning` string
 * (Master-Prompt §15: "Supporting evidence") — `unknown` included.
 */
export function resolveSheetRole(sheetName: string): SheetRoleAssignment {
  // Named `sheetModule`, not `module` — Next.js's no-assign-module-variable
  // rule flags any variable literally named `module` (CommonJS global shadow
  // risk), even a `for...of` loop binding.
  for (const sheetModule of MODULE_PRIORITY) {
    if (matchesModuleSheetName(sheetName, sheetModule)) {
      return {
        sheetName,
        role: MODULE_TO_ROLE[sheetModule],
        confidence: NAME_SIGNAL_MAX_CONFIDENCE,
        signal: `module-sheet-name-registry:${sheetModule}`,
        reasoning: `Worksheet tab name "${sheetName}" matches the module-sheet-names.ts DE/EN alias registry for ${sheetModule}.`,
      }
    }
  }
  for (const alias of EXTRA_ROLE_ALIASES) {
    if (sheetName.toLowerCase().includes(alias.name)) {
      return {
        sheetName,
        role: alias.role,
        confidence: alias.confidence,
        signal: alias.evidence,
        reasoning: `Worksheet tab name "${sheetName}" matches the capability-layer corpus alias "${alias.name}" (${alias.evidence}).`,
      }
    }
  }
  return {
    sheetName,
    role: 'unknown',
    confidence: 0,
    signal: 'no-match',
    reasoning: `Worksheet tab name "${sheetName}" matched no known module-sheet-names.ts alias and no capability-layer corpus alias — Master-Prompt §15: "No sheet may disappear silently", retained as unknown rather than omitted.`,
  }
}

/**
 * Classify every sheet in a workbook (Master-Prompt §15: "Classify every
 * sheet." — no sheet is ever dropped from the result, `unknown` included).
 * Accepts the same minimal `{name: string}[]` shape
 * workbook-adapter.ts's WorkbookSheetSummary already provides.
 */
export function resolveSheetRoles(sheets: ReadonlyArray<{ name: string }>): SheetRoleAssignment[] {
  return sheets.map((s) => resolveSheetRole(s.name))
}

/**
 * True when `name` resolves to the `premise` role. A standalone export
 * (rather than making every caller resolve the full SheetRole and compare)
 * so workbook-adapter.ts's parseSummarySheetFromWorkbook (KAR-962/P5 §21
 * premise-field-candidates.ts wiring — see that module header) can locate
 * every premise-role worksheet in a workbook WITHOUT re-deriving this
 * module's own alias substrings a second time. A workbook may carry more than
 * one premise sheet (e.g. "Prämissenblatt" + "Prämissenblatt_Rohstoff") —
 * callers that need "all of them" should filter `wb.worksheets` with this
 * predicate directly rather than going through sheetForRole (which only
 * returns the single BEST match).
 *
 * PR #329 review fix (finding [4]): delegates to `resolveSheetRole` itself
 * instead of re-implementing a bare substring check against
 * EXTRA_ROLE_ALIASES's `premise` entries. The bare check diverged from
 * resolveSheetRole's own MODULE_PRIORITY collision guard — e.g. "Assumptions
 * sheet raw material" resolves to role `material` via resolveSheetRole
 * (MODULE_PRIORITY's MATERIAL check runs before EXTRA_ROLE_ALIASES, a
 * documented, accepted imprecision — see module header ASSUMPTION 2 /
 * sheet-resolver.test.ts), but the old bare substring check ALSO matched it
 * against the `premise` alias `"assumptions sheet"`, so it was independently
 * (and wrongly) treated as a premise sheet too. Delegating guarantees this
 * function can never disagree with resolveSheetRole for the same sheet name
 * by construction, rather than by keeping two independent checks in sync by
 * hand.
 */
export function isPremiseSheetName(name: string): boolean {
  return resolveSheetRole(name).role === 'premise'
}

/**
 * The best (highest-confidence, then first-listed) sheet assigned a given
 * role, or undefined when no sheet carries that role. `role: 'unknown'` is
 * never looked up this way — callers that need the unknown set read
 * `assignments` directly (unknown sheets are individually meaningful, not a
 * single "the" unknown sheet).
 */
export function sheetForRole(assignments: ReadonlyArray<SheetRoleAssignment>, role: Exclude<SheetRole, 'unknown'>): SheetRoleAssignment | undefined {
  return assignments
    .filter((a) => a.role === role)
    .sort((a, b) => b.confidence - a.confidence)[0]
}

/**
 * ASSUMPTIONS this resolver's output rests on (Master-Prompt §15:
 * "ASSUMPTIONS explizit") — surfaced as plain strings so
 * capability-detector.ts can fold them into WorkbookCapabilityMatrix.assumptions
 * without every caller having to re-read this module's header comment.
 */
export const SHEET_RESOLVER_ASSUMPTIONS: readonly string[] = [
  'Sheet-role classification uses worksheet-tab-NAME signal only (module-sheet-names.ts registry + a capability-layer-local corpus-alias extension) — not the full Master-Prompt §15 multi-signal evidence set (labels/data types/formulas/cross-sheet references/position/hidden-status). Confidence is capped accordingly and never implies a content-level verification.',
  'A sheet name matching both the RMR and MATERIAL alias lists (e.g. "Rohstoffrisiken"/"RAW MATERIAL RISK") is classified RMR, not MATERIAL — fixed MODULE_PRIORITY check order (sheet-resolver.ts), the same precedent material-parser.ts\'s own isMaterialSheetName already established for its own routing.',
]
