// Namensnormalisierung für den Material-Abgleich (Spezifikation Kap. 8.3).
//
// Grundlage jedes Positions-Mappings: zwei Schreibweisen desselben Bauteils
// müssen auf dieselbe Form fallen, ohne dass dabei Bedeutung verloren geht.
// Die Reihenfolge der Schritte ist Teil der Spezifikation und nicht beliebig —
// die Mengenextraktion etwa muss nach dem Kollabieren der Leerzeichen laufen,
// sonst greift ihr Muster bei "2  x  Schraube" nicht.
//
// Bewusst NICHT enthalten: Ähnlichkeitsmaße. Diese Datei stellt Gleichheit her,
// wo sie besteht — sie beurteilt keine Nähe. Wer Kandidaten bewerten will, tut
// das in einem eigenen, nachvollziehbaren Schritt (Kap. 8.4).

/** Ergebnis der Normalisierung: kanonischer Name plus abgetrenntes Beiwerk. */
export interface NormalizedName {
  /** Kanonische Form für den Gleichheitsvergleich. */
  normalized: string
  /**
   * Mengenpräfix, sofern der Name eines trug ("3 x Schraube" → 3).
   * Abgetrennt, weil "3 x Schraube" und "Schraube" dieselbe Position mit
   * unterschiedlicher Menge sind — sie zu verschiedenen Positionen zu machen
   * wäre falsch, die Menge zu verlieren aber auch.
   */
  quantityPrefix: number | null
  /** Bedeutungstragende Tokens für spätere Kandidatenfilter. */
  tokens: string[]
}

/**
 * Typografische Varianten, die dieselbe Bedeutung tragen. Gedankenstriche,
 * Pfeile und Anführungszeichen kommen in denselben Dateien in mehreren
 * Formen vor — meist abhängig davon, wer die Zeile getippt hat.
 */
const TYPOGRAPHIC: Array<[RegExp, string]> = [
  [/[‐-―−]/g, '-'], // Bindestrich-Varianten, Minuszeichen
  [/[→➔➡]/g, '-'], // Pfeile
  [/[‘’‚‛′]/g, "'"], // einfache Anführungszeichen
  [/[“”„‟″]/g, '"'], // doppelte Anführungszeichen
  [/ /g, ' '], // geschütztes Leerzeichen
]

/**
 * Kuratierte Synonyme (Kap. 8.3). Bewusst klein und erweiterbar nur per
 * Release: eine gewachsene Synonymliste, die niemand mehr überblickt, ordnet
 * irgendwann Positionen zusammen, die nichts miteinander zu tun haben.
 */
const SYNONYMS: ReadonlyMap<string, string> = new Map([
  ['steuergeraet', 'control unit'],
  ['steuergerät', 'control unit'],
  ['ecu', 'control unit'],
  ['schraube', 'screw'],
  ['dichtung', 'seal'],
  ['halter', 'bracket'],
  ['halterung', 'bracket'],
])

/** Tokens ohne eigene Aussagekraft — als Unterscheidungsmerkmal wertlos. */
const STOPWORDS = new Set(['der', 'die', 'das', 'und', 'the', 'and', 'for', 'fuer', 'für', 'mit', 'with', 'von', 'of'])

const QUANTITY_PREFIX = /^(\d+)\s*x\s+/i

/**
 * Einen Positionsnamen in seine kanonische Form bringen.
 *
 * Klammerinhalte bleiben erhalten: „(links)" und „(rechts)" unterscheiden zwei
 * echte Positionen. Sie zu entfernen würde zwei Bauteile zu einem verschmelzen —
 * ein Fehler, der erst in der Summe auffällt und dann kaum zu finden ist.
 */
export function normalizeMaterialName(raw: unknown): NormalizedName {
  if (raw === null || raw === undefined) return { normalized: '', quantityPrefix: null, tokens: [] }

  let s = String(raw).normalize('NFC')
  for (const [pattern, replacement] of TYPOGRAPHIC) s = s.replace(pattern, replacement)
  s = s.replace(/\s+/g, ' ').trim().toLowerCase()

  let quantityPrefix: number | null = null
  const qty = QUANTITY_PREFIX.exec(s)
  if (qty) {
    quantityPrefix = Number(qty[1])
    s = s.slice(qty[0].length).trim()
  }

  const tokens = s
    .split(/[^\p{L}\p{N}]+/u)
    .filter((t) => t.length > 0 && !STOPWORDS.has(t))
    .map((t) => SYNONYMS.get(t) ?? t)

  // Synonyme auch in der kanonischen Form anwenden, sonst fallen "Schraube"
  // und "Screw" in den Tokens zusammen, im Namen aber nicht.
  const normalized = s.replace(/\p{L}+/gu, (word) => SYNONYMS.get(word) ?? word)

  return { normalized, quantityPrefix, tokens }
}

/** Zwei Namen gelten als gleich, wenn ihre kanonische Form übereinstimmt. */
export function namesMatch(a: unknown, b: unknown): boolean {
  const na = normalizeMaterialName(a)
  const nb = normalizeMaterialName(b)
  return na.normalized !== '' && na.normalized === nb.normalized
}

/**
 * Anteil gemeinsamer bedeutungstragender Tokens (0..1).
 *
 * Ausdrücklich nur als **Kandidatenfilter** gedacht, nie als Entscheidung:
 * die Spezifikation verbietet probabilistische Ähnlichkeitswerte als alleinige
 * Grundlage einer Zuordnung. Wer hiermit eine Zuordnung trifft, statt damit
 * eine Vorauswahl zu treffen, baut genau die Fehlerklasse, die V1 hatte.
 */
export function tokenOverlap(a: NormalizedName, b: NormalizedName): number {
  if (a.tokens.length === 0 || b.tokens.length === 0) return 0
  const setB = new Set(b.tokens)
  const shared = a.tokens.filter((t) => setB.has(t)).length
  return shared / Math.max(a.tokens.length, b.tokens.length)
}
