// Chart of Accounts screen — Task 2 of
// docs/superpowers/plans/2026-08-02-chart-management.md (spec Part 2,
// binding). Direct owner directives drove this screen: hide closed accounts,
// add categories from the UI, and STANDARD accounting grouping (CLAUDE.md §9
// as amended 2026-08-02) — Chase Ink files under "Credit cards", never
// "Other".
//
// One list, grouped under the five standard headings with subtype sections
// (accounts/subtypes.ts holds the matrix + labels; untagged accounts render
// in a plain section labeled with just the type). Columns: Code | Name |
// Balance | actions. Balances come from ?withBalances=1 (derived at query
// time from trial_balance(), natural-signed — server/src/accounts.ts); no
// section subtotals on purpose (v1 keeps aggregation out of this screen —
// per-row balances only).
//
// Row click -> that account's register (#/transactions/account/:id — the
// Transactions screen with the account preselected). "Edit" swaps the row to
// the SalesTaxCard-style inline edit row (name + code + grouping in one
// place); Deactivate/Reactivate is a direct per-row action. The API's
// deactivation guard errors (settings-designated AR/AP/sales-tax accounts)
// surface inline under the row, verbatim — they already name the blocking
// setting in English.
//
// Bank-account courtesy (final-review note): before deactivating a
// bank-subtype account, a client-side peek at GET /api/c/bank/overview
// checks whether it has statements (open ones, or a reconciled-through date)
// and, if so, asks via a plain confirm() — deactivating hides it from
// pickers and the reconcile screen, history stays. Courtesy only; the server
// needs no new API and allows the deactivation either way.
//
// "Show inactive" is OFF by default (the owner's ask: closed accounts
// invisible unless requested). The full list including inactive rows is
// fetched regardless and filtered client-side, so flipping the toggle is
// instant and a just-deactivated row doesn't vanish-then-reappear oddly.

import { useCallback, useEffect, useMemo, useRef, useState, type KeyboardEvent } from "react";
import { useCompany } from "../lib/company";
import { useApiGet } from "../lib/useApiGet";
import { apiGet, apiPatch, ApiError } from "../lib/api";
import { fmtMoney } from "../lib/money";
import { ErrorBanner } from "../components/ErrorBanner";
import type { NavTarget } from "../lib/useHashView";
import { NewAccountModal } from "./NewAccountModal";
import {
  LEDGER_TYPES,
  SUBTYPES_BY_TYPE,
  SUBTYPE_LABELS,
  TYPE_HEADINGS,
  type AccountSubtype,
  type ApiAccount,
  type LedgerType,
} from "./subtypes";

interface BankOverviewAccount {
  accountId: string;
  reconciledThrough: string | null;
  openStatements: unknown[];
}

const BANK_DEACTIVATE_NOTE =
  "This account has bank statements. Deactivating hides it from pickers and " +
  "the reconcile screen — its history stays. Continue?";

/** One subtype section of one type's table: header label + its rows. */
interface Section {
  key: string;
  label: string;
  rows: ApiAccount[];
}

function sectionsForType(type: LedgerType, accounts: ApiAccount[]): Section[] {
  const ofType = accounts.filter((a) => a.type === type);
  const sections: Section[] = SUBTYPES_BY_TYPE[type].map((s) => ({
    key: s,
    label: SUBTYPE_LABELS[s],
    rows: ofType.filter((a) => a.subtype === s),
  }));
  // Untagged accounts: a plain section labeled with just the type (spec).
  sections.push({
    key: "untagged",
    label: TYPE_HEADINGS[type],
    rows: ofType.filter((a) => a.subtype === null),
  });
  const nonEmpty = sections.filter((s) => s.rows.length > 0);
  // Review finding 4: when tagged sections coexist with untagged rows, the
  // bare-type label can duplicate a sibling section header verbatim
  // ("Expenses / Other expenses / Expenses") — relabel the untagged section
  // "No grouping" in that case. The bare-type label survives only in the
  // single-section case, where SectionRows suppresses it anyway (the type
  // heading right above already says it).
  if (nonEmpty.length > 1) {
    for (const s of nonEmpty) {
      if (s.key === "untagged") s.label = "No grouping";
    }
  }
  return nonEmpty;
}

