"use client";

/**
 * Inventory, rebuilt on the Medora design system (Stage 7, 24 Oct 2026) to match `/design-system/inventory`:
 * the inventory list pattern (search, category, stock status, Products / Low stock / Expiring tabs, rows that
 * open their batches), the product record form, the stock adjustment form and the setup lists. Wired to the
 * Inventory API plan (DECISIONS.md, 24 Oct 2026): product codes, units, tax codes and adjustment reasons.
 *
 * Phase 1.2 (26 Oct 2026): a By branch tab (the design's branch stock matrix, allocation and the stock-per-branch
 * switch) and an Inventory log tab, in `branch-stock-view.tsx`. The list's Stock column stays the pharmacy's total.
 *
 * Kept from the old page because nothing else does it yet: a Services tab (services never appear in stock
 * views), opening stock on a new product, receiving stock into a new batch, and putting received
 * purchase-order lines into stock (receiving a PO doesn't add stock; this does, until goods-in, IN-16).
 */

import * as React from "react";
import { ChevronRightIcon, GitMergeIcon, HistoryIcon, LayersIcon, ListTreeIcon, ScissorsIcon, PackagePlusIcon, PencilIcon, PlusIcon, SearchIcon, SlidersHorizontalIcon, Trash2Icon, TruckIcon } from "lucide-react";

import { ConfirmDialog } from "@/components/medora/confirm-dialog";
import { RecordDrawer } from "@/components/medora/drawer";
import { ManagedList, type ListItem, type ListSpec } from "@/components/medora/managed-list";
import { PanelError } from "@/components/medora/errors";
import { MedoraPageHeader } from "@/components/medora/page-header";
import { ProductRecordForm, productProblems, type ProductDraft, type ProductLists } from "@/components/medora/product-record";
import { RowActions } from "@/components/medora/row-actions";
import { BatchChip, StockAdjustmentForm, StockLevel, type StockAdjustment } from "@/components/medora/stock";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Field, FieldDescription, FieldError, FieldLabel } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { InputGroup, InputGroupAddon, InputGroupInput } from "@/components/ui/input-group";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Spinner } from "@/components/ui/spinner";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { toast } from "@/components/ui/toast";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
import { InventoryApi, ProductsApi, SetupListsApi, ShelvesApi, SuppliersApi, TaxCodesApi, type SetupListName, PurchasingApi } from "@/lib/api";
import { useTabParam, validTab } from "@/lib/use-tab-param";
import { count, daysBetween, formatDate, formatKES, localToday, stockStatus } from "@/lib/format";
import { useMenuPermissions } from "@/lib/permissions";
import { BranchStockView, InventoryLogView } from "./branch-stock-view";
import { ConversionsView, ConvertDrawer } from "./conversion-view";
import { DeletedView, HistoryDrawer, MergeDrawer, MergesView, ShelfImportDrawer } from "./housekeeping-view";
import { Loading } from "@/components/medora/loading";

/* ------------------------------------ API mapping ------------------------------------ */

type ApiBatch = { id: number; batch?: string | null; expiryDate?: string | null; stock: number; costPrice: number };
type ApiProduct = {
  id: number; name: string; code?: string | null; barcode?: string | null; description?: string | null; category: string; subCategory?: string | null; brand?: string | null; unit?: string | null;
  type?: string; stock: number; minStock: number; sellPrice: number; costPrice: number; taxCodeId?: number | null; minMargin?: number | null; reorderQty?: number | null; manageStock?: boolean;
  shelfLocation?: string | null; requiresRx?: boolean; supplierId?: number | null; supplier?: { id: number; name: string } | null; imageUrl?: string | null;
  hsCode?: string | null; itemClass?: string | null; itemType?: string | null; origin?: string | null; packaging?: string | null; batches?: ApiBatch[];
  /** Phase 1.2: units at the branch the user sells from (their own, or the main branch). */
  branchStock?: number;
};
type ApiTaxCode = { id: number; code: string; name: string; rate: number; type: string; active: boolean };
type ApiListEntry = { id: string; name: string; short?: string | null; parent?: string | null; active: boolean; uses: number };
type ApiSupplier = { id: number; name: string };
type PoLine = { poId: number; supplier: string; supplierId?: number | null; itemId: number; productId?: number | null; name: string; qty: number; unitCost: number };

/** One row of the list, mapped once (CLAUDE.md: add product fields here, never at a call site). */
type Row = { id: number; name: string; code: string; unit: string; category: string; stock: number; branchStock: number; minStock: number; price: number; type: "Product" | "Service"; manageStock: boolean; batches: ApiBatch[]; raw: ApiProduct };

function mapProduct(p: ApiProduct): Row {
  const type = p.type === "Service" ? "Service" : "Product";
  return {
    id: p.id,
    name: p.name,
    code: p.code ?? "",
    unit: p.unit ?? "",
    category: p.category,
    stock: Number(p.stock) || 0,
    branchStock: p.branchStock != null ? Number(p.branchStock) || 0 : Number(p.stock) || 0,
    minStock: Number(p.minStock) || 0,
    price: Number(p.sellPrice) || 0,
    type,
    manageStock: type === "Product" && p.manageStock !== false,
    batches: (p.batches ?? []).map((b) => ({ ...b, stock: Number(b.stock) || 0, costPrice: Number(b.costPrice) || 0 })),
    raw: p,
  };
}

const EXPIRING_DAYS = 90; // the design's warning threshold for batch chips

