// Canonical QAF field model + alias registry (KAR-892 / P1.1, Phase-1 foundation).
//
// Problem this closes (backlog 05-backlog-phasenplan.md [P1.1]): Kadi-v2 has
// two separate DE/EN dictionaries for Fertigungskosten (qaf-parser.ts
// HEADER_TO_KEY), one synonym system only for Summary-Metrics
// (summary-metrics.ts SYNONYMS/NORM_SYN), and no alias system at all for G60
// or the seven still-unparsed QAF-9.1 modules (MATERIAL, SBM-DEVICES-FWZ,
// LOGISTICS&CUSTOM, RAW MATERIAL RISKS, LC-CN, Carbon Footprint, WAF/LAF/LEK).
// This module is the language-independent, data-driven registry every future
// module (P1.6/P1.7/P2.x) registers into instead of inventing another
// ad-hoc dictionary — exactly the "Fundament" role the backlog item names.
//
// Scope discipline (backlog P1.1 + Master-Prompt §6, deliberately followed
// here — see canonical-fields.ts module header for the per-module coverage
// table):
//   - Additive only. HEADER_TO_KEY (qaf-parser.ts) and SYNONYMS/NORM_SYN
//     (summary-metrics.ts) are UNTOUCHED — this module ships a *parallel*
//     mapping (QAF_FIELD_KEY_TO_CANONICAL / SUMMARY_METRIC_KEY_TO_CANONICAL
//     in canonical-fields.ts) from the 22 existing QAFFieldKey values and the
//     19 existing SummaryMetricKey values onto stable canonical ids. Wiring
//     the parsers themselves onto this registry is P1.2 — explicitly out of
//     scope here (backlog: "Bestehende SYNONYMS und HEADER_TO_KEY-Wörterbücher
//     werden als Aliase in die neue Struktur migriert (kein Verhaltens-Change,
//     nur Refactor der Datenhaltung)" happens later, once this fundament is
//     reviewed).
//   - No DB persistence (backlog: "DB-Persistenz der Registry ... ist ein
//     separates, späteres Item", ties to P1.5 engine-config governance).
//     Versioned TS constant data, same pattern as summary-metrics.ts's
//     QAF_LEGACY_DE_SUMMARY/QAF_V9_SUMMARY template configs.
//   - Pure. No I/O, no Excel access — this module only classifies/looks up
//     field metadata, it never reads a workbook.
//
// tdd-guard:skip — type declarations only (same category as types.ts); the
// access functions below (findByAlias/byModule/byCanonicalId/validateRegistry)
// and the label normalizer are covered by __tests__/canonical-model.test.ts.

import { normalizeProcessName } from "./normalizer"
import type {
  CanonicalField,
  RegistryValidationResult,
  QafModule,
} from "./canonical-fields.types"
import { CANONICAL_FIELDS } from "./canonical-fields"

export type {
  CanonicalField,
  CanonicalFieldEvidence,
  CanonicalClassification,
  CanonicalDataType,
  CanonicalLevel,
  CanonicalRequirement,
  QafModule,
  RegistryValidationResult,
} from "./canonical-fields.types"

/** The eleven QAF-9.1 modules named in the Master-Prompt §6 / backlog P1.1. */
export const QAF_MODULES: readonly QafModule[] = [
  "SUMMARY",
  "MANUFACTURING",
  "MATERIAL",
  "SBM",
  "LOGISTICS",
  "RMR",
  "LC_CN",
  "CO2E",
  "WAF",
  "LAF",
  "LEK",
  "G60",
] as const

// ── Label normalization ────────────────────────────────────────────────────

/**
 * Normalize a field label for alias lookup: strip footnote asterisks and
 * leading numbering (as used throughout the Leitfaden tables, e.g.
 * "1. Materialkosten*", "6a. Rohstoff Preisanteil Material"), collapse
 * embedded newlines, then reuse normalizer.ts's `normalizeProcessName` for
 * umlaut-folding + lowercasing + whitespace collapsing — KAR-892 explicitly
 * calls for reusing that normalizer rather than reimplementing umlaut
 * folding here.
 *
 * Deliberately does NOT strip unit brackets ("[BW]", "[AW]", "[s]"): several
 * canonical fields share a label stem but differ only by unit/currency
 * context (e.g. "Fertigungskosten FK [BW]" vs. "Fertigungskosten FK [AW]",
 * already two distinct QAFFieldKey values — `fk` vs. `fkAW`). Stripping
 * brackets would silently merge those into one alias key.
 */
export function normalizeCanonicalLabel(raw: string): string {
  const stripped = raw
    .replace(/\*+/g, " ")
    .replace(/^\s*\d+[a-zA-Z]?[.)]?\s+/, "")
    .replace(/[\r\n]+/g, " ")
  return normalizeProcessName(stripped)
}

