// Shipped starter presets for the universal import wizard — per-product
// column-name GUESSES plus the fuzzy header matcher that applies them. See
// docs/superpowers/specs/2026-08-02-switch-from-anywhere-design.md, Part B.
//
// HONESTY NOTE (binding, from the spec): every candidate header below is a
// guess from general knowledge of each product's exports — NOT a verified
// sample of a real file. The wizard's copy says "We'll guess the columns and
// you confirm", and nothing here is applied without the user confirming the
// mapping. Confidence varies by product and is called out per preset; the
// two generic presets are the honest floor everything falls back to.
//
// Candidates are matched via universal-map-logic's normalizeWords (lowercase,
// punctuation collapsed), whole-word-bounded, exact-name matches before
// fuzzy contains — so "JournalDate" only matches presets that expect Xero's
// run-together spelling, and "date" never claims it by accident.

import { normalizeWords } from "./universal-map-logic.js";

export type ChartField = "name" | "type" | "code" | "fullPath" | "parent";
export type JournalField =
  | "date"
  | "account"
  | "debit"
  | "credit"
  | "amount"
  | "groupKey"
  | "memo"
  | "name"
  | "ref";

/** Candidate header names per mapping field, in preference order. Key order
 *  in the object is the field-priority order the matcher claims columns in. */
export type ChartHints = Partial<Record<ChartField, readonly string[]>>;
export type JournalHints = Partial<Record<JournalField, readonly string[]>>;

export interface UniversalPreset {
  key: string;
  label: string;
  /** Which export/report of the product these guesses target. */
  description: string;
  chartHints: ChartHints;
  journalHints: JournalHints;
}

