/**
 * @license
 * SPDX-License-Identifier: Apache-2.0
 */

/**
 * POS console — counter sales and till health.
 *
 * One component serves both workspaces:
 *
 *   • **admin** (no `locationid`) — every outlet under the tenant, fanned out
 *     over `useFiestaTenantLocations`, with a store leaderboard and a store
 *     picker on the bill list.
 *   • **store** (`locationid` set) — that one outlet, no picker, no
 *     cross-store comparison.
 *
 * The split is a prop rather than two components because every panel is the
 * same read with a different scope; duplicating them would guarantee the two
 * drift.
 *
 * Notes that are load-bearing rather than stylistic:
 *   – `grosssales` from the API is **net** of discount and round-off, so it is
 *     labelled "Net collected" here and never "Gross".
 *   – Average bill is always recomputed as Σtotal / Σbills.
 *   – A negative sync lag is a legacy timestamp, not a fast upload.
 *   – Terminal health has four states; see `parseTerminal`.
 */

import React, { useMemo, useState } from 'react';
import {
  AlertTriangle,
  Barcode,
  Battery,
  Clock,
  CreditCard,
  HardDrive,
  Monitor,
  Package,
  Printer,
  Receipt,
  RefreshCcw,
  Search,
  Store,
  TrendingUp,
  Users,
  Wifi,
} from 'lucide-react';
import { useFiestaTenantLocations, FIESTA_TENANT_ID } from '../services/fiestaQueries';
import { num as fnum, str as fstr, type Row } from '../services/fiestaApi';
import {
  averageBill,
  billCustomer,
  billPayments,
  billPromos,
  billTaxBreakdown,
  billTime,
  daysAgoISO,
  humanDuration,
  inr,
  inrCompact,
  syncLag,
  terminalLabel,
  todayISO,
  TERMINAL_STATE_COLOR,
  TERMINAL_STATE_LABEL,
  type PosBill,
  type PosCatalogue,
  type PosSalesSummary,
  type PosTerminal,
} from '../services/posApi';
import {
  usePosCatalogue,
  usePosFleetHealth,
  usePosSaleDetail,
  usePosSales,
  usePosSalesSummary,
} from '../services/posQueries';
import {
  BORDER,
  BRAND,
  Card,
  DIVIDER,
  FilterBar,
  GradientHeader,
  KpiStrip,
  LiveStatus,
  MetricPill,
  Pill,
  SlideDrawer,
  StatusChip,
  SURFACE_ALT,
  TH_STYLE,
  TEXT,
  TEXT_2,
  TEXT_3,
  edge,
  tint,
} from './consoleUi';

interface PosConsoleViewProps {
  tenantId?: number;
  /**
   * Set for the store workspace; omitted for the tenant-wide admin view.
   *
   * This must be a real `tenantlocations.locationid`. It is the authorisation
   * boundary on every POS read, so a caller that falls back to an
   * `applocationid` (a city id) would query a scope that is either empty or,
   * if the ids happen to collide, somebody else's outlet.
   */
  locationid?: number;
  storeName?: string;
}

type Tab = 'overview' | 'terminals' | 'bills' | 'catalogue' | 'exceptions';

const TABS: Array<{ id: Tab; label: string; icon: React.ReactNode }> = [
  { id: 'overview', label: 'Overview', icon: <TrendingUp size={13} /> },
  { id: 'terminals', label: 'Terminals', icon: <Monitor size={13} /> },
  { id: 'bills', label: 'Bills', icon: <Receipt size={13} /> },
  { id: 'catalogue', label: 'Catalogue', icon: <Package size={13} /> },
  { id: 'exceptions', label: 'Attention', icon: <AlertTriangle size={13} /> },
];

const RANGES: Array<{ id: string; label: string; from: () => string }> = [
  { id: 'today', label: 'Today', from: todayISO },
  { id: '7d', label: '7 days', from: () => daysAgoISO(6) },
  { id: '30d', label: '30 days', from: () => daysAgoISO(29) },
];

const PAGE_SIZE = 50;