function AccountEditRow({
  account,
  companyId,
  onCancel,
  onSaved,
  onDirtyChange,
}: {
  account: ApiAccount;
  companyId: string;
  onCancel: () => void;
  onSaved: () => void;
  /** Reports whether this row holds unsaved changes — the parent uses it to
   * confirm before an Edit click on ANOTHER row silently discards them
   * (review finding 5). */
  onDirtyChange: (dirty: boolean) => void;
}) {
  const [name, setName] = useState(account.name);
  const [code, setCode] = useState(account.code);
  // "" = untagged (subtype null) in the select below.
  const [subtype, setSubtype] = useState<string>(account.subtype ?? "");
  const [saving, setSaving] = useState(false);
  // Synchronous twin of `saving` — same Enter-auto-repeat guard rationale as
  // NewAccountModal's savingRef (review finding 2).
  const savingRef = useRef(false);
  const [error, setError] = useState<string | null>(null);
  const [touched, setTouched] = useState(false);

  const dirty =
    name !== account.name || code !== account.code || subtype !== (account.subtype ?? "");
  useEffect(() => {
    onDirtyChange(dirty);
    return () => onDirtyChange(false);
  }, [dirty, onDirtyChange]);

  // Review finding 6 (a11y): Enter saves, Escape cancels, from any field.
  function handleKeyDown(ev: KeyboardEvent<HTMLElement>) {
    if (ev.key === "Enter") {
      ev.preventDefault();
      void handleSave();
    } else if (ev.key === "Escape") {
      onCancel();
    }
  }

  const validation = touched
    ? name.trim() === ""
      ? "An account needs a name."
      : code.trim() === ""
        ? "An account code can't be blank."
        : null
    : null;

  async function handleSave() {
    if (savingRef.current) return; // Enter auto-repeats — one save at a time
    setTouched(true);
    if (name.trim() === "" || code.trim() === "") return;
    savingRef.current = true;
    setSaving(true);
    setError(null);
    try {
      // True partial PATCH: only what actually changed goes over the wire
      // (subtype: null is meaningful — it untags the account).
      const body: Record<string, unknown> = {};
      if (name.trim() !== account.name) body.name = name.trim();
      if (code.trim() !== account.code) body.code = code.trim();
      if (subtype !== (account.subtype ?? "")) body.subtype = subtype === "" ? null : subtype;
      if (Object.keys(body).length > 0) {
        await apiPatch(`/api/c/accounts/${account.id}`, body, companyId);
      }
      onSaved();
    } catch (err) {
      // Duplicate name/code and grouping-mismatch refusals arrive in the
      // API's own English — shown verbatim.
      setError(err instanceof ApiError ? err.message : "Couldn't save that account. Try again.");
    } finally {
      savingRef.current = false;
      setSaving(false);
    }
  }

  return (
    <>
      <tr className="chart-edit-row">
        <td className="chart-code">
          <input
            className="text-input chart-code-input"
            type="text"
            aria-label="Account code"
            value={code}
            onChange={(ev) => setCode(ev.target.value)}
            onKeyDown={handleKeyDown}
          />
        </td>
        <td>
          <div className="chart-edit-fields">
            <input
              className="text-input"
              type="text"
              aria-label="Account name"
              autoFocus
              value={name}
              onChange={(ev) => setName(ev.target.value)}
              onKeyDown={handleKeyDown}
            />
            <select
              className="select-input"
              aria-label="Grouping"
              value={subtype}
              onChange={(ev) => setSubtype(ev.target.value)}
              onKeyDown={handleKeyDown}
            >
              <option value="">No grouping (just {TYPE_HEADINGS[account.type]})</option>
              {SUBTYPES_BY_TYPE[account.type].map((s) => (
                <option key={s} value={s}>
                  {SUBTYPE_LABELS[s]}
                </option>
              ))}
            </select>
          </div>
        </td>
        <td className="num" />
        <td className="chart-actions" style={{ whiteSpace: "nowrap" }}>
          <button type="button" className="btn" disabled={saving} onClick={() => void handleSave()}>
            {saving ? "Saving…" : "Save"}
          </button>{" "}
          <button type="button" className="acct-link" disabled={saving} onClick={onCancel}>
            Cancel
          </button>
        </td>
      </tr>
      {(error || validation) && (
        <tr>
          <td colSpan={4} className="field-error">
            {error ?? validation}
          </td>
        </tr>
      )}
    </>
  );
}

