// Inventory adjustment — Task 5 of docs/superpowers/plans/2026-08-03-inventory.md.
// The door for "the shelf and the books disagree": shrinkage, breakage, a
// physical count correction, stock found behind a pallet — and, because v1 has
// no return-to-vendor document, the stock half of a vendor return. The screen
// says that last one out loud (see the note in the form); a limit a user meets
// here is a limit that has to be disclosed here.
//
// An adjustment is TWO facts written together — the quantity changed, and some
// account absorbed what that was worth. record_adjustment() (0018) writes both
// in one transaction, which is why this form asks for the account rather than
// pretending a quantity can change on its own.
//
// WHICH ACCOUNT IS ALLOWED, and why, is the interesting part of this screen.
// 0018's decision 6 restricts the other side to an expense, income or equity
// account, and forbids ANY item's inventory account (not merely this item's):
//   - a bank account or A/R would silently misstate a balance somebody
//     reconciles — shrinkage is not cash leaving the bank;
//   - another item's inventory account breaks the ledger tie on THAT item's
//     account, where the per-entry check cannot see it, because that check
//     builds its account set from the movements and the only movement here
//     belongs to this item.
// The picker therefore offers exactly what is allowed, and the note says why in
// English. The database still refuses anything else in its own words — this is
// the friendlier surface on top, never the guard (CLAUDE.md §12).

