// Setup wizard — Task 10 of docs/superpowers/plans/2026-07-31-local-app.md.
// One component owns the step state machine internally, per the task brief;
// a handful of small presentational pieces (Dropzone, YearReportRow) live
// alongside it in this directory.
//
// ENTRY POINTS. This header used to say "reached two ways", and that stale
// premise is what kept a bug alive: the wizard now has several doors, and
// they do NOT all mean the same thing.
//   - App.tsx's zero-companies first run (plain #setup, no company exists)
//   - the company switcher's "+ Add company…" (Sidebar.tsx) — #/setup?new=1
//   - Settings' "+ Add another company" — #/setup?new=1
//   - a typed URL, a bookmark, or Back into a half-finished setup (#/setup)
// The first three are the reason the `entry` prop exists. Do not add a door
// without deciding which SetupEntry it states.
//
// RESUMABILITY (design spec, kept simple per the task brief) applies to the
// no-intent doors only. On mount, if a company is already selected in context,
// its accounts are checked once. Zero accounts means a company row exists but
// setup never finished (e.g. the tab closed after step 1) — skip straight to
// "choose a path" for it. More than zero means this is an established company:
// offer to continue to journal upload (in case the chart got created but the
// journal never got posted) or go to the dashboard, with an escape hatch to
// start a genuinely new company instead.
//
// Arriving with entry="new-company" skips that entirely — the user already
// told us what they want, so re-deriving an intent from whichever company the
// switcher happens to show is not a helpful guess, it is contradicting them.
// The decision lives in setupEntry.ts (pure, and tested there) rather than
// inline here, precisely so it cannot drift back into an inference.
//
// Nothing is checked again after the first mount-time pass — every subsequent
// step transition is driven by this component's own state, not by re-polling
// the server.

import { useEffect, useState } from "react";
import { useCompany } from "../lib/company";
import { apiGet, apiPost, ApiError } from "../lib/api";
import { apiUpload } from "./apiUpload";
import { Dropzone } from "./Dropzone";
import { YearReportRow, type YearReportData } from "./YearReportRow";
import { groupPreview, type PreviewAccount } from "./chartPreview";
import { UniversalWizard } from "./UniversalWizard";
import { ErrorBanner } from "../components/ErrorBanner";
import { replaceHash, type NavTarget } from "../lib/useHashView";
import { asksAboutSelectedCompany, setupStepFor, type SetupEntry } from "./setupEntry";

// "universal" (Switch From Anywhere Task 5) is the "Restore from other
// software" path — one more arm of this same step machine, but its several
// internal steps (source picker, chart upload/map/commit, journal
// upload/map/stage/post) live in UniversalWizard.tsx rather than swelling
// this file. Company creation/naming stays here, exactly as for the other
// paths, and the wizard is only ever mounted once a companyId exists; it
// backs out to "choose-path" and hands off to the same navigate() targets.
type Step =
  | "checking"
  | "resume-notice"
  | "name"
  | "choose-path"
  | "fresh-picker"
  | "fresh-preview"
  | "fresh-done"
  | "qb-chart"
  | "qb-chart-report"
  | "qb-journal"
  | "qb-journal-report"
  | "qb-done"
  | "universal"
  | "sample-done";

interface CreateCompanyResponse {
  id: string;
  name: string;
}

interface FreshSetupResponse {
  accountsCreated: number;
}

// One card from GET /api/setup/templates — picker copy plus the full chart
// that template stamps, so the preview step shows exactly what will be
// created (no hardcoded client-side account list anywhere).
interface TemplateCard {
  key: string;
  label: string;
  blurb: string;
  examples: string[];
  accounts: PreviewAccount[];
}

interface SampleCompanyResponse {
  companyId: string;
  name: string;
}

interface ChartImportResponse {
  companyCreated: boolean;
  accountsCreated: number;
  accountsSkipped: number;
  aliasesCreated: number;
  aliasesSkipped: number;
}

interface ChartImportErrorBody {
  error?: string;
  duplicateNames?: string[];
  unknownTypes?: { name: string; qbType: string }[];
  conflicts?: { name: string; existingType: string; mappedType: string; qbType: string }[];
}

interface JournalImportResponse {
  years: YearReportData[];
}

interface PostYearResult {
  year: number;
  batchId: number;
  posted: number | null;
  alreadyPosted: boolean;
  reconciliation: { matches?: boolean } | null;
  error?: string;
}

