// QAF Normalizer (KAR-799, spec A5.3 / B-Normalizer).
//
// Unifies raw values WITHOUT altering business meaning:
//   - German/English number formats → number | null
//   - empty ("") kept distinct from real 0
//   - position numbers stay text ("2a" stays "2a")
//   - process/machine names normalized for matching (original kept elsewhere)
//   - currencies trimmed/uppercased
//
// All functions are pure. Distinct from qaf-parser::toNum, which only coerces
// already-numeric ExcelJS cells — this handles locale-formatted strings.

/** True for null/undefined/whitespace-only. A real 0 (number or "0") is NOT blank. */
export function isBlank(v: unknown): boolean {
  if (v === null || v === undefined) return true
  if (typeof v === 'number') return false
  return String(v).trim() === ''
}

/**
 * Parse a value that may be a number, a German-formatted string ("1.234,56"),
 * an English-formatted string ("1,234.56"), or a plain decimal. Strips percent
 * signs and currency symbols. Returns null for blank or non-numeric input —
 * crucially, "" → null while "0" → 0 (spec: leer ≠ 0).
 */
export function parseLocaleNumber(raw: unknown): number | null {
  if (raw === null || raw === undefined) return null
  if (typeof raw === 'number') return Number.isFinite(raw) ? raw : null

  let s = String(raw).trim()
  if (s === '') return null

  // Drop currency symbols, percent signs, and all spaces (incl. NBSP/thin).
  s = s.replace(/[%€$£¥]/g, '').replace(/[\s  ]/g, '')
  if (s === '' || s === '-') return null

  const hasComma = s.includes(',')
  const hasDot = s.includes('.')

  if (hasComma && hasDot) {
    // Last separator is the decimal separator; the other groups thousands.
    if (s.lastIndexOf(',') > s.lastIndexOf('.')) {
      // German: dot = thousands, comma = decimal.
      s = s.replace(/\./g, '').replace(',', '.')
    } else {
      // English: comma = thousands, dot = decimal.
      s = s.replace(/,/g, '')
    }
  } else if (hasComma) {
    // Only commas. Treat as decimal separator unless it looks like a thousands
    // grouping (exactly 3 digits after a single comma, e.g. "1,000").
    const parts = s.split(',')
    if (parts.length === 2 && parts[1].length === 3 && parts[0].length > 0) {
      s = parts.join('') // 1,000 → 1000
    } else {
      s = s.replace(',', '.')
    }
  } else if (hasDot) {
    // Only dots. Ambiguous ("1.000" = de:1000 vs en:1.0). Treat as German
    // thousands grouping ONLY for the strict pattern of 1–3 leading digits
    // followed by one or more ".ddd" groups (e.g. "1.000", "1.234.567").
    // Everything else (e.g. "0.5", "100.5") is a decimal.
    if (/^-?[1-9]\d{0,2}(\.\d{3})+$/.test(s)) {
      s = s.replace(/\./g, '')
    }
  }
  // Remaining form is JS-parseable.

  const n = Number(s)
  return Number.isFinite(n) ? n : null
}

/**
 * Explicit "not applicable" text markers (DE/EN), spec Master-Prompt §13 /
 * P0.6 (KAR-891). Conservative and closed: only exact matches (after trim +
 * lowercase) qualify — an arbitrary non-numeric text is NOT automatically
 * "n.a.", it stays plain unparseable text.
 */
export const NOT_APPLICABLE_MARKERS: readonly string[] = [
  'n.a.',
  'n.a',
  'n/a',
  'na',
  'entfällt',
  'entfaellt',
  'nicht anwendbar',
  'nicht zutreffend',
  'not applicable',
  '-',
] as const

/**
 * True when `raw` is an explicit not-applicable marker (case-insensitive,
 * trimmed) — distinct from a genuinely empty cell (isBlank) and distinct from
 * arbitrary non-numeric text. A field with "n.a." was consciously filled in
 * by the supplier; a truly empty field was simply left blank. Both currently
 * collapse to `null` via parseLocaleNumber — this detector lets callers keep
 * them apart (spec: empty ≠ zero ≠ not-applicable).
 */
export function isNotApplicableValue(raw: unknown): boolean {
  if (isBlank(raw)) return false
  if (typeof raw === 'number') return false
  const normalized = String(raw).trim().toLowerCase()
  return NOT_APPLICABLE_MARKERS.includes(normalized)
}

const UMLAUT_MAP: Record<string, string> = {
  ä: 'ae',
  ö: 'oe',
  ü: 'ue',
  ß: 'ss',
}

/**
 * Canonical key for fuzzy/exact process- or machine-name comparison: lowercased,
 * umlauts folded, whitespace collapsed, trimmed. The ORIGINAL value is preserved
 * by the caller — this is only a comparison key.
 */
export function normalizeProcessName(raw: unknown): string {
  if (isBlank(raw)) return ''
  return String(raw)
    .toLowerCase()
    .replace(/[äöüß]/g, (c) => UMLAUT_MAP[c] ?? c)
    .replace(/\s+/g, ' ')
    .trim()
}

/** Position numbers must remain text so "2a" / "007" survive intact. Only trims. */
export function normalizePosition(raw: unknown): string {
  if (raw === null || raw === undefined) return ''
  return String(raw).trim()
}

/** Currency code: trimmed + uppercased, or null when blank. */
export function normalizeCurrency(raw: unknown): string | null {
  if (isBlank(raw)) return null
  return String(raw).trim().toUpperCase()
}