/** The design's product draft for a product, and back. Supplier and tax code travel by name and code. */
function toDraft(p: ApiProduct | null, taxCodes: ApiTaxCode[], suppliers: ApiSupplier[]): ProductDraft {
  return {
    type: p?.type === "Service" ? "Service" : "Product",
    name: p?.name ?? "",
    code: p?.code ?? "",
    barcode: p?.barcode ?? "",
    description: p?.description ?? "",
    category: p?.category ?? "",
    subCategory: p?.subCategory ?? "",
    brand: p?.brand ?? "",
    unit: p?.unit ?? "",
    supplier: suppliers.find((s) => s.id === (p?.supplierId ?? p?.supplier?.id))?.name ?? "",
    cost: p ? String(Number(p.costPrice) || 0) : "",
    price: p ? String(Number(p.sellPrice) || 0) : "",
    taxCode: taxCodes.find((t) => t.id === p?.taxCodeId)?.code ?? (p ? "" : "B"),
    rx: !!p?.requiresRx,
    minMargin: p?.minMargin != null ? String(p.minMargin) : "",
    manageStock: p ? p.manageStock !== false : true,
    image: p?.imageUrl ?? "",
    reorderLevel: p ? String(p.minStock ?? 0) : "",
    reorderQty: p?.reorderQty != null ? String(p.reorderQty) : "",
    shelf: p?.shelfLocation ?? "",
    hsCode: p?.hsCode ?? "",
    itemClass: p?.itemClass ?? "",
    itemType: p?.itemType ?? "",
    origin: p?.origin ?? "",
    packaging: p?.packaging ?? "",
  };
}

function fromDraft(d: ProductDraft, taxCodes: ApiTaxCode[], suppliers: ApiSupplier[], original?: ProductDraft): Record<string, unknown> {
  const svc = d.type === "Service";
  const body: Record<string, unknown> = {
    type: d.type,
    name: d.name.trim(),
    code: d.code.trim(),
    barcode: d.barcode.trim(),
    description: d.description.trim(),
    category: d.category,
    subCategory: d.subCategory,
    brand: d.brand,
    unit: d.unit,
    taxCodeId: taxCodes.find((t) => t.code === d.taxCode)?.id ?? null,
    sellPrice: Number(d.price.replace(/,/g, "")) || 0,
    requiresRx: d.rx,
    minMargin: d.minMargin.trim() ? Number(d.minMargin) : null,
    manageStock: !svc && d.manageStock,
    minStock: svc ? 0 : Number(d.reorderLevel) || 0,
    reorderQty: d.reorderQty.trim() ? Number(d.reorderQty) : null,
    shelfLocation: svc ? "" : d.shelf,
    supplierId: suppliers.find((s) => s.name === d.supplier)?.id ?? null,
    hsCode: d.hsCode,
    itemClass: d.itemClass,
    itemType: d.itemType,
    origin: d.origin,
    packaging: d.packaging,
  };
  // Sending a cost rewrites the newest batch's cost (ProductsService.update), so only send it when changed
  if (!original || d.cost !== original.cost) body.costPrice = Number(d.cost.replace(/,/g, "")) || 0;
  return body;
}

/**
 * The next free code in the PRD-#### series, as the API would assign it (product-rules.ts nextProductCode). A deleted
 * product keeps its code (it can be restored), so `held` lists those too.
 */
function nextCode(rows: Row[], held: string[] = []): string {
  const codes = [...rows.map((r) => r.code), ...held].map((c) => c.toUpperCase());
  const taken = new Set(codes);
  let n = 1 + Math.max(0, ...codes.map((c) => Number(/^PRD-(\d+)$/.exec(c)?.[1] ?? 0)));
  while (taken.has(`PRD-${String(n).padStart(4, "0")}`)) n++;
  return `PRD-${String(n).padStart(4, "0")}`;
}

const apiError = (e: unknown, fallback: string): string => {
  const m = (e as { response?: { data?: { message?: string | string[] } } })?.response?.data?.message;
  return Array.isArray(m) ? m.join(" ") : m || fallback;
};

/* ------------------------------------- List row ------------------------------------- */

function ProductRow({ r, actions, today }: { r: Row; actions: React.ComponentProps<typeof RowActions>["actions"]; today: string }) {
  const [open, setOpen] = React.useState(false);
  const st = stockStatus({ stock: r.stock, minStock: r.minStock });
  const fefo = [...r.batches].filter((b) => b.stock > 0).sort((a, b) => (a.expiryDate ?? "9999") < (b.expiryDate ?? "9999") ? -1 : 1);
  return (
    <React.Fragment>
      <TableRow>
        <TableCell>
          <button type="button" onClick={() => setOpen((o) => !o)} aria-expanded={open} className="flex cursor-pointer items-center gap-1 outline-none focus-visible:ring-2 focus-visible:ring-ring" aria-label={`Batches of ${r.name}`}>
            <ChevronRightIcon className="size-4 transition-transform" style={{ transform: open ? "rotate(90deg)" : undefined }} />
            <span className="font-medium">{r.name}</span>
          </button>
          <div className="pl-5 text-xs text-muted-foreground">
            {r.code}
            {r.unit ? ` · ${r.unit}` : ""}
          </div>
        </TableCell>
        <TableCell>{r.category}</TableCell>
        <TableCell className="text-right tabular-nums">{r.manageStock ? r.stock : "—"}</TableCell>
        <TableCell className="text-right tabular-nums">{formatKES(r.price, { decimals: true })}</TableCell>
        <TableCell>{r.manageStock ? <Badge variant={st === "In stock" ? "outline" : st === "Low stock" ? "secondary" : "destructive"}>{st}</Badge> : <Badge variant="outline">Not counted</Badge>}</TableCell>
        <TableCell className="py-1 text-right">{actions.length ? <RowActions label={r.name} actions={actions} /> : null}</TableCell>
      </TableRow>
      {open && (
        <tr>
          <td colSpan={6} className="bg-muted/50 px-4 py-2">
            {r.manageStock ? (
              <div className="space-y-2">
                <StockLevel stock={r.stock} minStock={r.minStock} className="max-w-sm" />
                {fefo.length ? (
                  <table className="w-full text-xs">
                    <thead>
                      <tr className="text-left text-muted-foreground">
                        <th className="py-1 font-medium">Batch</th>
                        <th className="font-medium">Expiry</th>
                        <th className="text-right font-medium">Stock</th>
                        <th className="text-right font-medium">Cost</th>
                      </tr>
                    </thead>
                    <tbody>
                      {fefo.map((b) => (
                        <tr key={b.id} className="border-t border-border">
                          <td className="py-1 font-mono">{b.batch || "—"}</td>
                          <td className="py-1">{b.expiryDate ? <span className="inline-flex items-center gap-2">{formatDate(b.expiryDate)} <BatchChip expiryDate={b.expiryDate} today={today} /></span> : "No expiry"}</td>
                          <td className="text-right tabular-nums">{b.stock}</td>
                          <td className="text-right tabular-nums">{formatKES(b.costPrice, { decimals: true })}</td>
                        </tr>
                      ))}
                    </tbody>
                  </table>
                ) : (
                  <p className="text-xs text-muted-foreground">No batches with stock.</p>
                )}
              </div>
            ) : (
              <p className="text-xs text-muted-foreground">{r.type === "Service" ? "A service: no stock or batches." : "Stock isn't counted for this item."}</p>
            )}
          </td>
        </tr>
      )}
    </React.Fragment>
  );
}

