// KAR-927 adversarial-review fix (F1/F2/F3, 12.07.2026) — shared BOUNDED,
// CHEAP header-region-presence scan used by material-parser.ts's
// parseMaterialSheet and sbm-parser.ts's parseSbmSheet to classify every
// OTHER name-matching worksheet (besides the one actually chosen/parsed).
//
// ── What this replaces (F1/F2) ──────────────────────────────────────────────
// The original KAR-927 implementation gated `ignoredCandidateSheets` on each
// candidate's OWN `coreFieldsFound` — computed by calling the module's full
// `parseXWorksheet` (worksheetToGrid over the WHOLE sheet, then a complete
// header-column mapping AND a full data-row extraction pass) on every
// name-matching candidate, discarding the result immediately after reading
// one boolean. Two adversarial-review findings on that approach:
//   F2 (perf/cost): `isSbmSheetName`'s alias list intentionally includes the
//     bare substring "sbm" (module-sheet-names.ts) — every real BMW template // allow-customer-string
//     bundles non-data "SBM_Matrix"/"SBM_Dropdown" reference/lookup tabs that // allow-customer-string
//     also match it (real-corpus finding, 12.07.2026). That means 2 EXTRA
//     full worksheet parses fired on nearly every real ingest for a signal
//     nobody asked for on the common path.
//   F1 (correctness): the ALL-OR-NOTHING `coreFieldsFound` gate (every one of
//     CORE_SBM_FIELD_KEYS/CORE_MATERIAL_FIELD_KEYS located) swallowed exactly
//     the genuinely-broken second sheets KAR-927 exists to surface — e.g. a
//     Multi-QAF file bundling a DE and an EN SBM-DEVICES-FWZ tab where the EN
//     copy's Positionsnummer header was renamed/corrupted: `coreFieldsFound`
//     came back false, so the whole candidate silently vanished from
//     `ignoredCandidateSheets` — invisible again, the exact failure mode
//     KAR-927 was supposed to fix.
//
// ── The fix ──────────────────────────────────────────────────────────────
// `ignoredCandidateSheets` is now unconditional (F1) — EVERY other
// name-matching worksheet is listed, tagged with a cheap `plausibleData`
// boolean instead of being filtered out. `plausibleData` is computed by
// scanning only the first `rowLimit` rows (the SAME window
// findMaterialHeaderRow/findSbmHeaderRow already use — HEADER_SCAN_MAX_ROWS
// in both callers, passed in here rather than hardcoded so the two callers
// cannot silently drift from their own header-row finder's window) x
// `colLimit` columns, read directly via `ws.getRow()`/`row.eachCell()` —
// NEVER `worksheetToGrid` (which walks ExcelJS's own row iterator over the
// ENTIRE sheet with no bound at all), no data-row extraction, no rule-engine
// run (F2). The check itself looks for the presence of ONE single strongest
// identity label per module (materialDesignation / toolFixtureType — both
// already members of that module's own CORE_*_FIELD_KEYS, reusing the exact
// same registry-driven `matchHeaderColumnSync` a real header-row scan uses,
// not a newly hardcoded label string) rather than requiring every core field
// — deliberately WEAKER than `coreFieldsFound`, which is what fixes F1: a
// sheet with a broken Positionsnummer column but an intact Werkzeug-/
// Vorrichtungsart or Materialbezeichnung column now still reports
// `plausibleData: true`.
//
// Deliberately generic over the identity check (`isIdentityLabel` callback)
// so this file never imports canonical-model.ts/canonical-fields.ts itself —
// each caller supplies its OWN already-loaded registry context, preserving
// the "heavy registry stays out of the static import graph" bundle
// discipline material-parser.ts/sbm-parser.ts's own module headers document
// (this file is transitively reachable from the same 'use client' barrel).

import type { Worksheet } from 'exceljs'
import { resolveCell } from './workbook-adapter'

/**
 * KAR-927 (Multi-QAF-Programm P0.2) candidate-sheet visibility entry.
 * Replaces the pre-fix `string[]` shape — every OTHER worksheet whose name
 * also matched the module's sheet-name predicate is now ALWAYS listed here
 * (never silently dropped, F1), tagged with whether the cheap bounded scan
 * (see module header) found this module's single strongest identity header
 * label somewhere in it.
 */
