// Year-end close screen — Task 12 of docs/superpowers/plans/2026-07-31-local-app.md.
// Renders the year ladder from GET /api/c/close/status, and — for the single
// EARLIEST OPEN year (CLAUDE.md §7: "Import oldest year first... reconcile...
// then lock the period", enforced oldest-first server-side by close.ts's own
// guard) — the upload → compare → lock workflow. Every other open year just
// shows "Locks after <that year>"; a locked year shows its badge and nothing
// else. No debit/credit language anywhere (CLAUDE.md §9).

import { useEffect, useRef, useState } from "react";
import { useCompany } from "../lib/company";
import { useApiGet } from "../lib/useApiGet";
import { apiUpload } from "../setup/apiUpload";
import { ApiError } from "../lib/api";
import { Dropzone } from "../setup/Dropzone";
import { ErrorBanner } from "../components/ErrorBanner";
import { fmtMoney } from "../lib/money";
import { DiffTable, type CompareRow } from "./DiffTable";

interface CloseStatusYear {
  year: number;
  postedEntries: number;
  locked: boolean;
}
interface CloseStatusResponse {
  lockedThrough: string | null;
  years: CloseStatusYear[];
}

interface CompareResult {
  year: number;
  matches: boolean;
  rows: CompareRow[];
  totals: { ours: string; theirs: string };
}

interface LockSuccess {
  locked: true;
  lockedThrough: string;
  summary: string;
}

interface LockConflictBody {
  error?: string;
  matches?: boolean;
  rows?: CompareRow[];
  totals?: { ours: string; theirs: string };
}

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