export const UNIVERSAL_PRESETS: readonly UniversalPreset[] = [
  {
    // Sage BusinessWorks: targets the G/L "General Ledger" / transaction
    // history report exported to CSV/Excel. LOWEST-confidence preset here —
    // BusinessWorks is a niche legacy product and these column names are
    // inferred from its general report vocabulary ("Trx" abbreviations),
    // not from a verified export.
    key: "sage-businessworks",
    label: "Sage BusinessWorks",
    description:
      "General Ledger report export (CSV/Excel). Column names are guesses — confirm every mapping.",
    chartHints: {
      name: ["account description", "description", "account name"],
      type: ["account type", "type", "classification"],
      code: ["account number", "account id", "account"],
    },
    journalHints: {
      date: ["date", "trx date", "transaction date", "posting date"],
      account: ["account", "account number", "account no", "gl account", "g l account"],
      debit: ["debit", "debit amount"],
      credit: ["credit", "credit amount"],
      groupKey: ["trx number", "transaction number", "entry number", "reference"],
      memo: ["description", "comment", "memo"],
      ref: ["document number", "check number"],
    },
  },
  {
    // Sage 50 US (the Peachtree lineage): targets the General Ledger report
    // exported to CSV/Excel, whose columns traditionally include "Account
    // ID", "Debit Amt"/"Credit Amt", and "Trans Description". Medium
    // confidence — the "Amt" abbreviations are long-standing Peachtree
    // vocabulary, but still guesses to confirm.
    key: "sage-50",
    label: "Sage 50 (Peachtree)",
    description:
      "General Ledger report export (CSV/Excel), classic \"Account ID\" / \"Debit Amt\" columns.",
    chartHints: {
      name: ["account description", "description", "account name"],
      type: ["account type", "type"],
      code: ["account id", "account no"],
    },
    journalHints: {
      date: ["date", "trans date"],
      account: ["account id", "account no", "account"],
      debit: ["debit amt", "debit amount", "debit"],
      credit: ["credit amt", "credit amount", "credit"],
      groupKey: ["reference", "trans no", "transaction reference"],
      memo: ["trans description", "description"],
      ref: ["jrnl", "journal"],
    },
  },
  {
    // Xero: targets the Journal Report CSV export (run-together headers like
    // "JournalDate", "JournalNumber", "NetAmount" — signed, debits positive)
    // and the Chart of Accounts export ("Code", "Name", "Type"). Highest
    // confidence of the product presets, but still guesses to confirm.
    key: "xero",
    label: "Xero",
    description:
      "Journal Report CSV (JournalDate/JournalNumber/NetAmount, signed amounts) + Chart of Accounts export.",
    chartHints: {
      name: ["name", "account name"],
      type: ["type", "account type"],
      code: ["code", "account code"],
    },
    journalHints: {
      date: ["journaldate", "journal date", "date"],
      account: ["accountname", "account name", "account"],
      debit: ["debit"],
      credit: ["credit"],
      amount: ["netamount", "net amount"],
      groupKey: ["journalnumber", "journal number"],
      memo: ["description"],
      ref: ["reference"],
    },
  },
  {
    // Wave: targets the "Accounting Transactions" CSV export (a Transaction
    // ID column groups lines; debit/credit amount columns). Medium-low
    // confidence — Wave has reshuffled its export columns over the years.
    key: "wave",
    label: "Wave",
    description:
      "Accounting Transactions CSV export (Transaction ID groups lines, debit/credit columns).",
    chartHints: {
      name: ["account name", "name"],
      type: ["account type", "type"],
      code: ["account id", "account code"],
    },
    journalHints: {
      date: ["transaction date", "date"],
      account: ["account name", "account"],
      debit: ["debit amount", "debit"],
      credit: ["credit amount", "credit"],
      amount: ["amount"],
      groupKey: ["transaction id"],
      memo: ["transaction line description", "transaction description", "description"],
    },
  },
  {
    // FreshBooks: targets the General Ledger / Journal Entries report export.
    // Low confidence — FreshBooks is invoice-first and its accounting
    // reports vary by plan generation; these are vocabulary-level guesses.
    key: "freshbooks",
    label: "FreshBooks",
    description:
      "General Ledger / Journal Entries report export (CSV/Excel). Column names are guesses — confirm every mapping.",
    chartHints: {
      name: ["account name", "name"],
      type: ["account type", "type"],
      code: ["account number", "account code"],
    },
    journalHints: {
      date: ["date", "entry date"],
      account: ["account name", "account", "sub account"],
      debit: ["debit", "debit amount"],
      credit: ["credit", "credit amount"],
      groupKey: ["journal entry", "journal entry name", "entry number", "je number"],
      memo: ["description", "details"],
    },
  },
  {
    // Honest floor #1: any export with separate Debit/Credit columns.
    key: "generic-debit-credit",
    label: "Generic debit/credit CSV",
    description:
      "Any software's GL export with separate debit and credit columns.",
    chartHints: {
      name: ["account", "account name", "name", "description"],
      type: ["type", "account type", "category", "classification"],
      code: ["code", "account number", "account code", "account id", "number"],
      fullPath: ["full name", "full account name", "account path", "path"],
      parent: ["parent", "parent account", "subaccount of"],
    },
    journalHints: {
      date: ["date", "transaction date", "posting date", "trans date"],
      account: ["account", "account name", "account number", "account no", "gl account"],
      debit: ["debit", "dr", "debit amount"],
      credit: ["credit", "cr", "credit amount"],
      groupKey: [
        "transaction", "transaction id", "transaction number", "entry number",
        "journal number", "entry", "trans no", "doc no",
      ],
      memo: ["memo", "description", "narrative", "details"],
      name: ["name", "payee", "vendor", "customer"],
      ref: ["reference", "ref", "num", "number", "check number"],
    },
  },
  {
    // Honest floor #2: any export with one signed amount column.
    key: "generic-signed",
    label: "Generic signed-amount CSV",
    description:
      "Any software's GL export with a single signed amount column.",
    chartHints: {
      name: ["account", "account name", "name", "description"],
      type: ["type", "account type", "category", "classification"],
      code: ["code", "account number", "account code", "account id", "number"],
      fullPath: ["full name", "full account name", "account path", "path"],
      parent: ["parent", "parent account", "subaccount of"],
    },
    journalHints: {
      date: ["date", "transaction date", "posting date", "trans date"],
      account: ["account", "account name", "account number", "account no", "gl account"],
      amount: ["amount", "net amount", "signed amount", "value"],
      groupKey: [
        "transaction", "transaction id", "transaction number", "entry number",
        "journal number", "entry", "trans no", "doc no",
      ],
      memo: ["memo", "description", "narrative", "details"],
      name: ["name", "payee", "vendor", "customer"],
      ref: ["reference", "ref", "num", "number", "check number"],
    },
  },
];

