// Statement import wizard — Task 4 of docs/superpowers/plans/2026-08-01-bank-rec.md.
// #bank/import, reached from a home account card's "Import statement" button
// (carrying ?accountId=, pre-selecting step 1) or from the sidebar/nothing
// (falls back to the first money account). Mirrors the setup wizard's own
// shape (setup/SetupWizard.tsx: one component owns the step state machine,
// Dropzone + apiUpload for the multipart upload) rather than inventing a new
// pattern.
//
// Step 1 (account) -> step 2 (dropzone) -> for OFX/QFX, straight to step 4
// (result); for CSV, an UPLOAD IS ATTEMPTED IMMEDIATELY with no mapping
// fields. There is no endpoint that reports ahead of time whether this
// account already has a saved column mapping, so this is the only honest way
// to find out: if a saved mapping exists, the server uses it and the upload
// just succeeds -> result screen, with a note that the saved layout was
// used. If none exists, the server refuses with its own documented English
// ("This account has no saved column mapping yet...") -- recognized here by
// that fixed prefix -- and THAT is the signal to walk the mapping step
// (step 3): fetch POST /api/c/bank/csv-preview for sample rows + best-effort
// guesses, let the user confirm/adjust column roles, then resubmit with
// mapping fields (which server/src/bank.ts's import route both applies AND
// saves for next time).
//
// Re-mapping (review fast-follow — a wrong first mapping used to have NO
// in-app recovery; the result screen's "change it" was a read-only stub):
// from the result screen, when the import used a saved layout, "change it"
// calls openRemap() below, which fetches the CURRENT saved mapping (GET
// /api/c/bank/accounts/:id/csv-map) and a fresh csv-preview of the file
// that was just imported, then opens the SAME mapping step used for a
// first-time import — but pre-filled from what's actually saved (via
// rolesFromMapping, not rolesFromDetection's best-effort guesses), and in
// "remap" mode: confirming calls POST .../csv-map FIRST (replacing the saved
// mapping), then re-imports the SAME file — which, now that the mapping is
// corrected, is read via the normal "use the saved map" path, no mapping
// fields riding along. This creates a NEW statement under the corrected
// mapping; it does not retroactively fix whatever the wrong mapping already
// imported (that statement's bad lines still need handling in the review
// workbench — excluding them, most likely). That's an accepted v1 tradeoff:
// there is no line-editing anywhere in this system (CLAUDE.md §3.2 territory
// — statement lines are as permanent a record as a posted entry), so
// "re-import correctly, then clean up the old statement" is the only honest
// recovery shape available, not a hidden defect.
//
// Result copy deliberately does NOT claim any lines were "matched
// automatically" (the design spec's own example text says "30 matched
// automatically, 7 need a look, 5 are new") — server/src/lib/statementMatch.ts's
// runMatchingEngine() only ever sets a line to 'suggested' (one click to
// confirm) or leaves it 'unmatched'; nothing becomes 'matched' without a
// user action (that module's own header: "Auto-CONFIRM never happens
// without the user"). Reporting a nonexistent "matched automatically" count
// would just be wrong, so this shows what the API actually returns:
// suggested (needs a one-click confirm) and unmatched (new — needs
// categorizing), per CLAUDE.md §9's "errors [and everything else] in
// English," not fiction.

import { useEffect, useMemo, useState } from "react";
import { useCompany } from "../lib/company";
import { useApiGet } from "../lib/useApiGet";
import { apiGet, apiPost, ApiError } from "../lib/api";
import { apiUpload } from "../setup/apiUpload";
import { Dropzone } from "../setup/Dropzone";
import { ErrorBanner } from "../components/ErrorBanner";
import { fmtMoney } from "../lib/money";
import { fmtDate } from "../lib/date";
import type { ApiAccount } from "../accounts/subtypes";
import { accountSections } from "../accounts/pickerGroups";
import type { NavTarget } from "../lib/useHashView";
import {
  buildMapping, rolesFromDetection, rolesFromMapping, previewDate, validateMapping,
  DATE_FORMAT_OPTIONS, COLUMN_ROLE_OPTIONS,
  type CsvDetection, type ColumnRole, type DateFormat, type DraftCsvMapping,
} from "./csvMapping";

