// Not-a-QAF detection + Unknown-Pattern collection (KAR-962/P5, Master-Prompt
// §30, task scope point 3).
//
// Problem this closes: capability-matrix.md's real-corpus analysis found
// that 8 of the 12 "unmatched standard" files in the full 374-file corpus are
// not QAF quote forms at all — they are DERIVED analysis/tracking workbooks
// (a price-comparison matrix, a should-cost checklist/calculator, a
// cost-trend tracker, a supplier-family reconciliation report) that happen to
// weakly match `standard_summary`'s classification today. Ingesting one of
// these produces a confusing, essentially EMPTY comparison (no SUMMARY/
// MANUFACTURING/MATERIAL/... module recognized anything) with no explanation
// of WHY — exactly the failure mode Master-Prompt §30 warns against ("An
// unknown workbook must not produce an empty result... Prefer
// UNKNOWN_RELEVANT over silent omission").
//
// Design (deliberately structural, not filename/hash-based — the task's own
// "beim Ingest sauber behandeln" wants this to generalize to a FUTURE
// not-yet-seen non-QAF upload, not just the 3 confirmed dev-split examples
// this PR's real-corpus scan found): a workbook is flagged `not_a_qaf`
// (SUSPECTED — this is evidence for a user-facing warning, never a hard
// reject/throw like foreign-form-detection.ts's WAF/LAF/LEK gate, because the
// signal is weaker: "recognizes nothing" could also mean "a genuinely new QAF
// family this PR's registries don't know about yet" — Master-Prompt §0: "a
// template/family label is a hint, never a blocking gate") when:
//   1. It is NOT a G60-Detail file (own fixed-coordinate pipeline, checked by
//      the caller BEFORE this function even runs — see actions.ts call site).
//   2. It does NOT match the Umlageschema-Kalkulation family
//      (cost-allocation-family.ts's own detector — a real, if structurally
//      unusual, QAF family that also recognizes none of the 8 standard
//      modules by sheet name, so it MUST be excluded here to avoid a false
//      positive).
//   3. Sheet-resolver.ts's resolveSheetRoles finds ZERO sheets carrying any
//      of the 8 canonical QAF-module roles (summary/manufacturing/material/
//      sbm/rmr/logistics/lccn/co2e) anywhere in the workbook — `setup`/
//      `input`/`premise`/`unknown` alone do not count (a genuine QAF's
//      Rüstkosten/INPUT/Prämissenblatt sheet can exist ALONGSIDE a
//      recognized module sheet; it never exists ALONE).
//
// Confidentiality discipline: no real filename/supplier/program name appears
// anywhere in this module or its tests — every example below is invented.
//
// tdd-guard:skip — pure detection + a thin PlausibilityIssue bridge (same
// category as template-fingerprint.ts's own persistence-bridge section) —
// covered by __tests__/not-a-qaf-detector.test.ts.

import { createHash } from 'node:crypto'
import { resolveSheetRoles } from './sheet-resolver'
import type { SheetRole, SheetRoleAssignment } from './types'
import type { PlausibilityIssue } from '../plausibility'

/** The 8 canonical QAF-module roles (Master-Prompt §31/capability/types.ts
 * CAPABILITY_MODULES, mapped onto their SheetRole — see field-registry.ts
 * moduleSheetRole) — presence of ANY ONE of these anywhere in a workbook is
 * enough to rule out `not_a_qaf` (a genuine QAF module was recognized). */
const CORE_QAF_SHEET_ROLES: readonly SheetRole[] = ['summary', 'manufacturing', 'material', 'sbm', 'rmr', 'logistics', 'lccn', 'co2e']

export interface NotAQafDetection {
  /** True when this workbook is SUSPECTED to not be a QAF at all — a
   * warning-strength finding, never a hard reject. */
  matched: boolean
  /** Every sheet name, for evidence/UnknownPatternRecord purposes — never
   * logged/committed as repo TEXT, only ever runtime DATA (see module
   * header). */
  sheetNames: readonly string[]
  reasonDe: string
  reasonEn: string
}

/**
 * Detect whether a workbook is likely NOT a QAF at all (Master-Prompt §30).
 * Pure — takes only the already-known sheet names plus the caller's own
 * G60/cost-allocation-family findings (never re-derives them, never re-parses
 * a workbook). `g60Detected`/`costAllocationFamilyMatched` are the two
 * existing, real QAF-family detectors this function must defer to.
 *
 * `opts.sheetRoles` (PR #329 review fix, finding [5]): OPTIONAL pre-computed
 * `resolveSheetRoles(sheetNames...)` result — pass the SAME
 * SheetRoleAssignment[] a caller's own computeWorkbookCapabilityMatrix /
 * buildCostAllocationCapabilityMatrix call already produced
 * (WorkbookCapabilityMatrix.sheetRoles) to avoid re-running the identical
 * sheet-name classification a second time on every ingest. Falls back to
 * computing it locally (the original, always-recompute behavior) when
 * omitted, so existing callers/tests are unaffected.
 */
