// Client-side mirror of tools/csv-map-logic.ts's validateMapping() (plus a
// small date-format example renderer) for the statement import wizard's
// live mapping validation — Task 4 of
// docs/superpowers/plans/2026-08-01-bank-rec.md.
//
// Deliberately a STANDALONE copy, not an import of tools/csv-map-logic.ts:
// that module is bundled for Node (used by tools/ and server/src/bank.ts,
// which import it via a relative "../../tools/..." path resolved by tsx at
// runtime) — pulling it into the Vite web bundle would work today (it has no
// node: imports itself), but would silently start coupling the browser
// bundle to a module whose contract is "server-authoritative parsing logic,"
// not "safe for the browser." Keeping a small, explicitly-labeled mirror
// here means a future change to the server's parser can't accidentally leak
// into (or break) the web build. This file's job is ONLY to give the user
// fast, honest feedback on their column picks before they submit — the
// server re-validates with the real tools/csv-map-logic.ts's own
// validateMapping() no matter what this says (server/src/bank.ts calls it
// directly), so any drift between the two copies can only cost an extra
// round trip, never produce a bad import. Keep them in sync by hand if
// either changes.

export type AmountMode = "signed" | "debit-credit";
export type DateFormat = "MDY" | "DMY" | "YMD";

/** Mirrors GET /api/c/bank/csv-preview's response shape (tools/csv-map-logic.ts's
 *  CsvDetection, as JSON). */
export interface CsvDetection {
  headerRow: string[] | null;
  sampleRows: string[][];
  columnCount: number;
  dateCol?: number;
  descriptionCol?: number;
  amountCol?: number;
  debitCol?: number;
  creditCol?: number;
  dateFormat?: DateFormat;
}

/** A mapping still being edited in the wizard — unlike the server's own
 *  CsvMapping (tools/csv-map-logic.ts), dateCol/descriptionCol are optional
 *  here because the user may not have picked them yet; validateMapping below
 *  reports that as "not set" rather than this file forcing a placeholder
 *  value that would read as a confusing "out of range" error instead. */
export interface DraftCsvMapping {
  headerRows: number;
  dateCol?: number;
  descriptionCol?: number;
  amountMode: AmountMode;
  amountCol?: number;
  debitCol?: number;
  creditCol?: number;
  negateAmount?: boolean;
  dateFormat: DateFormat;
}

/** Column role assigned per-column in the mapping table's header dropdowns. */
export type ColumnRole = "date" | "description" | "amount" | "debit" | "credit" | "ignore";

/** Same English validation rules as tools/csv-map-logic.ts's validateMapping():
 *  required columns missing, out-of-range column indexes, the same column
 *  mapped to more than one field. */
export function validateMapping(mapping: DraftCsvMapping, columnCount: number): string[] {
  const errors: string[] = [];
  const usedBy = new Map<number, string[]>();

  function checkCol(label: string, col: number | undefined, required: boolean): void {
    if (col === undefined) {
      if (required) errors.push(`${label} is not set.`);
      return;
    }
    if (!Number.isInteger(col) || col < 0 || col >= columnCount) {
      errors.push(`${label} (column ${col}) is out of range — this file has ${columnCount} column(s).`);
      return;
    }
    const list = usedBy.get(col) ?? [];
    list.push(label);
    usedBy.set(col, list);
  }

  checkCol("Date column", mapping.dateCol, true);
  checkCol("Description column", mapping.descriptionCol, true);

  if (mapping.amountMode === "signed") {
    checkCol("Amount column", mapping.amountCol, true);
  } else if (mapping.amountMode === "debit-credit") {
    checkCol("Debit column", mapping.debitCol, true);
    checkCol("Credit column", mapping.creditCol, true);
  } else {
    errors.push(`Unknown amount mode "${String(mapping.amountMode)}".`);
  }

  if (!Number.isInteger(mapping.headerRows) || mapping.headerRows < 0) {
    errors.push("Header rows must be a non-negative whole number.");
  }

  for (const [col, labels] of usedBy) {
    if (labels.length > 1) {
      errors.push(`Column ${col} is mapped to more than one field: ${labels.join(", ")}.`);
    }
  }

  return errors;
}

/** Builds a DraftCsvMapping from the wizard's per-column role picks — the
 *  amount mode is AUTO-derived from whichever role(s) got picked (a Debit or
 *  Credit role anywhere switches to "debit-credit"; otherwise "signed"),
 *  never a separate manual choice, so the roles table is always the single
 *  source of truth (never two controls that could disagree). */