interface ImportApiResult {
  statementId: string;
  lines: { imported: number; skippedDuplicates: number };
  matches: { suggested: number; unmatched: number };
  prefills: { endingBalance?: string; periodEnd?: string };
}

interface ImportResult extends ImportApiResult {
  /** True when a CSV import succeeded with NO mapping fields sent — i.e. the
   *  server used a saved column layout for this account (see file header). */
  usedSavedMap: boolean;
}

type WizardStep = "account" | "dropzone" | "csv-mapping" | "result";

const NO_SAVED_MAP_PREFIX = "This account has no saved column mapping yet";

function isCsvFile(file: File): boolean {
  return file.name.toLowerCase().endsWith(".csv");
}

/**
 * lib/money.ts's fmtMoney() expects a NUMERIC(19,4)-shaped string (exactly
 * 4 fraction digits) — the DB's own on-the-wire convention. prefills.endingBalance
 * comes from tools/ofx-logic.ts's centsToStr() instead (the parsers' shared
 * 2-decimal-place convention, e.g. "12345.67" or "-75.25"), so fmtMoney would
 * silently fail its regex and echo the raw string back unformatted (caught
 * during this task's live browser verification: the result screen was
 * showing "12345.67" instead of "$12,345.67"). Zero-padding the fraction to
 * 4 digits is exact (no rounding, no arithmetic) — safe under CLAUDE.md §2 —
 * so this reuses fmtMoney's own tested comma/sign formatting rather than
 * duplicating it.
 */
function toDbAmountShape(amount: string): string {
  const negative = amount.startsWith("-");
  const unsigned = negative ? amount.slice(1) : amount;
  const [whole, frac = ""] = unsigned.split(".");
  return `${negative ? "-" : ""}${whole}.${(frac + "0000").slice(0, 4)}`;
}

function summarize(result: ImportResult): string {
  const { imported, skippedDuplicates } = result.lines;
  const { suggested, unmatched } = result.matches;
  let s = `${imported} line${imported === 1 ? "" : "s"} imported`;
  const detail: string[] = [];
  if (suggested > 0) detail.push(`${suggested} suggested for review`);
  if (unmatched > 0) detail.push(`${unmatched} new`);
  if (detail.length > 0) s += `: ${detail.join(", ")}`;
  if (skippedDuplicates > 0) {
    s += ` · ${skippedDuplicates} duplicate${skippedDuplicates === 1 ? "" : "s"} skipped`;
  }
  return `${s}.`;
}