export function ChartScreen({ navigate }: { navigate: (target: NavTarget) => void }) {
  const { selectedCompanyId } = useCompany();
  const { data, loading, error, retry } = useApiGet<ApiAccount[]>(
    "/api/c/accounts?includeInactive=1&withBalances=1",
    selectedCompanyId,
  );

  const [showInactive, setShowInactive] = useState(false);
  const [editingId, setEditingId] = useState<string | null>(null);
  const [addOpen, setAddOpen] = useState(false);
  // Whether the open edit row holds unsaved changes (review finding 5) — a
  // ref, not state: it only gates the confirm below, never a render.
  const editDirtyRef = useRef(false);
  const handleDirtyChange = useCallback((dirty: boolean) => {
    editDirtyRef.current = dirty;
  }, []);
  // Per-row action state: which row a deactivate/reactivate is in flight
  // for, and the last action's English error, shown under that row.
  const [busyId, setBusyId] = useState<string | null>(null);
  const [rowError, setRowError] = useState<{ id: string; message: string } | null>(null);

  const accounts = useMemo(() => data ?? [], [data]);
  const visible = useMemo(
    () => (showInactive ? accounts : accounts.filter((a) => a.active)),
    [accounts, showInactive],
  );
  const inactiveCount = accounts.filter((a) => !a.active).length;

  async function setActive(account: ApiAccount, active: boolean) {
    if (!selectedCompanyId || busyId !== null) return;
    // Courtesy note before deactivating a bank account that has statements —
    // see the file header. Any failure of the peek itself is swallowed: it's
    // a courtesy, not a guard.
    if (!active && account.subtype === "bank") {
      try {
        const overview = await apiGet<{ accounts: BankOverviewAccount[] }>(
          "/api/c/bank/overview",
          selectedCompanyId,
        );
        const bank = overview.accounts.find((a) => a.accountId === account.id);
        const hasStatements =
          bank !== undefined &&
          (bank.openStatements.length > 0 || bank.reconciledThrough !== null);
        if (hasStatements && !window.confirm(BANK_DEACTIVATE_NOTE)) return;
      } catch {
        // best-effort peek only
      }
    }
    setBusyId(account.id);
    setRowError(null);
    try {
      await apiPatch(`/api/c/accounts/${account.id}`, { active }, selectedCompanyId);
      retry();
    } catch (err) {
      // The designated-account refusals name the blocking setting in English.
      setRowError({
        id: account.id,
        message: err instanceof ApiError ? err.message : "Couldn't change that account. Try again.",
      });
    } finally {
      setBusyId(null);
    }
  }

  function openRegister(account: ApiAccount) {
    navigate({ view: "transactions", accountId: account.id });
  }

  // Opening Edit on a different row while the current edit row holds unsaved
  // changes asks first (review finding 5) — otherwise those edits silently
  // vanish. Re-clicking the SAME row is a no-op, not a discard.
  function beginEdit(id: string) {
    if (editingId !== null && editingId !== id && editDirtyRef.current) {
      const editing = accounts.find((a) => a.id === editingId);
      if (!window.confirm(`Discard unsaved changes to ${editing?.name ?? "this account"}?`)) {
        return;
      }
    }
    setEditingId(id);
    setRowError(null);
  }

  return (
    <div className="wrap">
      <header className="top">
        <h1>Chart of Accounts</h1>
      </header>

      {!selectedCompanyId ? (
        <p className="msg-inline">Choose a company from the sidebar to see its accounts.</p>
      ) : (
        <>
          <p className="sub" style={{ marginBottom: 14 }}>
            Every account in this company&rsquo;s books, grouped the standard way. Click an account
            to open its register.
          </p>

          <div className="report-toolbar chart-toolbar">
            <label className="register-toggle">
              <input
                type="checkbox"
                checked={showInactive}
                onChange={(ev) => setShowInactive(ev.target.checked)}
              />
              Show inactive{inactiveCount > 0 ? ` (${inactiveCount})` : ""}
            </label>
            <div className="register-toolbar-end">
              <button type="button" className="btn" onClick={() => setAddOpen(true)}>
                New account
              </button>
            </div>
          </div>

          {error && <ErrorBanner message={error} onRetry={retry} />}
          {loading && !data && <p className="msg-inline">Loading your accounts&hellip;</p>}
          {data && visible.length === 0 && (
            <div className="empty-state">No accounts to show yet.</div>
          )}

          {LEDGER_TYPES.map((type) => {
            const sections = sectionsForType(type, visible);
            if (sections.length === 0) return null;
            return (
              <section key={type} className="chart-type-section">
                <h2>{TYPE_HEADINGS[type]}</h2>
                <div className="card">
                  <table>
                    <thead>
                      <tr>
                        <th className="chart-code">Code</th>
                        <th>Name</th>
                        <th className="num">Balance</th>
                        <th aria-label="Actions" />
                      </tr>
                    </thead>
                    <tbody>
                      {sections.map((section) => (
                        <SectionRows
                          key={section.key}
                          section={section}
                          soloSectionLabel={TYPE_HEADINGS[type]}
                          sectionCount={sections.length}
                          companyId={selectedCompanyId}
                          editingId={editingId}
                          busyId={busyId}
                          rowError={rowError}
                          onEdit={beginEdit}
                          onDirtyChange={handleDirtyChange}
                          onCancelEdit={() => setEditingId(null)}
                          onSavedEdit={() => {
                            setEditingId(null);
                            retry();
                          }}
                          onSetActive={(a, active) => void setActive(a, active)}
                          onOpen={openRegister}
                        />
                      ))}
                    </tbody>
                  </table>
                </div>
              </section>
            );
          })}

          {addOpen && (
            <NewAccountModal
              companyId={selectedCompanyId}
              onClose={() => setAddOpen(false)}
              onCreated={() => {
                setAddOpen(false);
                retry();
              }}
            />
          )}
        </>
      )}
    </div>
  );
}