/* ----------------------------------- Receive stock ----------------------------------- */

type Receive = { product?: Row; poLine?: PoLine; qty: string; batch: string; expiry: string; cost: string; tried: boolean };

function ReceiveForm({ value, onChange, products }: { value: Receive; onChange: (v: Receive) => void; products: Row[] }) {
  const p = receiveProblems(value);
  const show = value.tried;
  return (
    <div className="grid gap-4">
      {value.poLine ? (
        <p className="rounded-md border border-[var(--color-border-panel)] bg-muted/40 px-3 py-2 text-sm sm:col-span-2">
          From purchase order PO-{value.poLine.poId} ({value.poLine.supplier}): {value.poLine.qty} × {value.poLine.name}.
        </p>
      ) : null}
      <Field data-invalid={show && !!p.product} className="sm:col-span-2">
        <FieldLabel htmlFor="rc-product">Product</FieldLabel>
        <select id="rc-product" className="h-9 w-full rounded-md border border-input bg-background px-3 text-sm" value={value.product?.id ?? ""} onChange={(e) => onChange({ ...value, product: products.find((x) => x.id === Number(e.target.value)) })} aria-invalid={show && !!p.product}>
          <option value="">Choose the product…</option>
          {products.filter((x) => x.manageStock).map((x) => (
            <option key={x.id} value={x.id}>
              {x.name} ({x.code})
            </option>
          ))}
        </select>
        {show && p.product ? <FieldError>{p.product}</FieldError> : value.poLine && !value.product ? <FieldDescription>No product matched &quot;{value.poLine.name}&quot;. Choose it, or add it first.</FieldDescription> : null}
      </Field>
      <Field data-invalid={show && !!p.qty}>
        <FieldLabel htmlFor="rc-qty">Quantity received</FieldLabel>
        <Input id="rc-qty" inputMode="numeric" className="tabular-nums" value={value.qty} onChange={(e) => onChange({ ...value, qty: e.target.value.replace(/\D/g, "") })} aria-invalid={show && !!p.qty} />
        {show && p.qty ? <FieldError>{p.qty}</FieldError> : null}
      </Field>
      <Field data-invalid={show && !!p.cost}>
        <FieldLabel htmlFor="rc-cost">Cost per unit (KES)</FieldLabel>
        <Input id="rc-cost" inputMode="decimal" className="tabular-nums" value={value.cost} onChange={(e) => onChange({ ...value, cost: e.target.value })} aria-invalid={show && !!p.cost} />
        {show && p.cost ? <FieldError>{p.cost}</FieldError> : null}
      </Field>
      <Field>
        <FieldLabel htmlFor="rc-batch">Batch number</FieldLabel>
        <Input id="rc-batch" className="font-mono" value={value.batch} onChange={(e) => onChange({ ...value, batch: e.target.value })} />
      </Field>
      <Field data-invalid={show && !!p.expiry}>
        <FieldLabel htmlFor="rc-exp">Expiry date</FieldLabel>
        <Input id="rc-exp" type="date" value={value.expiry} onChange={(e) => onChange({ ...value, expiry: e.target.value })} aria-invalid={show && !!p.expiry} />
        {show && p.expiry ? <FieldError>{p.expiry}</FieldError> : <FieldDescription>Stock is sold earliest expiry first.</FieldDescription>}
      </Field>
    </div>
  );
}

function receiveProblems(v: Receive) {
  return {
    product: !v.product ? "Choose the product." : undefined,
    qty: !(Number(v.qty) > 0) ? "Enter how many units arrived." : undefined,
    cost: !/^\d+(\.\d{1,2})?$/.test(v.cost.replace(/,/g, "")) ? "Enter the cost per unit in shillings." : undefined,
    expiry: v.expiry && v.expiry < localToday() ? "This batch has already expired." : undefined,
  };
}

/* ------------------------------------ Setup lists ------------------------------------ */

const LIST_SPECS: Record<SetupListName, { title: string; spec: ListSpec }> = {
  categories: { title: "Categories", spec: { noun: "category", usedBy: "products" } },
  "sub-categories": { title: "Sub-categories", spec: { noun: "sub-category", usedBy: "products" } },
  brands: { title: "Brands", spec: { noun: "brand", usedBy: "products" } },
  units: { title: "Units", spec: { noun: "unit", usedBy: "products", short: { label: "Short form", max: 10 } } },
  "waste-reasons": { title: "Waste reasons", spec: { noun: "waste reason", usedBy: "write-off lines" } },
};