export default function PosConsoleView({
  tenantId = FIESTA_TENANT_ID,
  locationid,
  storeName,
}: PosConsoleViewProps) {
  const isStoreScope = Boolean(locationid && locationid > 0);

  // The outlet roster is the page's ONLY non-POS read, and it is unavoidable:
  // every POS endpoint takes `locationid` as an input and none of them
  // enumerates a tenant's outlets, so a tenant-wide view has to learn the list
  // from somewhere else before it can fan out.
  //
  // In store scope it is not needed at all — the caller already knows which
  // outlet this is — so the query is switched off there (the shared hook is
  // `enabled: Boolean(tenantid)`), leaving the store workspace served entirely
  // by the POS API.
  const locationsQ = useFiestaTenantLocations(isStoreScope ? 0 : tenantId);

  /** Every outlet in scope, with a display name. */
  const outlets = useMemo(() => {
    if (isStoreScope) {
      return [{ locationid: locationid as number, name: storeName || `Store ${locationid}` }];
    }
    return (locationsQ.data ?? [])
      .map((r: Row) => ({
        locationid: fnum(r.locationid),
        name: fstr(r.locationname) || `Store ${fnum(r.locationid)}`,
      }))
      .filter((o) => o.locationid > 0)
      .sort((a, b) => a.name.localeCompare(b.name));
  }, [locationsQ.data, isStoreScope, locationid, storeName]);

  const scopeIds = useMemo(() => outlets.map((o) => o.locationid), [outlets]);
  const outletName = useMemo(() => {
    const m = new Map<number, string>();
    outlets.forEach((o) => m.set(o.locationid, o.name));
    return m;
  }, [outlets]);

  const [tab, setTab] = useState<Tab>('overview');
  const [rangeId, setRangeId] = useState('today');
  const range = RANGES.find((r) => r.id === rangeId) ?? RANGES[0];
  const fromdate = range.from();
  const todate = todayISO();

  const healthQ = usePosFleetHealth(scopeIds);
  const summaryQ = usePosSalesSummary(scopeIds, fromdate, todate);

  const merged = summaryQ.data?.merged;
  const perOutlet = summaryQ.data?.perOutlet ?? [];

  const allTerminals: Array<PosTerminal & { locationid: number }> = useMemo(
    () =>
      (healthQ.data ?? []).flatMap((o) =>
        o.terminals.map((t) => ({ ...t, locationid: o.locationid })),
      ),
    [healthQ.data],
  );

  const tillsOnline = allTerminals.filter((t) => t.state === 'online').length;
  const pendingTotal = allTerminals.reduce((s, t) => s + t.pendingBills, 0);

  // ── Bill list scope ────────────────────────────────────────────────────────
  // /sales takes exactly one locationid, so the tenant view needs a picker.
  // It defaults to the outlet that actually has bills rather than the first
  // alphabetically — with one live outlet on the platform, defaulting blind
  // would open on an empty table every time.
  const busiestOutlet = useMemo(() => {
    const withBills = perOutlet.filter((p) => (p.summary?.billcount ?? 0) > 0);
    if (withBills.length) {
      return withBills.reduce((a, b) =>
        (b.summary?.billcount ?? 0) > (a.summary?.billcount ?? 0) ? b : a,
      ).locationid;
    }
    return scopeIds[0] ?? 0;
  }, [perOutlet, scopeIds]);

  const [pickedOutlet, setPickedOutlet] = useState<number | null>(null);
  const billOutlet = isStoreScope ? (locationid as number) : (pickedOutlet ?? busiestOutlet);

  const [terminalFilter, setTerminalFilter] = useState('');
  const [cashierFilter, setCashierFilter] = useState('');
  const [modeFilter, setModeFilter] = useState('');
  const [pageno, setPageno] = useState(0);
  const [reference, setReference] = useState('');
  const [openRef, setOpenRef] = useState('');

  const salesQ = usePosSales({
    locationid: billOutlet,
    fromdate,
    todate,
    terminalid: terminalFilter,
    cashiername: cashierFilter,
    paymentmode: modeFilter,
    pageno,
    pagesize: PAGE_SIZE,
  });

  // Only fetched once its panel is opened — a catalogue pull is the whole shelf.
  const catalogueQ = usePosCatalogue(billOutlet, tab === 'catalogue');

  const detailQ = usePosSaleDetail(billOutlet, openRef);
  const bills = salesQ.data?.bills ?? [];
  const billTotal = salesQ.data?.total ?? 0;

  const isLoading = locationsQ.isLoading || summaryQ.isLoading || healthQ.isLoading;
  const isError = locationsQ.isError || summaryQ.isError;

  const resetPaging = () => setPageno(0);

  // ── Exceptions ─────────────────────────────────────────────────────────────
  const exceptions = useMemo(() => {
    const stranded = allTerminals.filter((t) => t.pendingBills > 0);
    const idle = allTerminals.filter((t) => t.state === 'online' && t.todayBills === 0);
    const dark = allTerminals.filter(
      (t) => t.state === 'offline_declared' || t.state === 'offline_vanished',
    );
    const stale = allTerminals.filter((t) => t.state === 'stale');
    const drifting = allTerminals.filter(
      (t) => t.clockDriftSeconds != null && Math.abs(t.clockDriftSeconds) > 120,
    );
    const noTills = outlets.filter(
      (o) => !(healthQ.data ?? []).some((h) => h.locationid === o.locationid && h.total > 0),
    );
    const delayed = bills.filter((b) => syncLag(b).kind === 'delayed');
    const legacy = bills.filter((b) => syncLag(b).kind === 'legacy');
    const unassigned = bills.filter((b) => !b.terminalid?.trim());
    return { stranded, idle, dark, stale, drifting, noTills, delayed, legacy, unassigned };
  }, [allTerminals, outlets, healthQ.data, bills]);

  const attentionCount =
    exceptions.stranded.length + exceptions.idle.length + exceptions.dark.length + exceptions.drifting.length;

  // ── Render ─────────────────────────────────────────────────────────────────

  // No padding of its own: both consoles already pad their route wrapper, and
  // adding more here made the admin page sit inside two sets of gutters.
  if (!isStoreScope && !locationsQ.isLoading && outlets.length === 0) {
    return (
      <EmptyState
        icon={<Store size={30} />}
        title="No outlets found"
        body="This tenant has no locations, so there is nothing for a till to be registered against."
      />
    );
  }

  return (
    <div className="w-full min-w-0">
      <GradientHeader
        title="POS"
        subtitle={
          isStoreScope
            ? `${outletName.get(billOutlet) || storeName || 'Your store'} · counter sales and till health`
            : `${outlets.length} outlet${outlets.length === 1 ? '' : 's'} · counter sales and till health`
        }
        status={
          <LiveStatus
            state={isError ? 'error' : isLoading ? 'loading' : 'live'}
            label={
              isError
                ? 'POS service unreachable'
                : isLoading
                  ? 'Loading counter sales…'
                  : `${tillsOnline} of ${allTerminals.length} till${allTerminals.length === 1 ? '' : 's'} online · refreshed every 30s`
            }
          />
        }
        right={
          // Wraps rather than overflowing: GradientHeader marks this side
          // shrink-0, so a fixed row would push the title off a narrow viewport.
          <div className="flex items-center gap-2 flex-wrap justify-end">
            {RANGES.map((r) => (
              <React.Fragment key={r.id}>
                <Pill
                  active={rangeId === r.id}
                  color={BRAND}
                  onClick={() => {
                    setRangeId(r.id);
                    resetPaging();
                  }}
                >
                  {r.label}
                </Pill>
              </React.Fragment>
            ))}
            <button
              onClick={() => {
                healthQ.refetch();
                summaryQ.refetch();
                salesQ.refetch();
              }}
              title="Refresh"
              className="inline-flex items-center justify-center rounded-full cursor-pointer transition-colors"
              style={{ width: 30, height: 30, background: tint(BRAND), border: `1.5px solid ${edge(BRAND)}`, color: BRAND }}
            >
              <RefreshCcw size={13} className={summaryQ.isFetching ? 'animate-spin' : ''} />
            </button>
          </div>
        }
      />

      <KpiStrip
        loading={summaryQ.isLoading}
        items={[
          {
            label: 'Net collected',
            value: inrCompact(merged?.grosssales ?? 0),
            color: BRAND,
            icon: <Receipt size={16} />,
            badge: `${merged?.billcount ?? 0} bills`,
          },
          {
            label: 'Tax collected',
            value: inrCompact(merged?.taxcollected ?? 0),
            color: '#0ea5e9',
            icon: <CreditCard size={16} />,
            badge: merged?.discountgiven ? `${inrCompact(merged.discountgiven)} disc.` : undefined,
          },
          {
            label: 'Average bill',
            value: inrCompact(merged?.averagebill ?? 0),
            color: '#10b981',
            icon: <TrendingUp size={16} />,
            badge: `${merged?.itemcount ?? 0} items`,
          },
          {
            label: 'Tills online',
            value: `${tillsOnline}/${allTerminals.length}`,
            color: pendingTotal > 0 ? '#f59e0b' : '#64748b',
            icon: <Wifi size={16} />,
            badge: pendingTotal > 0 ? `${pendingTotal} unsynced` : undefined,
            onClick: () => setTab('terminals'),
          },
        ]}
      />

      {pendingTotal > 0 && (
        <div
          className="mt-4 rounded-2xl border px-4 py-3 flex items-start gap-3"
          style={{ borderColor: edge('#f59e0b'), background: tint('#f59e0b') }}
        >
          <AlertTriangle size={16} style={{ color: '#f59e0b' }} className="mt-0.5 shrink-0" />
          <div className="text-[13px]" style={{ color: TEXT }}>
            <strong>{pendingTotal}</strong> bill{pendingTotal === 1 ? '' : 's'} still sitting on{' '}
            {exceptions.stranded.length} till{exceptions.stranded.length === 1 ? '' : 's'} and not yet synced.
            <button onClick={() => setTab('terminals')} className="ml-2 underline font-semibold cursor-pointer" style={{ color: BRAND }}>
              See which
            </button>
          </div>
        </div>
      )}

      <div className="mt-4">
        <FilterBar>
          <div className="flex items-center gap-2 flex-wrap">
            {TABS.map((t) => (
              <React.Fragment key={t.id}>
                <Pill
                  active={tab === t.id}
                  color={t.id === 'exceptions' && attentionCount > 0 ? '#f59e0b' : BRAND}
                  onClick={() => setTab(t.id)}
                  count={t.id === 'exceptions' && attentionCount > 0 ? attentionCount : undefined}
                >
                  {t.icon}
                  {t.label}
                </Pill>
              </React.Fragment>
            ))}
          </div>
        </FilterBar>
      </div>

      <div className="mt-4 space-y-4">
        {tab === 'overview' && (
          <OverviewTab
            merged={merged}
            perOutlet={perOutlet}
            outletName={outletName}
            isStoreScope={isStoreScope}
            loading={summaryQ.isLoading}
            onPickOutlet={(id) => {
              setPickedOutlet(id);
              setTab('bills');
              resetPaging();
            }}
          />
        )}

        {tab === 'terminals' && (
          <TerminalsTab
            health={healthQ.data ?? []}
            outletName={outletName}
            isStoreScope={isStoreScope}
            loading={healthQ.isLoading}
          />
        )}

        {tab === 'bills' && (
          <BillsTab
            isStoreScope={isStoreScope}
            outlets={outlets}
            perOutlet={perOutlet}
            billOutlet={billOutlet}
            onPickOutlet={(id) => {
              setPickedOutlet(id);
              resetPaging();
            }}
            bills={bills}
            total={billTotal}
            pageno={pageno}
            setPageno={setPageno}
            loading={salesQ.isLoading}
            fetching={salesQ.isFetching}
            terminalFilter={terminalFilter}
            setTerminalFilter={(v) => {
              setTerminalFilter(v);
              resetPaging();
            }}
            cashierFilter={cashierFilter}
            setCashierFilter={(v) => {
              setCashierFilter(v);
              resetPaging();
            }}
            modeFilter={modeFilter}
            setModeFilter={(v) => {
              setModeFilter(v);
              resetPaging();
            }}
            reference={reference}
            setReference={setReference}
            onLookup={() => setOpenRef(reference.trim())}
            onOpen={(b) => setOpenRef(b.terminalorderid || String(b.posorderid))}
            terminalOptions={(merged?.byterminal ?? []).map((t) => t.terminalid)}
          />
        )}

        {tab === 'catalogue' && (
          <CatalogueTab
            data={catalogueQ.data}
            loading={catalogueQ.isLoading}
            isStoreScope={isStoreScope}
            outlets={outlets}
            billOutlet={billOutlet}
            onPickOutlet={setPickedOutlet}
            outletLabel={outletName.get(billOutlet) || `Store ${billOutlet}`}
          />
        )}

        {tab === 'exceptions' && (
          <ExceptionsTab
            exceptions={exceptions}
            outletName={outletName}
            loading={healthQ.isLoading}
            onOpenBill={(b) => setOpenRef(b.terminalorderid || String(b.posorderid))}
          />
        )}
      </div>

      <SlideDrawer isOpen={Boolean(openRef)} onClose={() => setOpenRef('')} title="Bill">
        <BillDetail bill={detailQ.data ?? null} loading={detailQ.isLoading} reference={openRef} />
      </SlideDrawer>
    </div>
  );
}

