// PerduraOnboardingWizard — Stage 4 guided setup.
//
// A full-screen, resumable, multi-step flow that captures a new tenant's
// Company Profile (basics → archetype/industry → fiscal config → data source →
// optional COA review) and writes it onto the company profile + industry
// linkage on completion. State persists to the `onboarding_state` table after
// every step, so "Save & Exit" is always safe and the user resumes where they
// left off.
//
// Voice: master FP&A specialist — calm, precise, explains WHY each field
// matters, never marketing hype.
//
// Zero industry-conditional code. The archetype catalog below is a declarative
// projection of src/intelligence/archetypes.yaml (the canonical "archetypes
// config"); industry choices + their preview text are read live from the
// `industries` table, so a new admin-added industry appears here automatically.
//
// Rendered by app.jsx when view === "onboarding". Props:
//   scopedCompanyId, companyProfile, onComplete(), onExit()

(function () {
  const R = window.React;
  if (!R) return;
  const { useState, useEffect, useRef, useCallback } = R;
  const db = () => window.supabaseClient;

  // ── Canonical archetype catalog ───────────────────────────────────────────
  // Declarative DATA — a browser projection of archetypes.yaml (slug, name,
  // revenue_model → blurb, default_kpis → "what we track"). Not conditional
  // code. Keep in sync with src/intelligence/archetypes.yaml. The industry
  // dropdown (live from the industries table) refines language + benchmarks on
  // top of whichever archetype is chosen.
  const ARCHETYPE_CATALOG = [
    { slug: "saas",           name: "SaaS / Subscription Software",
      blurb: "Recurring subscription revenue recognized over the contract term.",
      tracks: ["MRR / ARR & deferred revenue", "Net revenue retention", "Gross margin", "Runway & operating cash flow", "Revenue concentration"] },
    { slug: "retail",         name: "Retail / Stores",
      blurb: "Transactional point-of-sale revenue; inventory-intensive, fast cash collection.",
      tracks: ["Gross margin", "Inventory turns & days on hand", "Cash conversion cycle", "DPO", "Days of cash"] },
    { slug: "manufacturing",  name: "Manufacturing",
      blurb: "Build-to-stock / build-to-order goods sold to B2B customers on terms.",
      tracks: ["Gross & EBITDA margin", "Cash conversion cycle", "DSO / DPO", "WIP & finished-goods inventory", "Cost per unit (when wired)"] },
    { slug: "distribution",   name: "Wholesale / Distribution",
      blurb: "Buy-and-resell to business customers on credit; volume-driven, thin margins.",
      tracks: ["Gross margin", "Inventory turns & days on hand", "Cash conversion cycle", "DSO / DPO", "Top-customer concentration"] },
    { slug: "ecommerce",      name: "E-commerce / D2C",
      blurb: "Online direct-to-consumer sales; card-up-front, negligible AR.",
      tracks: ["Gross margin", "Inventory turns", "Cash conversion cycle", "Days of cash", "Operating cash flow"] },
    { slug: "services",       name: "Professional Services",
      blurb: "Billable engagements — time & materials or fixed-fee — invoiced on terms.",
      tracks: ["Operating margin", "DSO & unbilled WIP", "Revenue per head", "Top-customer concentration", "Days of cash"] },
    { slug: "medical_clinic", name: "Medical Clinic / Outpatient",
      blurb: "Fee-for-service patient encounters billed to payers; net of contractual adjustments.",
      tracks: ["Net patient revenue & collection rate", "Payer-driven DSO", "Gross & operating margin", "Days of cash", "Payer concentration"] },
    { slug: "home_health",    name: "Home Health / In-home Care",
      blurb: "Per-visit / per-episode care billed to payers; authorization-gated.",
      tracks: ["Payer DSO", "Operating margin", "Days of cash", "Labor cost ratio", "Payer concentration"] },
    { slug: "restaurants",    name: "Restaurants / Food Service",
      blurb: "Transactional food & beverage across dayparts; prime cost is the margin lever.",
      tracks: ["Gross margin (prime cost)", "Opex-to-revenue", "Days of cash", "Operating cash flow", "DPO"] },
    { slug: "nonprofit",      name: "Not for Profit",
      blurb: "Donations + grants + program revenue; mission-driven, surplus reinvested.",
      tracks: ["Program vs. M&G vs. fundraising split", "Months of operating reserve", "Days of cash", "Restricted vs. unrestricted net assets", "Revenue concentration"] },
  ];
  const ARCHETYPE_BY_SLUG = Object.fromEntries(ARCHETYPE_CATALOG.map((a) => [a.slug, a]));

  // ── Currency defaults by country (sensible starting point, user can change) ─
  const COUNTRY_CURRENCY = {
    "United States": "USD", "Canada": "CAD", "United Kingdom": "GBP",
    "Australia": "AUD", "New Zealand": "NZD", "Ireland": "EUR", "Germany": "EUR",
    "France": "EUR", "Spain": "EUR", "Italy": "EUR", "Netherlands": "EUR",
    "Israel": "ILS", "India": "INR", "Mexico": "MXN", "Japan": "JPY",
  };
  const ISO_CURRENCY = /^[A-Z]{3}$/;
  const MONTHS = ["January","February","March","April","May","June","July","August","September","October","November","December"];

  // The five steps. COA (5) is conditional — only when a CSV upload carried a
  // chart of accounts; otherwise it is skipped automatically (see isStepActive).
  const STEPS = [
    { n: 1, key: "basics",   title: "Company basics" },
    { n: 2, key: "classify", title: "Archetype & industry" },
    { n: 3, key: "fiscal",   title: "Fiscal configuration" },
    { n: 4, key: "source",   title: "Connect accounting" },
    { n: 5, key: "coa",      title: "Account mapping" },
  ];

  // ── Persistence helpers ─────────────────────────────────────────────────
  async function loadState(companyId) {
    const sb = db(); if (!sb || !companyId) return null;
    const { data } = await sb.from("onboarding_state")
      .select("id,current_step,completed_steps,draft_profile,status")
      .eq("company_id", companyId).maybeSingle();
    return data || null;
  }
  async function saveState(companyId, patch) {
    const sb = db(); if (!sb || !companyId) return { error: "no client" };
    // Upsert on the unique company_id so a brand-new tenant (no row yet) is
    // created on first save. updated_at is app-managed (no trigger).
    const row = { company_id: companyId, updated_at: new Date().toISOString(), ...patch };
    const { error } = await sb.from("onboarding_state")
      .upsert(row, { onConflict: "company_id" });
    return { error };
  }

  // ════════════════════════════════════════════════════════════════════════
  // Step bodies are defined further down. Each receives { draft, patch } where
  // `draft` is the accumulated draft_profile and `patch(section, obj)` merges
  // into it. They render only inputs + helper copy; the shell owns navigation.
  // ════════════════════════════════════════════════════════════════════════

  function StepShell(props) {
    // props: scopedCompanyId, companyProfile, onComplete, onExit
    const companyId = props.scopedCompanyId;
    const [loading, setLoading]   = useState(true);
    const [saving, setSaving]     = useState(false);
    const [stepIdx, setStepIdx]   = useState(0);     // index into STEPS
    const [draft, setDraft]       = useState({});    // draft_profile
    const [completed, setCompleted] = useState([]);  // completed step numbers
    const [error, setError]       = useState(null);
    const [showWelcome, setShowWelcome] = useState(false); // Step 0 intro overlay
    const stateId = useRef(null);

    // One-time welcome dismissal, keyed per company (reuses existing state — no
    // schema change). See dashboard setup-progress bar in app.jsx.
    const welcomeKey = "perdura_welcomed_" + companyId;
    const markWelcomed = () => { try { localStorage.setItem(welcomeKey, "true"); } catch (_e) { /* private mode */ } };

    // Merge a section into the draft (immutably).
    const patch = useCallback((section, obj) => {
      setDraft((d) => ({ ...d, [section]: { ...(d[section] || {}), ...obj } }));
    }, []);

    // Whether a step is active for THIS tenant. The mapping step (5) shows when
    // a live GL connector is chosen (real accounts sync in) or a CSV upload
    // carried a chart of accounts; it's auto-skipped for the sample-data path.
    const isStepActive = useCallback((n, d) => {
      if (n !== 5) return true;
      const src = (d || draft).source || {};
      if (src.choice === "connector") return true;
      return src.choice === "csv" && !!src.has_coa;
    }, [draft]);

    const activeSteps = STEPS.filter((s) => isStepActive(s.n, draft));
    const step = activeSteps[Math.min(stepIdx, activeSteps.length - 1)] || activeSteps[0];

    // Load existing state on mount (resume).
    useEffect(() => {
      let alive = true;
      (async () => {
        const st = await loadState(companyId);
        if (!alive) return;
        let priorSteps = [];
        if (st) {
          stateId.current = st.id;
          setDraft(st.draft_profile || {});
          priorSteps = Array.isArray(st.completed_steps) ? st.completed_steps : [];
          setCompleted(priorSteps);
          // Resume on the saved step (clamp into the active range).
          const savedN = st.current_step || 1;
          const idx = STEPS.findIndex((s) => s.n === savedN);
          setStepIdx(idx >= 0 ? idx : 0);
        }
        // Show the welcome intro only on a genuinely fresh start (no step ever
        // completed) that hasn't already been dismissed on this device.
        let welcomed = false;
        try { welcomed = localStorage.getItem(welcomeKey) === "true"; } catch (_e) { /* ignore */ }
        if (priorSteps.length === 0 && !welcomed) setShowWelcome(true);
        setLoading(false);
      })();
      return () => { alive = false; };
    }, [companyId]);

    async function persist(nextStepN, nextCompleted, status) {
      setSaving(true); setError(null);
      const { error: e } = await saveState(companyId, {
        current_step: nextStepN,
        completed_steps: nextCompleted,
        draft_profile: draft,
        ...(status ? { status } : {}),
      });
      setSaving(false);
      if (e) { setError(typeof e === "string" ? e : (e.message || "Could not save your progress.")); return false; }
      return true;
    }

    async function goNext() {
      const nextCompleted = completed.includes(step.n) ? completed : [...completed, step.n];
      setCompleted(nextCompleted);
      // Recompute active steps with the latest draft (data-source choice may
      // have just turned the COA step on/off).
      const active = STEPS.filter((s) => isStepActive(s.n, draft));
      const curPos = active.findIndex((s) => s.n === step.n);
      if (curPos >= active.length - 1) { await complete(nextCompleted); return; }
      const nextStep = active[curPos + 1];
      const ok = await persist(nextStep.n, nextCompleted, "in_progress");
      if (ok) setStepIdx(STEPS.findIndex((s) => s.n === nextStep.n));
    }

    function goBack() {
      const active = STEPS.filter((s) => isStepActive(s.n, draft));
      const curPos = active.findIndex((s) => s.n === step.n);
      if (curPos <= 0) return;
      setStepIdx(STEPS.findIndex((s) => s.n === active[curPos - 1].n));
    }

    async function saveAndExit() {
      const ok = await persist(step.n, completed, "in_progress");
      if (ok && props.onExit) props.onExit();
    }

    async function complete(finalCompleted) {
      // Apply draft_profile to the company profile + industry linkage, then
      // mark onboarding complete. Implemented in Step 4.8.
      setSaving(true); setError(null);
      const res = await window.PerduraOnboardingWizard._applyCompletion(companyId, draft);
      if (res && res.error) { setSaving(false); setError(res.error); return; }
      await saveState(companyId, {
        current_step: step.n,
        completed_steps: finalCompleted,
        draft_profile: draft,
        status: "completed",
        completed_at: new Date().toISOString(),
      });
      setSaving(false);
      if (props.onComplete) props.onComplete(draft);
    }

    if (loading) {
      return R.createElement("div", { className: "pc-wiz-root" },
        R.createElement("div", { className: "pc-wiz-bg" }),
        R.createElement("div", { className: "pc-wiz-shell", style: { alignItems: "center", justifyContent: "center" } },
          R.createElement("div", { className: "pc-wiz-lead", style: { padding: 48 } }, "Loading your setup…")));
    }

    // ── Step 0 · Welcome intro ────────────────────────────────────────────────
    if (showWelcome) {
      const cp = props.companyProfile || {};
      const companyName = cp.display_name || cp.name || "your business";
      const firstName = (() => {
        const n = (cp.owner_name || "").trim();
        return n ? n.split(/\s+/)[0] : null;
      })();
      const startSetup = () => { markWelcomed(); setShowWelcome(false); };
      const skipForNow = () => { markWelcomed(); if (props.onExit) props.onExit(); };
      const stepRow = (label, meta) => (
        <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", padding: "12px 16px", borderTop: "1px solid var(--border, #e3e8ef)" }}>
          <span style={{ fontSize: 13.5, color: "var(--text-1)" }}>{label}</span>
          <span style={{ fontSize: 12.5, fontWeight: 600, color: "var(--text-3)" }}>{meta}</span>
        </div>
      );
      return (
        <div className="pc-wiz-root">
          <div className="pc-wiz-bg" />
          <div className="pc-wiz-shell" style={{ alignItems: "center", justifyContent: "center" }}>
            <div style={{ maxWidth: 560, width: "100%", padding: "8px 24px" }}>
              <div style={{ fontSize: 34, marginBottom: 12 }}>👋</div>
              <h1 className="pc-wiz-title">Welcome to PerduraCFO</h1>
              <p className="pc-wiz-lead" style={{ marginBottom: 24 }}>
                {firstName ? `Hi ${firstName}, your` : "Your"} CFO intelligence platform is ready.
                Let's get your data connected so we can start generating insights for{" "}
                <strong style={{ color: "var(--text-1)" }}>{companyName}</strong>. This takes about 3 minutes.
              </p>
              <div style={{ border: "1px solid var(--border, #e3e8ef)", borderRadius: 12, overflow: "hidden", marginBottom: 28 }}>
                {stepRow("Step 1 · Connect your accounting software", "~2 min")}
                {stepRow("Step 2 · Map your accounts", "~1 min")}
                {stepRow("Step 3 · Your dashboard goes live", "✨")}
              </div>
              <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", gap: 12 }}>
                <button className="pc-btn pc-btn-ghost" onClick={skipForNow}>Skip for now</button>
                <button className="pc-btn pc-btn-primary" onClick={startSetup}>Let's get started →</button>
              </div>
            </div>
          </div>
        </div>
      );
    }

    const Body = STEP_BODIES[step.key] || (() => R.createElement("div", { className: "pc-wiz-lead" }, "…"));
    const isLast = (() => {
      const active = STEPS.filter((s) => isStepActive(s.n, draft));
      return active.findIndex((s) => s.n === step.n) >= active.length - 1;
    })();
    const validation = (STEP_VALIDATORS[step.key] || (() => null))(draft);
    const canContinue = !validation && !saving;

    return (
      <div className="pc-wiz-root">
        <div className="pc-wiz-bg" />
        <div className="pc-wiz-shell">
          <button className="pc-wiz-close" onClick={saveAndExit} title="Save & exit setup">×</button>

          {/* Progress rail */}
          <div className="pc-wiz-side">
            <div className="pc-wiz-eyebrow">Guided setup</div>
            <div className="pc-wiz-steps">
              {activeSteps.map((s, i) => {
                const done = completed.includes(s.n);
                const current = s.n === step.n;
                return (
                  <div key={s.key} className={"pc-wiz-step" + (current ? " is-current" : "") + (done ? " is-done" : "")}>
                    <div className="pc-wiz-step-dot">{done ? "✓" : (i + 1)}</div>
                    <div className="pc-wiz-step-label">{s.title}</div>
                  </div>
                );
              })}
            </div>
            <div className="pc-wiz-aside-foot">
              Step {activeSteps.findIndex((s) => s.n === step.n) + 1} of {activeSteps.length}.
              Your answers save as you go — you can leave and resume anytime.
            </div>
          </div>

          {/* Step body + footer */}
          <div className="pc-wiz-main">
            <div className="pc-wiz-pane">
              <Body draft={draft} patch={patch} companyProfile={props.companyProfile} />
            </div>
            {error && (
              <div style={{ color: "var(--danger, #c0392b)", padding: "8px 0", fontSize: 13 }}>
                {error}
              </div>
            )}
            <div className="pc-wiz-actions">
              <button className="pc-btn pc-btn-ghost" onClick={goBack}
                disabled={activeSteps.findIndex((s) => s.n === step.n) === 0 || saving}>
                Back
              </button>
              <div style={{ display: "flex", gap: 10, alignItems: "center" }}>
                <button className="pc-btn pc-btn-ghost" onClick={saveAndExit} disabled={saving}>
                  Save &amp; exit
                </button>
                <button className="pc-btn pc-btn-primary" onClick={goNext} disabled={!canContinue}
                  title={validation || ""}>
                  {saving ? "Saving…" : (isLast ? "Finish setup" : "Continue")}
                </button>
              </div>
            </div>
            {validation && (
              <div style={{ color: "var(--text-3)", fontSize: 12, textAlign: "right", marginTop: 6 }}>
                {validation}
              </div>
            )}
          </div>
        </div>
      </div>
    );
  }

  // ── Step bodies (filled in Steps 4.3–4.7) ────────────────────────────────
  const STEP_BODIES = {};
  const STEP_VALIDATORS = {};

  // Placeholder bodies — replaced in subsequent commits.
  ["basics", "classify", "fiscal", "source", "coa"].forEach((k) => {
    STEP_BODIES[k] = function Placeholder() {
      return R.createElement("div", null,
        R.createElement("h1", { className: "pc-wiz-title" }, "Coming up next"),
        R.createElement("p", { className: "pc-wiz-lead" }, "This step is being built."));
    };
    STEP_VALIDATORS[k] = () => null;
  });

  // ── Shared field helper ──────────────────────────────────────────────────
  function Field(props) {
    // { label, hint, children, optional }
    return (
      <label className="pc-wiz-field" style={{ display: "block", marginBottom: 18 }}>
        <div style={{ fontSize: 13, fontWeight: 600, color: "var(--text-1)", marginBottom: 4 }}>
          {props.label}
          {props.optional && <span style={{ color: "var(--text-3)", fontWeight: 400 }}> · optional</span>}
        </div>
        {props.children}
        {props.hint && (
          <div style={{ fontSize: 12, color: "var(--text-3)", marginTop: 5, lineHeight: 1.45 }}>{props.hint}</div>
        )}
      </label>
    );
  }
  const inputStyle = {
    width: "100%", padding: "9px 11px", borderRadius: 8,
    border: "1px solid var(--border, #d8dee6)", fontSize: 14, background: "var(--surface, #fff)",
    color: "var(--text-1)", boxSizing: "border-box",
  };

  // ── Step 1 · Company basics ──────────────────────────────────────────────
  STEP_BODIES.basics = function Basics(props) {
    const { draft, patch, companyProfile } = props;
    const b = draft.basics || {};
    // Prefill once from the existing company row (name/country/currency) so an
    // admin-created shell tenant isn't re-typed.
    useEffect(() => {
      if (draft.basics || !companyProfile) return;
      patch("basics", {
        legal_name: companyProfile.name || "",
        display_name: companyProfile.display_name || companyProfile.name || "",
        country: companyProfile.country || "United States",
        base_currency: companyProfile.base_currency || companyProfile.currency || "USD",
        website: companyProfile.website || "",
        description: "",
      });
      // eslint-disable-next-line
    }, []);
    const onCountry = (v) => {
      const cur = COUNTRY_CURRENCY[v];
      patch("basics", cur && !b.currency_touched ? { country: v, base_currency: cur } : { country: v });
    };
    return (
      <div>
        <div className="pc-wiz-eyebrow">Step 1</div>
        <h1 className="pc-wiz-title">Let's start with the essentials.</h1>
        <p className="pc-wiz-lead">
          These anchor every report we produce for you. A few minutes here keeps the
          numbers labeled correctly everywhere downstream.
        </p>
        <div className="pc-wiz-form" style={{ maxWidth: 560 }}>
          <Field label="Legal entity name"
            hint="The name on your filings and financial statements.">
            <input style={inputStyle} value={b.legal_name || ""} placeholder="Acme Industrial, Inc."
              onChange={(e) => patch("basics", { legal_name: e.target.value })} />
          </Field>
          <Field label="Display name"
            hint="What you'd like to see in the app header and on reports — often a shorter form of the legal name.">
            <input style={inputStyle} value={b.display_name || ""} placeholder="Acme Industrial"
              onChange={(e) => patch("basics", { display_name: e.target.value })} />
          </Field>
          <div style={{ display: "flex", gap: 16 }}>
            <div style={{ flex: 1 }}>
              <Field label="Country"
                hint="Sets sensible defaults — currency, date format — that you can override.">
                <select style={inputStyle} value={b.country || "United States"} onChange={(e) => onCountry(e.target.value)}>
                  {Object.keys(COUNTRY_CURRENCY).map((c) => <option key={c} value={c}>{c}</option>)}
                  <option value="Other">Other</option>
                </select>
              </Field>
            </div>
            <div style={{ flex: 1 }}>
              <Field label="Base currency"
                hint="Determines how every number is reported. You can change it later, but it's used everywhere — so set it right now.">
                <input style={inputStyle} value={b.base_currency || ""} placeholder="USD" maxLength={3}
                  onChange={(e) => patch("basics", { base_currency: e.target.value.toUpperCase(), currency_touched: true })} />
              </Field>
            </div>
          </div>
          <Field label="Website" optional
            hint="Helps us tailor industry context. Not shared anywhere.">
            <input style={inputStyle} value={b.website || ""} placeholder="https://acme.com"
              onChange={(e) => patch("basics", { website: e.target.value })} />
          </Field>
          <Field label="Brief description" optional
            hint="One or two sentences on what the business does — sharpens the language we use in your narratives.">
            <textarea style={{ ...inputStyle, minHeight: 64, resize: "vertical" }} value={b.description || ""}
              placeholder="We distribute industrial fasteners to regional contractors across the Midwest."
              onChange={(e) => patch("basics", { description: e.target.value })} />
          </Field>
        </div>
      </div>
    );
  };
  STEP_VALIDATORS.basics = function (draft) {
    const b = draft.basics || {};
    if (!b.legal_name || !b.legal_name.trim()) return "Enter the legal entity name to continue.";
    if (!b.display_name || !b.display_name.trim()) return "Enter a display name to continue.";
    if (!b.country) return "Select a country to continue.";
    if (!b.base_currency || !ISO_CURRENCY.test(b.base_currency)) return "Base currency must be a 3-letter ISO code (e.g. USD).";
    return null;
  };

  // ── Step 2 · Archetype & industry ────────────────────────────────────────
  // Humanize a kpi_emphasis token ("gross_margin_pct" → "Gross margin pct").
  function humanizeKpi(k) {
    return String(k || "").replace(/_pct\b/g, " %").replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()).trim();
  }
  function industryPreview(ind) {
    if (!ind) return null;
    const parts = [];
    if (ind.description) parts.push(ind.description);
    const kpis = Array.isArray(ind.kpi_emphasis) ? ind.kpi_emphasis.slice(0, 4).map(humanizeKpi) : [];
    if (kpis.length) parts.push("Emphasis: " + kpis.join(", ") + ".");
    const vocab = ind.vocabulary && typeof ind.vocabulary === "object" ? Object.entries(ind.vocabulary).slice(0, 3) : [];
    if (vocab.length) parts.push("Language: " + vocab.map(([k, v]) => `${k} → ${v}`).join("; ") + ".");
    return parts.join(" ");
  }
  STEP_BODIES.classify = function Classify(props) {
    const { draft, patch } = props;
    const c = draft.classify || {};
    const [industries, setIndustries] = useState(null); // null=loading, []=loaded
    const [loadErr, setLoadErr] = useState(false);
    useEffect(() => {
      let alive = true;
      (async () => {
        const sb = db();
        if (!sb) { setIndustries([]); return; }
        const { data, error } = await sb.from("industries")
          .select("id,slug,name,archetype_slug,description,kpi_emphasis,vocabulary,is_active")
          .eq("is_active", true).order("name");
        if (!alive) return;
        if (error) { setLoadErr(true); setIndustries([]); return; }
        setIndustries(data || []);
      })();
      return () => { alive = false; };
    }, []);
    const forArchetype = (industries || []).filter((i) => i.archetype_slug === c.archetype);
    const selectArchetype = (slug) => {
      // Reset industry when the archetype changes.
      patch("classify", { archetype: slug, industry_slug: null, industry_id: null, industry_name: null });
    };
    const selectIndustry = (slug) => {
      const ind = forArchetype.find((i) => i.slug === slug);
      patch("classify", ind ? { industry_slug: ind.slug, industry_id: ind.id, industry_name: ind.name } : { industry_slug: null, industry_id: null, industry_name: null });
    };
    const selectedInd = forArchetype.find((i) => i.slug === c.industry_slug);
    return (
      <div>
        <div className="pc-wiz-eyebrow">Step 2</div>
        <h1 className="pc-wiz-title">How does your business actually work?</h1>
        <p className="pc-wiz-lead">
          Your archetype shapes which financial patterns we look for. Your industry
          refines the language and benchmarks we use. Pick the closest fit — you can
          refine it later in settings.
        </p>

        <div style={{ fontSize: 13, fontWeight: 600, margin: "8px 0 10px", color: "var(--text-1)" }}>Archetype</div>
        <div className="pc-wiz-cards" style={{ display: "grid", gridTemplateColumns: "repeat(2, 1fr)", gap: 12 }}>
          {ARCHETYPE_CATALOG.map((a) => {
            const on = c.archetype === a.slug;
            return (
              <button key={a.slug} type="button" className={"pc-wiz-card" + (on ? " is-selected" : "")}
                onClick={() => selectArchetype(a.slug)}
                style={{
                  textAlign: "left", padding: 14, borderRadius: 10, cursor: "pointer",
                  border: on ? "2px solid var(--accent, #2f6fed)" : "1px solid var(--border, #d8dee6)",
                  background: on ? "var(--accent-soft, #eef6ff)" : "var(--surface, #fff)",
                }}>
                <div style={{ fontWeight: 700, fontSize: 14, color: "var(--text-1)" }}>{a.name}</div>
                <div style={{ fontSize: 12.5, color: "var(--text-2)", margin: "4px 0 8px", lineHeight: 1.45 }}>{a.blurb}</div>
                <div style={{ fontSize: 11.5, color: "var(--text-3)", lineHeight: 1.5 }}>
                  <strong style={{ color: "var(--text-2)" }}>What we'll track:</strong> {a.tracks.join(" · ")}
                </div>
              </button>
            );
          })}
        </div>

        {c.archetype && (
          <div style={{ marginTop: 22, maxWidth: 560 }}>
            <Field label="Industry"
              hint="Refines benchmarks and the exact words we use in your narratives. Pulled live from the platform's industry library — new ones added by your admin appear here automatically.">
              {industries === null ? (
                <div style={{ ...inputStyle, color: "var(--text-3)" }}>Loading industries…</div>
              ) : forArchetype.length === 0 ? (
                <div style={{ fontSize: 12.5, color: "var(--text-3)", lineHeight: 1.5 }}>
                  No industry-specific tuning is published for this archetype yet — general{" "}
                  {ARCHETYPE_BY_SLUG[c.archetype] ? ARCHETYPE_BY_SLUG[c.archetype].name : c.archetype} patterns apply.
                  You can add one later in settings without redoing setup.
                </div>
              ) : (
                <select style={inputStyle} value={c.industry_slug || ""} onChange={(e) => selectIndustry(e.target.value)}>
                  <option value="">— Select an industry (optional) —</option>
                  {forArchetype.map((i) => <option key={i.slug} value={i.slug}>{i.name}</option>)}
                </select>
              )}
            </Field>
            {selectedInd && industryPreview(selectedInd) && (
              <div style={{
                fontSize: 12.5, color: "var(--text-2)", lineHeight: 1.55, padding: "10px 12px",
                background: "var(--surface-2, #f6f8fb)", borderRadius: 8, border: "1px solid var(--border, #e3e8ef)",
              }}>
                {industryPreview(selectedInd)}
              </div>
            )}
          </div>
        )}
        {loadErr && (
          <div style={{ fontSize: 12, color: "var(--text-3)", marginTop: 10 }}>
            (Couldn't load the industry library right now — you can proceed on the archetype alone and set an industry later.)
          </div>
        )}
      </div>
    );
  };
  STEP_VALIDATORS.classify = function (draft) {
    const c = draft.classify || {};
    if (!c.archetype) return "Pick the archetype that best fits the business.";
    return null; // industry is optional (not every archetype has one yet)
  };

  // ── Step 3 · Fiscal configuration ────────────────────────────────────────
  STEP_BODIES.fiscal = function Fiscal(props) {
    const { draft, patch } = props;
    const f = draft.fiscal || {};
    useEffect(() => {
      if (draft.fiscal) return;
      patch("fiscal", { fiscal_year_start_month: 1, reporting_basis: "accrual", first_period_start: "" });
      // eslint-disable-next-line
    }, []);
    return (
      <div>
        <div className="pc-wiz-eyebrow">Step 3</div>
        <h1 className="pc-wiz-title">How should we keep your calendar and your books?</h1>
        <p className="pc-wiz-lead">
          These two settings decide how quarters and years are drawn and whether we
          report on what's been earned or what's moved in cash. Most businesses can
          leave the defaults.
        </p>
        <div className="pc-wiz-form" style={{ maxWidth: 560 }}>
          <Field label="Fiscal year start month"
            hint="When your financial year begins. Drives every quarterly and annual rollup. Calendar-year businesses start in January.">
            <select style={inputStyle} value={f.fiscal_year_start_month || 1}
              onChange={(e) => patch("fiscal", { fiscal_year_start_month: parseInt(e.target.value, 10) })}>
              {MONTHS.map((m, i) => <option key={m} value={i + 1}>{m}</option>)}
            </select>
          </Field>
          <Field label="Reporting basis"
            hint="Accrual recognizes revenue and expenses when earned or incurred — the standard for board reporting. Cash reports only when money actually moves.">
            <select style={inputStyle} value={f.reporting_basis || "accrual"}
              onChange={(e) => patch("fiscal", { reporting_basis: e.target.value })}>
              <option value="accrual">Accrual (recommended)</option>
              <option value="cash">Cash</option>
            </select>
          </Field>
          <Field label="First period to compute from" optional
            hint="If your data goes back further than you want reported, set the earliest month we should start from. Leave blank to use everything we receive.">
            <input style={inputStyle} type="month" value={f.first_period_start || ""}
              onChange={(e) => patch("fiscal", { first_period_start: e.target.value })} />
          </Field>
        </div>
      </div>
    );
  };
  STEP_VALIDATORS.fiscal = function (draft) {
    const f = draft.fiscal || {};
    const m = f.fiscal_year_start_month;
    if (!(m >= 1 && m <= 12)) return "Choose a fiscal year start month.";
    if (f.reporting_basis !== "cash" && f.reporting_basis !== "accrual") return "Choose a reporting basis.";
    return null;
  };

  // ── Step 4 · Data source ─────────────────────────────────────────────────
  // Live GL connectors reuse the same oauth-connect flow as Settings →
  // Integrations. Clicking Connect persists wizard progress, then hands off to
  // the provider; the user returns to this step and the active integration is
  // detected from the `integrations` table (status active/connected). Excel
  // upload and sample data remain as no-connector paths.
  const WIZARD_CONNECTORS = [
    { k: "xero", n: "Xero",              blurb: "Read-only sync of your P&L, balance sheet & GL." },
    { k: "qbo",  n: "QuickBooks Online", blurb: "Read-only sync of your P&L, balance sheet & GL." },
    { k: "wave", n: "Wave",              blurb: "Read-only sync of your P&L & transactions." },
  ];
  STEP_BODIES.source = function Source(props) {
    const { draft, patch } = props;
    const s = draft.source || {};
    const companyId = props.companyProfile && props.companyProfile.id;
    const [conn, setConn] = useState({ loading: true, provider: null });
    const [connErr, setConnErr] = useState(null);
    const [connecting, setConnecting] = useState(null); // provider key mid-redirect

    // Detect an already-active GL integration (e.g. the user just returned from
    // the provider's OAuth screen). Reflect it as the chosen source so the step
    // validates and completion records the real provenance.
    useEffect(() => {
      let alive = true;
      (async () => {
        const sb = db();
        if (!sb || !companyId) { setConn({ loading: false, provider: null }); return; }
        try {
          const { data } = await sb.from("integrations")
            .select("provider,status").eq("company_id", companyId)
            .in("status", ["active", "connected"]);
          if (!alive) return;
          const active = (data || [])[0];
          if (active) {
            setConn({ loading: false, provider: active.provider });
            patch("source", { choice: "connector", provider: active.provider, connected: true });
          } else {
            setConn({ loading: false, provider: null });
          }
        } catch (_e) { if (alive) setConn({ loading: false, provider: null }); }
      })();
      return () => { alive = false; };
      // eslint-disable-next-line
    }, [companyId]);

    async function connectProvider(providerKey) {
      if (!companyId) { setConnErr("No company context — reload and try again."); return; }
      setConnErr(null); setConnecting(providerKey);
      // Persist so the wizard resumes on this step after the redirect round-trip.
      try {
        await saveState(companyId, {
          current_step: 4, status: "in_progress",
          draft_profile: { ...draft, source: { ...(draft.source || {}), choice: "connector", provider: providerKey } },
        });
      } catch (_e) { /* non-fatal — detection on return still recovers */ }
      try {
        const sb = db();
        const { data: { session } } = await sb.auth.getSession();
        const token = session && session.access_token;
        const baseUrl = (window.SUPABASE_URL || sb.supabaseUrl || "").replace(/\/$/, "");
        const res = await fetch(baseUrl + "/functions/v1/oauth-connect", {
          method: "POST",
          headers: { "Content-Type": "application/json", "Authorization": "Bearer " + token },
          body: JSON.stringify({ provider: providerKey, companyId }),
        });
        const data = await res.json();
        if (data.error === "not_configured") { setConnErr(data.message || "This connector isn't configured yet. You can upload a workbook or use sample data for now."); setConnecting(null); return; }
        if (data.error) throw new Error(data.error);
        window.location.href = data.authUrl; // hand off to the provider
      } catch (e) {
        setConnErr((e && e.message) || "Could not start the connection. Please try again.");
        setConnecting(null);
      }
    }
    const choose = (choice, extra) => patch("source", { choice, ...(extra || {}) });
    const card = (active, onClick, disabled, title, desc, badge) => (
      <button type="button" disabled={disabled} onClick={onClick}
        className={"pc-wiz-card" + (active ? " is-selected" : "")}
        style={{
          textAlign: "left", padding: 14, borderRadius: 10, cursor: disabled ? "not-allowed" : "pointer",
          opacity: disabled ? 0.55 : 1,
          border: active ? "2px solid var(--accent, #2f6fed)" : "1px solid var(--border, #d8dee6)",
          background: active ? "var(--accent-soft, #eef6ff)" : "var(--surface, #fff)",
          position: "relative",
        }}>
        <div style={{ fontWeight: 700, fontSize: 14, color: "var(--text-1)" }}>{title}</div>
        <div style={{ fontSize: 12.5, color: "var(--text-2)", marginTop: 4, lineHeight: 1.45 }}>{desc}</div>
        {badge && (
          <span style={{
            position: "absolute", top: 12, right: 12, fontSize: 10.5, fontWeight: 700, letterSpacing: 0.3,
            textTransform: "uppercase", color: "var(--text-3)", background: "var(--surface-2, #eef1f5)",
            padding: "2px 7px", borderRadius: 999,
          }}>{badge}</span>
        )}
      </button>
    );
    return (
      <div>
        <div className="pc-wiz-eyebrow">Step 4</div>
        <h1 className="pc-wiz-title">Connect your accounting software.</h1>
        <p className="pc-wiz-lead">
          A live, read-only connection keeps your dashboard current automatically.
          Prefer to start another way? Upload a workbook or explore with sample data
          below. We only report on data you actually provide — no invented figures.
        </p>

        {/* Connected banner */}
        {conn.provider && (
          <div style={{ display: "flex", alignItems: "center", gap: 10, padding: "12px 14px", marginBottom: 14, borderRadius: 10, background: "rgba(16,185,129,0.10)", border: "1px solid rgba(16,185,129,0.35)" }}>
            <span style={{ fontSize: 16 }}>✓</span>
            <div style={{ fontSize: 13, color: "var(--text-1)" }}>
              <strong>{(WIZARD_CONNECTORS.find((c) => c.k === conn.provider) || {}).n || conn.provider}</strong> is connected.
              Your financial data is importing — you can continue.
            </div>
          </div>
        )}
        {connErr && (
          <div style={{ padding: "10px 14px", marginBottom: 14, borderRadius: 8, background: "rgba(192,57,43,0.08)", border: "1px solid rgba(192,57,43,0.3)", fontSize: 12.5, color: "var(--danger, #c0392b)" }}>{connErr}</div>
        )}

        {/* Live GL connectors */}
        <div className="pc-wiz-cards" style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: 12, marginBottom: 12 }}>
          {WIZARD_CONNECTORS.map((c) => {
            const isConnected = conn.provider === c.k;
            const isBusy = connecting === c.k;
            return (
              <div key={c.k} className={"pc-wiz-card" + (isConnected ? " is-selected" : "")}
                style={{ padding: 14, borderRadius: 10, border: isConnected ? "2px solid var(--accent, #2f6fed)" : "1px solid var(--border, #d8dee6)", background: isConnected ? "var(--accent-soft, #eef6ff)" : "var(--surface, #fff)" }}>
                <div style={{ fontWeight: 700, fontSize: 14, color: "var(--text-1)" }}>{c.n}</div>
                <div style={{ fontSize: 12, color: "var(--text-2)", margin: "4px 0 10px", lineHeight: 1.45 }}>{c.blurb}</div>
                <button type="button" className={"pc-btn " + (isConnected ? "pc-btn-ghost" : "pc-btn-primary")}
                  style={{ width: "100%", fontSize: 13 }} disabled={isConnected || isBusy || conn.loading}
                  onClick={() => connectProvider(c.k)}>
                  {isConnected ? "Connected ✓" : (isBusy ? "Opening…" : "Connect")}
                </button>
              </div>
            );
          })}
        </div>
        <div style={{ fontSize: 11.5, color: "var(--text-3)", marginBottom: 18 }}>
          Use NetSuite, Sage, Stripe, Shopify and more? You can add those from
          Settings → Integrations once setup is done.
        </div>

        <div style={{ fontSize: 11.5, fontWeight: 700, letterSpacing: 0.4, textTransform: "uppercase", color: "var(--text-3)", margin: "4px 0 8px" }}>Or start without a live connection</div>
        <div className="pc-wiz-cards" style={{ display: "grid", gridTemplateColumns: "repeat(2, 1fr)", gap: 12 }}>
          {/* Live: file capture */}
          <div className={"pc-wiz-card" + (s.choice === "csv" ? " is-selected" : "")}
            style={{
              padding: 14, borderRadius: 10,
              border: s.choice === "csv" ? "2px solid var(--accent, #2f6fed)" : "1px solid var(--border, #d8dee6)",
              background: s.choice === "csv" ? "var(--accent-soft, #eef6ff)" : "var(--surface, #fff)",
            }}>
            <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", gap: 8 }}>
              <div style={{ fontWeight: 700, fontSize: 14, color: "var(--text-1)" }}>Upload Excel workbook</div>
              {window.PerduraIngest && (
                <button type="button" className="pc-btn pc-btn-ghost" style={{ fontSize: 11.5 }}
                  onClick={() => window.PerduraIngest.downloadTemplate()}>Download template</button>
              )}
            </div>
            <div style={{ fontSize: 12.5, color: "var(--text-2)", margin: "4px 0 10px", lineHeight: 1.45 }}>
              Export your books into the canonical workbook (or use the template), then upload — we
              validate the tie-outs and import on the spot.
            </div>
            {window.WorkbookUploader
              ? <window.WorkbookUploader compact companyId={props.companyProfile && props.companyProfile.id}
                  onImported={(report) => choose("csv", {
                    imported: true, upload_name: (report && report.profile && report.profile.name) || "workbook",
                    counts: (report && report.counts) || null, has_coa: false,
                  })} />
              : <div style={{ fontSize: 12, color: "var(--text-3)" }}>Uploader loading…</div>}
            {s.choice === "csv" && s.imported && (
              <div style={{ fontSize: 12, color: "#10b981", marginTop: 8 }}>✓ Imported — you can continue.</div>
            )}
          </div>
          {/* Live: sample data */}
          {card(s.choice === "sample", () => choose("sample", { sample_set: (draft.classify || {}).archetype || "generic", has_coa: false }),
            false, "Use sample data",
            "Populate a realistic, archetype-appropriate dataset so you can explore the dashboard before connecting real numbers.",
            null)}
        </div>
        {s.choice === "sample" && (
          <div style={{ fontSize: 12, color: "var(--text-3)", marginTop: 12, lineHeight: 1.5 }}>
            We'll mark this tenant to load the {(draft.classify || {}).archetype || "generic"} sample set.
            (Sample population runs through the existing loader; if it isn't available in your
            environment yet, the dashboard simply starts empty until real data arrives.)
          </div>
        )}
      </div>
    );
  };
  STEP_VALIDATORS.source = function (draft) {
    const s = draft.source || {};
    if (s.choice === "connector") {
      return s.connected ? null : "Finish connecting your accounting software, or choose a workbook or sample data instead.";
    }
    if (s.choice !== "csv" && s.choice !== "sample") return "Connect your accounting software, upload a workbook, or use sample data to continue.";
    if (s.choice === "csv" && !s.imported) return "Upload and import your workbook to continue, or choose sample data instead.";
    return null;
  };

  // ── Step 5 · Chart of accounts review (conditional) ──────────────────────
  // Only reachable when a CSV/Excel upload carried a chart of accounts
  // (source.has_coa). The ingestion connector that parses + auto-maps accounts
  // is not wired yet, so has_coa stays false and this step is auto-skipped
  // today; the UI is built so it lights up the moment parsed accounts arrive in
  // draft.source.coa_accounts (each { account_code, account_name,
  // suggested_category, confidence }).
  function canonicalCategories() {
    const tax = window.PerduraTaxonomy;
    try {
      if (tax && typeof tax.allCategories === "function") {
        const cs = tax.allCategories();
        if (Array.isArray(cs) && cs.length) return cs.map((c) => (typeof c === "string" ? c : c.key)).filter(Boolean);
      }
    } catch (_e) { /* fall through to a safe default set */ }
    return [
      "Revenue", "COGS / Cost of Sales", "Operating Expenses", "Payroll", "Cash & Bank",
      "Accounts Receivable", "Accounts Payable", "Inventory", "Other Income", "Other Expense",
    ];
  }
  // Connector path: review the REAL accounts that have synced from the live GL.
  // No fabricated counts — everything below is read from account_mappings /
  // gl_transactions. If the first sync hasn't landed yet, we say so honestly.
  function ConnectorMapping(props) {
    const { companyId, patch } = props;
    const cats = canonicalCategories();
    const [state, setState] = useState({ loading: true, rows: [], glCodes: [] });
    const [savingCode, setSavingCode] = useState(null);

    const load = useCallback(async () => {
      const sb = db();
      if (!sb || !companyId) { setState({ loading: false, rows: [], glCodes: [] }); return; }
      try {
        const [{ data: maps }, { data: txns }] = await Promise.all([
          sb.from("account_mappings").select("id, account_code, account_name, canonical_category, confidence").eq("company_id", companyId),
          sb.from("gl_transactions").select("account_code, account_name").eq("company_id", companyId).limit(5000),
        ]);
        const glSeen = {};
        (txns || []).forEach((t) => { if (t.account_code && !glSeen[t.account_code]) glSeen[t.account_code] = t.account_name || t.account_code; });
        setState({ loading: false, rows: maps || [], glCodes: Object.keys(glSeen).map((c) => ({ account_code: c, account_name: glSeen[c] })) });
      } catch (_e) { setState({ loading: false, rows: [], glCodes: [] }); }
    }, [companyId]);
    useEffect(() => { load(); }, [load]);

    // Persist a category choice for one account, then refresh + record progress.
    async function setCategory(acct, category) {
      const sb = db();
      if (!sb || !companyId || !category) return;
      setSavingCode(acct.account_code);
      try {
        const existing = state.rows.find((r) => r.account_code === acct.account_code);
        if (existing) {
          await sb.from("account_mappings").update({ canonical_category: category, source: "User · onboarding", confidence: 100 }).eq("id", existing.id);
        } else {
          await sb.from("account_mappings").insert({ company_id: companyId, account_code: acct.account_code, account_name: acct.account_name, canonical_category: category, source: "User · onboarding", confidence: 100 });
        }
        patch("coa", { touched: true });
        await load();
      } catch (_e) { /* surfaced by lack of state change; non-blocking */ }
      finally { setSavingCode(null); }
    }

    if (state.loading) return <div className="pc-wiz-lead" style={{ padding: "8px 0" }}>Checking your synced accounts…</div>;

    const mappedCodes = new Set(state.rows.map((r) => r.account_code));
    const mappedCount = state.rows.length;
    // Accounts seen in the GL but not yet mapped, plus low-confidence mappings —
    // the ones genuinely worth the user's attention.
    const unmapped = state.glCodes.filter((g) => !mappedCodes.has(g.account_code));
    const lowConf = state.rows.filter((r) => (r.confidence != null ? r.confidence : 100) < 70)
      .map((r) => ({ account_code: r.account_code, account_name: r.account_name }));
    const needsInput = unmapped.concat(lowConf);
    const totalSeen = state.glCodes.length;

    // Honest empty state — connected, but the first sync hasn't produced a chart
    // of accounts yet (syncs run asynchronously after the OAuth handshake).
    if (totalSeen === 0 && mappedCount === 0) {
      return (
        <div style={{ fontSize: 13, color: "var(--text-2)", lineHeight: 1.6, padding: "14px 16px", background: "var(--surface-2, #f6f8fb)", borderRadius: 8, border: "1px solid var(--border, #e3e8ef)" }}>
          Your accounting software is connected and your first sync is running. As soon as
          it lands, we auto-map your chart of accounts — you can review and fine-tune every
          account anytime in <strong>Settings → Account mapping</strong>. Nothing here is
          fabricated; this step fills in the moment real accounts arrive. You're safe to continue.
        </div>
      );
    }

    const REVIEW_CAP = 8;
    const toReview = needsInput.slice(0, REVIEW_CAP);
    return (
      <div>
        <div style={{ display: "flex", gap: 10, flexWrap: "wrap", marginBottom: 14 }}>
          <span style={{ fontSize: 12.5, fontWeight: 600, color: "var(--success, #1f9d55)", background: "rgba(16,185,129,0.1)", border: "1px solid rgba(16,185,129,0.3)", borderRadius: 999, padding: "4px 12px" }}>✅ {mappedCount} account{mappedCount === 1 ? "" : "s"} mapped</span>
          {needsInput.length > 0
            ? <span style={{ fontSize: 12.5, fontWeight: 600, color: "var(--warning, #b7791f)", background: "rgba(183,121,31,0.1)", border: "1px solid rgba(183,121,31,0.3)", borderRadius: 999, padding: "4px 12px" }}>⚠️ {needsInput.length} need{needsInput.length === 1 ? "s" : ""} your input</span>
            : <span style={{ fontSize: 12.5, fontWeight: 600, color: "var(--text-3)", background: "var(--surface-2, #eef1f5)", borderRadius: 999, padding: "4px 12px" }}>All synced accounts mapped</span>}
        </div>
        {toReview.length > 0 ? (
          <div className="pc-wiz-map" style={{ border: "1px solid var(--border, #e3e8ef)", borderRadius: 10, overflow: "hidden" }}>
            <div style={{ display: "grid", gridTemplateColumns: "1.4fr 1fr", gap: 10, padding: "8px 12px", fontSize: 11.5, fontWeight: 700, textTransform: "uppercase", color: "var(--text-3)", background: "var(--surface-2, #f6f8fb)" }}>
              <div>Account</div><div>Category</div>
            </div>
            <div style={{ maxHeight: 300, overflow: "auto" }}>
              {toReview.map((a) => (
                <div key={a.account_code} style={{ display: "grid", gridTemplateColumns: "1.4fr 1fr", gap: 10, padding: "8px 12px", alignItems: "center", borderTop: "1px solid var(--border, #eef1f5)" }}>
                  <div style={{ fontSize: 13 }}><span style={{ color: "var(--text-3)" }}>{a.account_code}</span> {a.account_name}</div>
                  <select style={{ ...inputStyle, padding: "6px 8px", fontSize: 13 }} defaultValue="" disabled={savingCode === a.account_code}
                    onChange={(e) => e.target.value && setCategory(a, e.target.value)}>
                    <option value="" disabled>{savingCode === a.account_code ? "Saving…" : "Choose category…"}</option>
                    {cats.map((c) => <option key={c} value={c}>{c}</option>)}
                  </select>
                </div>
              ))}
            </div>
          </div>
        ) : (
          <div style={{ fontSize: 13, color: "var(--text-2)", lineHeight: 1.55, padding: "12px 14px", background: "var(--surface-2, #f6f8fb)", borderRadius: 8, border: "1px solid var(--border, #e3e8ef)" }}>
            Every account we've synced so far is mapped. New accounts are auto-mapped as they
            arrive — review them anytime in <strong>Settings → Account mapping</strong>.
          </div>
        )}
        {needsInput.length > REVIEW_CAP && (
          <div style={{ fontSize: 12, color: "var(--text-3)", marginTop: 10 }}>
            Showing the first {REVIEW_CAP} of {needsInput.length}. The rest use sensible
            defaults now — finish the full review anytime in <strong>Settings → Account mapping</strong>.
          </div>
        )}
      </div>
    );
  }

  STEP_BODIES.coa = function COA(props) {
    const { draft, patch } = props;
    const src = draft.source || {};
    const coa = draft.coa || {};
    // Connector path → review the real synced accounts.
    if (src.choice === "connector") {
      return (
        <div>
          <div className="pc-wiz-eyebrow">Step 5</div>
          <h1 className="pc-wiz-title">Map your accounts.</h1>
          <p className="pc-wiz-lead">
            We auto-map your chart of accounts from the live connection and flag anything
            that needs a human call. Confirm the highlighted ones below — you can revisit
            every mapping later in settings.
          </p>
          <ConnectorMapping companyId={props.companyProfile && props.companyProfile.id} patch={patch} />
          <div style={{ marginTop: 12 }}>
            <button className="pc-btn pc-btn-ghost" onClick={() => patch("coa", { skipped: true })} style={{ fontSize: 13 }}>
              Skip for now — use defaults
            </button>
            {coa.skipped && <span style={{ fontSize: 12, color: "var(--text-3)", marginLeft: 10 }}>Mappings deferred; revisit anytime in settings.</span>}
          </div>
        </div>
      );
    }
    const accounts = Array.isArray(src.coa_accounts) ? src.coa_accounts : [];
    const cats = canonicalCategories();
    const setMapping = (code, category) =>
      patch("coa", { mappings: { ...(coa.mappings || {}), [code]: category }, skipped: false });
    const confColor = (c) => (c >= 90 ? "var(--success, #1f9d55)" : c >= 70 ? "var(--warning, #b7791f)" : "var(--danger, #c0392b)");
    return (
      <div>
        <div className="pc-wiz-eyebrow">Step 5</div>
        <h1 className="pc-wiz-title">Let's confirm how your accounts roll up.</h1>
        <p className="pc-wiz-lead">
          Every business posts a little differently. We've proposed a canonical category
          for each account in your upload — review the low-confidence ones and override
          where you know better. You stay in control; nothing is locked in.
        </p>
        {accounts.length === 0 ? (
          <div style={{
            fontSize: 13, color: "var(--text-2)", lineHeight: 1.6, padding: "14px 16px",
            background: "var(--surface-2, #f6f8fb)", borderRadius: 8, border: "1px solid var(--border, #e3e8ef)",
          }}>
            We don't have a parsed chart of accounts to review yet. Once your uploaded file
            is ingested, the accounts and their proposed mappings appear here for you to
            confirm. You can finish setup now — the engine uses sensible defaults and you
            can revisit mappings anytime in settings.
          </div>
        ) : (
          <div className="pc-wiz-map" style={{ border: "1px solid var(--border, #e3e8ef)", borderRadius: 10, overflow: "hidden" }}>
            <div className="pc-wiz-map-head" style={{ display: "grid", gridTemplateColumns: "1.4fr 1fr 80px", gap: 10, padding: "8px 12px", fontSize: 11.5, fontWeight: 700, textTransform: "uppercase", color: "var(--text-3)", background: "var(--surface-2, #f6f8fb)" }}>
              <div>Account</div><div>Canonical category</div><div>Confidence</div>
            </div>
            <div className="pc-wiz-map-body" style={{ maxHeight: 320, overflow: "auto" }}>
              {accounts.map((a) => {
                const code = a.account_code || a.code;
                const val = (coa.mappings && coa.mappings[code]) || a.suggested_category || "";
                const conf = Math.round(a.confidence != null ? a.confidence : 0);
                return (
                  <div key={code} className="pc-wiz-map-row" style={{ display: "grid", gridTemplateColumns: "1.4fr 1fr 80px", gap: 10, padding: "8px 12px", alignItems: "center", borderTop: "1px solid var(--border, #eef1f5)" }}>
                    <div style={{ fontSize: 13 }}>
                      <span style={{ color: "var(--text-3)" }}>{code}</span> {a.account_name || a.name}
                    </div>
                    <select style={{ ...inputStyle, padding: "6px 8px", fontSize: 13 }} value={val} onChange={(e) => setMapping(code, e.target.value)}>
                      {!cats.includes(val) && val && <option value={val}>{val}</option>}
                      {cats.map((c) => <option key={c} value={c}>{c}</option>)}
                    </select>
                    <div style={{ fontSize: 12.5, fontWeight: 700, color: confColor(conf) }}>{conf}%</div>
                  </div>
                );
              })}
            </div>
          </div>
        )}
        <div style={{ marginTop: 12 }}>
          <button className="pc-btn pc-btn-ghost" onClick={() => patch("coa", { skipped: true })}
            style={{ fontSize: 13 }}>
            Skip for now — use defaults
          </button>
          {coa.skipped && <span style={{ fontSize: 12, color: "var(--text-3)", marginLeft: 10 }}>Mappings deferred; you can revisit them in settings.</span>}
        </div>
      </div>
    );
  };
  STEP_VALIDATORS.coa = function () { return null; }; // always skippable

  // ── Completion ───────────────────────────────────────────────────────────
  // Applies the accumulated draft_profile to the companies row + industry
  // linkage. Only writes fields the user actually provided (safe defaults, no
  // fabrication). The shell flips onboarding_state.status to 'completed' after
  // this resolves without error.
  async function _applyCompletion(companyId, draft) {
    const sb = db();
    if (!sb || !companyId) return { error: "No database connection — could not save your setup." };
    const b = draft.basics || {}, c = draft.classify || {}, f = draft.fiscal || {}, s = draft.source || {};
    const fields = {};
    const set = (k, v) => { if (v !== undefined && v !== null && v !== "") fields[k] = v; };

    // Basics
    set("name", b.legal_name && b.legal_name.trim());
    set("display_name", b.display_name && b.display_name.trim());
    set("country", b.country);
    set("base_currency", b.base_currency);
    set("currency", b.base_currency); // legacy mirror used by some pages
    set("website", b.website);

    // Archetype + industry. business_type is set to the archetype slug so the
    // legacy setup view (which only checks business_type truthiness) won't fire,
    // and classify() understands the slug directly.
    set("archetype", c.archetype);
    set("business_type", c.archetype);
    set("business_model", c.archetype);
    if (c.industry_id) set("industry_id", c.industry_id);
    set("industry", c.industry_name || c.industry_slug);

    // Fiscal
    if (f.fiscal_year_start_month >= 1 && f.fiscal_year_start_month <= 12) {
      set("fiscal_year_start_month", f.fiscal_year_start_month);
      set("fiscal_year_start", MONTHS[f.fiscal_year_start_month - 1]); // legacy text mirror
    }
    set("reporting_basis", f.reporting_basis);
    if (f.first_period_start) set("data_period_anchor", f.first_period_start);

    // Data source provenance
    if (s.choice === "csv") set("source_system", "manual_excel");
    else if (s.choice === "sample") set("source_system", "sample_data");
    else if (s.choice === "connector" && s.provider) set("source_system", s.provider);

    const { error } = await sb.from("companies").update(fields).eq("id", companyId);
    if (error) return { error: (error.message || "Could not save your company profile.") };

    // Note: parsed COA mappings (draft.coa.mappings) are persisted in
    // onboarding_state.draft_profile for the ingestion connector to apply once
    // it lands; we do not write account_mappings here because no real parse has
    // produced them yet (honest — no fabricated mappings).
    return { error: null };
  }

  window.PerduraOnboardingWizard = {
    Component: StepShell,
    ARCHETYPE_CATALOG,
    COUNTRY_CURRENCY,
    STEPS,
    _applyCompletion,
    _loadState: loadState,
    _saveState: saveState,
  };
})();