function SetupLists({ lists, reload }: { lists: Record<SetupListName, ApiListEntry[]>; reload: () => Promise<void> }) {
  const [busy, setBusy] = React.useState(false);
  const categories = lists.categories.filter((c) => c.active).map((c) => c.name);

  /** ManagedList hands back the whole list; turn the difference into API calls, then reload. */
  const apply = async (list: SetupListName, next: ListItem[]) => {
    const before = new Map(lists[list].map((e) => [e.id, e]));
    setBusy(true);
    try {
      for (const it of next) {
        const old = before.get(it.id);
        if (!old) await SetupListsApi.add(list, { name: it.name, short: it.short ?? null, parent: it.parent ?? null });
        else if (old.name !== it.name || (old.short ?? "") !== (it.short ?? "") || (old.parent ?? "") !== (it.parent ?? "") || old.active !== it.active) {
          await SetupListsApi.update(list, it.id, { name: it.name, short: it.short ?? null, parent: it.parent ?? null, active: it.active });
        }
      }
      for (const id of before.keys()) if (!next.some((it) => it.id === id)) await SetupListsApi.remove(list, id);
    } catch (e) {
      toast.add({ type: "error", title: "Not saved", description: apiError(e, "Try again.") });
    } finally {
      await reload();
      setBusy(false);
    }
  };

  return (
    <Tabs defaultValue="categories">
      <TabsList variant="line" className="w-full justify-start border-b border-border">
        {(Object.keys(LIST_SPECS) as SetupListName[]).map((k) => (
          <TabsTrigger key={k} value={k} className="flex-none">
            {LIST_SPECS[k].title}
          </TabsTrigger>
        ))}
        {busy ? <Spinner className="ml-auto" /> : null}
      </TabsList>
      {(Object.keys(LIST_SPECS) as SetupListName[]).map((k) => (
        <TabsContent key={k} value={k} className="pt-4">
          <ManagedList
            spec={k === "sub-categories" ? { ...LIST_SPECS[k].spec, parents: { label: "Category", options: categories } } : LIST_SPECS[k].spec}
            items={lists[k].map((e) => ({ id: e.id, name: e.name, short: e.short ?? undefined, parent: e.parent ?? undefined, active: e.active, uses: e.uses }))}
            onChange={(items) => void apply(k, items)}
            onNotice={(title, description) => toast.add({ title, description })}
          />
        </TabsContent>
      ))}
    </Tabs>
  );
}

/* --------------------------------------- Page --------------------------------------- */

const emptyLists: Record<SetupListName, ApiListEntry[]> = { categories: [], "sub-categories": [], brands: [], units: [], "waste-reasons": [] };