export interface IgnoredCandidateSheetEntry {
  name: string
  /**
   * True when the bounded header-region scan located the module's identity
   * label. False both when it genuinely was not found AND when the scan
   * itself threw on a pathological/parser-hostile sheet (F3 — a throwing
   * reference tab must never abort the whole ingest) — those two cases are
   * indistinguishable to a downstream consumer ON PURPOSE: both mean "do not
   * show a user-facing message for this sheet" (see
   * material/sbmParseMetaToPlausibilityIssue), while the sheet NAME itself
   * still always surfaces in the meta either way (F1).
   */
  plausibleData: boolean
}

/** Pre-redesign shape a caller might still read back from a persisted
 * `qaf_file.g60_meta` entry written before this fix shipped (preview/staging
 * environment, no DB migration for this JSONB field) — a bare sheet name
 * with no plausibility tag. */
export type LegacyOrIgnoredCandidateSheetEntry = string | IgnoredCandidateSheetEntry

/**
 * The shape material/sbmParseMetaToPlausibilityIssue (material-parser.ts/
 * sbm-parser.ts) and compare.ts's QafFileParsed.materialParseMeta/
 * sbmParseMeta accept — deliberately NOT `Pick<MaterialParseMeta,
 * 'ignoredCandidateSheets'>`/`Pick<SbmParseMeta, 'ignoredCandidateSheets'>`
 * (which would only type-accept the CURRENT `IgnoredCandidateSheetEntry[]`
 * shape): this type accepts the tolerant `LegacyOrIgnoredCandidateSheetEntry`
 * union so a rehydrated pre-redesign `string[]` value type-checks at every
 * call site, not only at runtime (normalizeIgnoredCandidateSheets handles
 * the runtime side of the same tolerance). */
export interface IgnoredCandidateSheetsMeta {
  ignoredCandidateSheets?: readonly LegacyOrIgnoredCandidateSheetEntry[]
}

/**
 * Normalize a persisted/live `ignoredCandidateSheets` array to the current
 * `IgnoredCandidateSheetEntry[]` shape, tolerating BOTH the current shape
 * and the pre-redesign `string[]` shape (migrationsfrei — no DB migration
 * for this JSONB field, per KAR-927 adversarial-review fix instruction). A
 * legacy bare-string entry only ever existed under the OLD strict
 * `coreFieldsFound` gate — i.e. it had ALREADY cleared every core field —
 * so it is mapped to `plausibleData: true` here, preserving that entry's
 * original (stronger) guarantee and keeping any pre-redesign persisted
 * `qaf_plausibility_issue` behavior byte-identical for a file re-compared
 * after this fix ships. `undefined` input -> `[]`.
 */
export function normalizeIgnoredCandidateSheets(
  raw: readonly LegacyOrIgnoredCandidateSheetEntry[] | undefined,
): IgnoredCandidateSheetEntry[] {
  if (!raw) return []
  return raw.map((entry) => (typeof entry === 'string' ? { name: entry, plausibleData: true } : entry))
}

/** Column bound for the cheap scan below — generous relative to the ~28-35
 * real MATERIAL/SBM field counts, but a fixed order-of-magnitude-smaller cap
 * than a pathologically wide reference/lookup tab could otherwise force the
 * scan to walk (F3). Shared (not duplicated per-caller) so both modules stay
 * in lockstep on this bound. */
export const CANDIDATE_SCAN_MAX_COLS = 80

function normalizeHeaderCellForScan(v: unknown): string {
  return String(v ?? '')
    .replace(/[\r\n]+/g, ' ')
    .replace(/\s+/g, ' ')
    .trim()
}

/**
 * Cheap, BOUNDED presence check for a single header-identity label within
 * the first `rowLimit` rows x `colLimit` columns of `ws`, read directly via
 * `ws.getRow()`/`row.eachCell()` — never `worksheetToGrid` (see module
 * header). Never throws (F3): any read error on a pathological/parser-
 * hostile worksheet resolves to `false`, exactly like "label not found" —
 * the caller's ingest continues unaffected either way.
 */
export function worksheetHasHeaderLabelInRegion(
  ws: Worksheet,
  rowLimit: number,
  colLimit: number,
  isIdentityLabel: (normalizedHeaderCell: string) => boolean,
): boolean {
  try {
    for (let r = 1; r <= rowLimit; r++) {
      const row = ws.getRow(r)
      let found = false
      let colsSeen = 0
      row.eachCell({ includeEmpty: false }, (cell) => {
        if (found || colsSeen >= colLimit) return
        colsSeen += 1
        const text = normalizeHeaderCellForScan(resolveCell(cell))
        if (text !== '' && isIdentityLabel(text)) found = true
      })
      if (found) return true
    }
    return false
  } catch {
    return false
  }
}