// ── Overview ─────────────────────────────────────────────────────────────────

function OverviewTab({
  merged,
  perOutlet,
  outletName,
  isStoreScope,
  loading,
  onPickOutlet,
}: {
  merged: PosSalesSummary | undefined;
  perOutlet: Array<{ locationid: number; summary: PosSalesSummary | null }>;
  outletName: Map<number, string>;
  isStoreScope: boolean;
  loading: boolean;
  onPickOutlet: (id: number) => void;
}) {
  if (loading) return <SkeletonPanel lines={5} />;
  if (!merged || merged.billcount === 0) {
    return (
      <EmptyState
        icon={<Receipt size={30} />}
        title="No counter sales in this range"
        body="Bills appear here as tills sync them. A till holds its own copy until we acknowledge, so nothing is lost while it is offline."
      />
    );
  }

  const maxMode = Math.max(...merged.bypaymentmode.map((m) => m.amount), 1);
  const maxDay = Math.max(...merged.byday.map((d) => d.amount), 1);

  const leaderboard = perOutlet
    .filter((p) => (p.summary?.billcount ?? 0) > 0)
    .map((p) => ({
      locationid: p.locationid,
      name: outletName.get(p.locationid) || `Store ${p.locationid}`,
      bills: p.summary?.billcount ?? 0,
      net: p.summary?.grosssales ?? 0,
      tax: p.summary?.taxcollected ?? 0,
      // Recomputed, never the API's own averagebill folded across outlets.
      avg: averageBill(p.summary),
    }))
    .sort((a, b) => b.net - a.net);

  return (
    <>
      <div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
        <Card className="p-4">
          <PanelTitle icon={<CreditCard size={14} />} title="Payment mix" hint="What the drawer is settled against" />
          <div className="space-y-2.5 mt-3">
            {merged.bypaymentmode.map((m) => (
              <div key={m.paymentmode || 'unknown'}>
                <div className="flex items-center justify-between text-[12.5px] mb-1">
                  <span className="font-bold capitalize" style={{ color: TEXT }}>
                    {m.paymentmode || 'Unrecorded'}
                  </span>
                  <span style={{ color: TEXT_2 }}>
                    {inr(m.amount)} <span style={{ color: TEXT_3 }}>· {m.billcount}</span>
                  </span>
                </div>
                <div className="h-2 rounded-full overflow-hidden" style={{ background: SURFACE_ALT }}>
                  <div
                    className="h-full rounded-full"
                    style={{ width: `${(m.amount / maxMode) * 100}%`, background: BRAND }}
                  />
                </div>
              </div>
            ))}
          </div>
        </Card>

        <Card className="p-4">
          <PanelTitle icon={<TrendingUp size={14} />} title="By day" hint="Business date, as the till filed it" />
          <div className="flex items-end gap-1.5 mt-4" style={{ height: 130 }}>
            {merged.byday.map((d) => (
              <div key={d.businessdate} className="flex-1 flex flex-col items-center justify-end gap-1 min-w-0">
                <span className="text-[9.5px] font-bold" style={{ color: TEXT_2 }}>
                  {inrCompact(d.amount)}
                </span>
                <div
                  className="w-full rounded-t"
                  style={{ height: `${Math.max((d.amount / maxDay) * 100, 3)}%`, background: BRAND, minHeight: 3 }}
                  title={`${d.businessdate} · ${d.billcount} bills · ${inr(d.amount)}`}
                />
                <span className="text-[9px] truncate w-full text-center" style={{ color: TEXT_3 }}>
                  {d.businessdate.slice(5)}
                </span>
              </div>
            ))}
          </div>
        </Card>
      </div>

      {!isStoreScope && leaderboard.length > 0 && (
        <PosTable
          minWidth={620}
          columns={[
            { label: 'Store' },
            { label: 'Bills', align: 'right' },
            { label: 'Net collected', align: 'right' },
            { label: 'Tax', align: 'right' },
            { label: 'Average bill', align: 'right' },
          ]}
        >
          {leaderboard.map((r) => (
            <tr
              key={r.locationid}
              className="hover:bg-slate-50/70 cursor-pointer"
              onClick={() => onPickOutlet(r.locationid)}
            >
              <td className="px-4 py-3 font-bold" style={{ color: TEXT }}>
                {r.name}
              </td>
              <td className="px-4 py-3 text-right" style={{ color: TEXT_2 }}>
                {r.bills}
              </td>
              <td className="px-4 py-3 text-right font-bold" style={{ color: TEXT }}>
                {inr(r.net)}
              </td>
              <td className="px-4 py-3 text-right" style={{ color: TEXT_2 }}>
                {inr(r.tax)}
              </td>
              <td className="px-4 py-3 text-right">
                <MetricPill color={BRAND}>{inr(r.avg)}</MetricPill>
              </td>
            </tr>
          ))}
        </PosTable>
      )}

      {merged.byterminal.length > 0 && (
        <PosTable
          minWidth={420}
          columns={[{ label: 'Terminal' }, { label: 'Bills', align: 'right' }, { label: 'Amount', align: 'right' }]}
        >
          {merged.byterminal.map((t) => (
            <tr key={t.terminalid || 'unassigned'} className="hover:bg-slate-50/70">
              <td className="px-4 py-3 font-bold font-mono text-[12.5px]" style={{ color: t.terminalid ? TEXT : TEXT_3 }}>
                {terminalLabel(t.terminalid)}
                {!t.terminalid && (
                  <span className="ml-2 font-sans font-semibold text-[10.5px]" style={{ color: TEXT_3 }}>
                    (ingested before terminal ids were recorded)
                  </span>
                )}
              </td>
              <td className="px-4 py-3 text-right" style={{ color: TEXT_2 }}>
                {t.billcount}
              </td>
              <td className="px-4 py-3 text-right font-bold" style={{ color: TEXT }}>
                {inr(t.amount)}
              </td>
            </tr>
          ))}
        </PosTable>
      )}

      <p className="text-[11px] px-1" style={{ color: TEXT_3 }}>
        “Net collected” is the sum of bill totals — after discount and round-off. Gross before discount is{' '}
        {inr((merged.grosssales ?? 0) + (merged.discountgiven ?? 0))}.
      </p>
    </>
  );
}