export default function InventoryPage() {
  const { can, loaded } = useMenuPermissions();
  const may = (page: string, action: string) => loaded && can(page, action);
  const today = localToday();
  const [rows, setRows] = React.useState<Row[] | null>(null);
  const [failed, setFailed] = React.useState(false); // R12 (card 037): a failed load says so, with Try again
  const [lists, setLists] = React.useState(emptyLists);
  const [taxCodes, setTaxCodes] = React.useState<ApiTaxCode[]>([]);
  const [deletedCodes, setDeletedCodes] = React.useState<string[]>([]);
  const [suppliers, setSuppliers] = React.useState<ApiSupplier[]>([]);
  const [poLines, setPoLines] = React.useState<PoLine[]>([]);
  const [tabParam, setTab] = useTabParam(); // page tabs change the URL (tabs spec 7.1)
  const tab = validTab(tabParam, ["products", "low", "expiring", "services", "branches", "log", "conversions", "merges", "deleted"], "products");
  const [q, setQ] = React.useState("");
  const [cat, setCat] = React.useState("All categories");
  const [status, setStatus] = React.useState<string[]>([]);

  const [form, setForm] = React.useState<{ id?: number; d: ProductDraft; original: ProductDraft; opening: { qty: string; batch: string; expiry: string }; tried: boolean } | null>(null);
  const [imageFile, setImageFile] = React.useState<File | null>(null);
  const [saving, setSaving] = React.useState(false);
  const [adjusting, setAdjusting] = React.useState<Row | null>(null);
  const [converting, setConverting] = React.useState<Row | null>(null);
  const [adjustState, setAdjustState] = React.useState<"default" | "loading" | "error">("default");
  const [adjustError, setAdjustError] = React.useState("");
  const [receiving, setReceiving] = React.useState<Receive | null>(null);
  const [deleting, setDeleting] = React.useState<Row | null>(null);
  // Plan B: history, merging (IN-07, IN-08) and shelves (ST-05, IN-05)
  const [historyOf, setHistoryOf] = React.useState<Row | null>(null);
  const [merging, setMerging] = React.useState<Row | null>(null);
  const [assigningShelves, setAssigningShelves] = React.useState(false);
  const [shelfCodes, setShelfCodes] = React.useState<string[]>([]);
  const [showLists, setShowLists] = React.useState(false);
  // Phase 1.2: with stock per branch on, an adjustment changes the user's own branch's stock
  const [byBranch, setByBranch] = React.useState(false);
  const loadSwitch = React.useCallback(() => InventoryApi.status().then((s) => setByBranch(s.stockByBranch)).catch(() => setByBranch(false)), []);

  const loadProducts = React.useCallback(async () => {
    const ps: ApiProduct[] = await ProductsApi.getAll();
    setRows(ps.map(mapProduct));
    setFailed(false);
    // Deleted products keep their codes, so a new product's suggested code skips them
    ProductsApi.deleted()
      .then((d) => setDeletedCodes(d.map((x) => x.code ?? "").filter(Boolean)))
      .catch(() => {});
  }, []);
  const loadLists = React.useCallback(async () => {
    const [categories, subs, brands, units, wasteReasons] = await Promise.all((["categories", "sub-categories", "brands", "units", "waste-reasons"] as SetupListName[]).map((l) => SetupListsApi.list(l) as Promise<ApiListEntry[]>));
    setLists({ categories, "sub-categories": subs, brands, units, "waste-reasons": wasteReasons });
  }, []);
  const loadPoLines = React.useCallback(async () => {
    const pos: { id: number; status: string; supplierId?: number; supplier?: { name: string }; items?: { id: number; productId?: number | null; productName?: string; product?: { name: string }; receivedQuantity?: number; quantity?: number; unitCost?: number | string; isImported?: boolean }[] }[] = await SuppliersApi.getAllPOs(undefined, undefined);
    setPoLines(
      pos
        .filter((po) => po.status === "Received" || po.status === "Partially Received")
        .flatMap((po) =>
          (po.items ?? [])
            .filter((it) => !it.isImported && (it.receivedQuantity ?? 0) > 0)
            .map((it) => ({ poId: po.id, supplier: po.supplier?.name ?? "Supplier", supplierId: po.supplierId ?? null, itemId: it.id, productId: it.productId, name: it.productName || it.product?.name || "Item", qty: it.receivedQuantity ?? it.quantity ?? 0, unitCost: Number(it.unitCost) || 0 })),
        ),
    );
  }, []);

  React.useEffect(() => {
    loadProducts().catch((e) => (setFailed(true), toast.add({ type: "error", title: "Couldn't load products", description: apiError(e, "Try again.") })));
    loadLists().catch(() => undefined);
    TaxCodesApi.list().then(setTaxCodes).catch(() => setTaxCodes([]));
    SuppliersApi.getAll().then((s: ApiSupplier[]) => setSuppliers(s)).catch(() => setSuppliers([]));
    loadPoLines().catch(() => setPoLines([]));
    void loadSwitch();
    ShelvesApi.list().then((s) => setShelfCodes(s.filter((x) => x.active).map((x) => x.code))).catch(() => setShelfCodes([]));
  }, [loadProducts, loadLists, loadPoLines, loadSwitch]);
  const onHand = (r: Row) => (byBranch ? r.branchStock : r.stock);

  const all = rows ?? [];
  const stocked = all.filter((r) => r.type === "Product");
  const services = all.filter((r) => r.type === "Service");
  const counted = stocked.filter((r) => r.manageStock);
  const low = counted.filter((r) => r.stock <= r.minStock);
  const expiring = counted.filter((r) => r.batches.some((b) => b.stock > 0 && b.expiryDate && daysBetween(today, b.expiryDate) < EXPIRING_DAYS));
  const categoryNames = ["All categories", ...Array.from(new Set(all.map((r) => r.category))).sort()];
  const base = tab === "low" ? low : tab === "expiring" ? expiring : tab === "services" ? services : stocked;
  const shown = base
    .filter((r) => !q.trim() || `${r.name} ${r.code} ${r.raw.barcode ?? ""}`.toLowerCase().includes(q.trim().toLowerCase()))
    .filter((r) => cat === "All categories" || r.category === cat)
    .filter((r) => tab === "services" || status.length === 0 || status.includes(r.manageStock ? stockStatus({ stock: r.stock, minStock: r.minStock }) : ""));

  const productLists: ProductLists = {
    categories: Object.fromEntries(lists.categories.filter((c) => c.active).map((c) => [c.name, lists["sub-categories"].filter((s) => s.active && s.parent === c.name).map((s) => s.name)])),
    brands: lists.brands.filter((b) => b.active).map((b) => b.name),
    units: lists.units.filter((u) => u.active).map((u) => u.name),
    suppliers: suppliers.map((s) => s.name),
    taxCodes: taxCodes.filter((t) => t.active).map((t) => ({ code: t.code, label: t.name })),
    // ST-05: the shelves in Settings; a product keeps the shelf it is already on
    shelves: [...new Set([...shelfCodes, form?.original.shelf ?? ""].filter(Boolean))].sort(),
    // eTIMS code lists come with the eTIMS work (decisions E1 to E4): the form shows the gaps and still saves
    itemClasses: [],
    itemTypes: [],
    origins: [],
    packaging: [],
  };

  const openForm = (r?: Row, type: "Product" | "Service" = "Product") => {
    const d = toDraft(r?.raw ?? null, taxCodes, suppliers);
    if (!r) {
      d.type = type;
      d.code = nextCode(all, deletedCodes); // "Suggested", as the form says; the user may change it
    }
    setImageFile(null);
    setForm({ id: r?.id, d, original: d, opening: { qty: "", batch: "", expiry: "" }, tried: false });
  };

  const formCheck = form
    ? productProblems(
        form.d,
        { codes: all.filter((r) => r.id !== form.id).map((r) => r.code.toUpperCase()), barcodes: all.filter((r) => r.id !== form.id).map((r) => r.raw.barcode ?? "").filter(Boolean) },
        // A product keeps a list value it already has, even if that item was since deactivated
        { ...productLists, units: [...new Set([...productLists.units, form.original.unit].filter(Boolean))], brands: [...new Set([...productLists.brands, form.original.brand].filter(Boolean))] },
      )
    : { problems: {}, etimsGaps: [] };
  const openingProblem = form && !form.id && form.d.type === "Product" && form.d.manageStock && form.opening.qty && form.opening.expiry && form.opening.expiry < today ? "This stock has already expired." : undefined;

  const saveProduct = async () => {
    if (!form) return;
    if (Object.keys(formCheck.problems).length || openingProblem) return setForm({ ...form, tried: true });
    const body = fromDraft(form.d, taxCodes, suppliers, form.id ? form.original : undefined);
    if (!form.id && form.d.type === "Product" && form.d.manageStock) {
      body.stock = Number(form.opening.qty) || 0;
      body.batch = form.opening.batch.trim() || null;
      body.expiryDate = form.opening.expiry || null;
    } else if (!form.id) body.stock = 0;
    setSaving(true);
    try {
      const saved: ApiProduct = form.id ? await ProductsApi.update(form.id, body) : await ProductsApi.create(body);
      if (imageFile) {
        try {
          await ProductsApi.uploadImage(saved.id, imageFile);
        } catch (e) {
          toast.add({ type: "error", title: "Saved, but the image didn't upload", description: apiError(e, "Try again from Edit.") });
        }
      }
      await loadProducts();
      toast.add({ title: form.id ? `${saved.name} saved` : `${saved.name} added`, description: form.id ? undefined : `Code ${saved.code}.` });
      if (formCheck.etimsGaps.length) toast.add({ title: "Not ready for eTIMS receipts yet", description: `Missing: ${formCheck.etimsGaps.join(", ")}.` });
      setForm(null);
    } catch (e) {
      toast.add({ type: "error", title: "Product not saved", description: apiError(e, "Try again.") });
    } finally {
      setSaving(false);
    }
  };

  const saveAdjustment = async (a: StockAdjustment) => {
    if (!adjusting) return;
    if (a.direction === "remove" && a.quantity > onHand(adjusting)) {
      setAdjustState("error");
      setAdjustError(`Only ${onHand(adjusting)} on hand${byBranch ? " at your branch" : ""}.`);
      return;
    }
    setAdjustState("loading");
    try {
      await ProductsApi.adjustStock(adjusting.id, a.direction === "add" ? a.quantity : -a.quantity, a.reason, a.note);
      await loadProducts();
      toast.add({ title: `${adjusting.name}: stock ${a.direction === "add" ? "added" : "removed"}`, description: `${count(Number(a.quantity), "unit")}, ${a.reason}.` });
      setAdjusting(null);
      setAdjustState("default");
    } catch (e) {
      setAdjustState("error");
      setAdjustError(apiError(e, "Not saved. Try again."));
    }
  };

  const saveReceipt = async () => {
    if (!receiving) return;
    const p = receiveProblems(receiving);
    if (Object.values(p).some(Boolean)) return setReceiving({ ...receiving, tried: true });
    const r = receiving.product!;
    setSaving(true);
    try {
      // A new batch for the arrival (ProductsService.update with appendBatch)
      await ProductsApi.update(r.id, { appendBatch: true, stock: Number(receiving.qty), costPrice: Number(receiving.cost.replace(/,/g, "")), batch: receiving.batch.trim() || null, expiryDate: receiving.expiry || null, ...(receiving.poLine?.supplierId ? { supplierId: receiving.poLine.supplierId } : {}), ...(receiving.poLine ? { fromPoItemId: receiving.poLine.itemId } : {}) });
      if (receiving.poLine) await SuppliersApi.markPOItemImported(receiving.poLine.itemId);
      await Promise.all([loadProducts(), loadPoLines()]);
      toast.add({ title: `${receiving.qty} × ${r.name} received` });
      setReceiving(null);
    } catch (e) {
      toast.add({ type: "error", title: "Stock not received", description: apiError(e, "Try again.") });
    } finally {
      setSaving(false);
    }
  };

  const addToOrder = async (r: Row) => {
    const qty = Number(r.raw.reorderQty) || Math.max(1, (Number(r.minStock) || 0) * 2 - Math.max(0, r.stock));
    try {
      const po = await PurchasingApi.addToDraft(r.id, qty);
      toast.add({ title: `${qty} × ${r.name} on ${po.number}`, description: `Draft order to ${po.supplier}. Open Purchase orders to send it for approval.` });
    } catch (e) {
      toast.add({ type: "error", title: "Not added", description: (e as { response?: { data?: { message?: string } } })?.response?.data?.message ?? "Try again." });
    }
  };

  const rowActions = (r: Row) => [
    ...(may("Inventory/Products", "Update") && r.manageStock
      ? [
          { label: "Adjust stock", icon: SlidersHorizontalIcon, onSelect: () => { setAdjustState("default"); setAdjusting(r); } },
          { label: "Receive stock", icon: PackagePlusIcon, onSelect: () => setReceiving({ product: r, qty: "", batch: "", expiry: "", cost: String(Number(r.raw.costPrice) || ""), tried: false }) },
          // IN-21 (plan A): break it into its smaller unit
          { label: "Convert stock", icon: ScissorsIcon, onSelect: () => setConverting(r) },
        ]
      : []),
    // IN-15: onto the supplier's draft order (a new draft if there's none), the reorder quantity or enough to reach twice the reorder level
    ...(may("Inventory/Purchase orders", "Create") && r.manageStock
      ? [{ label: "Add to order", icon: TruckIcon, onSelect: () => void addToOrder(r) }]
      : []),
    ...(may("Inventory/Products", "Update") ? [{ label: r.type === "Service" ? "Edit service" : "Edit product", icon: PencilIcon, onSelect: () => openForm(r) }] : []),
    { label: "History", icon: HistoryIcon, onSelect: () => setHistoryOf(r) },
    // IN-07, Q2: merging can't be undone, so it needs Delete
    ...(may("Inventory/Products", "Delete") ? [{ label: "Merge into…", icon: GitMergeIcon, onSelect: () => setMerging(r) }] : []),
    ...(may("Inventory/Products", "Delete") ? [{ label: "Delete…", icon: Trash2Icon, destructive: true, onSelect: () => setDeleting(r) }] : []),
  ];

  return (
    <div className="medora mx-auto w-full max-w-6xl space-y-4">
      <MedoraPageHeader
        title="Inventory"
        description={rows ? `${count(stocked.length, "product")} across all batches${services.length ? `, ${count(services.length, "service")}` : ""}` : "Loading…"}
        primary={may("Inventory/Products", "Create") ? { label: tab === "services" ? "Add service" : "Add product", icon: PlusIcon, onSelect: () => openForm(undefined, tab === "services" ? "Service" : "Product") } : undefined}
        secondary={[
          ...(may("Inventory/Setup lists", "Read") ? [{ label: "Setup lists", icon: ListTreeIcon, onSelect: () => setShowLists(true) }] : []),
          ...(may("Inventory/Products", "Update") ? [{ label: "Assign shelves", icon: LayersIcon, onSelect: () => setAssigningShelves(true) }] : []),
        ]}
      />

      {poLines.length && may("Inventory/Products", "Update") ? (
        <section className="rounded-lg border border-[var(--color-border-panel)] bg-card">
          <p className="flex items-center gap-2 border-b border-border px-4 py-2 text-sm font-medium">
            <TruckIcon className="size-4" aria-hidden /> Received from suppliers, not yet in stock ({poLines.length})
          </p>
          <ul className="divide-y divide-[var(--color-border-subtle)] text-sm">
            {poLines.slice(0, 6).map((l) => (
              <li key={l.itemId} className="flex flex-wrap items-center gap-3 px-4 py-2">
                <span className="font-medium">{l.name}</span>
                <span className="text-xs text-muted-foreground tabular-nums">
                  {count(l.qty, "unit")} · PO-{l.poId} · {l.supplier}
                </span>
                <Button size="xs" variant="outline" className="ml-auto" onClick={() => setReceiving({ poLine: l, product: all.find((r) => (l.productId && r.id === l.productId) || r.name.toLowerCase() === l.name.toLowerCase()), qty: String(l.qty), batch: `PO-${l.poId}`, expiry: "", cost: String(l.unitCost || ""), tried: false })}>
                  Put into stock
                </Button>
              </li>
            ))}
          </ul>
        </section>
      ) : null}

      <section className="overflow-hidden rounded-lg border border-border bg-background">
        <div className={`flex flex-wrap items-center gap-2 border-b border-border px-4 py-2${["branches", "log", "conversions", "deleted", "merges"].includes(tab) ? " hidden" : ""}`}>
          <InputGroup className="w-56">
            <InputGroupAddon>
              <SearchIcon />
            </InputGroupAddon>
            <InputGroupInput placeholder="Search products" value={q} onChange={(e) => setQ(e.target.value)} aria-label="Search products" />
          </InputGroup>
          <Select value={cat} onValueChange={(v) => v && setCat(String(v))}>
            <SelectTrigger size="sm" className="w-44">
              <SelectValue />
            </SelectTrigger>
            <SelectContent>
              {categoryNames.map((c) => (
                <SelectItem key={c} value={c}>
                  {c}
                </SelectItem>
              ))}
            </SelectContent>
          </Select>
          {tab !== "services" ? (
            <ToggleGroup variant="outline" size="sm" value={status} onValueChange={(v) => setStatus(v as string[])} multiple spacing={0}>
              {["In stock", "Low stock", "Out of stock"].map((s) => (
                <ToggleGroupItem key={s} value={s}>
                  {s}
                </ToggleGroupItem>
              ))}
            </ToggleGroup>
          ) : null}
        </div>
        <Tabs value={tab} onValueChange={(v) => setTab(String(v))} className="gap-0">
          <TabsList variant="line" className="w-full justify-start border-b border-border px-4">
            <TabsTrigger value="products" className="flex-none">
              Products <Badge variant="secondary">{stocked.length}</Badge>
            </TabsTrigger>
            <TabsTrigger value="low" className="flex-none">
              Low stock <Badge variant="secondary">{low.length}</Badge>
            </TabsTrigger>
            <TabsTrigger value="expiring" className="flex-none">
              Expiring <Badge variant="secondary">{expiring.length}</Badge>
            </TabsTrigger>
            <TabsTrigger value="services" className="flex-none">
              Services <Badge variant="secondary">{services.length}</Badge>
            </TabsTrigger>
            <TabsTrigger value="branches" className="flex-none">
              By branch
            </TabsTrigger>
            <TabsTrigger value="log" className="flex-none">
              Inventory log
            </TabsTrigger>
            <TabsTrigger value="conversions" className="flex-none">
              Conversions
            </TabsTrigger>
            <TabsTrigger value="merges" className="flex-none">
              Merges
            </TabsTrigger>
            <TabsTrigger value="deleted" className="flex-none">
              Deleted
            </TabsTrigger>
          </TabsList>
          <div className="overflow-auto">
            {tab === "branches" ? (
              <div className="p-4">
                <BranchStockView
                  canAllocate={may("Inventory/Products", "Update") && may("Inventory/Products", "All branches")}
                  canSwitch={may("Settings/General settings", "Update")}
                  onChanged={() => void Promise.all([loadProducts(), loadSwitch()])}
                />
              </div>
            ) : tab === "log" ? (
              <div className="p-4">
                <InventoryLogView />
              </div>
            ) : tab === "conversions" ? (
              <div className="p-4">
                <ConversionsView />
              </div>
            ) : tab === "merges" ? (
              <div className="p-4">
                <MergesView />
              </div>
            ) : tab === "deleted" ? (
              <div className="p-4">
                <DeletedView canRestore={may("Inventory/Products", "Delete")} onRestored={() => void loadProducts()} />
              </div>
            ) : failed ? (
              <PanelError what="products" onRetry={() => void loadProducts().catch(() => setFailed(true))} />
            ) : rows === null ? (
              <Loading what="products" columns={6} rows={10} numeric={[2, 3]} bare />
            ) : (
              <Table>
                <TableHeader>
                  <TableRow>
                    <TableHead>{tab === "services" ? "Service" : "Product"}</TableHead>
                    <TableHead>Category</TableHead>
                    <TableHead className="text-right">Stock</TableHead>
                    <TableHead className="text-right">Price</TableHead>
                    <TableHead>Status</TableHead>
                    <TableHead className="w-10">
                      <span className="sr-only">Actions</span>
                    </TableHead>
                  </TableRow>
                </TableHeader>
                <TableBody>
                  {shown.map((r) => (
                    <ProductRow key={`${tab}-${r.id}`} r={r} today={today} actions={rowActions(r)} />
                  ))}
                  {shown.length === 0 ? (
                    <TableRow>
                      <TableCell colSpan={6} className="py-10 text-center text-muted-foreground">
                        {base.length ? "No products match these filters" : tab === "services" ? "No services yet" : tab === "low" ? "Nothing is at or below its reorder level" : tab === "expiring" ? `Nothing expires in the next ${EXPIRING_DAYS} days` : "No products yet"}
                      </TableCell>
                    </TableRow>
                  ) : null}
                </TableBody>
              </Table>
            )}
          </div>
        </Tabs>
      </section>

      {form ? (
        <RecordDrawer
          open
          onOpenChange={(o) => !o && setForm(null)}
          variant="edit"
          width="lg"
          saving={saving}
          dirty={JSON.stringify(form.d) !== JSON.stringify(form.original) || !!form.opening.qty || !!imageFile}
          title={form.id ? `Edit ${form.original.name}` : form.d.type === "Service" ? "New service" : "New product"}
          footer={
            <>
              <Button variant="outline" disabled={saving} onClick={() => setForm(null)}>
                Cancel
              </Button>
              <Button disabled={saving} onClick={() => void saveProduct()}>
                {saving ? <Spinner /> : null}
                {form.id ? "Save" : form.d.type === "Service" ? "Add service" : "Add product"}
              </Button>
            </>
          }
        >
          <div className="space-y-6">
            <ProductRecordForm d={form.d} onChange={(d) => setForm({ ...form, d })} lists={productLists} problems={formCheck.problems} etimsGaps={formCheck.etimsGaps} show={form.tried} onImageFile={setImageFile} />
            {!form.id && form.d.type === "Product" && form.d.manageStock ? (
              <fieldset className="space-y-3 rounded-md border border-[var(--color-border-panel)] p-3 bg-card">
                <legend className="px-1 text-sm font-medium">Opening stock (optional)</legend>
                <div className="grid gap-3">
                  <Field>
                    <FieldLabel htmlFor="op-qty">Units on hand</FieldLabel>
                    <Input id="op-qty" inputMode="numeric" className="tabular-nums" value={form.opening.qty} onChange={(e) => setForm({ ...form, opening: { ...form.opening, qty: e.target.value.replace(/\D/g, "") } })} />
                  </Field>
                  <Field>
                    <FieldLabel htmlFor="op-batch">Batch number</FieldLabel>
                    <Input id="op-batch" className="font-mono" value={form.opening.batch} onChange={(e) => setForm({ ...form, opening: { ...form.opening, batch: e.target.value } })} />
                  </Field>
                  <Field data-invalid={form.tried && !!openingProblem}>
                    <FieldLabel htmlFor="op-exp">Expiry date</FieldLabel>
                    <Input id="op-exp" type="date" value={form.opening.expiry} onChange={(e) => setForm({ ...form, opening: { ...form.opening, expiry: e.target.value } })} />
                    {form.tried && openingProblem ? <FieldError>{openingProblem}</FieldError> : null}
                  </Field>
                </div>
                <p className="text-xs text-muted-foreground">Valued at the purchase price (Price and tax). Later deliveries go in with Receive stock.</p>
              </fieldset>
            ) : null}
          </div>
        </RecordDrawer>
      ) : null}

      {adjusting ? (
        <RecordDrawer open onOpenChange={(o) => !o && setAdjusting(null)} variant="edit" title={`Adjust stock: ${adjusting.name}`} saving={adjustState === "loading"}>
          <StockAdjustmentForm productName={adjusting.name} currentStock={onHand(adjusting)} state={adjustState} errorMessage={adjustError || undefined} onSubmit={(a) => void saveAdjustment(a)} />
        </RecordDrawer>
      ) : null}

      {receiving ? (
        <RecordDrawer
          open
          onOpenChange={(o) => !o && setReceiving(null)}
          variant="edit"
          saving={saving}
          dirty
          title={receiving.product ? `Receive stock: ${receiving.product.name}` : "Receive stock"}
          footer={
            <>
              <Button variant="outline" disabled={saving} onClick={() => setReceiving(null)}>
                Cancel
              </Button>
              <Button disabled={saving} onClick={() => void saveReceipt()}>
                {saving ? <Spinner /> : null}
                Receive
              </Button>
            </>
          }
        >
          <ReceiveForm value={receiving} onChange={setReceiving} products={stocked} />
        </RecordDrawer>
      ) : null}

      {deleting ? (
        <ConfirmDialog
          open
          onOpenChange={(o) => !o && setDeleting(null)}
          level="B"
          title={`Delete ${deleting.name}?`}
          consequences={["It leaves the till and every list. Its sales, batches and history stay.", "It can be restored from the Deleted tab.", "A product still holding stock can't be deleted: write it off or adjust it to 0 first."]}
          reasons={["Discontinued", "Created by mistake", "Replaced by another product", "Other"]}
          actionLabel="Delete"
          onConfirm={async ({ reason, details }) => {
            try {
              await ProductsApi.delete(deleting.id, [reason, details].filter(Boolean).join(": ") || undefined);
            } catch (e) {
              throw new Error(apiError(e, `${deleting.name} wasn't deleted.`));
            }
            setDeleting(null);
            await loadProducts();
            toast.add({ title: `${deleting.name} deleted`, description: "Restore it from the Deleted tab if needed." });
          }}
        />
      ) : null}

      {historyOf ? <HistoryDrawer product={{ id: historyOf.id, name: historyOf.name }} onClose={() => setHistoryOf(null)} /> : null}
      {merging ? (
        <MergeDrawer
          duplicate={{ id: merging.id, name: merging.name }}
          // Only what the API can merge it with: the same kind (product or service) and the same stock keeping
          products={all.filter((p) => p.type === merging.type && p.manageStock === merging.manageStock).map((p) => ({ id: p.id, name: p.name, code: p.code }))}
          onClose={() => setMerging(null)}
          onDone={() => { setMerging(null); void loadProducts(); }}
        />
      ) : null}
      {assigningShelves ? <ShelfImportDrawer products={all.map((p) => ({ code: p.code, name: p.name }))} onClose={() => setAssigningShelves(false)} onDone={() => void loadProducts()} /> : null}

      {converting ? (
        <ConvertDrawer
          product={{ id: converting.id, name: converting.name }}
          products={stocked.map((p) => ({ id: p.id, name: p.name }))}
          onClose={() => setConverting(null)}
          onDone={() => { setConverting(null); void loadProducts(); }}
        />
      ) : null}

      {showLists ? (
        <RecordDrawer open onOpenChange={(o) => !o && setShowLists(false)} variant="detail" width="lg" title="Setup lists">
          {may("Inventory/Setup lists", "Update") ? (
            <SetupLists lists={lists} reload={loadLists} />
          ) : (
            <p className="text-sm text-muted-foreground">You can view these lists in the product form; changing them needs the Setup lists permission.</p>
          )}
        </RecordDrawer>
      ) : null}
    </div>
  );
}