interface PostResponse {
  years: PostYearResult[];
}

interface PostErrorBody {
  error?: string;
  dirtyYears?: number[];
}

function isRecord(body: unknown): body is Record<string, unknown> {
  return typeof body === "object" && body !== null;
}

function ChartErrorDetail({ body }: { body: unknown }) {
  if (!isRecord(body)) return null;
  const b = body as ChartImportErrorBody;
  if (b.duplicateNames?.length) {
    return (
      <ul className="error-groups">
        {b.duplicateNames.map((name) => (
          <li key={name}>
            <span>&ldquo;{name}&rdquo; appears more than once in the file</span>
          </li>
        ))}
      </ul>
    );
  }
  if (b.unknownTypes?.length) {
    return (
      <ul className="error-groups">
        {b.unknownTypes.map((row, i) => (
          <li key={i}>
            <span>
              {row.name} &mdash; unrecognized QuickBooks type &ldquo;{row.qbType}&rdquo;
            </span>
          </li>
        ))}
      </ul>
    );
  }
  if (b.conflicts?.length) {
    return (
      <ul className="error-groups">
        {b.conflicts.map((row, i) => (
          <li key={i}>
            <span>
              {row.name} &mdash; already exists as {row.existingType}, this file expects {row.mappedType}
            </span>
          </li>
        ))}
      </ul>
    );
  }
  return null;
}

