// The parts list ("bill of materials") for a stocked item — Task 5 of
// docs/superpowers/plans/2026-08-03-inventory.md. Lives on a stocked item's own
// page (items/ItemDetail.tsx), because a parts list is a property of the thing
// being made, not a separate object to go and find.
//
// CLAUDE.md §9 as amended: the standard term is on the card ("Bill of
// materials") and the plain sentence sits on top of it — "What it's made
// from". Neither hides the other.
//
// The extended-cost column and the running "one of these costs" total are
// computed here so the number moves as the user types, using the same
// half-away-from-zero integer arithmetic record_movement() will use when a
// build actually posts (items/quantityMath.ts). They are a PREVIEW of a cost
// that does not exist yet — no BOM row stores a cost, and none ever will
// (CLAUDE.md §3.4). What a build actually costs is decided at build time from
// the average cost of the parts at that moment, which is why this card says
// "as things stand today" rather than presenting the figure as the price of
// the finished item.
//
// Two rules are surfaced in English BEFORE the server has to say them: only
// stocked items can be parts, and nothing can be a part of itself. The picker
// simply doesn't offer a non-stocked item or this item, so the commonest way
// to hit either rule is closed. The deeper case — A is made of B, B is made of
// A — is 0018's assemblies_no_recursion(), which raises in English of its own;
// this screen shows that refusal rather than duplicating the graph walk.

import { Fragment, useEffect, useMemo, useState } from "react";
import { useCompany } from "../lib/company";
import { useApiGet } from "../lib/useApiGet";
import { apiPut, ApiError } from "../lib/api";
import { fmtMoney } from "../lib/money";
import { ErrorBanner } from "../components/ErrorBanner";
import { SearchablePicker, type PickerGroup } from "../components/SearchablePicker";
import { sumMoney } from "../bills/lineAmount";
import { fmtQuantity, fmtUnitCost, trimStoredDecimal, type ApiItem } from "../items/itemTypes";
import { issueValuePreview } from "../items/quantityMath";
import type { NavTarget } from "../lib/useHashView";
import {
  BOM_PART_RULE, BOM_RECURSION_RULE, BUILD_COST_NOTE,
  type ApiComponentsBody,
} from "./buildTypes";

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

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

interface RowDraft {
  key: string;
  itemId: string | null;
  qtyPer: string;
}

function rowError(row: RowDraft): string | null {
  if (row.itemId === null) return "Pick the part this line is for.";
  const qty = row.qtyPer.trim();
  if (qty === "" || !QUANTITY_RE.test(qty) || !/[1-9]/.test(qty)) {
    return "Enter how many go into one — a number greater than zero, like 2 or 0.5.";
  }
  return null;
}