function SectionRows({
  section,
  soloSectionLabel,
  sectionCount,
  companyId,
  editingId,
  busyId,
  rowError,
  onEdit,
  onDirtyChange,
  onCancelEdit,
  onSavedEdit,
  onSetActive,
  onOpen,
}: {
  section: Section;
  /** The bare type heading — a lone untagged section under it would just
   * repeat the heading, so that one case skips its section-label row. */
  soloSectionLabel: string;
  sectionCount: number;
  companyId: string;
  editingId: string | null;
  busyId: string | null;
  rowError: { id: string; message: string } | null;
  onEdit: (id: string) => void;
  onDirtyChange: (dirty: boolean) => void;
  onCancelEdit: () => void;
  onSavedEdit: () => void;
  onSetActive: (account: ApiAccount, active: boolean) => void;
  onOpen: (account: ApiAccount) => void;
}) {
  const hideLabel = sectionCount === 1 && section.label === soloSectionLabel;
  return (
    <>
      {!hideLabel && (
        <tr className="chart-section-row">
          <td colSpan={4}>{section.label}</td>
        </tr>
      )}
      {section.rows.map((a) =>
        editingId === a.id ? (
          <AccountEditRow
            key={a.id}
            account={a}
            companyId={companyId}
            onCancel={onCancelEdit}
            onSaved={onSavedEdit}
            onDirtyChange={onDirtyChange}
          />
        ) : (
          <AccountRow
            key={a.id}
            account={a}
            busy={busyId === a.id}
            error={rowError?.id === a.id ? rowError.message : null}
            onEdit={() => onEdit(a.id)}
            onSetActive={onSetActive}
            onOpen={() => onOpen(a)}
          />
        ),
      )}
    </>
  );
}

function AccountRow({
  account,
  busy,
  error,
  onEdit,
  onSetActive,
  onOpen,
}: {
  account: ApiAccount;
  busy: boolean;
  error: string | null;
  onEdit: () => void;
  onSetActive: (account: ApiAccount, active: boolean) => void;
  onOpen: () => void;
}) {
  return (
    <>
      <tr
        className="chart-account-row"
        style={{ cursor: "pointer", opacity: account.active ? 1 : 0.55 }}
        onClick={onOpen}
      >
        <td className="chart-code">{account.code}</td>
        <td>
          {account.name}
          {!account.active && <span className="chip chip-muted" style={{ marginLeft: 8 }}>Inactive</span>}
        </td>
        <td className="num">{account.balance !== undefined ? fmtMoney(account.balance) : ""}</td>
        <td className="chart-actions" style={{ whiteSpace: "nowrap" }} onClick={(ev) => ev.stopPropagation()}>
          <button type="button" className="acct-link" disabled={busy} onClick={onEdit}>
            Edit
          </button>{" "}
          {account.active ? (
            <button
              type="button"
              className="acct-link"
              disabled={busy}
              onClick={() => onSetActive(account, false)}
            >
              {busy ? "Working…" : "Deactivate"}
            </button>
          ) : (
            <button
              type="button"
              className="acct-link"
              disabled={busy}
              onClick={() => onSetActive(account, true)}
            >
              {busy ? "Working…" : "Reactivate"}
            </button>
          )}
        </td>
      </tr>
      {error && (
        <tr>
          <td colSpan={4} className="field-error">
            {error}
          </td>
        </tr>
      )}
    </>
  );
}
