// Opening quantities — Task 5 of docs/superpowers/plans/2026-08-03-inventory.md,
// spec section "Opening quantities for a company that imported full history".
//
// This screen exists to TEACH a decision, not just collect a form. There are
// two shapes of company here and they need opposite answers, and getting it
// wrong is not recoverable in place — once an item has moved,
// items_frozen_once_moved() welds its inventory account shut.
//
//   FRESH BOOKS. Nothing was imported. The stock on the shelf has never been
//   accounted for, so recording it CREATES value: Dr Inventory / Cr Opening
//   Balance Equity. That is what Opening Balance Equity is for.
//
//   IMPORTED HISTORY. CLAUDE.md §7 imports every transaction, so the old
//   "Inventory" account ALREADY holds the value — recording opening quantities
//   against equity as well would count the same stock twice, which is exactly
//   what §7 warns about for opening balances generally. The cure is a
//   reclassification: point the items at a NEW inventory account and take the
//   opening value OUT of the old, history-funded one:
//
//       Dr <new inventory account>    Cr <old history-funded account>
//
//   That drains the old account, leaves total assets unchanged, and ties by
//   construction — the old account is nobody's inventory account, so it isn't
//   in the tie set at all. Doing it as ONE entry (which is what
//   record_opening_quantities() posts) also closes the window in which the two
//   accounts both hold the value and the balance sheet double-counts.
//
// WHAT THE SOURCE ACCOUNT MAY BE is enforced by 0023/0024 and surfaced here
// before the round trip: equity or asset only; never a bank account, never what
// customers owe you, never an account that has ever been reconciled against a
// bank statement, and never an item's own inventory account. The reconciliation
// rule is the one this screen cannot pre-check — the accounts list carries no
// reconciliation history — so it is stated in the note and the database's own
// refusal is shown if it fires.
//
// One more thing 0024 made date-sensitive: the source account's balance is
// checked AS AT the date of the count, not today. An account funded in June
// cannot back a count dated in January, and the refusal says so with the date
// in it.

import { Fragment, useMemo, useState } from "react";
import { useCompany } from "../lib/company";
import { useApiGet } from "../lib/useApiGet";
import { apiPost, ApiError } from "../lib/api";
import { fmtMoney } from "../lib/money";
import { fmtDate, todayIso } from "../lib/date";
import { ErrorBanner } from "../components/ErrorBanner";
import { SearchablePicker, type PickerGroup } from "../components/SearchablePicker";
import { sumMoney } from "../bills/lineAmount";
import { accountPickerGroups } from "../accounts/pickerGroups";
import type { ApiAccount } from "../accounts/subtypes";
import { trimStoredDecimal, type ApiItem } from "../items/itemTypes";
import { extendedCost, isZeroQuantity } from "../items/quantityMath";
import type { NavTarget } from "../lib/useHashView";

const QUANTITY_RE = /^(0|[1-9]\d{0,7})(\.\d{1,4})?$/;
const UNIT_COST_RE = /^(0|[1-9]\d{0,12})(\.\d{1,4})?$/;

type Shape = "fresh" | "migrated";

let rowKeySeq = 0;
function nextRowKey(): string {
  rowKeySeq += 1;
  return `open-row-${rowKeySeq}`;
}

interface RowDraft {
  key: string;
  itemId: string | null;
  quantity: string;
  unitCost: string;
}

function blankRow(): RowDraft {
  return { key: nextRowKey(), itemId: null, quantity: "", unitCost: "" };
}

function rowHasContent(r: RowDraft): boolean {
  return r.itemId !== null || r.quantity.trim() !== "" || r.unitCost.trim() !== "";
}

function rowError(r: RowDraft): string | null {
  if (!rowHasContent(r)) return null;
  if (r.itemId === null) return "Pick the item this line counts.";
  const q = r.quantity.trim();
  if (!QUANTITY_RE.test(q) || !/[1-9]/.test(q)) {
    return "Enter how many you have on hand — more than zero. Leave the item out entirely if you have none of it.";
  }
  const c = r.unitCost.trim();
  if (!UNIT_COST_RE.test(c)) {
    return "Enter what one of these cost you, like 7.50 (up to 4 decimal places).";
  }
  return null;
}