export function BankImportWizard({
  navigate,
  initialAccountId,
}: {
  navigate: (target: NavTarget) => void;
  initialAccountId?: string;
}) {
  const { selectedCompanyId } = useCompany();
  const { data: accounts } = useApiGet<ApiAccount[]>("/api/c/accounts", selectedCompanyId);
  // Asset-restricted, standard sections (Chart Management Task 3) — same
  // restriction the old flat "money" list had, now with Bank accounts (etc.)
  // optgroup headers.
  const accountSectionList = useMemo(() => accountSections(accounts, ["asset"]), [accounts]);

  const [step, setStep] = useState<WizardStep>("account");
  const [accountId, setAccountId] = useState<string | null>(initialAccountId ?? null);

  const [file, setFile] = useState<File | null>(null);
  const [fileExtError, setFileExtError] = useState<string | null>(null);
  const [uploading, setUploading] = useState(false);
  const [uploadError, setUploadError] = useState<string | null>(null);

  const [detection, setDetection] = useState<CsvDetection | null>(null);
  const [roles, setRoles] = useState<ColumnRole[]>([]);
  const [headerRows, setHeaderRows] = useState(0);
  const [dateFormat, setDateFormat] = useState<DateFormat>("MDY");
  const [negateAmount, setNegateAmount] = useState(false);
  // "first-time": no saved mapping existed yet — submitting both applies AND
  // saves it as part of the SAME import call. "remap": a saved mapping
  // already existed and the user opened "change it" from the result screen —
  // submitting saves the CORRECTED mapping via its own endpoint first, then
  // re-imports the same file (see file header).
  const [mappingMode, setMappingMode] = useState<"first-time" | "remap">("first-time");

  const [result, setResult] = useState<ImportResult | null>(null);

  // Default the account to the first one available once accounts load, if
  // nothing was pre-selected from the calling card — first section, first
  // row. Bank accounts lead, so this prefers a bank account over strict
  // code order; the right bias for a statement-import default.
  useEffect(() => {
    if (!accountId && accountSectionList.length > 0) {
      setAccountId(accountSectionList[0]!.accounts[0]!.id);
    }
  }, [accountSectionList, accountId]);

  const accountName = accounts?.find((a) => a.id === accountId)?.name ?? null;

  function handleFileSelected(f: File) {
    const lower = f.name.toLowerCase();
    if (!lower.endsWith(".csv") && !lower.endsWith(".ofx") && !lower.endsWith(".qfx")) {
      setFileExtError(`"${f.name}" doesn't look like a bank statement — choose a .csv, .ofx, or .qfx file.`);
      return;
    }
    setFileExtError(null);
    setUploadError(null);
    setFile(f);
  }

  async function startUpload() {
    if (!accountId || !file || !selectedCompanyId) return;
    setMappingMode("first-time");
    setUploading(true);
    setUploadError(null);
    try {
      const res = await apiUpload<ImportApiResult>("/api/c/bank/statements", file, selectedCompanyId, { accountId });
      setResult({ ...res, usedSavedMap: isCsvFile(file) });
      setStep("result");
    } catch (err) {
      const noSavedMap =
        err instanceof ApiError && isCsvFile(file) && typeof err.message === "string" &&
        err.message.startsWith(NO_SAVED_MAP_PREFIX);
      if (!noSavedMap) {
        setUploadError(err instanceof ApiError ? err.message : "Couldn't upload that file.");
        setUploading(false);
        return;
      }
      try {
        const det = await apiUpload<CsvDetection>("/api/c/bank/csv-preview", file, selectedCompanyId);
        setDetection(det);
        setRoles(rolesFromDetection(det));
        setHeaderRows(det.headerRow ? 1 : 0);
        setDateFormat(det.dateFormat ?? "MDY");
        setNegateAmount(false);
        setStep("csv-mapping");
      } catch (err2) {
        setUploadError(err2 instanceof ApiError ? err2.message : "Couldn't read that CSV file.");
      } finally {
        setUploading(false);
      }
      return;
    }
    setUploading(false);
  }

  /**
   * "Change it" from the result screen (see file header): opens the SAME
   * mapping step a first-time import uses, but pre-filled from what's
   * actually saved for this account, not a fresh guess. Reuses the file
   * already held in state (the one that was just imported) for the preview —
   * there's no need to ask the user to re-drop it.
   */
  async function openRemap() {
    if (!accountId || !file || !selectedCompanyId) return;
    setMappingMode("remap");
    setUploading(true);
    setUploadError(null);
    try {
      const det = await apiUpload<CsvDetection>("/api/c/bank/csv-preview", file, selectedCompanyId);
      setDetection(det);
      try {
        const saved = await apiGet<{ accountId: string; mapping: DraftCsvMapping }>(
          `/api/c/bank/accounts/${accountId}/csv-map`, selectedCompanyId,
        );
        setRoles(rolesFromMapping(saved.mapping, det.columnCount));
        setHeaderRows(saved.mapping.headerRows);
        setDateFormat(saved.mapping.dateFormat);
        setNegateAmount(Boolean(saved.mapping.negateAmount));
      } catch {
        // No saved mapping to pre-fill from (shouldn't happen — "change it"
        // only appears when the import just used one) — fall back to a
        // fresh best-effort guess rather than blocking the recovery flow.
        setRoles(rolesFromDetection(det));
        setHeaderRows(det.headerRow ? 1 : 0);
        setDateFormat(det.dateFormat ?? "MDY");
        setNegateAmount(false);
      }
      setStep("csv-mapping");
    } catch (err) {
      setUploadError(err instanceof ApiError ? err.message : "Couldn't read that file.");
    } finally {
      setUploading(false);
    }
  }

  const mapping = useMemo(
    () => buildMapping(roles, headerRows, dateFormat, negateAmount),
    [roles, headerRows, dateFormat, negateAmount],
  );
  const mappingErrors = useMemo(
    () => (detection ? validateMapping(mapping, detection.columnCount) : []),
    [mapping, detection],
  );

  const dateSampleRaw = useMemo(() => {
    if (!detection || mapping.dateCol === undefined) return null;
    const col = mapping.dateCol;
    const row = detection.sampleRows.find((r) => (r[col] ?? "").trim() !== "");
    return row?.[col] ?? null;
  }, [detection, mapping.dateCol]);
  const dateExample = dateSampleRaw ? previewDate(dateSampleRaw, dateFormat) : null;

  function updateRole(colIndex: number, role: ColumnRole) {
    setRoles((prev) => prev.map((r, i) => (i === colIndex ? role : r)));
  }

  async function submitMapping() {
    if (!accountId || !file || !selectedCompanyId || !detection || mappingErrors.length > 0) return;
    setUploading(true);
    setUploadError(null);
    try {
      if (mappingMode === "remap") {
        // Save the CORRECTED mapping first — a plain JSON call, no file
        // attached (server/src/bank.ts's POST .../accounts/:id/csv-map) —
        // then re-import the SAME file, which now reads via the account's
        // (just-updated) saved mapping with no mapping fields riding along.
        await apiPost(`/api/c/bank/accounts/${accountId}/csv-map`, {
          columnCount: detection.columnCount,
          headerRows: mapping.headerRows,
          dateCol: mapping.dateCol,
          descriptionCol: mapping.descriptionCol,
          amountMode: mapping.amountMode,
          amountCol: mapping.amountCol,
          debitCol: mapping.debitCol,
          creditCol: mapping.creditCol,
          negateAmount: mapping.negateAmount ?? false,
          dateFormat: mapping.dateFormat,
        }, selectedCompanyId);
        const res = await apiUpload<ImportApiResult>("/api/c/bank/statements", file, selectedCompanyId, { accountId });
        setResult({ ...res, usedSavedMap: true });
      } else {
        const fields: Record<string, string> = {
          accountId,
          headerRows: String(mapping.headerRows),
          dateCol: String(mapping.dateCol),
          descriptionCol: String(mapping.descriptionCol),
          amountMode: mapping.amountMode,
          negateAmount: mapping.negateAmount ? "true" : "false",
          dateFormat: mapping.dateFormat,
        };
        if (mapping.amountCol !== undefined) fields.amountCol = String(mapping.amountCol);
        if (mapping.debitCol !== undefined) fields.debitCol = String(mapping.debitCol);
        if (mapping.creditCol !== undefined) fields.creditCol = String(mapping.creditCol);
        const res = await apiUpload<ImportApiResult>("/api/c/bank/statements", file, selectedCompanyId, fields);
        setResult({ ...res, usedSavedMap: false });
      }
      setStep("result");
    } catch (err) {
      setUploadError(err instanceof ApiError ? err.message : "Couldn't import that file.");
    } finally {
      setUploading(false);
    }
  }

  function resetToDropzone() {
    setFile(null);
    setFileExtError(null);
    setUploadError(null);
    setDetection(null);
    setStep("dropzone");
  }

  /** The mapping step's secondary action: "Choose a different file" for a
   *  first-time mapping (discards everything, back to the dropzone), or
   *  "Cancel" for a remap (the file and the ORIGINAL result are still good —
   *  just drop back to the result screen without saving the edited mapping). */
  function cancelMapping() {
    if (mappingMode === "remap") {
      setUploadError(null);
      setDetection(null);
      setStep("result");
      return;
    }
    resetToDropzone();
  }

  return (
    <div className="wrap setup-wrap">
      <header className="top">
        <h1>Import a bank statement</h1>
      </header>

      {step === "account" && (
        <div className="card">
          <p className="sub" style={{ marginBottom: 12 }}>Which account is this statement for?</p>
          <div className="field" style={{ maxWidth: 360 }}>
            <label htmlFor="import-account">Account</label>
            <select
              id="import-account"
              className="select-input"
              value={accountId ?? ""}
              onChange={(ev) => setAccountId(ev.target.value)}
            >
              {accountSectionList.map((section) => (
                <optgroup key={section.label} label={section.label}>
                  {section.accounts.map((a) => (
                    <option key={a.id} value={a.id}>
                      {a.name}
                    </option>
                  ))}
                </optgroup>
              ))}
            </select>
          </div>
          <div className="wizard-actions">
            <button type="button" className="btn" disabled={!accountId} onClick={() => setStep("dropzone")}>
              Continue
            </button>
            <button type="button" className="acct-link" onClick={() => navigate("home")}>
              Cancel
            </button>
          </div>
        </div>
      )}

      {step === "dropzone" && (
        <div className="card">
          <p className="sub" style={{ marginBottom: 4 }}>
            Importing to {accountName ?? "this account"}.
          </p>
          <p className="sub" style={{ marginBottom: 12 }}>
            Drop your bank statement file — .csv, .ofx, or .qfx.
          </p>
          {fileExtError && <p className="field-error" style={{ marginBottom: 12 }}>{fileExtError}</p>}
          {uploadError && (
            <div style={{ marginBottom: 12 }}>
              <ErrorBanner message={uploadError} onRetry={() => void startUpload()} />
            </div>
          )}
          <Dropzone
            label="Bank statement (.csv, .ofx, .qfx)"
            accept=".csv,.ofx,.qfx"
            selectedFileName={file?.name}
            disabled={uploading}
            onFileSelected={handleFileSelected}
          />
          <div className="wizard-actions">
            <button type="button" className="btn" disabled={uploading || !file} onClick={() => void startUpload()}>
              {uploading ? "Uploading…" : "Import"}
            </button>
            <button type="button" className="acct-link" disabled={uploading} onClick={() => setStep("account")}>
              Back
            </button>
          </div>
        </div>
      )}

      {step === "csv-mapping" && detection && (
        <div className="card">
          <h2>Match up the columns</h2>
          {mappingMode === "remap" ? (
            <p className="sub" style={{ marginBottom: 4 }}>
              Update the column layout for this account. We've pre-filled what's currently saved — fix
              whatever's wrong, then save and re-import. This won't change any statement already imported;
              only what's imported from here on.
            </p>
          ) : (
            <p className="sub" style={{ marginBottom: 4 }}>
              This account doesn't have a saved column layout yet — tell us what each column means. We
              guessed based on your file; check that it's right, since this layout will be reused for every
              future import on this account.
            </p>
          )}
          {detection.headerRow === null && (
            <p className="sub" style={{ marginBottom: 12 }}>
              This file doesn't look like it has a header row, so every row shown below is a real
              transaction.
            </p>
          )}

          <div style={{ overflowX: "auto", marginTop: 12 }}>
            <table className="mapping-table">
              <thead>
                <tr>
                  {Array.from({ length: detection.columnCount }).map((_, i) => (
                    <th key={i}>
                      <select
                        className="select-input"
                        aria-label={`Column ${i + 1} role`}
                        value={roles[i] ?? "ignore"}
                        onChange={(ev) => updateRole(i, ev.target.value as ColumnRole)}
                      >
                        {COLUMN_ROLE_OPTIONS.map((o) => (
                          <option key={o.value} value={o.value}>
                            {o.label}
                          </option>
                        ))}
                      </select>
                    </th>
                  ))}
                </tr>
                {detection.headerRow && (
                  <tr>
                    {detection.headerRow.map((h, i) => (
                      <th key={i} style={{ fontWeight: 400, color: "var(--ink-muted)" }}>
                        {h}
                      </th>
                    ))}
                  </tr>
                )}
              </thead>
              <tbody>
                {detection.sampleRows.map((row, ri) => (
                  <tr key={ri}>
                    {Array.from({ length: detection.columnCount }).map((_, ci) => (
                      <td key={ci}>{row[ci] ?? ""}</td>
                    ))}
                  </tr>
                ))}
              </tbody>
            </table>
          </div>

          <div className="field-row" style={{ marginTop: 16 }}>
            <div className="field" style={{ maxWidth: 260 }}>
              <label htmlFor="date-format">Date format</label>
              <select
                id="date-format"
                className="select-input"
                value={dateFormat}
                onChange={(ev) => setDateFormat(ev.target.value as DateFormat)}
              >
                {DATE_FORMAT_OPTIONS.map((o) => (
                  <option key={o.value} value={o.value}>
                    {o.label}
                  </option>
                ))}
              </select>
              {dateSampleRaw && (
                <p className="sub" style={{ marginTop: 4 }}>
                  {dateExample ? `Example: "${dateSampleRaw}" → ${dateExample}` : `"${dateSampleRaw}" doesn't look right in this format.`}
                </p>
              )}
            </div>
            <div className="field" style={{ maxWidth: 160 }}>
              <label htmlFor="header-rows">Header rows to skip</label>
              <input
                id="header-rows"
                className="text-input"
                type="number"
                min={0}
                value={headerRows}
                onChange={(ev) => setHeaderRows(Math.max(0, Number(ev.target.value) || 0))}
              />
            </div>
          </div>

          <div className="field" style={{ marginTop: 4 }}>
            <label className="method-option">
              <input
                type="checkbox"
                checked={negateAmount}
                onChange={(ev) => setNegateAmount(ev.target.checked)}
              />
              <span>Positive numbers mean money out in this file</span>
            </label>
          </div>

          <p className="sub" style={{ marginTop: 10 }}>
            Reading amounts from{" "}
            {mapping.amountMode === "debit-credit" ? "separate Debit and Credit columns" : "one signed Amount column"}
            , based on the columns you picked above.
          </p>

          {mappingErrors.length > 0 && (
            <ul className="error-groups" style={{ marginTop: 12 }}>
              {mappingErrors.map((e, i) => (
                <li key={i}>
                  <span>{e}</span>
                </li>
              ))}
            </ul>
          )}

          {uploadError && (
            <div style={{ marginTop: 12 }}>
              <ErrorBanner message={uploadError} onRetry={() => void submitMapping()} />
            </div>
          )}

          <div className="wizard-actions">
            <button
              type="button"
              className="btn"
              disabled={uploading || mappingErrors.length > 0}
              onClick={() => void submitMapping()}
            >
              {uploading
                ? (mappingMode === "remap" ? "Saving…" : "Importing…")
                : (mappingMode === "remap" ? "Save & re-import" : "Import")}
            </button>
            <button type="button" className="acct-link" disabled={uploading} onClick={cancelMapping}>
              {mappingMode === "remap" ? "Cancel" : "Choose a different file"}
            </button>
          </div>
        </div>
      )}

      {step === "result" && result && (
        <div className="card success-screen">
          <h2>Statement imported</h2>
          <p>{summarize(result)}</p>

          {result.usedSavedMap && (
            <p className="sub" style={{ marginTop: 8 }}>
              Used your saved column layout for this account.{" "}
              <button type="button" className="acct-link" disabled={uploading} onClick={() => void openRemap()}>
                Change it
              </button>
            </p>
          )}

          {uploadError && (
            <div style={{ marginTop: 12 }}>
              <ErrorBanner message={uploadError} onRetry={() => void openRemap()} />
            </div>
          )}

          {result.prefills.endingBalance && (
            <p className="sub" style={{ marginTop: 8 }}>
              Ending balance {fmtMoney(toDbAmountShape(result.prefills.endingBalance))} read from the file
              {result.prefills.periodEnd ? ` (as of ${fmtDate(result.prefills.periodEnd)})` : ""}.
            </p>
          )}

          <div className="wizard-actions">
            <button
              type="button"
              className="btn"
              onClick={() => navigate({ view: "bank-statement", id: result.statementId })}
            >
              Review now
            </button>
            <button type="button" className="acct-link" onClick={() => navigate("home")}>
              Back to Home
            </button>
          </div>
        </div>
      )}
    </div>
  );
}
