// Shapes, grouping and the tie's English for the inventory valuation report —
// Task 5 of docs/superpowers/plans/2026-08-03-inventory.md. Shared by the
// on-screen tab (InventoryValuation.tsx) and the print view
// (PrintInventoryValuation.tsx) so the two can never say different things
// about the same numbers.
//
// THE TIE IS THE POINT OF THIS REPORT. Every other inventory report in every
// other package asks you to trust that the valuation and the balance sheet
// agree. This one shows both numbers and the gap between them, per account,
// on its own face — the spec's "the tie shown on the report itself".
//
//   ledgerValue    the inventory account's balance, straight out of journal_lines
//   movementValue  the same figure summed from inventory_movements instead
//   difference     ledgerValue − movementValue, which must be 0.0000
//
// PER ACCOUNT, never one total: an error that moves value BETWEEN two inventory
// accounts sums to a serene zero while both are individually wrong (0018
// decision 7). `outOfTie` on the payload sums ABSOLUTE differences for the same
// reason, so it cannot be cancelled clean either.
//
// Neither number is stored. inventory_valuation() sums movements and
// check_inventory_ties() reads journal_lines, both at query time, both as at
// the report's own date (CLAUDE.md §3.4).

import { absMoney, fmtMoney, isZeroMoney } from "../lib/money.js";

export interface ValuationItem {
  itemId: string;
  sku: string | null;
  name: string;
  qtyOnHand: string;
  averageCost: string;
  value: string;
  inventoryAccountId: string | null;
  inventoryAccountCode: string | null;
  inventoryAccountName: string | null;
}

export interface ValuationAccount {
  accountId: string;
  code: string;
  name: string;
  ledgerValue: string;
  movementValue: string;
  difference: string;
}

export interface ValuationData {
  asOf: string | null;
  items: ValuationItem[];
  accounts: ValuationAccount[];
  totalValue: string;
  outOfTie: string;
  ties: boolean;
}

export interface ValuationGroup {
  /** null for the (shouldn't-happen) case of a stocked item with no inventory
   *  account — shown rather than dropped, because a row this report silently
   *  omitted would be the one row worth seeing. */
  account: ValuationAccount | null;
  items: ValuationItem[];
}

/** Items grouped under the inventory account they belong to, accounts in code
 *  order, then anything unattached. */
export function groupByAccount(data: ValuationData): ValuationGroup[] {
  const accounts = [...data.accounts].sort((a, b) => a.code.localeCompare(b.code));
  const groups: ValuationGroup[] = accounts.map((account) => ({
    account,
    items: data.items.filter((i) => i.inventoryAccountId === account.accountId),
  }));
  const known = new Set(accounts.map((a) => a.accountId));
  const orphans = data.items.filter(
    (i) => i.inventoryAccountId === null || !known.has(i.inventoryAccountId),
  );
  if (orphans.length > 0) groups.push({ account: null, items: orphans });
  return groups;
}

/** "Inventory account: $41,207.18 · Items on hand: $41,207.18 · Difference:
 *  $0.00" — the two independent derivations and the gap between them, in the
 *  spec's own words. */
export function tieLine(account: ValuationAccount): string {
  return `Inventory account: ${fmtMoney(account.ledgerValue)} · Items on hand: ${fmtMoney(account.movementValue)} · Difference: ${fmtMoney(account.difference)}`;
}

/**
 * What a non-zero difference MEANS, in plain English — never a bare number
 * (CLAUDE.md §9). Returns null when the account ties, which is the normal case
 * and needs no explanation beyond the zero itself.
 */
export function tieExplanation(account: ValuationAccount): string | null {
  if (isZeroMoney(account.difference)) return null;
  const gap = fmtMoney(absMoney(account.difference));
  const ledgerIsHigher = !account.difference.trim().startsWith("-");
  return ledgerIsHigher
    ? `${account.name} holds ${gap} more than the stock recorded against it is worth. Money went into this account without any stock coming in with it — most often a bill or a journal entry pointed straight at the account instead of at an item. Until that is fixed, your balance sheet says you own more stock than the shelf does.`
    : `There is ${gap} more stock recorded here than ${account.name} holds. Stock moved without the money moving with it, or money was taken out of this account on its own. Until that is fixed, your balance sheet says you own less stock than the shelf does.`;
}

/** The one-line verdict at the top of the report. */
export function tieVerdict(data: ValuationData): string {
  if (data.ties) {
    return "Every inventory account matches the stock recorded against it, to the penny. These two numbers are worked out separately — one from your journal entries, one from your stock movements — so agreeing is meaningful, not automatic.";
  }
  return `These don't agree. Adding up the differences on every inventory account comes to ${fmtMoney(absMoney(data.outOfTie))}. Each account below says which way it is out and what that means — start with the ones that aren't zero.`;
}