// ── Access API ──────────────────────────────────────────────────────────────

/** All fields belonging to one QAF module, in registry declaration order. */
export function byModule(
  module: QafModule,
  registry: readonly CanonicalField[] = CANONICAL_FIELDS
): CanonicalField[] {
  return registry.filter((f) => f.module === module)
}

/** Exact lookup by stable canonical id. */
export function byCanonicalId(
  id: string,
  registry: readonly CanonicalField[] = CANONICAL_FIELDS
): CanonicalField | undefined {
  return registry.find((f) => f.id === id)
}

/**
 * Find every canonical field whose DE label, EN label, or any known alias
 * (including documented BMW typos/variants) normalizes to the same key as // allow-customer-string
 * `label`. Can legitimately return more than one match — the registry allows
 * *documented* alias collisions (e.g. the real duplicate "Mengeneinheit"
 * label in MATERIAL, see canonical-fields.ts) rather than forbidding them;
 * callers that need disambiguation use `collisionGroup` plus their own
 * position/context signal (same approach as rule-engine.ts's
 * disambiguateLabels, R6).
 *
 * `lang` restricts which "primary" label is checked (`labelDe` xor
 * `labelEn`) — the `aliases` list itself is language-agnostic and always
 * checked regardless of `lang`, since aliases are not tagged by language in
 * this registry (pragmatic — see canonical-fields.ts header).
 */
export function findByAlias(
  label: string,
  lang?: "de" | "en",
  registry: readonly CanonicalField[] = CANONICAL_FIELDS
): CanonicalField[] {
  const key = normalizeCanonicalLabel(label)
  if (key === "") return []
  return registry.filter((f) => {
    const primary =
      lang === "de"
        ? [f.labelDe]
        : lang === "en"
          ? [f.labelEn]
          : [f.labelDe, f.labelEn]
    const candidates = [...primary, ...f.aliases]
    return candidates.some((c) => normalizeCanonicalLabel(c) === key)
  })
}

// ── Registry validation ──────────────────────────────────────────────────────

/**
 * Structural validation of the registry (KAR-892 acceptance criteria):
 *   1. no duplicate canonical ids;
 *   2. every entry carries at least one source-evidence reference;
 *   3. every mandatory field has both a DE and an EN label;
 *   4. alias collisions within the same module are only allowed when every
 *      colliding entry declares the SAME non-empty `collisionGroup` — an
 *      undeclared collision (two unrelated fields that happen to normalize
 *      to the same label) is a registry bug, a declared one (like the real
 *      "Mengeneinheit" duplicate) is expected data.
 *
 * Pure — takes an explicit registry so tests can validate synthetic
 * fixtures without touching the production data.
 */
export function validateRegistry(
  registry: readonly CanonicalField[] = CANONICAL_FIELDS
): RegistryValidationResult {
  const errors: string[] = []

  const seenIds = new Set<string>()
  for (const f of registry) {
    if (seenIds.has(f.id)) errors.push(`Duplicate canonical id: "${f.id}"`)
    seenIds.add(f.id)

    if (f.evidence.length === 0)
      errors.push(`Field "${f.id}" is missing evidence`)

    if (f.requirement === "mandatory") {
      if (f.labelDe.trim() === "")
        errors.push(`Mandatory field missing DE label: "${f.id}"`)
      if (f.labelEn.trim() === "")
        errors.push(`Mandatory field missing EN label: "${f.id}"`)
    }
  }

  errors.push(...findUnintendedAliasCollisions(registry))

  return { ok: errors.length === 0, errors }
}

function findUnintendedAliasCollisions(
  registry: readonly CanonicalField[]
): string[] {
  const byModuleAndKey = new Map<
    string,
    Array<{ id: string; collisionGroup?: string }>
  >()

  for (const f of registry) {
    const keys = new Set(
      [f.labelDe, f.labelEn, ...f.aliases]
        .map(normalizeCanonicalLabel)
        .filter((k) => k !== "")
    )
    for (const key of keys) {
      const mapKey = `${f.module}::${key}`
      const list = byModuleAndKey.get(mapKey) ?? []
      list.push({ id: f.id, collisionGroup: f.collisionGroup })
      byModuleAndKey.set(mapKey, list)
    }
  }

  const errors: string[] = []
  for (const [mapKey, entries] of byModuleAndKey) {
    if (entries.length < 2) continue
    const groups = new Set(entries.map((e) => e.collisionGroup))
    const allSameDeclaredGroup = groups.size === 1 && !groups.has(undefined)
    if (!allSameDeclaredGroup) {
      const ids = entries.map((e) => e.id).join(", ")
      errors.push(
        `Unintended alias collision in ${mapKey}: [${ids}] — mark a shared collisionGroup if this duplicate label is intentional (documented source-data duplicate), otherwise fix the alias.`
      )
    }
  }
  return errors
}