export function detectNotAQaf(
  sheetNames: readonly string[],
  opts: { g60Detected: boolean; costAllocationFamilyMatched: boolean; sheetRoles?: readonly SheetRoleAssignment[] },
): NotAQafDetection {
  if (opts.g60Detected || opts.costAllocationFamilyMatched) {
    return {
      matched: false,
      sheetNames,
      reasonDe: 'Erkannte QAF-Familie (G60 oder Umlageschema-Kalkulation) — kein Nicht-QAF-Verdacht.',
      reasonEn: 'Recognized QAF family (G60 or cost-allocation-scheme) — no not-a-QAF suspicion.',
    }
  }

  const roles = opts.sheetRoles ?? resolveSheetRoles(sheetNames.map((name) => ({ name })))
  const coreHits = roles.filter((r) => CORE_QAF_SHEET_ROLES.includes(r.role))

  if (coreHits.length > 0) {
    return {
      matched: false,
      sheetNames,
      reasonDe: `${coreHits.length} Blatt/Blätter mit erkannter QAF-Modul-Rolle gefunden (${[...new Set(coreHits.map((r) => r.role))].join(', ')}) — kein Nicht-QAF-Verdacht.`,
      reasonEn: `${coreHits.length} sheet(s) with a recognized QAF module role found (${[...new Set(coreHits.map((r) => r.role))].join(', ')}) — no not-a-QAF suspicion.`,
    }
  }

  return {
    matched: true,
    sheetNames,
    reasonDe:
      'Keine der 8 QAF-Modul-Rollen (Zusammenfassung/Fertigungskosten/Material/SBM/RMR/Logistics/LC-CN/CO2e) auf irgendeinem Blatt erkannt — diese Datei sieht nicht wie ein QAF-Angebotsformular aus (z. B. Preismatrix, Checkliste oder Analyse-Workbook). Ein Vergleich wird trotzdem versucht, liefert aber voraussichtlich keine oder kaum Felder.',
    reasonEn:
      'None of the 8 QAF module roles (Summary/Manufacturing/Material/SBM/RMR/Logistics/LC-CN/CO2e) were recognized on any sheet — this file does not look like a QAF quote form (e.g. a price matrix, checklist, or analysis workbook). A comparison is still attempted, but is expected to yield few or no fields.',
  }
}

/** Bridges a NotAQafDetection into the existing PlausibilityIssue channel —
 * same "reuse the Section/Pill rendering, zero new UI code" pattern
 * template-fingerprint.ts's templateFingerprintToPlausibilityIssue already
 * established (see that function's own module-header comment). `matched:
 * false` (the overwhelming majority of files) never produces an issue. */
export function notAQafToPlausibilityIssue(detection: NotAQafDetection, side: 'ALT' | 'NEU', fileLabel: string): PlausibilityIssue | null {
  if (!detection.matched) return null
  return {
    type: 'not_a_qaf_suspected',
    severity: 'pruefen',
    step: `${side} · ${fileLabel}`,
    explanation: detection.reasonDe,
    explanationEn: detection.reasonEn,
  }
}

// ── UnknownPatternCollector (Master-Prompt §30 point 19: "Add the pattern to
// the review queue") ────────────────────────────────────────────────────────

export interface UnknownPatternRecord {
  /** SHA-256 (hex, first 16 chars — same truncation convention
   * profiles/corpus.duckdb's sha256_16 column already uses) of the sorted,
   * newline-joined sheet-name list — a stable, content-only structural
   * fingerprint. Deliberately NOT the file's own content hash (that would
   * require re-reading the whole file here) and NOT the filename
   * (confidentiality — see module header) — two unrelated files with the
   * same sheet-name SET collapse to the same fingerprint on purpose, so the
   * review queue naturally deduplicates recurring unknown structures instead
   * of growing one entry per upload. */
  structuralFingerprint: string
  sheetNames: readonly string[]
  reasonDe: string
  reasonEn: string
  /** ISO-8601, caller-supplied (this module has no I/O — Master-Prompt §11
   * "pure" discipline, same as every other capability/ module) — never
   * `new Date()` inside a pure function. */
  detectedAt: string
}

/** Sorted, newline-joined sheet-name hash — the structural fingerprint
 * UnknownPatternRecord keys on. Exported so a caller (or a future dedup pass
 * over the persisted g60_meta.notAQaf.pattern column) can recompute/compare
 * it without re-deriving the exact hashing convention. */
export function structuralPatternFingerprint(sheetNames: readonly string[]): string {
  return createHash('sha256').update([...sheetNames].sort().join('\n')).digest('hex').slice(0, 16)
}

/**
 * Minimal UnknownPatternCollector (task scope point 3: "persistierte Liste
 * unbekannter Strukturen für spätere Analyse"). Deliberately NOT a class/
 * stateful singleton — every other capability/ module is a pure function
 * over already-known data (Master-Prompt §11), and a collector that
 * accumulates cross-request in-process state would not survive a serverless
 * function instance boundary anyway. This is instead a pure record BUILDER;
 * the actual "persisted list" is app/qaf-differences/actions.ts's existing
 * g60_meta JSONB-bag persistence (same additive pattern as
 * costAllocationFamily/capability_matrix — see that call site's own
 * comment) — every qaf_file row with a `not_a_qaf` classification carries
 * its own UnknownPatternRecord in g60_meta.notAQaf.pattern, so "the review
 * queue" is a plain `SELECT ... WHERE g60_meta->'notAQaf'->>'matched' =
 * 'true'` scan across qaf_file, not a new table. `detectedAt` is threaded in
 * by the caller (actions.ts has the real ingest timestamp; this module must
 * stay pure/no-I/O per the rest of capability/, see module header).
 */
export function collectUnknownPattern(detection: NotAQafDetection, detectedAt: string): UnknownPatternRecord | null {
  if (!detection.matched) return null
  return {
    structuralFingerprint: structuralPatternFingerprint(detection.sheetNames),
    sheetNames: detection.sheetNames,
    reasonDe: detection.reasonDe,
    reasonEn: detection.reasonEn,
    detectedAt,
  }
}