// ── Terminals ────────────────────────────────────────────────────────────────

function TerminalsTab({
  health,
  outletName,
  isStoreScope,
  loading,
}: {
  health: Array<{ locationid: number; total: number; online: number; terminals: PosTerminal[] }>;
  outletName: Map<number, string>;
  isStoreScope: boolean;
  loading: boolean;
}) {
  if (loading) return <SkeletonPanel lines={4} />;

  const withTills = health.filter((h) => h.total > 0);
  const without = health.filter((h) => h.total === 0);

  if (withTills.length === 0) {
    return (
      <EmptyState
        icon={<Monitor size={30} />}
        title="No terminals registered"
        body={
          isStoreScope
            ? 'No till has ever reported from this store. A terminal appears here the first time it sends a heartbeat.'
            : `None of these ${health.length} outlets has a till reporting. A terminal appears the first time it sends a heartbeat.`
        }
      />
    );
  }

  return (
    <>
      {withTills.map((h) => (
        <div key={h.locationid}>
          {!isStoreScope && (
            <div className="flex items-center gap-2 mb-2 px-1">
              <Store size={13} style={{ color: TEXT_2 }} />
              <h3 className="text-[13px] font-extrabold" style={{ color: TEXT }}>
                {outletName.get(h.locationid) || `Store ${h.locationid}`}
              </h3>
              <span className="text-[11px]" style={{ color: TEXT_3 }}>
                {h.online}/{h.total} online
              </span>
            </div>
          )}
          <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3">
            {h.terminals.map((t) => (
              <React.Fragment key={`${h.locationid}-${t.terminalId}`}>
                <TerminalCard t={t} />
              </React.Fragment>
            ))}
          </div>
        </div>
      ))}

      {without.length > 0 && !isStoreScope && (
        <Card className="p-4">
          <PanelTitle icon={<Store size={14} />} title={`${without.length} outlet${without.length === 1 ? '' : 's'} with no till`} hint="Never registered a terminal" />
          <div className="flex flex-wrap gap-1.5 mt-3">
            {without.map((h) => (
              <span
                key={h.locationid}
                className="rounded-full px-2.5 py-1 text-[11px] font-semibold"
                style={{ background: SURFACE_ALT, color: TEXT_2, border: `1px solid ${BORDER}` }}
              >
                {outletName.get(h.locationid) || `Store ${h.locationid}`}
              </span>
            ))}
          </div>
        </Card>
      )}
    </>
  );
}

function TerminalCard({ t }: { t: PosTerminal }) {
  const color = TERMINAL_STATE_COLOR[t.state];
  const drift = t.clockDriftSeconds != null && Math.abs(t.clockDriftSeconds) > 120;

  return (
    <Card className="p-4">
      <div className="flex items-start justify-between gap-2">
        <div className="min-w-0">
          <div className="font-mono font-extrabold text-[14px] truncate" style={{ color: TEXT }}>
            {t.terminalId || '—'}
          </div>
          {t.storeName && (
            <div className="text-[11px] truncate" style={{ color: TEXT_3 }}>
              {t.storeName}
            </div>
          )}
        </div>
        <StatusChip label={TERMINAL_STATE_LABEL[t.state]} color={color} />
      </div>

      {t.state === 'offline_vanished' ? (
        <p className="text-[11.5px] mt-3" style={{ color: TEXT_2 }}>
          {t.reason || 'No heartbeat inside the presence window.'}
        </p>
      ) : (
        <>
          <div className="grid grid-cols-2 gap-2 mt-3">
            <Stat label="Today" value={`${t.todayBills} bills`} sub={inr(t.todayAmount)} />
            <Stat
              label="Unsynced"
              value={`${t.pendingBills}`}
              sub={t.oldestPendingAt ? `oldest ${billTime(t.oldestPendingAt)}` : undefined}
              alert={t.pendingBills > 0}
            />
          </div>

          <div className="flex flex-wrap items-center gap-1.5 mt-3">
            {t.appVersion && <Chip label={`v${t.appVersion}`} />}
            {t.ageSeconds != null && (
              <Chip label={`seen ${humanDuration(t.ageSeconds)} ago`} icon={<Clock size={10} />} />
            )}
            {t.printerReachable != null && (
              <Chip
                label={t.printerReachable ? 'printer ok' : 'printer down'}
                icon={<Printer size={10} />}
                color={t.printerReachable ? undefined : '#ef4444'}
              />
            )}
            {t.batteryLevel != null && (
              <Chip
                label={`${t.batteryLevel}%${t.batteryCharging ? ' ⚡' : ''}`}
                icon={<Battery size={10} />}
                color={t.batteryLevel < 20 ? '#ef4444' : undefined}
              />
            )}
            {t.storageFreeMb != null && (
              <Chip label={`${t.storageFreeMb} MB free`} icon={<HardDrive size={10} />} color={t.storageFreeMb < 200 ? '#f59e0b' : undefined} />
            )}
            {t.drawerStatus && <Chip label={`drawer ${t.drawerStatus}`} />}
          </div>

          {drift && (
            <p className="text-[11px] mt-2.5 font-semibold" style={{ color: '#f59e0b' }}>
              Clock is {humanDuration(t.clockDriftSeconds as number)}{' '}
              {(t.clockDriftSeconds as number) > 0 ? 'ahead of' : 'behind'} the server — bills may be filed under the
              wrong business date.
            </p>
          )}
        </>
      )}
    </Card>
  );
}

// ── Bills ────────────────────────────────────────────────────────────────────