export function SetupWizard({
  navigate,
  entry = "unspecified",
}: {
  navigate: (target: NavTarget) => void;
  /** Which door the user came through — see the file header. Defaults to
   *  "unspecified" so a bare #setup (typed URL, first run) behaves exactly as
   *  it always has. */
  entry?: SetupEntry;
}) {
  const { selectedCompanyId, companies, selectCompany, refresh } = useCompany();

  // "checking" only for a door that has something to check; "+ Add company…"
  // opens straight on the name step with no one-frame "One moment…" flash.
  const [step, setStep] = useState<Step>(() =>
    setupStepFor(entry, { selectedCompanyId, accountsCount: undefined }),
  );
  const [companyId, setCompanyId] = useState<string | null>(null);
  const [companyName, setCompanyName] = useState<string>("");
  const [resumeAccountsCount, setResumeAccountsCount] = useState(0);
  // Which screen the "Restore from other software" path was entered from
  // (review F8). Backing out of it used to always land on "choose-path", a
  // screen written for a brand-new empty company — its sample card claims
  // "Your new company X stays empty", which is simply false for a company
  // that already has books, and there was no way back except a reload.
  const [universalReturnStep, setUniversalReturnStep] = useState<Step>("choose-path");

  const [busy, setBusy] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [errorBody, setErrorBody] = useState<unknown>(null);
  // The "name" step now offers TWO actions that can each fail (create a
  // company, or explore with sample data) sharing the one `error` state
  // above — this remembers which one to retry so the ErrorBanner there
  // doesn't always retry the wrong handler.
  const [nameStepErrorSource, setNameStepErrorSource] = useState<"name" | "sample" | null>(null);

  const [nameInput, setNameInput] = useState("");

  const [freshResult, setFreshResult] = useState<FreshSetupResponse | null>(null);

  // Industry picker ("What kind of work do you do?"). templates stays null
  // until the one fetch on entering the picker step resolves; if that fetch
  // fails we FAIL OPEN (same convention as the mount-time resumability check
  // above): skip the picker, keep 'general' selected, and let the preview
  // step degrade to a one-line description — a server hiccup must never
  // block someone from finishing setup with the standard chart.
  const [templates, setTemplates] = useState<TemplateCard[] | null>(null);
  const [templateKey, setTemplateKey] = useState("general");

  const [sampleResult, setSampleResult] = useState<SampleCompanyResponse | null>(null);

  const [chartFile, setChartFile] = useState<File | null>(null);
  const [chartReport, setChartReport] = useState<ChartImportResponse | null>(null);

  const [journalFile, setJournalFile] = useState<File | null>(null);
  const [journalYears, setJournalYears] = useState<YearReportData[] | null>(null);

  const [postResult, setPostResult] = useState<PostResponse | null>(null);

  // Everything the wizard accumulates for ONE company. Cleared whenever it
  // is pointed at a different one (see the switch guard below), so no file,
  // report or post result can survive into another company's flow.
  function resetFlowState() {
    setError(null);
    setErrorBody(null);
    setNameStepErrorSource(null);
    setNameInput("");
    setFreshResult(null);
    setTemplateKey("general");
    setSampleResult(null);
    setChartFile(null);
    setChartReport(null);
    setJournalFile(null);
    setJournalYears(null);
    setPostResult(null);
    setResumeAccountsCount(0);
  }

  /**
   * Review F3: the company switcher sits on screen throughout, but every
   * request this wizard makes carries ITS OWN companyId — the one claimed on
   * mount or created in step 1 — not whatever the sidebar now shows. A user
   * who switched companies mid-restore therefore kept importing into the
   * company they had just navigated away from, with the switcher confidently
   * displaying the other name. This points the wizard at whatever company is
   * actually selected and starts it over there.
   *
   * Deliberately NOT a key={selectedCompanyId} remount at App.tsx's two
   * mount sites: this component calls selectCompany() itself (step 1 after
   * creating a company, and "Explore with sample data"), and a blind remount
   * would discard the success screen those paths are in the middle of
   * showing. Comparing against the company the wizard has CLAIMED tells the
   * two apart — the wizard's own calls always set companyId in the same
   * batch, so they never look like an outside switch.
   */
  useEffect(() => {
    if (!selectedCompanyId || companyId === null || selectedCompanyId === companyId) return;
    let cancelled = false;
    const switchedTo = selectedCompanyId;
    async function adopt() {
      const name = companies.find((c) => c.id === switchedTo)?.name ?? "This company";
      let count = 0;
      try {
        const accounts = await apiGet<unknown[]>("/api/c/accounts", switchedTo);
        count = Array.isArray(accounts) ? accounts.length : 0;
      } catch {
        // fall through — treat as "not set up yet" rather than trapping
      }
      if (cancelled) return;
      resetFlowState();
      setCompanyId(switchedTo);
      setCompanyName(name);
      setResumeAccountsCount(count);
      // Deliberately "unspecified" whatever door this mount came through:
      // picking a specific company out of the switcher is itself a fresh,
      // more recent statement of intent, and it is about THAT company. (The
      // effect above can't fire before the wizard owns a companyId anyway, so
      // an entry="new-company" arrival never reaches here until the user has
      // created something.) A failed lookup leaves count at 0 → "choose-path",
      // exactly as before.
      setStep(setupStepFor("unspecified", { selectedCompanyId: switchedTo, accountsCount: count }));
    }
    void adopt();
    return () => {
      cancelled = true;
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps -- companies is read for a label only
  }, [selectedCompanyId, companyId]);

  // One-time entry decision — see the file header. Deliberately an empty
  // dependency array: this must run exactly once per mount (every time the
  // user navigates TO #setup, since App.tsx unmounts this component when they
  // navigate away), never again mid-wizard — refiring later (e.g. right after
  // this same wizard creates a fresh chart) would wrongly re-diagnose the
  // company it just finished setting up as "already set up, want to resume?".
  // `entry` is read here for the same reason: it describes the ARRIVAL, so it
  // is read on arrival and never again (the wizard's own transitions replace
  // it — handleNameSubmit spends it, see replaceHash there).
  useEffect(() => {
    let cancelled = false;
    async function check() {
      // The user stated what they came for ("+ Add company…"), so there is
      // nothing to resume and nothing to ask: no request is made at all, and
      // the selected company is left strictly alone.
      if (!asksAboutSelectedCompany(entry) || !selectedCompanyId) {
        setStep(setupStepFor(entry, { selectedCompanyId, accountsCount: null }));
        return;
      }
      let count: number | null = null; // null = the lookup failed; fails open to "name"
      try {
        const accounts = await apiGet<unknown[]>("/api/c/accounts", selectedCompanyId);
        if (cancelled) return;
        count = Array.isArray(accounts) ? accounts.length : 0;
        const name = companies.find((c) => c.id === selectedCompanyId)?.name ?? "This company";
        setCompanyId(selectedCompanyId);
        setCompanyName(name);
        setResumeAccountsCount(count);
      } catch {
        if (cancelled) return; // fail open — never trap the wizard on a failed check
      }
      setStep(setupStepFor(entry, { selectedCompanyId, accountsCount: count }));
    }
    void check();
    return () => {
      cancelled = true;
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps -- deliberately mount-only, see comment above
  }, []);

  // Fetch the template list the first time the picker step is shown. Keyed
  // on `step` rather than fired on mount so the QB-restore and sample paths
  // never pay for a request they don't use; if the fetch fails, advance
  // straight to the preview (fail open — see the templates state comment).
  // Re-entering the picker after a failure retries naturally, since
  // `templates` is still null.
  useEffect(() => {
    if (step !== "fresh-picker" || templates !== null) return;
    let cancelled = false;
    async function load() {
      try {
        const res = await apiGet<TemplateCard[]>("/api/setup/templates");
        if (!cancelled) setTemplates(res);
      } catch {
        if (!cancelled) setStep("fresh-preview");
      }
    }
    void load();
    return () => {
      cancelled = true;
    };
  }, [step, templates]);

  const selectedTemplate = templates?.find((t) => t.key === templateKey) ?? null;

  // "Or, set up a different company instead" on the resume notice — the same
  // statement of intent the switcher's "+ Add company…" makes, just made a
  // screen later. It goes into the URL for the same reason: F5 here must keep
  // naming a new company, not bounce back to the resume notice. Replaced, not
  // pushed, so Back still leaves the wizard rather than landing on a history
  // entry that would silently re-arm the intent.
  function startNewCompany() {
    setCompanyId(null);
    setCompanyName("");
    setError(null);
    setErrorBody(null);
    replaceHash({ view: "setup", intent: "new-company" });
    setStep("name");
  }

  async function handleNameSubmit() {
    const name = nameInput.trim();
    if (!name) return;
    setBusy(true);
    setError(null);
    setNameStepErrorSource("name");
    try {
      const res = await apiPost<CreateCompanyResponse>("/api/companies", { name });
      setCompanyId(res.id);
      setCompanyName(res.name);
      // Order matters: refresh() must land BEFORE selectCompany() — see
      // lib/company.tsx's refresh() comment. Selecting first would race the
      // CompanyProvider's auto-select-if-exactly-one-company effect against
      // the still-stale (pre-refetch) companies list and silently snap the
      // selection back to whatever was selected before this company existed.
      await refresh();
      selectCompany(res.id);
      // The "add a company" intent is now SPENT — the company exists. Drop it
      // from the URL (in place; see replaceHash) so a reload from here resumes
      // this half-finished setup instead of restarting at "name" and orphaning
      // the company just created.
      replaceHash("setup");
      setStep("choose-path");
    } catch (err) {
      setError(err instanceof ApiError ? err.message : "Couldn't create the company.");
    } finally {
      setBusy(false);
    }
  }

  async function handleFreshConfirm() {
    if (!companyId) return;
    setBusy(true);
    setError(null);
    try {
      const res = await apiPost<FreshSetupResponse>(
        "/api/c/setup/fresh", { templateKey }, companyId,
      );
      setFreshResult(res);
      setStep("fresh-done");
    } catch (err) {
      setError(err instanceof ApiError ? err.message : "Couldn't create the starter chart.");
    } finally {
      setBusy(false);
    }
  }

  // "Explore with sample data" — reachable from TWO places:
  //  - the "name" step, BEFORE any company has been created (the common
  //    case: a first-run/zero-companies user, or anyone who'd rather look
  //    around before typing their own company's name) — nothing is left
  //    behind here, since this never touches POST /api/companies at all.
  //  - the "choose-path" step, AFTER the "name" step already created a real
  //    (still-empty) company — that company is simply left as-is, same as
  //    if the user had abandoned setup after naming it (the resumability
  //    check at the top of this component already handles resuming it fine
  //    later); this path exists for someone who typed a name, then changed
  //    their mind about which one to set up first.
  // Either way this creates a wholly separate, pre-populated company. POST
  // /api/sample-company needs no X-Company-Id (there's no company to scope
  // into yet — same reason POST /api/companies doesn't either).
  async function handleSampleConfirm() {
    setBusy(true);
    setError(null);
    setNameStepErrorSource("sample");
    try {
      const res = await apiPost<SampleCompanyResponse>("/api/sample-company");
      setSampleResult(res);
      // Same ordering rule as handleNameSubmit above: refresh() must land
      // BEFORE selectCompany(), or the CompanyProvider's auto-select effect
      // can race the still-stale companies list and revert the selection.
      await refresh();
      selectCompany(res.companyId);
      setCompanyId(res.companyId);
      setCompanyName(res.name);
      // Same reasoning as handleNameSubmit: a company now exists, so the
      // intent is spent and the URL should stop asserting it.
      replaceHash("setup");
      setStep("sample-done");
    } catch (err) {
      setError(err instanceof ApiError ? err.message : "Couldn't create the sample company.");
    } finally {
      setBusy(false);
    }
  }

  async function handleChartUpload() {
    if (!companyId || !chartFile) return;
    setBusy(true);
    setError(null);
    setErrorBody(null);
    try {
      const res = await apiUpload<ChartImportResponse>("/api/c/import/chart", chartFile, companyId);
      setChartReport(res);
      setStep("qb-chart-report");
    } catch (err) {
      if (err instanceof ApiError) {
        setError(err.message);
        setErrorBody(err.body);
      } else {
        setError("Couldn't upload that file.");
      }
    } finally {
      setBusy(false);
    }
  }

  async function handleJournalUpload() {
    if (!companyId || !journalFile) return;
    setBusy(true);
    setError(null);
    setErrorBody(null);
    try {
      const res = await apiUpload<JournalImportResponse>("/api/c/import/journal", journalFile, companyId);
      setJournalYears(res.years);
      setStep("qb-journal-report");
    } catch (err) {
      setError(err instanceof ApiError ? err.message : "Couldn't upload that file.");
    } finally {
      setBusy(false);
    }
  }

  async function handlePost() {
    if (!companyId || !journalYears) return;
    setBusy(true);
    setError(null);
    setErrorBody(null);
    try {
      const years = journalYears.map((y) => y.year);
      const res = await apiPost<PostResponse>("/api/c/import/post", { years }, companyId);
      setPostResult(res);
      setStep("qb-done");
    } catch (err) {
      if (err instanceof ApiError) {
        setError(err.message);
        setErrorBody(err.body);
      } else {
        setError("Couldn't post to the ledger.");
      }
    } finally {
      setBusy(false);
    }
  }

  const heading = (() => {
    switch (step) {
      case "checking":
        return "Setting up your books";
      case "resume-notice":
        return `Welcome back${companyName ? `, ${companyName}` : ""}`;
      case "name":
        return "Let's set up your company";
      case "sample-done":
        return "Sample company ready";
      default:
        return companyName ? `Setting up ${companyName}` : "Set up a company";
    }
  })();

  return (
    <div className="wrap setup-wrap">
      <header className="top">
        <h1>{heading}</h1>
      </header>

      {step === "checking" && <p className="msg-inline">One moment&hellip;</p>}

      {step === "resume-notice" && (
        <div className="card">
          <p>
            {companyName} already has a chart of accounts ({resumeAccountsCount} accounts). What would you
            like to do?
          </p>
          <div className="wizard-actions">
            <button type="button" className="btn" onClick={() => setStep("qb-journal")}>
              Continue to journal upload
            </button>
            <button type="button" className="btn" onClick={() => navigate("dashboard")}>
              Go to dashboard
            </button>
          </div>
          <p className="sub" style={{ marginTop: 14 }}>
            <button
              type="button"
              className="acct-link"
              onClick={() => {
                setUniversalReturnStep("resume-notice");
                setStep("universal");
              }}
            >
              Or, bring in history from other software (Sage, Xero, a spreadsheet&hellip;)
            </button>
          </p>
          <p className="sub" style={{ marginTop: 6 }}>
            <button type="button" className="acct-link" onClick={startNewCompany}>
              Or, set up a different company instead
            </button>
          </p>
        </div>
      )}

      {step === "name" && (
        <div className="card">
          <p className="sub" style={{ marginBottom: 12 }}>
            What's the name of the company you're setting up? You can add more companies later.
          </p>
          {error && (
            <ErrorBanner
              message={error}
              onRetry={nameStepErrorSource === "sample" ? handleSampleConfirm : handleNameSubmit}
            />
          )}
          <div className="field">
            <label htmlFor="company-name">Company name</label>
            <input
              id="company-name"
              className="text-input"
              type="text"
              value={nameInput}
              disabled={busy}
              onChange={(ev) => setNameInput(ev.target.value)}
              onKeyDown={(ev) => {
                if (ev.key === "Enter") void handleNameSubmit();
              }}
              placeholder="e.g. Acme Landscaping LLC"
              autoFocus
            />
          </div>
          <div className="wizard-actions">
            <button
              type="button"
              className="btn"
              disabled={busy || !nameInput.trim()}
              onClick={() => void handleNameSubmit()}
            >
              {busy ? "Creating…" : "Continue"}
            </button>
          </div>
          <p className="sub" style={{ marginTop: 14 }}>
            <button type="button" className="acct-link" disabled={busy} onClick={() => void handleSampleConfirm()}>
              &hellip;or explore with sample data first
            </button>
          </p>
        </div>
      )}

      {step === "choose-path" && companyId && (
        <>
          {error && <ErrorBanner message={error} onRetry={handleSampleConfirm} />}
          <div className="path-cards">
            <div className="path-card">
              <h2>Start fresh</h2>
              <p>
                Tell us what kind of work you do and we'll set up everyday categories that fit, so
                you're never staring at an empty ledger. Good for a company with no prior bookkeeping
                to bring in.
              </p>
              <button
                type="button"
                className="btn"
                onClick={() => {
                  setError(null);
                  setStep("fresh-picker");
                }}
              >
                Start fresh
              </button>
            </div>
            <div className="path-card">
              <h2>Restore from QuickBooks</h2>
              <p>You'll need two exports from QuickBooks Desktop:</p>
              <ol className="menu-path-list">
                <li>
                  <div className="menu-path">Reports &rarr; List &rarr; Account Listing</div>
                  <div className="sub">Check &ldquo;Include inactive&rdquo; so no account gets skipped.</div>
                </li>
                <li>
                  <div className="menu-path">Reports &rarr; Accountant &amp; Taxes &rarr; Journal</div>
                  <div className="sub">Set the date range to All, then export to CSV/Excel.</div>
                </li>
              </ol>
              <button type="button" className="btn" onClick={() => setStep("qb-chart")}>
                Begin restore
              </button>
            </div>
            <div className="path-card">
              <h2>Restore from other software</h2>
              <p>
                Sage, Xero, Wave, FreshBooks &mdash; or anything else that can export a spreadsheet.
                You'll need two exports:
              </p>
              <ol className="menu-path-list">
                <li>
                  <div className="menu-path">The chart of accounts (your account list)</div>
                  <div className="sub">Include inactive accounts if it offers the choice.</div>
                </li>
                <li>
                  <div className="menu-path">The general ledger or journal, all dates</div>
                  <div className="sub">CSV or Excel, with a date, an account and an amount per line.</div>
                </li>
              </ol>
              <p className="sub">We'll guess the columns and you confirm.</p>
              <button
                type="button"
                className="btn"
                onClick={() => {
                  setError(null);
                  setErrorBody(null);
                  setUniversalReturnStep("choose-path");
                  setStep("universal");
                }}
              >
                Begin restore
              </button>
            </div>
            <div className="path-card">
              <h2>Explore with sample data</h2>
              <p>
                We'll set up a separate, fictional design agency &mdash; Riverbend Design Studio &mdash;
                with about a year of realistic invoices, bills, and a reconciled bank account, so you can
                click around before bringing in your own numbers. Your new company {companyName} stays
                empty &mdash; you can set it up any time.
              </p>
              <button type="button" className="btn" disabled={busy} onClick={() => void handleSampleConfirm()}>
                {busy ? "Setting up…" : "Explore with sample data"}
              </button>
            </div>
          </div>
        </>
      )}

      {step === "fresh-picker" && (
        <div className="card">
          <h2 style={{ fontSize: 17, marginBottom: 4 }}>What kind of work do you do?</h2>
          <p className="sub" style={{ marginBottom: 12 }}>
            We'll start you with everyday categories that fit — you can rename, add, or remove any of
            them later.
          </p>
          {templates === null ? (
            <p className="msg-inline">One moment&hellip;</p>
          ) : (
            <div className="tpl-cards" role="group" aria-label="Kind of work">
              {templates.map((t) => (
                <button
                  key={t.key}
                  type="button"
                  className="tpl-card"
                  aria-pressed={t.key === templateKey}
                  onClick={() => setTemplateKey(t.key)}
                >
                  <span className="tpl-card-label">{t.label}</span>
                  <span className="tpl-card-blurb">{t.blurb}</span>
                  <span className="tpl-card-examples">e.g. {t.examples.join(", ")}</span>
                </button>
              ))}
            </div>
          )}
          <div className="wizard-actions">
            <button
              type="button"
              className="btn"
              disabled={templates === null}
              onClick={() => setStep("fresh-preview")}
            >
              Continue
            </button>
            <button type="button" className="acct-link" onClick={() => setStep("choose-path")}>
              Back
            </button>
          </div>
        </div>
      )}

      {step === "fresh-preview" && (
        <div className="card">
          <p className="sub" style={{ marginBottom: 12 }}>
            {selectedTemplate
              ? `Here are the categories we'll set up for ${selectedTemplate.label.toLowerCase()}. ` +
                "You can rename, add, or remove any of them afterward."
              : "We couldn't load the category choices right now, so you'll get our standard set of " +
                "about 40 everyday categories. You can rename, add, or remove any of them afterward."}
          </p>
          {error && <ErrorBanner message={error} onRetry={handleFreshConfirm} />}
          {selectedTemplate && (
            <div className="preview-groups">
              {groupPreview(selectedTemplate.accounts).map((group) => (
                <div className="preview-group" key={group.heading}>
                  <h3>
                    {group.heading} <span className="preview-group-count">({group.count})</span>
                  </h3>
                  <p className="preview-group-subtitle">{group.subtitle}</p>
                  {group.sections.map((section) => (
                    <div className="preview-section" key={section.label || group.heading}>
                      {section.label !== "" && (
                        <h4 className="preview-section-label">{section.label}</h4>
                      )}
                      <ul>
                        {section.names.map((name) => (
                          <li key={name}>{name}</li>
                        ))}
                      </ul>
                    </div>
                  ))}
                </div>
              ))}
            </div>
          )}
          <div className="wizard-actions">
            <button type="button" className="btn" disabled={busy} onClick={() => void handleFreshConfirm()}>
              {busy ? "Creating…" : "Create these categories"}
            </button>
            <button
              type="button"
              className="acct-link"
              disabled={busy}
              onClick={() => setStep(templates !== null ? "fresh-picker" : "choose-path")}
            >
              Back
            </button>
          </div>
        </div>
      )}

      {step === "fresh-done" && (
        <div className="card success-screen">
          <h2>Your chart is ready</h2>
          <p>
            {companyName} now has {freshResult?.accountsCreated ?? 0} accounts ready to use — record your
            first transaction or explore the dashboard.
          </p>
          <div className="wizard-actions">
            <button type="button" className="btn" onClick={() => navigate("dashboard")}>
              Go to dashboard
            </button>
            <button type="button" className="btn" onClick={() => navigate("record")}>
              Record your first transaction
            </button>
          </div>
        </div>
      )}

      {step === "sample-done" && sampleResult && (
        <div className="card success-screen">
          <h2>{sampleResult.name} is ready</h2>
          <p>Poke around &mdash; it&apos;s all fictional. Your real companies are untouched.</p>
          <div className="wizard-actions">
            <button type="button" className="btn" onClick={() => navigate("home")}>
              See the home screen
            </button>
            <button type="button" className="btn" onClick={() => navigate("dashboard")}>
              Go to dashboard
            </button>
          </div>
        </div>
      )}

      {step === "universal" && companyId && (
        <UniversalWizard
          companyId={companyId}
          navigate={navigate}
          onExit={() => setStep(universalReturnStep)}
        />
      )}

      {step === "qb-chart" && (
        <div className="card">
          <p className="sub" style={{ marginBottom: 4 }}>
            Step 1 of 2: upload the Account Listing.
          </p>
          <div className="menu-path">Reports &rarr; List &rarr; Account Listing</div>
          <p className="sub" style={{ marginBottom: 12 }}>
            Check &ldquo;Include inactive&rdquo; before exporting, so no account gets left out — export to
            Excel (.xlsx).
          </p>
          {error && (
            <div style={{ marginBottom: 12 }}>
              <ErrorBanner message={error} onRetry={handleChartUpload} />
              <ChartErrorDetail body={errorBody} />
            </div>
          )}
          <Dropzone
            label="Account Listing (.xlsx)"
            selectedFileName={chartFile?.name}
            disabled={busy}
            onFileSelected={(file) => {
              setChartFile(file);
              setError(null);
              setErrorBody(null);
            }}
          />
          <div className="wizard-actions">
            <button
              type="button"
              className="btn"
              disabled={busy || !chartFile}
              onClick={() => void handleChartUpload()}
            >
              {busy ? "Uploading…" : "Upload and check"}
            </button>
            <button type="button" className="acct-link" disabled={busy} onClick={() => setStep("choose-path")}>
              Back
            </button>
          </div>
        </div>
      )}

      {step === "qb-chart-report" && chartReport && (
        <div className="card">
          <h2>Chart of accounts loaded</h2>
          <div className="rows">
            <div className="row">
              <span className="n">Accounts created</span>
              <span className="a">{chartReport.accountsCreated}</span>
            </div>
            <div className="row">
              <span className="n">Accounts already on file (skipped)</span>
              <span className="a">{chartReport.accountsSkipped}</span>
            </div>
            <div className="row">
              <span className="n">Shortcuts (aliases) created</span>
              <span className="a">{chartReport.aliasesCreated}</span>
            </div>
            <div className="row">
              <span className="n">Shortcuts already on file (skipped)</span>
              <span className="a">{chartReport.aliasesSkipped}</span>
            </div>
          </div>
          <div className="wizard-actions">
            <button type="button" className="btn" onClick={() => setStep("qb-journal")}>
              Continue to journal upload
            </button>
            <button type="button" className="acct-link" onClick={() => setStep("qb-chart")}>
              Upload a different Account Listing file
            </button>
          </div>
        </div>
      )}

      {step === "qb-journal" && (
        <div className="card">
          <p className="sub" style={{ marginBottom: 4 }}>
            Step 2 of 2: upload the Journal.
          </p>
          <div className="menu-path">Reports &rarr; Accountant &amp; Taxes &rarr; Journal</div>
          <p className="sub" style={{ marginBottom: 12 }}>
            Set the date range to All, then export to Excel (.xlsx). Make sure it&apos;s the
            Journal report — not the General Ledger report.
          </p>
          {error && <ErrorBanner message={error} onRetry={handleJournalUpload} />}
          <Dropzone
            label="Journal (.xlsx)"
            selectedFileName={journalFile?.name}
            disabled={busy}
            onFileSelected={(file) => {
              setJournalFile(file);
              setError(null);
              setErrorBody(null);
            }}
          />
          <div className="wizard-actions">
            <button
              type="button"
              className="btn"
              disabled={busy || !journalFile}
              onClick={() => void handleJournalUpload()}
            >
              {busy ? "Checking…" : "Upload and check"}
            </button>
          </div>
        </div>
      )}

      {step === "qb-journal-report" && journalYears && (
        <div className="card">
          <h2>Validation report</h2>
          <p className="sub" style={{ marginBottom: 12 }}>
            Every year found in the file, checked against your chart of accounts.
          </p>
          <div className="year-report-list">
            {journalYears.map((y) => (
              <YearReportRow key={y.year} year={y} />
            ))}
          </div>

          {journalYears.some((y) => y.errors > 0) && (
            <p className="sub" style={{ marginTop: 14 }}>
              Add the missing accounts or aliases, then upload the same file again — already-loaded years
              are re-checked automatically.
            </p>
          )}

          {error && (
            <div style={{ marginTop: 12 }}>
              <ErrorBanner message={error} onRetry={handlePost} />
              {isRecord(errorBody) && (errorBody as PostErrorBody).dirtyYears && (
                <p className="sub">
                  Still failing for: {(errorBody as PostErrorBody).dirtyYears!.join(", ")}
                </p>
              )}
            </div>
          )}

          <div className="wizard-actions">
            <button
              type="button"
              className="btn"
              disabled={busy || journalYears.some((y) => y.errors > 0)}
              onClick={() => void handlePost()}
            >
              {busy
                ? "Posting…"
                : `Post ${journalYears.length} ${journalYears.length === 1 ? "year" : "years"} to the ledger`}
            </button>
            <button type="button" className="acct-link" disabled={busy} onClick={() => setStep("qb-journal")}>
              Upload the file again
            </button>
          </div>
        </div>
      )}

      {step === "qb-done" && postResult && journalYears && (
        <div className="card success-screen">
          <h2>Posted to the ledger</h2>
          <p>
            {journalYears.reduce((sum, y) => sum + y.entries, 0)} entries posted across {journalYears.length}{" "}
            {journalYears.length === 1 ? "year" : "years"}.
          </p>
          <div className="rows">
            {postResult.years.map((y) => (
              <div className="row" key={y.year}>
                <span className="n">
                  {y.year}
                  {y.alreadyPosted ? " (already posted)" : ""}
                </span>
                <span className="a">
                  {y.reconciliation?.matches ? (
                    <span className="chip">Reconciled</span>
                  ) : y.reconciliation?.matches === false ? (
                    <span className="chip chip-bad">Doesn't reconcile</span>
                  ) : (
                    ""
                  )}
                </span>
              </div>
            ))}
          </div>
          <div className="wizard-actions">
            <button type="button" className="btn" onClick={() => navigate("dashboard")}>
              Go to dashboard
            </button>
          </div>
        </div>
      )}
    </div>
  );
}
