// "Does this company keep stock at all?" — Task 5 of
// docs/superpowers/plans/2026-08-03-inventory.md.
//
// A service business has no builds, no stock adjustments, no opening
// quantities and nothing to value. Showing it two dead nav entries and two
// empty report tabs is exactly the QuickBooks failure mode CLAUDE.md §9 exists
// to avoid — every one of them would be a door onto a blank room. So the
// inventory screens and the inventory valuation tab appear only once at least
// one STOCKED item exists, the same "invisible until it applies" gate the
// Sales tax tab already uses (Reports.tsx).
//
// Item profitability has a wider gate — hasItems, not hasStockedItems —
// because a company that sells services through items still has revenue per
// item worth looking at (item_profitability() reports every kind).
//
// ---------------------------------------------------------------------------
// WHY THE REPORTS ARE GATED ON A THIRD, WIDER QUESTION.
//
// Active-only is right for NAV and PICKERS and wrong for REPORTS, and the
// difference is not cosmetic. An item that has ever moved can never be
// deleted, only deactivated — so take a company with 40 units and $100.00 of
// inventory value and turn that item off (entirely legitimate: it is how you
// retire a product). The company is still carrying $100.00 of inventory on its
// balance sheet, but an active-only gate removes the Inventory valuation tab
// AND redirects anyone standing on it away — the one report that explains that
// $100.00 vanishes at the exact moment it is the only thing that would.
//
// So `everHeldStock` (GET /api/c/items, server/src/items.ts) answers "has this
// company ever held stock", from inventory movements OR an inventory account
// carrying a balance, and the report gates below are `active-now OR ever`.
// Nav and pickers keep the active-only predicate untouched.
// ---------------------------------------------------------------------------
//
// ---------------------------------------------------------------------------
// WHY THERE IS A MODULE-LEVEL STORE HERE AND NOT JUST A HOOK.
//
// The Sidebar never unmounts. A plain per-hook fetch therefore answers "no
// stocked items" for the rest of the session after a user creates their very
// first one, and the two nav entries only appear after a page reload.
//
// The first attempt at fixing that keyed the fetch on the current route, on the
// theory that the user navigates after adding an item. Review caught that this
// does not do what it claims: creating an item on #items does not change the
// route, so nothing refetched — it downgraded "until you reload" to "until you
// navigate somewhere else", while the comment claimed the bug was fixed. Worse
// in reverse: deactivate the last stocked item and the entries linger, pointing
// at screens whose pickers are now empty.
//
// So the answer lives in one place that every consumer subscribes to, and the
// screens that CHANGE it say so: `itemsChanged()` after any create, edit or
// activate/deactivate. That is a real signal at the real moment, it costs one
// query per actual mutation instead of one per navigation, and a consumer that
// mounts later (the Reports tabs) gets the cached answer immediately instead of
// flashing an empty tab strip while it refetches.
// ---------------------------------------------------------------------------
//
// Nothing cached here is depended on for correctness: /api/c/items derives
// on-hand and value at query time from inventory_movements (§3.4), and this
// only ever reads `kind` off the rows it returns. A failure means no extra nav
// entries and no extra report tabs — never a broken Sidebar (the same posture
// as checks/checksQueueCount.ts).

import { useEffect, useState } from "react";
import { apiGet } from "../lib/api.js";
import type { ApiItem } from "./itemTypes.js";

export interface ItemsPresence {
  /** At least one active item of any kind. */
  hasItems: boolean;
  /** At least one active item you keep a quantity of. */
  hasStockedItems: boolean;
  /** This company has held stock at some point — a movement was recorded, or
   *  an inventory account carries a balance. Stays true after the last stocked
   *  item is deactivated, which is the whole reason it exists (see the header).
   *  Report gates only; nav and pickers must not read it. */
  everHeldStock: boolean;
  /** True until the answer is actually known — callers hide a gated tab until
   *  then rather than flashing one that is about to disappear. */
  loading: boolean;
}