function BillsTab(props: {
  isStoreScope: boolean;
  outlets: Array<{ locationid: number; name: string }>;
  perOutlet: Array<{ locationid: number; summary: PosSalesSummary | null }>;
  billOutlet: number;
  onPickOutlet: (id: number) => void;
  bills: PosBill[];
  total: number;
  pageno: number;
  setPageno: (n: number) => void;
  loading: boolean;
  fetching: boolean;
  terminalFilter: string;
  setTerminalFilter: (v: string) => void;
  cashierFilter: string;
  setCashierFilter: (v: string) => void;
  modeFilter: string;
  setModeFilter: (v: string) => void;
  reference: string;
  setReference: (v: string) => void;
  onLookup: () => void;
  onOpen: (b: PosBill) => void;
  terminalOptions: string[];
}) {
  const {
    isStoreScope, outlets, perOutlet, billOutlet, onPickOutlet, bills, total, pageno, setPageno,
    loading, fetching, terminalFilter, setTerminalFilter, cashierFilter, setCashierFilter,
    modeFilter, setModeFilter, reference, setReference, onLookup, onOpen, terminalOptions,
  } = props;

  const pages = Math.max(1, Math.ceil(total / PAGE_SIZE));
  const billsFor = (id: number) => perOutlet.find((p) => p.locationid === id)?.summary?.billcount ?? 0;

  return (
    <>
      <FilterBar>
        <div className="flex flex-col gap-3">
          {!isStoreScope && (
            <div className="flex items-center gap-2 flex-wrap">
              <span className="text-[11px] font-extrabold uppercase tracking-wide" style={{ color: TEXT_3 }}>
                Store
              </span>
              {outlets.map((o) => (
                <React.Fragment key={o.locationid}>
                  <Pill
                    active={billOutlet === o.locationid}
                    color={BRAND}
                    onClick={() => onPickOutlet(o.locationid)}
                    count={billsFor(o.locationid) || undefined}
                  >
                    {o.name}
                  </Pill>
                </React.Fragment>
              ))}
            </div>
          )}

          <div className="flex items-center gap-2 flex-wrap">
            <div className="flex items-center gap-1.5 rounded-full px-3 py-1.5" style={{ background: SURFACE_ALT, border: `1px solid ${BORDER}` }}>
              <Barcode size={13} style={{ color: TEXT_3 }} />
              <input
                value={reference}
                onChange={(e) => setReference(e.target.value)}
                onKeyDown={(e) => e.key === 'Enter' && onLookup()}
                placeholder="Invoice, order UUID or bill id…"
                className="bg-transparent outline-none text-[12.5px] w-56"
                style={{ color: TEXT }}
              />
              <button onClick={onLookup} className="cursor-pointer" title="Look up bill" style={{ color: BRAND }}>
                <Search size={13} />
              </button>
            </div>

            <select
              value={terminalFilter}
              onChange={(e) => setTerminalFilter(e.target.value)}
              className="rounded-full px-3 py-1.5 text-[12.5px] font-semibold cursor-pointer outline-none"
              style={{ background: SURFACE_ALT, border: `1px solid ${BORDER}`, color: TEXT }}
            >
              <option value="">All terminals</option>
              {terminalOptions.filter(Boolean).map((t) => (
                <option key={t} value={t}>{t}</option>
              ))}
            </select>

            <select
              value={modeFilter}
              onChange={(e) => setModeFilter(e.target.value)}
              className="rounded-full px-3 py-1.5 text-[12.5px] font-semibold cursor-pointer outline-none"
              style={{ background: SURFACE_ALT, border: `1px solid ${BORDER}`, color: TEXT }}
            >
              <option value="">All tenders</option>
              {['cash', 'card', 'upi', 'wallet'].map((m) => (
                <option key={m} value={m}>{m}</option>
              ))}
            </select>

            <div className="flex items-center gap-1.5 rounded-full px-3 py-1.5" style={{ background: SURFACE_ALT, border: `1px solid ${BORDER}` }}>
              <Users size={13} style={{ color: TEXT_3 }} />
              <input
                value={cashierFilter}
                onChange={(e) => setCashierFilter(e.target.value)}
                placeholder="Cashier (exact name)"
                className="bg-transparent outline-none text-[12.5px] w-40"
                style={{ color: TEXT }}
              />
            </div>

            <span className="text-[11.5px] ml-auto" style={{ color: TEXT_3 }}>
              {total} bill{total === 1 ? '' : 's'}
              {fetching && ' · updating…'}
            </span>
          </div>
        </div>
      </FilterBar>

      {loading ? (
        <SkeletonPanel lines={6} />
      ) : bills.length === 0 ? (
        <EmptyState
          icon={<Receipt size={30} />}
          title="No bills match"
          body="Nothing was rung at this outlet in this range, or the filters exclude everything. Note the cashier filter is an exact, case-sensitive match."
        />
      ) : (
        <PosTable
          columns={[
            { label: 'Invoice' },
            { label: 'Time' },
            { label: 'Terminal' },
            { label: 'Cashier' },
            { label: 'Customer' },
            { label: 'Tender' },
            { label: 'Items', align: 'right' },
            { label: 'Tax', align: 'right' },
            { label: 'Total', align: 'right' },
          ]}
          minWidth={980}
          footer={
            pages > 1 ? (
              <div className="flex items-center justify-between px-4 py-3">
                <span className="text-[11.5px]" style={{ color: TEXT_3 }}>
                  Page {pageno + 1} of {pages}
                </span>
                <div className="flex items-center gap-2">
                  <button
                    disabled={pageno === 0}
                    onClick={() => setPageno(pageno - 1)}
                    className="rounded-full px-3 py-1 text-[12px] font-bold cursor-pointer disabled:opacity-40 disabled:cursor-default"
                    style={{ background: tint(BRAND), color: BRAND, border: `1px solid ${edge(BRAND)}` }}
                  >
                    Previous
                  </button>
                  <button
                    disabled={pageno + 1 >= pages}
                    onClick={() => setPageno(pageno + 1)}
                    className="rounded-full px-3 py-1 text-[12px] font-bold cursor-pointer disabled:opacity-40 disabled:cursor-default"
                    style={{ background: tint(BRAND), color: BRAND, border: `1px solid ${edge(BRAND)}` }}
                  >
                    Next
                  </button>
                </div>
              </div>
            ) : undefined
          }
        >
          {bills.map((b) => {
            const lag = syncLag(b);
            return (
              <tr key={b.posorderid} className="hover:bg-slate-50/70 cursor-pointer" onClick={() => onOpen(b)}>
                <td className="px-4 py-3">
                  <div className="font-bold font-mono text-[12px]" style={{ color: TEXT }}>
                    {b.invoicenumber || `#${b.posorderid}`}
                  </div>
                  <div className="text-[10.5px]" style={{ color: TEXT_3 }}>
                    {b.businessdate}
                    {lag.kind === 'delayed' && (
                      <span style={{ color: '#f59e0b' }}> · synced {humanDuration(lag.seconds)} late</span>
                    )}
                    {lag.kind === 'legacy' && <span style={{ color: TEXT_3 }}> · legacy timestamp</span>}
                  </div>
                </td>
                <td className="px-4 py-3 text-[12.5px]" style={{ color: TEXT_2 }}>
                  {billTime(b.billedat)}
                </td>
                <td className="px-4 py-3 font-mono text-[12px]" style={{ color: b.terminalid ? TEXT_2 : TEXT_3 }}>
                  {terminalLabel(b.terminalid)}
                </td>
                <td className="px-4 py-3 text-[12.5px]" style={{ color: TEXT_2 }}>
                  {b.cashiername || '—'}
                </td>
                <td className="px-4 py-3 text-[12.5px]" style={{ color: TEXT_2 }}>
                  {billCustomer(b)}
                </td>
                <td className="px-4 py-3">
                  <StatusChip label={b.paymentmode || 'n/a'} color={b.paymentmode ? BRAND : TEXT_3} />
                </td>
                <td className="px-4 py-3 text-right" style={{ color: TEXT_2 }}>
                  {b.itemcount}
                </td>
                <td className="px-4 py-3 text-right text-[12.5px]" style={{ color: TEXT_2 }}>
                  {inr(b.taxamount)}
                </td>
                <td className="px-4 py-3 text-right font-extrabold" style={{ color: TEXT }}>
                  {inr(b.total)}
                </td>
              </tr>
            );
          })}
        </PosTable>
      )}
    </>
  );
}

// ── Bill detail ──────────────────────────────────────────────────────────────