export function buildMapping(
  roles: ColumnRole[],
  headerRows: number,
  dateFormat: DateFormat,
  negateAmount: boolean,
): DraftCsvMapping {
  const dateCol = colFor(roles, "date");
  const descriptionCol = colFor(roles, "description");
  const amountCol = colFor(roles, "amount");
  const debitCol = colFor(roles, "debit");
  const creditCol = colFor(roles, "credit");
  const amountMode: AmountMode = debitCol !== undefined || creditCol !== undefined ? "debit-credit" : "signed";
  return {
    headerRows,
    dateCol,
    descriptionCol,
    amountMode,
    amountCol,
    debitCol,
    creditCol,
    negateAmount: negateAmount || undefined,
    dateFormat,
  };
}

function colFor(roles: ColumnRole[], role: ColumnRole): number | undefined {
  const i = roles.indexOf(role);
  return i === -1 ? undefined : i;
}

/** Turns GET /api/c/bank/csv-preview's own best-effort guesses into an
 *  initial per-column role array, so the mapping table opens pre-filled
 *  (design spec: "pre-filled from guesses") rather than all-"Ignore". */
export function rolesFromDetection(detection: CsvDetection): ColumnRole[] {
  const roles: ColumnRole[] = Array.from({ length: detection.columnCount }, () => "ignore");
  if (detection.dateCol !== undefined) roles[detection.dateCol] = "date";
  if (detection.descriptionCol !== undefined) roles[detection.descriptionCol] = "description";
  if (detection.amountCol !== undefined) roles[detection.amountCol] = "amount";
  if (detection.debitCol !== undefined) roles[detection.debitCol] = "debit";
  if (detection.creditCol !== undefined) roles[detection.creditCol] = "credit";
  return roles;
}

/** The re-mapping counterpart of rolesFromDetection above: turns an
 *  ALREADY-SAVED mapping (GET /api/c/bank/accounts/:id/csv-map) into the
 *  same per-column role array, so the "change it" recovery flow opens
 *  pre-filled from what's actually saved — not a fresh best-effort guess —
 *  letting the user see and adjust exactly what's wrong rather than starting
 *  over from scratch. columnCount comes from the file's own csv-preview
 *  (the saved mapping doesn't carry it), since a mapping's column count
 *  could in principle exceed what the CURRENT file happens to show. */
export function rolesFromMapping(mapping: DraftCsvMapping, columnCount: number): ColumnRole[] {
  const roles: ColumnRole[] = Array.from({ length: columnCount }, () => "ignore");
  if (mapping.dateCol !== undefined) roles[mapping.dateCol] = "date";
  if (mapping.descriptionCol !== undefined) roles[mapping.descriptionCol] = "description";
  if (mapping.amountCol !== undefined) roles[mapping.amountCol] = "amount";
  if (mapping.debitCol !== undefined) roles[mapping.debitCol] = "debit";
  if (mapping.creditCol !== undefined) roles[mapping.creditCol] = "credit";
  return roles;
}

const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];

/**
 * Best-effort preview of what a sample date cell would parse to under a
 * candidate format (mirrors tools/csv-map-logic.ts's parseCsvDate closely
 * enough for a live example, e.g. "07/30/2026" -> "Jul 30, 2026") — never
 * throws, returns null for anything it can't confidently read, since this is
 * purely a confirmation aid; the server's own parser is the real one.
 */
export function previewDate(raw: string, format: DateFormat): string | null {
  const s = raw.trim();
  const m = /^(\d{1,4})([-/.])(\d{1,2})\2(\d{1,4})$/.exec(s);
  if (!m) return null;
  const [, a, , b, c] = m as unknown as [string, string, string, string, string];

  let yearStr: string;
  let monthStr: string;
  let dayStr: string;
  if (format === "YMD") [yearStr, monthStr, dayStr] = [a, b, c];
  else if (format === "MDY") [monthStr, dayStr, yearStr] = [a, b, c];
  else [dayStr, monthStr, yearStr] = [a, b, c];

  if (yearStr.length !== 2 && yearStr.length !== 4) return null;
  let year = Number(yearStr);
  if (yearStr.length === 2) year += 2000;
  const month = Number(monthStr);
  const day = Number(dayStr);
  if (month < 1 || month > 12) return null;
  const maxDay = new Date(Date.UTC(year, month, 0)).getUTCDate();
  if (day < 1 || day > maxDay) return null;

  return `${MONTHS[month - 1]} ${day}, ${year}`;
}

export const DATE_FORMAT_OPTIONS: { value: DateFormat; label: string }[] = [
  { value: "MDY", label: "Month/Day/Year (US)" },
  { value: "DMY", label: "Day/Month/Year" },
  { value: "YMD", label: "Year-Month-Day" },
];

export const COLUMN_ROLE_OPTIONS: { value: ColumnRole; label: string }[] = [
  { value: "ignore", label: "Ignore" },
  { value: "date", label: "Date" },
  { value: "description", label: "Description" },
  { value: "amount", label: "Amount" },
  { value: "debit", label: "Debit" },
  { value: "credit", label: "Credit" },
];