const UNKNOWN: ItemsPresence = {
  hasItems: false, hasStockedItems: false, everHeldStock: false, loading: true,
};
const NO_COMPANY: ItemsPresence = {
  hasItems: false, hasStockedItems: false, everHeldStock: false, loading: false,
};

/**
 * Should the Inventory valuation tab exist for this company?
 *
 * A live predicate rather than an expression inlined in Reports.tsx, because
 * this is the answer that was wrong (see the header) and it is worth a test
 * that does not need a React renderer to run.
 */
export function showsInventoryValuation(presence: ItemsPresence): boolean {
  return presence.hasStockedItems || presence.everHeldStock;
}

/**
 * Should the Item profitability tab exist? Wider than valuation — any item of
 * any kind will do — and widened again by everHeldStock for the same reason:
 * deactivating the items does not delete the sales history the report reads.
 */
export function showsItemProfitability(presence: ItemsPresence): boolean {
  return presence.hasItems || presence.everHeldStock;
}

/** Last known answer per company, so a component mounting later doesn't flash
 *  an empty tab strip while it refetches what the Sidebar already knows. */
const cache = new Map<string, ItemsPresence>();
const subscribers = new Set<() => void>();
/** Companies with a fetch already in flight — several consumers mounting in the
 *  same tick share one request. */
const inFlight = new Set<string>();

function publish(companyId: string, value: ItemsPresence): void {
  cache.set(companyId, value);
  for (const notify of [...subscribers]) notify();
}

function load(companyId: string): void {
  if (inFlight.has(companyId)) return;
  inFlight.add(companyId);
  // Active items only (the API's default): an item that has been turned off
  // shouldn't keep a nav entry alive on its own.
  apiGet<{ items: ApiItem[]; everHeldStock?: boolean }>("/api/c/items", companyId)
    .then((body) => {
      const items = body.items ?? [];
      publish(companyId, {
        hasItems: items.length > 0,
        hasStockedItems: items.some((i) => i.kind === "inventory"),
        everHeldStock: body.everHeldStock === true,
        loading: false,
      });
    })
    .catch(() => {
      publish(companyId, {
        hasItems: false, hasStockedItems: false, everHeldStock: false, loading: false,
      });
    })
    .finally(() => {
      inFlight.delete(companyId);
    });
}

/**
 * Tell every gate that this company's item list has changed — call it after any
 * create, edit, or activate/deactivate. Cheap and idempotent: it refetches once
 * and notifies whoever is listening.
 *
 * Passing no company refreshes every company already known, which is what a
 * caller wants when it doesn't know (or care) which one was touched.
 */
export function itemsChanged(companyId?: string | null): void {
  if (!companyId) {
    for (const id of [...cache.keys()]) load(id);
    return;
  }
  load(companyId);
}

/**
 * What the store currently says about a company — the whole answer, with no
 * React in it. The hook below is a subscription wrapped around this call, and
 * this is the seam the tests drive (tests/web/items-presence.test.ts), because
 * a hook cannot be called outside a renderer and the behaviour worth pinning is
 * the store's, not React's.
 */
export function itemsPresenceFor(companyId: string | null | undefined): ItemsPresence {
  if (!companyId) return NO_COMPANY;
  return cache.get(companyId) ?? UNKNOWN;
}

export function useItemsPresence(companyId: string | null | undefined): ItemsPresence {
  const [, forceRender] = useState(0);

  useEffect(() => {
    const notify = () => forceRender((n) => n + 1);
    subscribers.add(notify);
    if (companyId && !cache.has(companyId)) load(companyId);
    return () => {
      subscribers.delete(notify);
    };
  }, [companyId]);

  return itemsPresenceFor(companyId);
}

/** Test seam — drops every cached answer so a fresh assertion starts clean. */
export function __resetItemsPresence(): void {
  cache.clear();
  inFlight.clear();
}