export function BomEditor({
  itemId,
  itemName,
  navigate,
  onChanged,
}: {
  itemId: string;
  itemName: string;
  navigate: (target: NavTarget) => void;
  /** Called after a successful save — the item page refetches so the "where
   *  it stands" numbers and this card can never disagree. */
  onChanged?: () => void;
}) {
  const { selectedCompanyId } = useCompany();
  const { data, loading, error, retry } = useApiGet<ApiComponentsBody>(
    `/api/c/items/${itemId}/components`,
    selectedCompanyId,
  );
  const { data: itemsBody } = useApiGet<{ items: ApiItem[] }>("/api/c/items", selectedCompanyId);
  const items = useMemo(() => itemsBody?.items ?? [], [itemsBody]);

  const [editing, setEditing] = useState(false);
  const [rows, setRows] = useState<RowDraft[]>([]);
  const [saving, setSaving] = useState(false);
  const [saveError, setSaveError] = useState<string | null>(null);

  // Leaving edit mode (or a fresh fetch landing) reseeds the draft from what
  // the server actually holds, so "Cancel" can never leave a half-typed row
  // looking saved.
  useEffect(() => {
    if (!editing && data) {
      setRows(data.components.map((c) => ({
        key: nextRowKey(),
        itemId: c.itemId,
        qtyPer: trimStoredDecimal(c.qtyPer),
      })));
    }
  }, [editing, data]);

  const itemsById = useMemo(() => new Map(items.map((i) => [i.id, i])), [items]);
  const chosen = new Set(rows.map((r) => r.itemId).filter((id): id is string => id !== null));

  /** Only stocked items, never this item itself, and never a part already on
   *  the list (each part appears once — the API refuses a duplicate with
   *  "Each part can only be listed once — add up the quantity instead"). */
  function partGroups(currentId: string | null): PickerGroup[] {
    const options = items
      .filter((i) => i.kind === "inventory")
      .filter((i) => i.id !== itemId)
      .filter((i) => i.id === currentId || !chosen.has(i.id))
      .map((i) => ({ id: i.id, label: i.sku ? `${i.name} (${i.sku})` : i.name }));
    return [{ label: "Stocked items", options }];
  }

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

  const savedRows = data?.components ?? [];
  const displayRows = editing ? rows : savedRows.map((c) => ({
    key: c.id,
    itemId: c.itemId,
    qtyPer: trimStoredDecimal(c.qtyPer),
  }));

  /**
   * Per-row extended cost, from the part's CURRENT average cost. Null while a
   * row is incomplete, so nothing wrong is ever shown mid-keystroke.
   *
   * issueValuePreview, not a bare qty x average: when a part would be drained
   * to EXACTLY zero, record_movement() assigns it the whole remaining value
   * rather than round(qty x average, 4), so that a fraction of a cent is not
   * stranded in the Inventory account with no quantity left to carry it out
   * (0018 as-built decision 5, which says in so many words that reports and
   * previews must not re-derive that last issue as qty x average). Building one
   * of these using every last unit of a part is an ordinary thing for a small
   * shop to do, and this is the number they consent to.
   */
  const extended = displayRows.map((r) => {
    const part = r.itemId ? itemsById.get(r.itemId) ?? null : null;
    const avg = part?.averageCost ?? null;
    if (avg === null || !QUANTITY_RE.test(r.qtyPer.trim())) return null;
    return issueValuePreview(
      r.qtyPer.trim(),
      part?.qtyOnHand ?? "0.0000",
      avg,
      part?.value ?? "0.0000",
    );
  });
  const total = sumMoney(extended.filter((v): v is string => v !== null));

  const anyRowError = rows.some((r) => rowError(r) !== null);

  async function save() {
    if (!selectedCompanyId || anyRowError) return;
    setSaving(true);
    setSaveError(null);
    try {
      await apiPut(
        `/api/c/items/${itemId}/components`,
        { components: rows.map((r) => ({ itemId: r.itemId!, qtyPer: r.qtyPer.trim() })) },
        selectedCompanyId,
      );
      setEditing(false);
      retry();
      onChanged?.();
    } catch (err) {
      setSaveError(
        err instanceof ApiError ? err.message : "Couldn't save that parts list. Try again.",
      );
    } finally {
      setSaving(false);
    }
  }

  return (
    <div className="card" style={{ marginBottom: 18 }}>
      <h2>What it&rsquo;s made from</h2>
      <p className="sub" style={{ marginTop: -6 }}>
        Its bill of materials — the parts one {itemName} draws off the shelf when you build it.
      </p>

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

      {data && displayRows.length === 0 && !editing && (
        <div className="empty-state">
          No parts list yet. Add one and you can build {itemName} out of what&rsquo;s on the shelf
          instead of buying it in.
        </div>
      )}

      {data && (displayRows.length > 0 || editing) && (
        <table className="invoice-lines-table">
          <thead>
            <tr>
              <th>Part</th>
              <th className="num">Per one</th>
              <th className="num">On hand</th>
              <th className="num">Cost each</th>
              <th className="num">Cost per one</th>
              {editing && <th />}
            </tr>
          </thead>
          <tbody>
            {displayRows.map((row, i) => {
              const part = row.itemId ? itemsById.get(row.itemId) ?? null : null;
              const saved = savedRows.find((c) => c.itemId === row.itemId) ?? null;
              // Defensive label lookup, same as the items screens: an item the
              // picker list doesn't carry (deactivated since, say) still has a
              // name on the saved row.
              const name = part?.name ?? saved?.name ?? "That part";
              const onHand = part?.qtyOnHand ?? saved?.qtyOnHand ?? null;
              const average = part?.averageCost ?? saved?.averageCost ?? null;
              const err = editing ? rowError(row) : null;
              const value = extended[i];
              return (
                <Fragment key={row.key}>
                  <tr>
                    <td>
                      {editing ? (
                        <SearchablePicker
                          id={`bom-part-${row.key}`}
                          label=""
                          placeholder="Search stocked items…"
                          groups={partGroups(row.itemId)}
                          value={row.itemId}
                          onChange={(id) => updateRow(row.key, { itemId: id })}
                          emptyMessage="No other stocked items to pick from."
                        />
                      ) : (
                        <button
                          type="button"
                          className="acct-link"
                          onClick={() => row.itemId && navigate({ view: "item", id: row.itemId })}
                        >
                          {name}
                        </button>
                      )}
                    </td>
                    <td className="num">
                      {editing ? (
                        <input
                          className="text-input line-qty"
                          type="text"
                          inputMode="decimal"
                          aria-label={`How many ${name} go into one ${itemName}`}
                          value={row.qtyPer}
                          onChange={(ev) => updateRow(row.key, { qtyPer: ev.target.value })}
                        />
                      ) : (
                        fmtQuantity(row.qtyPer)
                      )}
                    </td>
                    <td className="num">{onHand !== null ? fmtQuantity(onHand) : "—"}</td>
                    <td className="num">{average !== null ? fmtUnitCost(average) : "—"}</td>
                    <td className="num invoice-line-amount">{value ? fmtMoney(value) : "—"}</td>
                    {editing && (
                      <td>
                        <button
                          type="button"
                          className="acct-link"
                          onClick={() => setRows((rs) => rs.filter((r) => r.key !== row.key))}
                        >
                          Remove
                        </button>
                      </td>
                    )}
                  </tr>
                  {err && (
                    <tr>
                      <td colSpan={editing ? 6 : 5} className="field-error">{err}</td>
                    </tr>
                  )}
                </Fragment>
              );
            })}
          </tbody>
        </table>
      )}

      {editing && (
        <button
          type="button"
          className="acct-link"
          style={{ marginTop: 6 }}
          onClick={() => setRows((rs) => [...rs, { key: nextRowKey(), itemId: null, qtyPer: "1" }])}
        >
          + Add a part
        </button>
      )}

      {data && displayRows.length > 0 && (
        <div className="invoice-form-total">
          <span>One {itemName} costs, as things stand today</span>
          <span className="invoice-form-total-amount">{fmtMoney(total)}</span>
        </div>
      )}

      {data && displayRows.length > 0 && (
        <p className="sub">
          Nothing above is stored. Each part&rsquo;s cost is its average cost right now, so this
          total moves as your costs move — what a build actually costs is settled the moment you
          build it, from the averages on that day.
        </p>
      )}

      {editing ? (
        <>
          <p className="line-note" style={{ marginTop: 10 }}>
            {BOM_PART_RULE} {BOM_RECURSION_RULE}
          </p>
          <div className="wizard-actions">
            <button
              type="button"
              className="btn"
              disabled={saving || anyRowError}
              onClick={() => void save()}
            >
              {saving ? "Saving…" : "Save parts list"}
            </button>
            <button
              type="button"
              className="acct-link"
              disabled={saving}
              onClick={() => {
                setEditing(false);
                setSaveError(null);
              }}
            >
              Cancel
            </button>
          </div>
        </>
      ) : (
        <>
          <p className="sub" style={{ marginBottom: 10 }}>{BUILD_COST_NOTE}</p>
          <div className="wizard-actions">
            <button
              type="button"
              className="btn"
              onClick={() => {
                setEditing(true);
                // An empty parts list opens with one blank row rather than an
                // empty table and a lone "+ Add a part" link — the first thing
                // somebody who just clicked "Add a parts list" wants is a place
                // to put the first part. (The reseeding effect above only runs
                // while NOT editing, so this survives the flip.)
                if (savedRows.length === 0) {
                  setRows([{ key: nextRowKey(), itemId: null, qtyPer: "1" }]);
                }
              }}
            >
              {savedRows.length === 0 ? "Add a parts list" : "Edit parts list"}
            </button>
          </div>
        </>
      )}
    </div>
  );
}