import { 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 { todayIso } from "../lib/date";
import { ErrorBanner } from "../components/ErrorBanner";
import { SearchablePicker, type PickerGroup } from "../components/SearchablePicker";
import { accountPickerGroups } from "../accounts/pickerGroups";
import type { ApiAccount } from "../accounts/subtypes";
import { fmtQuantity, fmtUnitCost, type ApiItem } from "../items/itemTypes";
import {
  addQuantity, compareQuantity, extendedCost, issueValuePreview, subtractQuantity,
} 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 Direction = "out" | "in";

interface AdjustmentResult {
  entryId: string;
  itemId: string;
  qty: string;
  value: string;
  unitCost: string;
  qtyOnHand: string;
  summary: string;
}

export function AdjustmentForm({ 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", selectedCompanyId);
  const items = useMemo(() => itemsBody?.items ?? [], [itemsBody]);

  const [itemId, setItemId] = useState<string | null>(null);
  const [date, setDate] = useState(todayIso());
  const [direction, setDirection] = useState<Direction>("out");
  const [quantity, setQuantity] = useState("");
  const [unitCost, setUnitCost] = useState("");
  const [accountId, setAccountId] = useState<string | null>(null);
  const [memo, setMemo] = useState("");

  const [saving, setSaving] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [done, setDone] = useState<AdjustmentResult | null>(null);

  const item = itemId ? items.find((i) => i.id === itemId) ?? null : null;

  const itemGroups: PickerGroup[] = useMemo(() => [{
    label: "Stocked items",
    options: items
      .filter((i) => i.kind === "inventory")
      .map((i) => ({ id: i.id, label: i.sku ? `${i.name} (${i.sku})` : i.name })),
  }], [items]);

  // Every account that is some item's inventory account — the set
  // record_adjustment() refuses, computed from the same items list the picker
  // above is built from.
  const inventoryAccountIds = useMemo(
    () => new Set(items.map((i) => i.inventoryAccountId).filter((id): id is string => id !== null)),
    [items],
  );
  const accountGroups: PickerGroup[] = useMemo(
    () => accountPickerGroups(
      (accounts ?? []).filter((a) => !inventoryAccountIds.has(a.id)),
      // Expenses first: shrinkage and breakage are what this form is mostly
      // used for. Income covers found stock; equity covers an opening-style
      // correction on a fresh set of books.
      ["expense", "income", "equity"],
    ),
    [accounts, inventoryAccountIds],
  );

  const qty = quantity.trim();
  const qtyOk = QUANTITY_RE.test(qty) && /[1-9]/.test(qty);
  const cost = unitCost.trim();
  const costOk = UNIT_COST_RE.test(cost);

  const onHand = item?.qtyOnHand ?? null;
  const newOnHand = onHand !== null && qtyOk
    ? direction === "out" ? subtractQuantity(onHand, qty) : addQuantity(onHand, qty)
    : null;
  const wouldGoNegative = direction === "out" && onHand !== null && qtyOk
    && (compareQuantity(qty, onHand) ?? 0) > 0;

  const valuePreview = !qtyOk || item === null
    ? null
    : direction === "out"
      ? issueValuePreview(qty, item.qtyOnHand ?? "0.0000", item.averageCost ?? "0.000000", item.value ?? "0.0000")
      : costOk ? extendedCost(qty, cost) : null;

  // §3.1 forbids a zero-amount journal line, and every movement rides on a
  // posted entry — so an adjustment that moves no money has nowhere to go.
  // record_adjustment() refuses it; this catches it a round trip earlier.
  const worthNothing = valuePreview !== null && /^-?0+\.0+$/.test(valuePreview);

  const formValid =
    itemId !== null && date !== "" && qtyOk && accountId !== null && memo.trim() !== ""
    && (direction === "in" ? costOk : true)
    && !wouldGoNegative && !worthNothing;

  async function submit() {
    if (!formValid || !selectedCompanyId) return;
    setSaving(true);
    setError(null);
    try {
      const result = await apiPost<AdjustmentResult>(
        "/api/c/inventory-adjustments",
        {
          itemId,
          date,
          quantity: direction === "out" ? `-${qty}` : qty,
          ...(direction === "in" ? { unitCost: cost } : {}),
          accountId,
          memo: memo.trim(),
        },
        selectedCompanyId,
      );
      setDone(result);
      setQuantity("");
      setUnitCost("");
      setMemo("");
      retryItems();
    } catch (err) {
      setError(err instanceof ApiError ? err.message : "Couldn't record that adjustment. 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: "item", id: done.itemId })}
            >
              See the item
            </button>
            <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 invoice-form">
        <h2>Correct a quantity</h2>
        <p className="sub" style={{ marginTop: -6 }}>
          Use this when the shelf and the books disagree — something broke, walked, or turned up.
          Not for buying or selling: a bill brings stock in and an invoice takes it out, and both
          do it on their own.
        </p>
        {/* The one v1 limit a user meets on this exact screen, said plainly
            rather than left to be discovered. There is no vendor-return
            DOCUMENT: bill_lines has CHECK (quantity > 0) and CHECK (amount > 0)
            from 0009, so a negative bill line — the natural shape of a return —
            is physically impossible, and widening those checks would weaken a
            guard every A/P path depends on. The honest interim path is this
            screen for the quantity plus a vendor credit for the money, and the
            spec's own instruction is to SAY that rather than pretend a returns
            flow works. Design spec, "Vendor returns of inventory". */}
        <p className="line-note" style={{ marginTop: 6 }}>
          <strong>Sending stock back to a vendor?</strong> This is where you do the stock half —
          take the quantity out here and say so in the reason. The money half is a vendor credit,
          recorded separately against that vendor. There is no single &ldquo;return to
          vendor&rdquo; document yet, so it is two steps on purpose rather than one that only
          half works.
        </p>

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

        <SearchablePicker
          id="adj-item"
          label="Which item?"
          placeholder="Search stocked items…"
          groups={itemGroups}
          value={itemId}
          onChange={(id) => {
            setItemId(id);
            setDone(null);
          }}
          emptyMessage="No stocked items yet."
        />

        {item && (
          <div className="rows" style={{ marginBottom: 14 }}>
            <div className="row">
              <span className="n">On hand right now</span>
              <span className="a">{fmtQuantity(item.qtyOnHand ?? "0.0000")}</span>
            </div>
            <div className="row">
              <span className="n">Average cost</span>
              <span className="a">{fmtUnitCost(item.averageCost ?? "0.000000")}</span>
            </div>
            <div className="row">
              <span className="n">Value on the balance sheet</span>
              <span className="a">{fmtMoney(item.value ?? "0.0000")}</span>
            </div>
          </div>
        )}

        <div className="field">
          <label htmlFor="adj-direction">Which way?</label>
          <select
            id="adj-direction"
            className="select-input"
            value={direction}
            onChange={(ev) => setDirection(ev.target.value === "in" ? "in" : "out")}
          >
            <option value="out">Fewer than the books say — gone, broken or miscounted</option>
            <option value="in">More than the books say — found, or miscounted the other way</option>
          </select>
        </div>

        <div className="field-row">
          <div className="field">
            <label htmlFor="adj-qty">
              {direction === "out" ? "How many are gone?" : "How many turned up?"}
            </label>
            <input
              id="adj-qty"
              className="text-input"
              type="text"
              inputMode="decimal"
              value={quantity}
              onChange={(ev) => setQuantity(ev.target.value)}
            />
            {qty !== "" && !qtyOk && (
              <div className="field-error">
                Enter a quantity greater than zero, like 3 or 0.5.
              </div>
            )}
            {wouldGoNegative && onHand !== null && (
              <div className="field-error">
                You only have {fmtQuantity(onHand)} on hand — this would take {fmtQuantity(qty)}.
                A quantity can&rsquo;t go below zero, because there is no honest cost for stock
                that isn&rsquo;t there.
              </div>
            )}
          </div>
          <div className="field">
            <label htmlFor="adj-date">Date</label>
            <input
              id="adj-date"
              className="text-input"
              type="date"
              value={date}
              onChange={(ev) => setDate(ev.target.value)}
            />
          </div>
        </div>

        {direction === "in" ? (
          <div className="field">
            <label htmlFor="adj-cost">What did each one cost you?</label>
            <input
              id="adj-cost"
              className="text-input"
              type="text"
              inputMode="decimal"
              placeholder="0.00"
              value={unitCost}
              onChange={(ev) => setUnitCost(ev.target.value)}
            />
            {cost !== "" && !costOk && (
              <div className="field-error">
                Enter an amount like 12 or 12.50 (up to 4 decimal places — a washer really can cost
                $0.0042).
              </div>
            )}
            <div className="line-note">
              Stock can&rsquo;t come in without a cost — something has to be added to what the shelf
              is worth.
            </div>
          </div>
        ) : (
          <p className="line-note">
            Stock going out is worth what it already cost you on average, so there&rsquo;s nothing
            to enter — naming a cost here would be a second version of the truth.
          </p>
        )}

        <SearchablePicker
          id="adj-account"
          label="What explains it?"
          placeholder="Search accounts…"
          groups={accountGroups}
          value={accountId}
          onChange={setAccountId}
          emptyMessage="No account here fits — add one from the Chart of Accounts first."
        />
        <p className="line-note" style={{ marginTop: -8, marginBottom: 14 }}>
          Usually an expense like Shrinkage or Breakage — the account that absorbs the loss (or, for
          stock found, the income that absorbs the gain). It can&rsquo;t be a bank account: shrinkage
          isn&rsquo;t cash leaving the bank, and posting it there would break your next
          reconciliation. It can&rsquo;t be an item&rsquo;s inventory account either — that&rsquo;s
          the side the stock is already moving on.
        </p>

        <div className="field">
          <label htmlFor="adj-memo">Why is it changing?</label>
          <input
            id="adj-memo"
            className="text-input"
            type="text"
            placeholder="Damaged in the September flood; counted 14, books said 17"
            value={memo}
            onChange={(ev) => setMemo(ev.target.value)}
          />
          <div className="line-note">
            Required. A quantity that changed for no recorded reason is impossible to explain a year
            from now — to you, or to anyone reviewing the books.
          </div>
        </div>

        {worthNothing && (
          <div className="field-error" style={{ marginBottom: 14 }}>
            That works out to no money at all, so there&rsquo;s nothing to record. An adjustment has
            to move some value as well as some quantity — every stock movement rides on a journal
            entry, and an entry can&rsquo;t be for nothing.
          </div>
        )}

        {item && newOnHand && valuePreview && !worthNothing && (
          <div className="confirm-panel" style={{ marginBottom: 14 }}>
            <p style={{ marginBottom: 0 }}>
              {item.name} goes from {fmtQuantity(item.qtyOnHand ?? "0.0000")} to{" "}
              <strong>{fmtQuantity(newOnHand)}</strong> on hand, and{" "}
              <strong>{fmtMoney(valuePreview)}</strong>{" "}
              {direction === "out" ? "comes off" : "goes onto"} what your stock is worth.
            </p>
          </div>
        )}

        <div className="wizard-actions">
          <button
            type="button"
            className="btn"
            disabled={!formValid || saving}
            onClick={() => void submit()}
          >
            {saving ? "Recording…" : "Record it"}
          </button>
        </div>
      </div>
    </div>
  );
}