export function CloseScreen() {
  const { selectedCompanyId } = useCompany();
  const { data, loading, error, retry } = useApiGet<CloseStatusResponse>(
    "/api/c/close/status", selectedCompanyId,
  );

  const [file, setFile] = useState<File | null>(null);
  const [compare, setCompare] = useState<CompareResult | null>(null);
  const [busy, setBusy] = useState<"compare" | "lock" | null>(null);
  const [actionError, setActionError] = useState<string | null>(null);
  const [lockNotice, setLockNotice] = useState<string | null>(null);

  const earliestOpen = data?.years.find((y) => !y.locked) ?? null;

  // Reset the in-progress upload whenever the actionable year itself changes
  // — most notably right after a successful lock, when the ladder refetches
  // and a NEW year becomes earliest-open. Without this, the just-uploaded
  // file/compare result from the year that JUST got locked would still be
  // sitting in state, attached to whatever year is now shown as actionable.
  const prevYearRef = useRef<number | null>(null);
  useEffect(() => {
    const y = earliestOpen?.year ?? null;
    if (prevYearRef.current !== y) {
      prevYearRef.current = y;
      setFile(null);
      setCompare(null);
      setActionError(null);
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps -- deliberately keyed only on the year number
  }, [earliestOpen?.year]);

  async function handleCompare(year: number) {
    if (!file || !selectedCompanyId) return;
    setBusy("compare");
    setActionError(null);
    try {
      const res = await apiUpload<CompareResult>(
        "/api/c/close/compare", file, selectedCompanyId, { year: String(year) },
      );
      setCompare(res);
    } catch (err) {
      setActionError(err instanceof ApiError ? err.message : "Couldn't compare that file.");
    } finally {
      setBusy(null);
    }
  }

  async function handleLock(year: number) {
    if (!file || !selectedCompanyId) return;
    setBusy("lock");
    setActionError(null);
    try {
      const res = await apiUpload<LockSuccess>(
        "/api/c/close/lock", file, selectedCompanyId, { year: String(year) },
      );
      setLockNotice(res.summary);
      setFile(null);
      setCompare(null);
      retry(); // refresh the ladder — the just-locked year flips to Locked
    } catch (err) {
      if (err instanceof ApiError) {
        const body = isRecord(err.body) ? (err.body as LockConflictBody) : undefined;
        if (body?.rows) {
          // The re-check inside /close/lock found the same (or a fresh) set
          // of differences — show them the same way /close/compare would.
          setCompare({ year, matches: body.matches ?? false, rows: body.rows, totals: body.totals ?? { ours: "0.0000", theirs: "0.0000" } });
        }
        setActionError(err.message);
      } else {
        setActionError("Couldn't lock that year.");
      }
    } finally {
      setBusy(null);
    }
  }

  return (
    <div className="wrap">
      <header className="top">
        <h1>Year-end close</h1>
      </header>
      <p className="sub" style={{ marginBottom: 16 }}>
        Reconcile each year against your old software's own numbers, oldest first. Once a year is
        locked, nothing can be posted into it again — corrections from then on go into whatever year
        is currently open.
      </p>

      {lockNotice && (
        <div className="success-banner">
          <span>{lockNotice}</span>
          <button type="button" onClick={() => setLockNotice(null)}>
            Dismiss
          </button>
        </div>
      )}

      {loading && !data && <p className="msg-inline">Loading&hellip;</p>}
      {error && !data && <ErrorBanner message={error} onRetry={retry} />}

      {data && data.years.length === 0 && (
        <div className="empty-state">No posted entries yet — nothing to close.</div>
      )}

      {data && data.years.length > 0 && (
        <div className="close-ladder">
          {data.years.map((y) => {
            const isActionable = earliestOpen !== null && y.year === earliestOpen.year;
            return (
              <div
                key={y.year}
                className={`close-year-row${isActionable ? " close-year-row-actionable" : ""}`}
              >
                <div className="close-year-head">
                  <span>
                    <span className="close-year-label">{y.year}</span>
                    <span className="close-year-stat">
                      {y.postedEntries} posted {y.postedEntries === 1 ? "entry" : "entries"}
                    </span>
                  </span>
                  {y.locked ? (
                    <span className="chip">Locked</span>
                  ) : isActionable ? (
                    <span className="chip chip-info">Next to close</span>
                  ) : (
                    <span className="badge">Open</span>
                  )}
                </div>

                {!y.locked && !isActionable && earliestOpen && (
                  <div className="close-year-waiting">Locks after {earliestOpen.year}.</div>
                )}

                {isActionable && (
                  <div className="close-workflow">
                    <p className="close-explain">
                      To lock {y.year}, we need your old software&apos;s own Trial Balance as of
                      December 31, {y.year}, exported to Excel (.xlsx). In QuickBooks Desktop:
                    </p>
                    <div className="menu-path">Reports &rarr; Accountant &amp; Taxes &rarr; Trial Balance</div>
                    <p className="close-explain" style={{ marginTop: 8 }}>
                      Set both the From and To dates to 12/31/{y.year}, then export to Excel (.xlsx).
                      In Sage, Xero or anything else, run that product&apos;s own Trial Balance for the
                      same date and save it as Excel (.xlsx) — we read any .xlsx with account, debit
                      and credit columns. If your software only exports CSV, open it in Excel and
                      Save As .xlsx.
                    </p>

                    {actionError && (
                      <div style={{ marginTop: 10 }}>
                        <ErrorBanner
                          message={actionError}
                          onRetry={() => (compare ? handleLock(y.year) : handleCompare(y.year))}
                        />
                      </div>
                    )}

                    <Dropzone
                      label={`Trial Balance for ${y.year} (.xlsx)`}
                      selectedFileName={file?.name}
                      disabled={busy !== null}
                      onFileSelected={(f) => {
                        setFile(f);
                        setCompare(null);
                        setActionError(null);
                      }}
                    />

                    <div className="wizard-actions">
                      <button
                        type="button"
                        className="btn"
                        disabled={!file || busy !== null}
                        onClick={() => void handleCompare(y.year)}
                      >
                        {busy === "compare" ? "Comparing…" : "Compare to our books"}
                      </button>
                    </div>

                    {compare && compare.matches && (
                      <>
                        <div className="match-banner">
                          <span>Everything matches to the penny.</span>
                          <span className="match-banner-totals">
                            Our books: {fmtMoney(compare.totals.ours)} · Your file:{" "}
                            {fmtMoney(compare.totals.theirs)}
                          </span>
                        </div>
                        <div className="lock-explain">
                          <p>
                            Locking is permanent. Nothing can be posted into {y.year} afterward; corrections
                            go into open years instead.
                          </p>
                          <div className="wizard-actions" style={{ marginTop: 0 }}>
                            <button
                              type="button"
                              className="btn"
                              disabled={busy !== null}
                              onClick={() => void handleLock(y.year)}
                            >
                              {busy === "lock" ? "Locking…" : `Lock ${y.year}`}
                            </button>
                          </div>
                        </div>
                      </>
                    )}

                    {compare && !compare.matches && (
                      <>
                        <p className="close-explain" style={{ marginTop: 12, color: "var(--bad-text)" }}>
                          {y.year} doesn't tie out yet — fix the differences below (or the export), then
                          upload again.
                        </p>
                        <DiffTable rows={compare.rows} />
                      </>
                    )}
                  </div>
                )}
              </div>
            );
          })}
        </div>
      )}
    </div>
  );
}
