/**
 * Column mapping: auto-suggest target-field → source-header pairings by fuzzy
 * header match, and apply a chosen mapping to a raw row.
 */
import type { ImportField, MappedRow, RawRow } from './types';

/** lowercase, strip everything but a-z0-9 → comparable token. */
function norm(s: string): string {
  return s.toLowerCase().replace(/[^a-z0-9]/g, '');
}

function lev(a: string, b: string): number {
  const m = a.length;
  const n = b.length;
  if (!m) return n;
  if (!n) return m;
  let prev = Array.from({ length: n + 1 }, (_, i) => i);
  for (let i = 1; i <= m; i++) {
    const cur = [i];
    for (let j = 1; j <= n; j++) {
      cur[j] = Math.min(
        prev[j]! + 1,
        cur[j - 1]! + 1,
        prev[j - 1]! + (a[i - 1] === b[j - 1] ? 0 : 1),
      );
    }
    prev = cur;
  }
  return prev[n]!;
}

/** A single auto-mapping suggestion. `confident` matches are safe to
 *  pre-select in the UI; `review` matches (substring overlap only) must be
 *  surfaced for the operator to confirm, never silently pre-applied. */
export interface MappingSuggestion {
  fieldKey: string;
  header: string;
  /**
   * - `exact`   — normalized key/label/alias equals the header (score 0).
   * - `fuzzy`   — close edit distance (≤2 edits) on a whole token; pre-selectable.
   * - `review`  — substring overlap only (e.g. "Billing Email" ⊃ "email");
   *   audit L33(a): these mis-map at scale, so they are NOT pre-selected.
   */
  tier: 'exact' | 'fuzzy' | 'review';
}

/**
 * For each field, find the best-matching header and classify the match tier.
 * Exact normalized match on key / label / alias is `exact`; a close
 * edit-distance match on a whole token is `fuzzy`; a bare substring overlap is
 * `review` (audit L33: substring scoring let "Billing Email" auto-map to
 * `email` and "Company Name" to `name`, so a careless confirm imported into the
 * wrong column). A header is claimed by at most one field (first-come by field
 * order). Returns every match with its tier; callers decide what to pre-select.
 */
export function suggestMappingDetailed(
  headers: string[],
  fields: ImportField[],
): MappingSuggestion[] {
  const out: MappingSuggestion[] = [];
  const taken = new Set<string>();
  const normHeaders = headers.map((h) => ({ raw: h, n: norm(h) }));

  for (const field of fields) {
    const candidates = [field.key, field.label, ...(field.aliases ?? [])].map(norm);
    // Lower score = better. 0 exact, 1 substring (review), 2+ edit-distance.
    let best: { header: string; score: number } | null = null;

    for (const h of normHeaders) {
      if (taken.has(h.raw) || !h.n) continue;
      let score = Infinity;
      for (const c of candidates) {
        if (!c) continue;
        if (c === h.n) score = Math.min(score, 0);
        else if (c.includes(h.n) || h.n.includes(c)) score = Math.min(score, 1);
        else {
          const d = lev(c, h.n);
          // Accept only close matches (≤2 edits, and not longer than the token).
          // Edit-distance scores land at 2..3 (1 + d); a bare substring scores
          // exactly 1. Tier classification below reads those bands back out.
          if (d <= 2 && d < Math.max(c.length, h.n.length)) score = Math.min(score, 1 + d);
        }
      }
      if (score < Infinity && (!best || score < best.score)) best = { header: h.raw, score };
    }

    if (best) {
      // score 0 → exact; score exactly 1 → substring overlap (review);
      // score ≥ 2 → close edit distance (fuzzy, pre-selectable).
      const tier: MappingSuggestion['tier'] =
        best.score === 0 ? 'exact' : best.score === 1 ? 'review' : 'fuzzy';
      out.push({ fieldKey: field.key, header: best.header, tier });
      taken.add(best.header);
    }
  }
  return out;
}

/**
 * Confident auto-mapping for pre-selection: `fieldKey → header` for `exact` and
 * `fuzzy` matches only. Substring-only (`review`) matches are intentionally
 * omitted (audit L33) — fetch them via {@link suggestMappingDetailed} and show
 * them as un-applied suggestions the operator must confirm.
 */
export function suggestMapping(headers: string[], fields: ImportField[]): Record<string, string> {
  const out: Record<string, string> = {};
  for (const s of suggestMappingDetailed(headers, fields)) {
    if (s.tier !== 'review') out[s.fieldKey] = s.header;
  }
  return out;
}

/**
 * Apply a `fieldKey → header` mapping to a raw row, producing `fieldKey → cell`.
 * Empty / whitespace-only cells are dropped so downstream "required" checks and
 * optional-field omission behave correctly.
 */
export function applyMapping(raw: RawRow, mapping: Record<string, string>): MappedRow {
  const out: MappedRow = {};
  for (const [fieldKey, header] of Object.entries(mapping)) {
    const cell = (raw[header] ?? '').trim();
    if (cell !== '') out[fieldKey] = cell;
  }
  return out;
}