export interface FieldGuess {
  field: string;
  col: number;
  /** The original header text, exactly as the file spells it. */
  header: string;
  /** "exact": the header IS a known candidate name; "fuzzy": a candidate
   *  appears word-bounded inside the header. Guesses either way — the UI
   *  always confirms. */
  confidence: "exact" | "fuzzy";
}

export interface HintMatch {
  guesses: FieldGuess[];
  /** 2 points per exact guess, 1 per fuzzy. */
  score: number;
}

function containsWholeWords(headerNormalized: string, candidate: string): boolean {
  return ` ${headerNormalized} `.includes(` ${candidate} `);
}

/**
 * Matches one file's headers against one hint set. Two passes so a fuzzy
 * match can never steal a column another field matches exactly: first every
 * field takes its best EXACT (normalized-equality) column, then unmatched
 * fields take word-bounded fuzzy columns. Each column is claimed at most
 * once. Fields are processed in the hint object's key order.
 */
export function matchHints(
  headers: string[],
  hints: ChartHints | JournalHints,
): HintMatch {
  const normalized = headers.map(normalizeWords);
  const claimed = new Set<number>();
  const guesses: FieldGuess[] = [];
  const fields = Object.entries(hints) as [string, readonly string[]][];

  // Pass 1: exact.
  const unmatched: [string, readonly string[]][] = [];
  for (const [field, candidates] of fields) {
    let found = false;
    for (const candidate of candidates) {
      const want = normalizeWords(candidate);
      const col = normalized.findIndex((h, c) => h !== "" && h === want && !claimed.has(c));
      if (col !== -1) {
        claimed.add(col);
        guesses.push({ field, col, header: headers[col]!, confidence: "exact" });
        found = true;
        break;
      }
    }
    if (!found) unmatched.push([field, candidates]);
  }

  // Pass 2: fuzzy, only over still-unclaimed columns.
  for (const [field, candidates] of unmatched) {
    outer: for (const candidate of candidates) {
      const want = normalizeWords(candidate);
      for (let col = 0; col < normalized.length; col++) {
        if (claimed.has(col) || normalized[col] === "") continue;
        if (containsWholeWords(normalized[col]!, want)) {
          claimed.add(col);
          guesses.push({ field, col, header: headers[col]!, confidence: "fuzzy" });
          break outer;
        }
      }
    }
  }

  const score = guesses.reduce((n, g) => n + (g.confidence === "exact" ? 2 : 1), 0);
  return { guesses, score };
}

export interface PresetMatch {
  preset: UniversalPreset;
  /** Whether the headers look more like a chart file or a journal file. */
  likelyKind: "chart" | "journal";
  chart: HintMatch;
  journal: HintMatch;
  /** max(chart.score, journal.score) for the winning preset. */
  score: number;
}

function isGeneric(preset: UniversalPreset): boolean {
  return preset.key.startsWith("generic-");
}

/**
 * Best preset for one file's headers, with per-column guesses for BOTH the
 * chart and journal interpretations (the caller knows which step it's on).
 * Ties go to the generic presets — when a file matches "Sage 50" no better
 * than plain "Generic debit/credit CSV", claiming it's Sage-shaped would be
 * an overclaim. Returns null when nothing matches at all.
 */
export function matchPreset(headers: string[]): PresetMatch | null {
  let best: PresetMatch | null = null;
  for (const preset of UNIVERSAL_PRESETS) {
    const chart = matchHints(headers, preset.chartHints);
    const journal = matchHints(headers, preset.journalHints);
    const score = Math.max(chart.score, journal.score);
    if (score === 0) continue;
    const candidate: PresetMatch = {
      preset,
      likelyKind: journal.score >= chart.score ? "journal" : "chart",
      chart,
      journal,
      score,
    };
    if (
      best === null ||
      score > best.score ||
      (score === best.score && isGeneric(preset) && !isGeneric(best.preset))
    ) {
      best = candidate;
    }
  }
  return best;
}