function BillDetail({ bill, loading, reference }: { bill: PosBill | null; loading: boolean; reference: string }) {
  if (loading) return <SkeletonPanel lines={6} />;
  if (!bill) {
    return (
      <EmptyState
        icon={<Search size={28} />}
        title="No bill found"
        body={`Nothing at this outlet matches “${reference}”. A reference that belongs to another store answers the same way — the lookup is scoped to one outlet on purpose.`}
      />
    );
  }

  const payments = billPayments(bill);
  const taxes = billTaxBreakdown(bill);
  const promos = billPromos(bill);
  const lag = syncLag(bill);
  const items = bill.items ?? [];
  const gross = bill.subtotal || items.reduce((s, i) => s + i.linetotal, 0);

  return (
    <div className="space-y-5">
      <div>
        <div className="font-mono font-extrabold text-[17px]" style={{ color: TEXT }}>
          {bill.invoicenumber || `Bill #${bill.posorderid}`}
        </div>
        <div className="text-[12px] mt-0.5" style={{ color: TEXT_2 }}>
          {bill.businessdate} · {billTime(bill.billedat)} · {terminalLabel(bill.terminalid)}
          {bill.cashiername && ` · ${bill.cashiername}`}
        </div>
      </div>

      <div className="grid grid-cols-2 gap-2">
        <Stat label="Total" value={inr(bill.total)} />
        <Stat label="Tax" value={inr(bill.taxamount)} />
        <Stat label="Customer" value={billCustomer(bill)} sub={bill.customermobile || undefined} />
        <Stat label="Tender" value={bill.paymentmode || '—'} sub={`${bill.itemcount} items`} />
      </div>

      {items.length > 0 ? (
        <div>
          <PanelTitle icon={<Receipt size={14} />} title={`${items.length} line${items.length === 1 ? '' : 's'}`} />
          {/* The drawer clips horizontally, so the line table scrolls inside
              its own box rather than being cut off at the panel edge. */}
          <div className="mt-2 rounded-xl border overflow-x-auto" style={{ borderColor: BORDER }}>
            <table className="w-full text-[12px]" style={{ minWidth: 420 }}>
              <thead>
                <tr style={{ background: SURFACE_ALT, color: TEXT_2 }}>
                  <th className="px-3 py-2 text-left font-extrabold">Item</th>
                  <th className="px-3 py-2 text-right font-extrabold">Qty</th>
                  <th className="px-3 py-2 text-right font-extrabold">Rate</th>
                  <th className="px-3 py-2 text-right font-extrabold">GST</th>
                  <th className="px-3 py-2 text-right font-extrabold">Total</th>
                </tr>
              </thead>
              <tbody className="divide-y" style={{ borderColor: DIVIDER }}>
                {items.map((it) => (
                  <tr key={it.posorderitemid}>
                    <td className="px-3 py-2">
                      <div className="font-bold" style={{ color: TEXT }}>{it.productname || `#${it.productid}`}</div>
                      {it.barcode && (
                        <div className="font-mono text-[10px]" style={{ color: TEXT_3 }}>{it.barcode}</div>
                      )}
                    </td>
                    <td className="px-3 py-2 text-right" style={{ color: TEXT_2 }}>
                      {it.quantity}
                      {it.unitname ? ` ${it.unitname}` : ''}
                    </td>
                    <td className="px-3 py-2 text-right" style={{ color: TEXT_2 }}>{inr(it.unitprice)}</td>
                    <td className="px-3 py-2 text-right" style={{ color: TEXT_2 }}>
                      {/* gstrate arrives as a fraction (0.18), not a percentage. */}
                      {(it.gstrate * 100).toFixed(it.gstrate * 100 % 1 === 0 ? 0 : 2)}%
                      <div className="text-[10px]" style={{ color: TEXT_3 }}>{inr(it.taxamount)}</div>
                    </td>
                    <td className="px-3 py-2 text-right font-bold" style={{ color: TEXT }}>{inr(it.linetotal)}</td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        </div>
      ) : (
        <p className="text-[12px]" style={{ color: TEXT_3 }}>
          This bill has no stored line items.
        </p>
      )}

      <div className="rounded-xl p-3 space-y-1.5" style={{ background: SURFACE_ALT, border: `1px solid ${BORDER}` }}>
        <Line label="Subtotal" value={inr(gross)} />
        {bill.discount > 0 && <Line label="Discount" value={`− ${inr(bill.discount)}`} />}
        <Line label="Tax" value={inr(bill.taxamount)} />
        {bill.roundoff !== 0 && <Line label="Round off" value={inr(bill.roundoff)} />}
        <div className="pt-1.5 mt-1.5 border-t" style={{ borderColor: BORDER }}>
          <Line label="Total" value={inr(bill.total)} bold />
        </div>
      </div>

      {payments.length > 0 && (
        <div>
          <PanelTitle icon={<CreditCard size={14} />} title="Tender split" hint="`paymentmode` names only the largest" />
          <div className="mt-2 space-y-1.5">
            {payments.map((p, i) => (
              <div key={i} className="flex items-center justify-between text-[12.5px]">
                <span className="capitalize font-semibold" style={{ color: TEXT }}>
                  {p.method}
                  {p.reference && (
                    <span className="font-mono ml-2 text-[10.5px]" style={{ color: TEXT_3 }}>{p.reference}</span>
                  )}
                </span>
                <span style={{ color: TEXT_2 }}>{inr(p.amount)}</span>
              </div>
            ))}
          </div>
        </div>
      )}

      {taxes.length > 0 && (
        <div>
          <PanelTitle icon={<CreditCard size={14} />} title="GST by slab" />
          <div className="mt-2 space-y-1.5">
            {taxes.map((t) => (
              <div key={t.rate} className="flex items-center justify-between text-[12.5px]">
                <span className="font-semibold" style={{ color: TEXT }}>{(t.rate * 100).toFixed(2)}%</span>
                <span style={{ color: TEXT_2 }}>{inr(t.amount)}</span>
              </div>
            ))}
          </div>
        </div>
      )}

      {promos.length > 0 && (
        <div>
          <PanelTitle icon={<TrendingUp size={14} />} title="Promotions applied" />
          <div className="mt-2 space-y-1.5">
            {promos.map((p, i) => (
              <div key={i} className="flex items-center justify-between text-[12.5px]">
                <span className="font-semibold" style={{ color: TEXT }}>{p.name || p.id || 'Promotion'}</span>
                <span style={{ color: TEXT_2 }}>{p.amount != null ? inr(p.amount) : '—'}</span>
              </div>
            ))}
          </div>
        </div>
      )}

      {(bill.pointsearned > 0 || bill.pointsredeemed > 0) && (
        <div className="grid grid-cols-2 gap-2">
          <Stat label="Points earned" value={String(bill.pointsearned)} />
          <Stat label="Points redeemed" value={String(bill.pointsredeemed)} />
        </div>
      )}

      <div className="rounded-xl p-3 space-y-1" style={{ background: SURFACE_ALT, border: `1px solid ${BORDER}` }}>
        <Meta label="Order UUID" value={bill.terminalorderid} mono />
        <Meta label="Bill id" value={String(bill.posorderid)} mono />
        <Meta label="Batch" value={bill.batchid || '—'} mono />
        <Meta label="Billed at" value={bill.billedat || '—'} mono />
        <Meta label="Received at" value={bill.receivedat || '—'} mono />
        <Meta
          label="Sync lag"
          value={
            lag.kind === 'unknown'
              ? '—'
              : lag.kind === 'legacy'
                ? 'legacy timestamp (pre-offset-fix bill)'
                : humanDuration(lag.seconds)
          }
        />
      </div>
    </div>
  );
}

// ── Catalogue ────────────────────────────────────────────────────────────────

/**
 * What a till at this outlet can actually sell.
 *
 * This is the terminal's own view, pulled from the same endpoint the till uses,
 * so it answers the question no other screen can: not "what is in the catalogue"
 * but "what would this counter let a cashier ring up right now". An unpriced
 * product comes down `is_active: false` precisely so a till cannot sell it at
 * ₹0, and that is invisible everywhere else in the product.
 */
function CatalogueTab({
  data,
  loading,
  isStoreScope,
  outlets,
  billOutlet,
  onPickOutlet,
  outletLabel,
}: {
  data: PosCatalogue | undefined;
  loading: boolean;
  isStoreScope: boolean;
  outlets: Array<{ locationid: number; name: string }>;
  billOutlet: number;
  onPickOutlet: (id: number) => void;
  outletLabel: string;
}) {
  const [onlyBlocked, setOnlyBlocked] = useState(false);



  if (loading) {
    return (
      <>

        <SkeletonPanel lines={6} />
      </>
    );
  }

  const products = data?.products ?? [];
  if (!data || products.length === 0) {
    return (
      <>

        <EmptyState
          icon={<Package size={30} />}
          title="Nothing in this till's catalogue"
          body="No product has been published to this outlet, so a terminal here has nothing to sell. Products appear once they are stocked and priced for the store."
        />
      </>
    );
  }

  const blocked = products.filter((p) => !p.is_active);
  const unpriced = products.filter((p) => p.price <= 0);
  const outOfStock = products.filter((p) => p.stock <= 0);
  const noGst = products.filter((p) => !p.gst_rate);
  const shown = onlyBlocked ? blocked : products;

  return (
    <>





      {blocked.length > 0 && (
        <div className="flex items-center gap-2">
          <Pill active={onlyBlocked} color="#f59e0b" onClick={() => setOnlyBlocked(!onlyBlocked)} count={blocked.length}>
            Only unsellable
          </Pill>
        </div>
      )}

      <PosTable
        minWidth={780}
        columns={[
          { label: 'Product' },
          { label: 'Category' },
          { label: 'Unit' },
          { label: 'Price', align: 'right' },
          { label: 'Stock', align: 'right' },
          { label: 'GST', align: 'right' },
          { label: 'Sellable', align: 'right' },
        ]}
      >
        {shown.map((p) => (
          <tr key={p.id} className="hover:bg-slate-50/70">
            <td className="px-4 py-3">
              <div className="font-bold text-[12.5px]" style={{ color: TEXT }}>{p.name || `#${p.id}`}</div>
              <div className="font-mono text-[10.5px]" style={{ color: TEXT_3 }}>
                {p.barcode || p.id}
                {p.brand ? ` · ${p.brand}` : ''}
                {p.hsn_code ? ` · HSN ${p.hsn_code}` : ''}
              </div>
            </td>
            <td className="px-4 py-3 text-[12.5px]" style={{ color: TEXT_2 }}>{p.category || '—'}</td>
            <td className="px-4 py-3 text-[12.5px]" style={{ color: TEXT_2 }}>{p.unit || '—'}</td>
            <td className="px-4 py-3 text-right font-bold" style={{ color: p.price > 0 ? TEXT : '#f59e0b' }}>
              {p.price > 0 ? inr(p.price) : 'Unpriced'}
            </td>
            <td className="px-4 py-3 text-right" style={{ color: p.stock > 0 ? TEXT_2 : '#f59e0b' }}>{p.stock}</td>
            <td className="px-4 py-3 text-right" style={{ color: TEXT_2 }}>
              {/* gst_rate is a fraction (0.05), matching how the till holds it. */}
              {(p.gst_rate * 100).toFixed((p.gst_rate * 100) % 1 === 0 ? 0 : 2)}%
            </td>
            <td className="px-4 py-3 text-right">
              <StatusChip label={p.is_active ? 'Yes' : 'No'} color={p.is_active ? '#10b981' : '#f59e0b'} />
            </td>
          </tr>
        ))}
      </PosTable>
    </>
  );
}

// ── Exceptions ───────────────────────────────────────────────────────────────

function ExceptionsTab({
  exceptions,
  outletName,
  loading,
  onOpenBill,
}: {
  exceptions: {
    stranded: Array<PosTerminal & { locationid: number }>;
    idle: Array<PosTerminal & { locationid: number }>;
    dark: Array<PosTerminal & { locationid: number }>;
    stale: Array<PosTerminal & { locationid: number }>;
    drifting: Array<PosTerminal & { locationid: number }>;
    noTills: Array<{ locationid: number; name: string }>;
    delayed: PosBill[];
    legacy: PosBill[];
    unassigned: PosBill[];
  };
  outletName: Map<number, string>;
  loading: boolean;
  onOpenBill: (b: PosBill) => void;
}) {
  if (loading) return <SkeletonPanel lines={4} />;

  const where = (t: { locationid: number }) => outletName.get(t.locationid) || `Store ${t.locationid}`;
  const nothing =
    exceptions.stranded.length === 0 &&
    exceptions.idle.length === 0 &&
    exceptions.dark.length === 0 &&
    exceptions.stale.length === 0 &&
    exceptions.drifting.length === 0 &&
    exceptions.delayed.length === 0;

  if (nothing) {
    return (
      <EmptyState
        icon={<Wifi size={30} />}
        title="Nothing needs attention"
        body="Every till is reporting, nothing is queued unsynced, and no bill took an unusual time to arrive."
      />
    );
  }

  return (
    <>
      <ExceptionGroup
        tone="#ef4444"
        title="Unsynced takings"
        hint="A shop accumulating these looks completely normal from the floor"
        rows={exceptions.stranded.map((t) => ({
          key: `${t.locationid}-${t.terminalId}`,
          primary: t.terminalId,
          secondary: where(t),
          value: `${t.pendingBills} bill${t.pendingBills === 1 ? '' : 's'}`,
          note: t.oldestPendingAt ? `oldest ${billTime(t.oldestPendingAt)}` : undefined,
        }))}
      />

      <ExceptionGroup
        tone="#ef4444"
        title="Tills not reporting"
        hint="Declared offline by the broker's last will, or the heartbeat simply expired"
        rows={exceptions.dark.map((t) => ({
          key: `${t.locationid}-${t.terminalId}`,
          primary: t.terminalId,
          secondary: where(t),
          value: TERMINAL_STATE_LABEL[t.state],
          note: t.reason || undefined,
        }))}
      />

      <ExceptionGroup
        tone="#f59e0b"
        title="Online but selling nothing"
        hint="Usually a jammed printer or an absent cashier — invisible on a plain online/offline board"
        rows={exceptions.idle.map((t) => ({
          key: `${t.locationid}-${t.terminalId}`,
          primary: t.terminalId,
          secondary: where(t),
          value: '0 bills today',
          note: t.lastBillAt ? `last bill ${billTime(t.lastBillAt)}` : 'no bill recorded',
        }))}
      />

      <ExceptionGroup
        tone="#f59e0b"
        title="Heartbeat going stale"
        hint="No reading for over 40s. Not yet offline — the presence window is 90s"
        rows={exceptions.stale.map((t) => ({
          key: `${t.locationid}-${t.terminalId}`,
          primary: t.terminalId,
          secondary: where(t),
          value: t.ageSeconds != null ? `${humanDuration(t.ageSeconds)} ago` : 'unknown',
        }))}
      />

      <ExceptionGroup
        tone="#f59e0b"
        title="Clock drift"
        hint="A till with a wrong clock files bills under the wrong business date"
        rows={exceptions.drifting.map((t) => ({
          key: `${t.locationid}-${t.terminalId}`,
          primary: t.terminalId,
          secondary: where(t),
          value: humanDuration(t.clockDriftSeconds as number),
          note: (t.clockDriftSeconds as number) > 0 ? 'ahead of server' : 'behind server',
        }))}
      />

      {exceptions.delayed.length > 0 && (
        <Card className="p-4">
          <PanelTitle
            icon={<Clock size={14} />}
            title={`${exceptions.delayed.length} bill${exceptions.delayed.length === 1 ? '' : 's'} arrived late`}
            hint="Over 15 minutes between being rung and reaching us — an outage backlog"
          />
          <div className="mt-3 space-y-1.5">
            {exceptions.delayed.slice(0, 12).map((b) => {
              const lag = syncLag(b);
              return (
                <button
                  key={b.posorderid}
                  onClick={() => onOpenBill(b)}
                  className="w-full flex items-center justify-between text-[12.5px] py-1 cursor-pointer hover:opacity-70"
                >
                  <span className="font-mono font-bold" style={{ color: TEXT }}>{b.invoicenumber || `#${b.posorderid}`}</span>
                  <span style={{ color: '#f59e0b' }}>
                    {lag.kind === 'delayed' ? humanDuration(lag.seconds) : '—'}
                  </span>
                </button>
              );
            })}
          </div>
        </Card>
      )}

      {(exceptions.legacy.length > 0 || exceptions.unassigned.length > 0) && (
        <Card className="p-4">
          <PanelTitle icon={<AlertTriangle size={14} />} title="Known legacy data" hint="Not alerts — recorded so the panels above read honestly" />
          <ul className="mt-3 space-y-1.5 text-[12px]" style={{ color: TEXT_2 }}>
            {exceptions.legacy.length > 0 && (
              <li>
                <strong style={{ color: TEXT }}>{exceptions.legacy.length}</strong> bill
                {exceptions.legacy.length === 1 ? '' : 's'} on this page carry a pre-fix timestamp (local time stamped as
                UTC), so their sync lag is not measurable. Business dates are unaffected.
              </li>
            )}
            {exceptions.unassigned.length > 0 && (
              <li>
                <strong style={{ color: TEXT }}>{exceptions.unassigned.length}</strong> bill
                {exceptions.unassigned.length === 1 ? '' : 's'} have no terminal id — ingested before the fallback landed.
                They group under “Unassigned”.
              </li>
            )}
          </ul>
        </Card>
      )}

      {exceptions.noTills.length > 0 && (
        <Card className="p-4">
          <PanelTitle icon={<Store size={14} />} title={`${exceptions.noTills.length} outlet${exceptions.noTills.length === 1 ? '' : 's'} with no till`} />
          <div className="flex flex-wrap gap-1.5 mt-3">
            {exceptions.noTills.map((o) => (
              <span
                key={o.locationid}
                className="rounded-full px-2.5 py-1 text-[11px] font-semibold"
                style={{ background: SURFACE_ALT, color: TEXT_2, border: `1px solid ${BORDER}` }}
              >
                {o.name}
              </span>
            ))}
          </div>
        </Card>
      )}
    </>
  );
}

function ExceptionGroup({
  tone,
  title,
  hint,
  rows,
}: {
  tone: string;
  title: string;
  hint?: string;
  rows: Array<{ key: string; primary: string; secondary: string; value: string; note?: string }>;
}) {
  if (rows.length === 0) return null;
  return (
    <Card className="p-4">
      <PanelTitle icon={<AlertTriangle size={14} />} title={`${title} (${rows.length})`} hint={hint} color={tone} />
      <div className="mt-3 divide-y" style={{ borderColor: DIVIDER }}>
        {rows.map((r) => (
          <div key={r.key} className="flex items-center justify-between gap-3 py-2">
            <div className="min-w-0">
              <div className="font-mono font-bold text-[12.5px]" style={{ color: TEXT }}>{r.primary || '—'}</div>
              <div className="text-[11px] truncate" style={{ color: TEXT_3 }}>{r.secondary}</div>
            </div>
            <div className="text-right shrink-0">
              <div className="font-extrabold text-[12.5px]" style={{ color: tone }}>{r.value}</div>
              {r.note && <div className="text-[10.5px]" style={{ color: TEXT_3 }}>{r.note}</div>}
            </div>
          </div>
        ))}
      </div>
    </Card>
  );
}

// ── Small shared pieces ──────────────────────────────────────────────────────

/**
 * Table shell with per-column alignment.
 *
 * The shared `TableShell` right-aligns every header from index 2 onward, which
 * is right for its own screens but disagrees with these tables: the leaderboard
 * has a numeric column at index 1, and the bill list has three text columns
 * after it. Declaring the alignment per column keeps each header sitting over
 * its own cells instead of drifting off to one side.
 */
interface PosColumn {
  label: string;
  align?: 'left' | 'right';
}

function PosTable({
  columns,
  minWidth,
  footer,
  children,
}: {
  columns: PosColumn[];
  minWidth?: number;
  footer?: React.ReactNode;
  children: React.ReactNode;
}) {
  return (
    <div className="bg-white rounded-2xl border overflow-hidden shadow-sm w-full min-w-0" style={{ borderColor: BORDER }}>
      <div className="overflow-x-auto">
        <table className="w-full text-sm whitespace-nowrap" style={{ minWidth }}>
          <thead style={TH_STYLE}>
            <tr>
              {columns.map((c, i) => (
                <th key={i} className={`px-4 py-3 ${c.align === 'right' ? 'text-right' : 'text-left'}`}>
                  {c.label}
                </th>
              ))}
            </tr>
          </thead>
          <tbody className="divide-y" style={{ borderColor: DIVIDER }}>
            {children}
          </tbody>
        </table>
      </div>
      {footer}
    </div>
  );
}

function PanelTitle({ icon, title, hint, color = TEXT_2 }: { icon: React.ReactNode; title: string; hint?: string; color?: string }) {
  return (
    <div className="flex items-start gap-2">
      <span style={{ color }} className="mt-0.5">{icon}</span>
      <div className="min-w-0">
        <h3 className="text-[13px] font-extrabold leading-tight" style={{ color: TEXT }}>{title}</h3>
        {hint && <p className="text-[11px] mt-0.5" style={{ color: TEXT_3 }}>{hint}</p>}
      </div>
    </div>
  );
}

function Stat({ label, value, sub, alert }: { label: string; value: string; sub?: string; alert?: boolean }) {
  return (
    <div className="rounded-xl px-3 py-2" style={{ background: SURFACE_ALT, border: `1px solid ${alert ? edge('#f59e0b') : BORDER}` }}>
      <div className="text-[10px] font-extrabold uppercase tracking-wide" style={{ color: TEXT_3 }}>{label}</div>
      <div className="text-[14px] font-extrabold truncate" style={{ color: alert ? '#f59e0b' : TEXT }}>{value}</div>
      {sub && <div className="text-[10.5px] truncate" style={{ color: TEXT_3 }}>{sub}</div>}
    </div>
  );
}

function Chip({ label, icon, color = TEXT_2 }: { label: string; icon?: React.ReactNode; color?: string }) {
  return (
    <span
      className="inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[10.5px] font-bold"
      style={{ background: tint(color), color, border: `1px solid ${edge(color)}` }}
    >
      {icon}
      {label}
    </span>
  );
}

function Line({ label, value, bold }: { label: string; value: string; bold?: boolean }) {
  return (
    <div className="flex items-center justify-between text-[12.5px]">
      <span style={{ color: bold ? TEXT : TEXT_2, fontWeight: bold ? 800 : 500 }}>{label}</span>
      <span style={{ color: TEXT, fontWeight: bold ? 800 : 600 }}>{value}</span>
    </div>
  );
}

function Meta({ label, value, mono }: { label: string; value: string; mono?: boolean }) {
  return (
    <div className="flex items-start justify-between gap-3 text-[11px]">
      <span className="shrink-0" style={{ color: TEXT_3 }}>{label}</span>
      <span className={`text-right break-all ${mono ? 'font-mono' : ''}`} style={{ color: TEXT_2 }}>{value}</span>
    </div>
  );
}

/**
 * Icon, heading and body stacked on one centred axis inside a fixed measure.
 *
 * The whole block is centred as a unit rather than each element being centred
 * independently — otherwise a long body wraps to the card's full width while
 * the short heading above it sits on a different visual centre, and the two
 * read as misaligned even though both are technically centred.
 */
function EmptyState({ icon, title, body }: { icon: React.ReactNode; title: string; body: string }) {
  return (
    <Card className="w-full min-w-0">
      <div className="mx-auto flex flex-col items-center text-center px-6 py-12" style={{ maxWidth: 420 }}>
        <div
          className="h-16 w-16 rounded-2xl flex items-center justify-center mb-5 shrink-0"
          style={{ background: tint(BRAND), color: BRAND, border: `1px solid ${edge(BRAND)}` }}
        >
          {icon}
        </div>
        <h3 className="text-[17px] font-extrabold mb-2 leading-snug" style={{ color: TEXT }}>{title}</h3>
        <p className="text-[13px] leading-relaxed" style={{ color: TEXT_2 }}>{body}</p>
      </div>
    </Card>
  );
}

function SkeletonPanel({ lines }: { lines: number }) {
  return (
    <Card className="p-4 space-y-3">
      {Array.from({ length: lines }).map((_, i) => (
        <div key={i} className="animate-pulse rounded-lg" style={{ height: 18, width: `${100 - i * 8}%`, background: SURFACE_ALT }} />
      ))}
    </Card>
  );
}