interface OpeningResult {
  entryId: string;
  sourceAccountId: string;
  sourceAccountName: string;
  itemCount: number;
  totalValue: string;
  summary: string;
}

export function OpeningQuantitiesForm({ navigate }: { navigate: (target: NavTarget) => void }) {
  const { selectedCompanyId } = useCompany();
  const { data: itemsBody, retry: retryItems } = useApiGet<{ items: ApiItem[] }>(
    "/api/c/items", selectedCompanyId,
  );
  const { data: accounts } = useApiGet<ApiAccount[]>(
    "/api/c/accounts?withBalances=1", selectedCompanyId,
  );
  const items = useMemo(() => itemsBody?.items ?? [], [itemsBody]);

  const [shape, setShape] = useState<Shape | null>(null);
  const [date, setDate] = useState(todayIso());
  const [sourceAccountId, setSourceAccountId] = useState<string | null>(null);
  const [memo, setMemo] = useState("");
  const [rows, setRows] = useState<RowDraft[]>([blankRow()]);

  const [saving, setSaving] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [done, setDone] = useState<OpeningResult | null>(null);
  /** The database's own double-count warning (0025), held while the user
   *  decides. Null when it hasn't fired, which on a company that never
   *  imported anything is always. */
  const [doubleCount, setDoubleCount] = useState<string | null>(null);

  const inventoryAccountIds = useMemo(
    () => new Set(items.map((i) => i.inventoryAccountId).filter((id): id is string => id !== null)),
    [items],
  );

  /** Only items that have never moved can have an opening count — anything
   *  with history takes an adjustment instead (record_opening_quantities()
   *  refuses it by name). Anything with stock on hand certainly has history,
   *  so it is kept out of the picker; a zero-quantity item that HAS moved is
   *  the one case only the server can see, and its refusal is shown. */
  const countableItems = useMemo(
    () => items.filter((i) => i.kind === "inventory" && isZeroQuantity(i.qtyOnHand ?? "0.0000")),
    [items],
  );
  const alreadyMovedCount = items.filter(
    (i) => i.kind === "inventory" && !isZeroQuantity(i.qtyOnHand ?? "0.0000"),
  ).length;

  const chosen = new Set(rows.map((r) => r.itemId).filter((id): id is string => id !== null));
  function itemGroups(currentId: string | null): PickerGroup[] {
    return [{
      label: "Stocked items with no history yet",
      options: countableItems
        .filter((i) => i.id === currentId || !chosen.has(i.id))
        .map((i) => ({ id: i.id, label: i.sku ? `${i.name} (${i.sku})` : i.name })),
    }];
  }

  // Equity for a fresh company, assets for a migrated one — never a bank
  // account, never what customers owe you, never an item's inventory account.
  const sourceGroups: PickerGroup[] = useMemo(() => {
    if (shape === null) return [];
    const allowed = (accounts ?? []).filter((a) => {
      if (inventoryAccountIds.has(a.id)) return false;
      if (shape === "fresh") return a.type === "equity";
      return a.type === "asset"
        && a.subtype !== "bank"
        && a.subtype !== "accounts-receivable";
    });
    return accountPickerGroups(allowed, shape === "fresh" ? ["equity"] : ["asset"]);
  }, [accounts, inventoryAccountIds, shape]);

  const sourceAccount = (accounts ?? []).find((a) => a.id === sourceAccountId) ?? null;

  const itemsById = useMemo(() => new Map(items.map((i) => [i.id, i])), [items]);
  const values = rows.map((r) => {
    if (rowError(r) !== null || r.itemId === null) return null;
    return extendedCost(r.quantity.trim(), r.unitCost.trim());
  });
  const total = sumMoney(values.filter((v): v is string => v !== null));

  const activeRows = rows.filter(rowHasContent);
  const rowsValid = activeRows.length > 0 && activeRows.every((r) => rowError(r) === null);
  const totalIsZero = /^-?0+\.0+$/.test(total);
  const formValid =
    shape !== null && sourceAccountId !== null && date !== "" && memo.trim() !== ""
    && rowsValid && !totalIsZero;

  function updateRow(key: string, patch: Partial<RowDraft>) {
    setRows((rs) => rs.map((r) => (r.key === key ? { ...r, ...patch } : r)));
  }

  async function submit(acknowledged = false) {
    if (!formValid || !selectedCompanyId) return;
    setSaving(true);
    setError(null);
    try {
      const result = await apiPost<OpeningResult>(
        "/api/c/inventory-opening",
        {
          date,
          sourceAccountId,
          memo: memo.trim(),
          ...(acknowledged ? { acknowledged: true } : {}),
          items: activeRows.map((r) => ({
            itemId: r.itemId!,
            quantity: r.quantity.trim(),
            unitCost: r.unitCost.trim(),
          })),
        },
        selectedCompanyId,
      );
      setDone(result);
      setDoubleCount(null);
      setRows([blankRow()]);
      setMemo("");
      retryItems();
    } catch (err) {
      // 0025's double-count refusal is the one the user can legitimately
      // answer, and the server flags it structurally (needsAcknowledgement)
      // rather than leaving this screen to match on prose. It gets a decision
      // panel carrying the database's own sentence — not a red banner, because
      // this is a question, and on a company that genuinely is starting its
      // stock from nothing the answer is "yes".
      const body = err instanceof ApiError ? (err.body as { needsAcknowledgement?: boolean } | null) : null;
      if (body?.needsAcknowledgement === true && err instanceof ApiError) {
        setDoubleCount(err.message);
      } else {
        setError(
          err instanceof ApiError ? err.message : "Couldn't record those opening quantities. Try again.",
        );
      }
    } finally {
      setSaving(false);
    }
  }

  return (
    <div>
      {done && (
        <div className="success-banner banner-stack">
          <p>{done.summary}</p>
          <div className="wizard-actions">
            <button
              type="button"
              className="acct-link"
              onClick={() => navigate({ view: "entry", id: done.entryId })}
            >
              See the journal entry
            </button>
            <button type="button" className="acct-link" onClick={() => setDone(null)}>
              Dismiss
            </button>
          </div>
        </div>
      )}

      <div className="card">
        <h2>What was already on the shelf</h2>
        <p className="sub" style={{ marginTop: -6 }}>
          A one-time count for stock you owned before this system knew about it. You only do this
          once per item — after that, bills, invoices, builds and adjustments keep the quantity
          right on their own.
        </p>

        <div className="field">
          <label htmlFor="opening-shape">Where did this stock come from?</label>
          <select
            id="opening-shape"
            className="select-input"
            value={shape ?? ""}
            onChange={(ev) => {
              const next = ev.target.value;
              setShape(next === "fresh" || next === "migrated" ? next : null);
              setSourceAccountId(null);
              // A different answer is a different question — never carry a
              // warning (or a consent to it) across the change.
              setDoubleCount(null);
            }}
          >
            <option value="" disabled>Choose one&hellip;</option>
            <option value="fresh">
              These are brand-new books — I didn&rsquo;t bring history in from anywhere
            </option>
            <option value="migrated">
              I imported my history from other software
            </option>
          </select>
        </div>

        {shape === "fresh" && (
          <div className="confirm-panel" style={{ marginBottom: 14 }}>
            <p style={{ marginBottom: 0 }}>
              Then this stock has never been in your books at all, and counting it puts value in
              them for the first time. That value comes out of <strong>Opening Balance Equity</strong>
              {" "}— the account that says &ldquo;this was already ours when we started&rdquo;. Pick it
              below.
            </p>
          </div>
        )}

        {shape === "migrated" && (
          <div className="confirm-panel" style={{ marginBottom: 14 }}>
            <p>
              Then your books already know what this stock is worth — your imported history put it
              into an inventory account long before you made an item for it. Counting it against
              Opening Balance Equity as well would count the same stock twice.
            </p>
            <p>
              So it comes out of that old account instead. Point your items at a{" "}
              <strong>new</strong> inventory account, then pick the{" "}
              <strong>old, history-funded one</strong> below. This records one entry that moves the
              value across: the old account empties out, the new one fills up, and your total assets
              don&rsquo;t move a cent — the value has only changed which account it sits in.
            </p>
            <p style={{ marginBottom: 0 }}>
              If your count comes to less than the old account holds, that&rsquo;s fine and it still
              goes through — history said you had more stock than the shelf does, which is a
              real-world difference worth knowing about. Whatever is left behind stays in the old
              account; clear it out to Opening Balance Equity with an ordinary journal entry once
              you&rsquo;ve satisfied yourself where it went.
            </p>
          </div>
        )}

        {shape !== null && (
          <>
            <SearchablePicker
              id="opening-source"
              label={shape === "fresh"
                ? "Which equity account?"
                : "Which account is holding the value today?"}
              placeholder="Search accounts…"
              groups={sourceGroups}
              value={sourceAccountId}
              onChange={(id) => {
                setSourceAccountId(id);
                setDoubleCount(null);
              }}
              emptyMessage={shape === "fresh"
                ? "No equity account yet — add Opening Balance Equity in the Chart of Accounts first."
                : "No account here fits. It has to be an asset account that isn't a bank account and isn't already an item's inventory account."}
            />
            <p className="line-note" style={{ marginTop: -8, marginBottom: 14 }}>
              It can&rsquo;t be a bank account or what customers owe you — stock you already own
              didn&rsquo;t come out of the bank, and draining either would break the next
              reconciliation. It can&rsquo;t be an account you&rsquo;ve already reconciled against a
              statement, or an account an item already points at. And it has to hold at least this
              much <em>as at the date below</em>, not just today.
            </p>

            {sourceAccount?.balance !== undefined && (
              <p className="sub" style={{ marginTop: -8 }}>
                {sourceAccount.name} holds {fmtMoney(sourceAccount.balance)} today
                {date !== todayIso() ? ` — what matters here is what it held on ${fmtDate(date)}.` : "."}
              </p>
            )}

            <div className="field-row">
              <div className="field">
                <label htmlFor="opening-date">As at</label>
                <input
                  id="opening-date"
                  className="text-input"
                  type="date"
                  value={date}
                  onChange={(ev) => setDate(ev.target.value)}
                />
                <div className="line-note">
                  The day you counted. For a migrated company that&rsquo;s usually your cutover
                  date, not the start of the history you imported.
                </div>
              </div>
              <div className="field">
                <label htmlFor="opening-memo">Note</label>
                <input
                  id="opening-memo"
                  className="text-input"
                  type="text"
                  placeholder="Physical count at cutover, 31 Dec"
                  value={memo}
                  onChange={(ev) => setMemo(ev.target.value)}
                />
                <div className="line-note">
                  Required — a count nobody explained is impossible to audit later.
                </div>
              </div>
            </div>

            {countableItems.length === 0 ? (
              <div className="empty-state">
                {alreadyMovedCount > 0
                  ? "Every stocked item already has history, so none of them needs an opening count. Use “Correct a quantity” instead."
                  : "No stocked items yet. Add the items you keep on hand first."}
              </div>
            ) : (
              <>
                <table className="invoice-lines-table">
                  <thead>
                    <tr>
                      <th>Item</th>
                      <th className="num">On hand</th>
                      <th className="num">Cost each</th>
                      <th className="num">Value</th>
                      <th />
                    </tr>
                  </thead>
                  <tbody>
                    {rows.map((row, i) => {
                      const err = rowError(row);
                      const value = values[i];
                      const picked = row.itemId ? itemsById.get(row.itemId) ?? null : null;
                      return (
                        <Fragment key={row.key}>
                          <tr>
                            <td>
                              <SearchablePicker
                                id={`opening-item-${row.key}`}
                                label=""
                                placeholder="Search items…"
                                groups={itemGroups(row.itemId)}
                                value={row.itemId}
                                onChange={(id) => {
                                  const item = itemsById.get(id) ?? null;
                                  updateRow(row.key, {
                                    itemId: id,
                                    unitCost: row.unitCost.trim() === "" && item?.purchaseCost
                                      ? trimStoredDecimal(item.purchaseCost)
                                      : row.unitCost,
                                  });
                                }}
                                emptyMessage="Nothing left to count."
                              />
                              {picked?.inventoryAccountName && (
                                <div className="line-note">
                                  lands in {picked.inventoryAccountName}
                                </div>
                              )}
                            </td>
                            <td className="num">
                              <input
                                className="text-input line-qty"
                                type="text"
                                inputMode="decimal"
                                aria-label="Quantity on hand"
                                value={row.quantity}
                                onChange={(ev) => updateRow(row.key, { quantity: ev.target.value })}
                              />
                            </td>
                            <td className="num">
                              <input
                                className="text-input line-price"
                                type="text"
                                inputMode="decimal"
                                placeholder="0.00"
                                aria-label="Cost each"
                                value={row.unitCost}
                                onChange={(ev) => updateRow(row.key, { unitCost: ev.target.value })}
                              />
                            </td>
                            <td className="num invoice-line-amount">
                              {value ? fmtMoney(value) : "—"}
                            </td>
                            <td>
                              <button
                                type="button"
                                className="acct-link"
                                disabled={rows.length <= 1}
                                onClick={() => setRows((rs) => rs.filter((r) => r.key !== row.key))}
                              >
                                Remove
                              </button>
                            </td>
                          </tr>
                          {err && (
                            <tr>
                              <td colSpan={5} className="field-error">{err}</td>
                            </tr>
                          )}
                        </Fragment>
                      );
                    })}
                  </tbody>
                </table>
                <button
                  type="button"
                  className="acct-link"
                  style={{ marginTop: 6 }}
                  onClick={() => setRows((rs) => [...rs, blankRow()])}
                >
                  + Count another item
                </button>

                <div className="invoice-form-total">
                  <span>Opening value</span>
                  <span className="invoice-form-total-amount">{fmtMoney(total)}</span>
                </div>

                {sourceAccount && !totalIsZero && rowsValid && (
                  <p className="sub">
                    This records one entry: {fmtMoney(total)} onto the shelf, taken out of{" "}
                    {sourceAccount.name}, dated {fmtDate(date)}.
                  </p>
                )}
              </>
            )}

            {alreadyMovedCount > 0 && countableItems.length > 0 && (
              <p className="line-note">
                {alreadyMovedCount} stocked{" "}
                {alreadyMovedCount === 1 ? "item already has" : "items already have"} history and
                won&rsquo;t appear above — an opening count is the first thing that ever happens to
                an item. Use &ldquo;Correct a quantity&rdquo; for those.
              </p>
            )}

            {error && <ErrorBanner message={error} onRetry={() => setError(null)} />}

            {/* CLAUDE.md §7, on the one screen that can break it: a company
                that imported its history has ALREADY recorded what it paid for
                this stock, so creating the value again out of equity counts the
                same inventory twice — and the entry carries stock movements, so
                it cannot simply be reversed afterwards. record_opening_quantities()
                (0025) is what refuses it; this is that refusal turned into a
                decision, because a company can legitimately have imported one
                year of payroll and still be starting its stock from nothing.
                Only the user knows which they are. */}
            {doubleCount !== null ? (
              <div className="confirm-panel">
                <p><strong>Wait — this may count the same stock twice.</strong></p>
                <p>{doubleCount}</p>
                <p>
                  If your imported history really doesn&rsquo;t include this stock — you brought in
                  payroll or bank history only, say — then taking it out of{" "}
                  {sourceAccount?.name ?? "equity"} is right and you can carry on. If it does, go
                  back and pick &ldquo;I imported my history&rdquo; instead, so the value moves
                  across from the account that&rsquo;s already holding it rather than being created
                  a second time.
                </p>
                <div className="wizard-actions">
                  <button
                    type="button"
                    className="btn"
                    disabled={saving}
                    onClick={() => void submit(true)}
                  >
                    {saving ? "Recording…" : "I understand — record it anyway"}
                  </button>
                  <button
                    type="button"
                    className="acct-link"
                    disabled={saving}
                    onClick={() => setDoubleCount(null)}
                  >
                    Let me change it
                  </button>
                </div>
              </div>
            ) : (
              <div className="wizard-actions">
                <button
                  type="button"
                  className="btn"
                  disabled={!formValid || saving}
                  onClick={() => void submit()}
                >
                  {saving ? "Recording…" : "Record the count"}
                </button>
              </div>
            )}
          </>
        )}
      </div>
    </div>
  );
}
