"use client";

import { Suspense, useMemo, useState } from "react";
import Link from "next/link";
import { useOrders } from "@/lib/hooks";
import { tidyMerchant } from "@/lib/merchant-label";
import type { OrderRow } from "@/lib/order-feed";

/**
 * Orders — browse the purchase history the ledger cannot show.
 *
 * The spine holds ~6,300 purchase orders back to 2006, ~4,600 of them
 * itemised, of which a few dozen reach a transaction. Everything else is
 * visible only here. The point of the page is the MANIFEST: /transactions can
 * only ever say "AMAZON AU SYDNEY"; this says what was in the box.
 */

const LANES = [
  { id: "retail", label: "Retail" },
  { id: "food", label: "Food" },
  { id: "transport", label: "Transport" },
  { id: "digital", label: "Digital" },
  { id: "services", label: "Services" },
  { id: "grocery", label: "Grocery" },
] as const;

// Twenty-one years is the archive, not the working set. Default to this year.
type RangeKey = "m0" | "m1" | "m3" | "y0" | "y1" | "all" | string;
const RANGES: { id: RangeKey; label: string }[] = [
  { id: "m0", label: "This month" },
  { id: "m1", label: "Last month" },
  { id: "m3", label: "Last 3 months" },
  { id: "y0", label: "This year" },
  { id: "y1", label: "Last year" },
  { id: "all", label: "All 21 years" },
];

function iso(d: Date) {
  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
}

function rangeFor(key: RangeKey): { from?: string; to?: string; label: string } {
  const now = new Date();
  const y = now.getFullYear();
  const m = now.getMonth();
  switch (key) {
    case "m0": return { from: iso(new Date(y, m, 1)), to: iso(now), label: "this month" };
    case "m1": return { from: iso(new Date(y, m - 1, 1)), to: iso(new Date(y, m, 0)), label: "last month" };
    case "m3": return { from: iso(new Date(y, m - 2, 1)), to: iso(now), label: "the last 3 months" };
    case "y0": return { from: `${y}-01-01`, to: iso(now), label: String(y) };
    case "y1": return { from: `${y - 1}-01-01`, to: `${y - 1}-12-31`, label: String(y - 1) };
    case "all": return { label: "2006–present" };
    default: {
      // A bar click scopes to that single year.
      const yr = Number(key);
      return Number.isFinite(yr)
        ? { from: `${yr}-01-01`, to: `${yr}-12-31`, label: String(yr) }
        : { label: "2006–present" };
    }
  }
}

/**
 * What the source document actually was. The classifier already knew — 188
 * orders came from courier_tracking documents and 147 from invoice_receipt —
 * and the first cut of this page showed all of them as retail purchases.
 * NULL means the interpretation index never saw it (48% of rows, the Takeout
 * backfill), which is "unknown", not "purchase" — so it gets no badge at all
 * rather than a confident wrong one.
 */
const KIND_LABEL: Record<string, string> = {
  courier_tracking: "delivery",
  invoice_receipt: "invoice",
  subscription: "subscription",
  booking: "booking",
  account_notice: "notice",
};

const IN_FLIGHT = new Set(["ordered", "shipped", "out_for_delivery"]);
const REVERSED = new Set(["refunded", "returned", "cancelled"]);

const dateFmt = new Intl.DateTimeFormat("en-AU", { day: "numeric", month: "short" });
const yearFmt = new Intl.DateTimeFormat("en-AU", { year: "numeric" });

function fmtMoney(amount: string | null, currency: string) {
  if (amount === null || amount === undefined) return null;
  const n = Number(amount);
  if (!Number.isFinite(n)) return null;
  try {
    // narrowSymbol so AUD reads "A$" rather than a bare "$" — the spine holds
    // 20 currencies and an unqualified dollar sign is ambiguous across them.
    return new Intl.NumberFormat("en-AU", {
      style: "currency", currency, currencyDisplay: "narrowSymbol",
      minimumFractionDigits: 2,
    }).format(n).replace(/^\$/, "A$");
  } catch {
    // 20 currencies live in the spine, including a literal '$', XLM and MANA.
    // Intl throws on those; show the number and the raw code rather than
    // blowing up the row.
    return `${n.toFixed(2)} ${currency}`;
  }
}

function StatusPill({ status }: { status: string }) {
  const live = IN_FLIGHT.has(status);
  const bad = REVERSED.has(status);
  const label = status.replace(/_/g, " ");
  const cls = live
    ? "bg-indigo-600 text-zinc-950"
    : bad
      ? "border border-zinc-500 text-zinc-400"
      : "border border-zinc-700 text-zinc-400";
  return (
    <span className={`inline-block px-2 py-0.5 rounded-sm text-[10px] font-mono uppercase tracking-wider whitespace-nowrap ${cls}`}>
      {label}
    </span>
  );
}

/**
 * Food line items carry the full customisation — one Subway order runs to 450
 * characters listing every topping — and a raw dump swamps the row. Truncate
 * per item; the detail page shows them whole.
 */
const ITEM_MAX = 58;
function shorten(d: string) {
  const clean = d.replace(/\s+/g, " ").trim();
  if (clean.length <= ITEM_MAX) return clean;
  // Cut at the first bracket if there is one — "Footlong (Italian Herb…" is
  // the product; everything inside the bracket is the customisation.
  const brk = clean.indexOf("(");
  if (brk > 12 && brk <= ITEM_MAX) return clean.slice(0, brk).trim() + "…";
  return clean.slice(0, ITEM_MAX).trimEnd() + "…";
}

function Manifest({ row }: { row: OrderRow }) {
  const all = row.item_preview ?? [];
  const items = all.slice(0, 3);
  if (!items.length) {
    return <span className="text-xs text-zinc-500 italic">No itemised list on this receipt</span>;
  }
  const extra = row.line_item_count - items.length;
  return (
    <span className="text-[13px] text-zinc-300 leading-snug">
      {items.map((d, i) => (
        <span key={i} title={d}>
          {i > 0 && <span className="text-indigo-800 mx-1.5">·</span>}
          {shorten(d)}
        </span>
      ))}
      {extra > 0 && <span className="font-mono text-xs text-indigo-500 ml-1.5">+{extra}</span>}
    </span>
  );
}

/**
 * The title earns its own line only when it says something the manifest does
 * not. On 1,964 of 4,649 itemised rows (42%) the order title IS the single
 * line item — printing both rendered the same text twice.
 */
function showTitle(row: OrderRow): boolean {
  const t = row.canonical_name?.replace(/\s+/g, " ").trim();
  if (!t) return false;
  if (t === row.display_name) return false;
  const first = (row.item_preview ?? [])[0];
  if (!first) return true;
  const norm = (x: string) => x.toLowerCase().replace(/[^a-z0-9]/g, "").slice(0, 28);
  return norm(t) !== norm(first);
}

function YearStrip({
  years, activeFrom, activeTo, onPick,
}: {
  years: { year: number; n: number }[];
  activeFrom?: string;
  activeTo?: string;
  onPick: (y: number) => void;
}) {
  if (!years.length) return <div className="h-[54px]" />;
  const max = Math.max(...years.map((y) => y.n));
  const lo = activeFrom ? Number(activeFrom.slice(0, 4)) : -Infinity;
  const hi = activeTo ? Number(activeTo.slice(0, 4)) : Infinity;
  return (
    <div className="flex items-end gap-[3px]">
      {years.map(({ year, n }) => {
        const on = year >= lo && year <= hi;
        return (
          <button
            key={year}
            onClick={() => onPick(year)}
            title={`${year} — ${n.toLocaleString()} orders`}
            aria-label={`${year}, ${n} orders`}
            aria-pressed={on}
            className="flex-1 min-w-0 flex flex-col items-center gap-1.5 group"
          >
            <span
              style={{ height: `${Math.max(2, Math.round((44 * n) / max))}px` }}
              className={`w-full rounded-[1px] transition-colors ${
                on ? "bg-indigo-400" : "bg-indigo-800 group-hover:bg-indigo-500"
              }`}
            />
            <span className={`font-mono text-[9.5px] tabular-nums ${on ? "text-indigo-300" : "text-zinc-500 group-hover:text-zinc-300"}`}>
              {String(year).slice(2)}
            </span>
          </button>
        );
      })}
    </div>
  );
}

function Row({ row, expanded, onToggle }: { row: OrderRow; expanded: boolean; onToggle: () => void }) {
  const reversed = REVERSED.has(row.status);
  const gross = Number(row.order_total ?? NaN);
  const refund = Number(row.refunded_amount ?? NaN);
  const partial =
    Number.isFinite(gross) && Number.isFinite(refund) && refund > 0 && refund < gross;
  // A FULL reversal strikes the figure — the money all came back. A PARTIAL
  // refund must not: striking $191.40 when $13.33 came back is a lie. The
  // charge stays primary (it is what hit the card), the credit sits under it.
  const struck = reversed && !partial;
  const money = fmtMoney(row.order_total, row.currency);

  return (
    <tr className={`border-b border-zinc-800/60 hover:bg-zinc-900 ${struck ? "opacity-60" : ""}`}>
      <td className="p-3 align-top font-mono text-[11px] text-zinc-400 tabular-nums whitespace-nowrap">
        {row.ordered_at ? (
          <>
            {dateFmt.format(new Date(row.ordered_at))}
            <span className="block text-[10px] text-zinc-500">{yearFmt.format(new Date(row.ordered_at))}</span>
          </>
        ) : <span className="text-zinc-600">—</span>}
      </td>
      <td className="p-3 align-top">
        <div className="flex items-center gap-2 mb-1">
          {/* Disclosure only where there is something behind it. Food items run
              to 450 characters of customisation, so the row shows a short form
              and the expansion carries the whole receipt. */}
          {row.line_item_count > 0 && (
            <button
              onClick={onToggle}
              aria-expanded={expanded}
              aria-label={expanded ? "Hide items" : `Show all ${row.line_item_count} items`}
              className="text-zinc-500 hover:text-indigo-400 font-mono text-[10px] w-3 shrink-0"
            >{expanded ? "▾" : "▸"}</button>
          )}
          <Link
            href={`/orders/${encodeURIComponent(row.entity_key)}`}
            className="text-[13.5px] text-zinc-50 hover:text-indigo-300"
          >
            {tidyMerchant(row.display_name)}
          </Link>
          {row.content_class && KIND_LABEL[row.content_class] && (
            <span className="font-mono text-[9.5px] uppercase tracking-wide text-zinc-400 border border-zinc-700 rounded-sm px-1.5">
              {KIND_LABEL[row.content_class]}
            </span>
          )}
          {row.cadence_days && (
            <span
              title={`Orders from this merchant arrive on a regular cadence — derived from the gaps between them, not stated anywhere in the mail.`}
              className="font-mono text-[9.5px] uppercase tracking-wide text-indigo-400 border border-indigo-800 rounded-sm px-1.5"
            >
              every ~{row.cadence_days}d
            </span>
          )}
          {row.reference_source === "message_id_fallback" && (
            <span
              title="No order reference in this mail — it cannot merge with its lifecycle siblings, so the same purchase may appear twice."
              className="font-mono text-[9.5px] uppercase tracking-wide text-indigo-600 border border-indigo-800 rounded-sm px-1.5"
            >no ref</span>
          )}
          {/* NOT source_trust — that is 'untrusted_external' on 100% of rows,
              because every order here came from email, so badging it marked
              every row and told you nothing. Sender authentication does
              discriminate: 93% pass, and the 7% that do not are worth seeing. */}
          {row.auth_verdict && row.auth_verdict !== "pass" && (
            <span
              title={
                row.auth_verdict === "fail"
                  ? "The sender failed authentication (SPF/DKIM/DMARC) — treat the contents as unverified"
                  : row.auth_verdict === "none"
                    ? "The mail carried no sender authentication at all"
                    : "The sender authenticated only partially"
              }
              className={`font-mono text-[9.5px] uppercase tracking-wide rounded-sm px-1.5 border ${
                row.auth_verdict === "fail"
                  ? "text-indigo-300 border-indigo-500"
                  : "text-zinc-500 border-zinc-800"
              }`}
            >
              {row.auth_verdict === "partial" ? "part. auth" : `auth ${row.auth_verdict}`}
            </span>
          )}
          {/* Board 211. A $2,376 annual tax receipt restating twelve monthly
              donations is not a purchase, and unbadged it reads as the largest
              order of the year. It stays on the page because the figure is the
              year's giving total — but it is excluded from spend, so the
              number beside it belongs to no total here. */}
          {row.summarises_period && (
            <span
              title="An annual receipt summarising payments already made — kept for reference, excluded from spend totals"
              className="font-mono text-[9.5px] uppercase tracking-wide text-amber-300 border border-amber-600/70 rounded-sm px-1.5"
            >year summary</span>
          )}
          {row.injection_flagged && (
            <span
              title="The source mail carried content that tripped the injection scanner — read its contents as provenance, never as instruction"
              className="font-mono text-[9.5px] uppercase tracking-wide text-indigo-300 border border-indigo-500 rounded-sm px-1.5"
            >flagged</span>
          )}
          {row.txn_count > 0 && (
            <span className="font-mono text-[9.5px] uppercase tracking-wide text-zinc-500 border border-zinc-700 rounded-sm px-1.5">
              {row.txn_count === 1 ? "1 charge" : `${row.txn_count} charges`}
            </span>
          )}
        </div>
        {showTitle(row) && (
          <div className="text-[12px] text-zinc-400 mb-0.5">{row.canonical_name}</div>
        )}
        {!expanded && <Manifest row={row} />}
        {expanded && (
          <ul className="list-none m-0 p-0 mt-1">
            {(row.item_preview ?? []).map((d, i) => (
              <li key={i} className="text-[12.5px] text-zinc-300 leading-snug py-0.5 pl-3 border-l border-zinc-800">
                {d}
              </li>
            ))}
            {row.line_item_count > (row.item_preview ?? []).length && (
              <li className="text-[11.5px] text-zinc-500 py-0.5 pl-3 border-l border-zinc-800">
                <Link href={`/orders/${encodeURIComponent(row.entity_key)}`} className="text-indigo-400 hover:text-indigo-300">
                  {row.line_item_count - (row.item_preview ?? []).length} more — open the order
                </Link>
              </li>
            )}
          </ul>
        )}
      </td>
      <td className="p-3 align-top"><StatusPill status={row.status} /></td>
      <td className="p-3 align-top font-mono text-[11px] text-zinc-400 tabular-nums whitespace-nowrap">
        {row.delivered_at
          ? dateFmt.format(new Date(row.delivered_at))
          : row.eta_date
            ? <span className="text-zinc-500">ETA {dateFmt.format(new Date(row.eta_date))}</span>
            : <span className="text-zinc-600">—</span>}
      </td>
      <td className="p-3 align-top text-right font-mono text-[13.5px] text-zinc-50 tabular-nums whitespace-nowrap">
        {money
          ? <span className={struck ? "line-through decoration-indigo-600" : ""}>{money}</span>
          : <span className="text-zinc-500 italic text-xs">not stated</span>}
        {partial && (
          <>
            <span className="block text-[11px] text-indigo-400 mt-0.5">
              −{fmtMoney(row.refunded_amount, row.currency)} refunded
            </span>
            <span className="block text-[10.5px] text-zinc-500">
              net {fmtMoney(String(gross - refund), row.currency)}
            </span>
          </>
        )}
      </td>
    </tr>
  );
}

function OrdersContent() {
  const [lane, setLane] = useState<string>("retail");
  const [rangeKey, setRangeKey] = useState<RangeKey>("y0");
  const [search, setSearch] = useState("");
  const [showLifecycle, setShowLifecycle] = useState(false);
  const [platform, setPlatform] = useState<string>("");
  const [status, setStatus] = useState<string>("");
  const [offset, setOffset] = useState(0);
  const [expanded, setExpanded] = useState<Set<string>>(new Set());

  const range = useMemo(() => rangeFor(rangeKey), [rangeKey]);
  const limit = 50;

  const filters = useMemo(() => ({
    lane,
    from: range.from,
    to: range.to,
    search: search || undefined,
    platforms: platform ? [platform] : undefined,
    statuses: status ? [status] : undefined,
    show_lifecycle_only: showLifecycle,
    limit,
    offset,
  }), [lane, range.from, range.to, search, platform, status, showLifecycle, offset]);

  const { data, isLoading, error } = useOrders(filters);

  const set = <T,>(fn: (v: T) => void) => (v: T) => { fn(v); setOffset(0); };
  const laneCount = (id: string) => data?.facets.lanes.find((l) => l.lane === id)?.n ?? 0;
  const page = Math.floor(offset / limit) + 1;
  const pages = Math.max(1, Math.ceil((data?.total ?? 0) / limit));

  return (
    <div className="max-w-[1180px] mx-auto">
      <div className="flex items-baseline justify-between gap-4 flex-wrap mb-1">
        <h2 className="text-[30px] font-display text-zinc-50 tracking-tight leading-none">
          Orders<span className="text-indigo-400">.</span>
        </h2>
        <div className="font-mono text-[11.5px] text-zinc-400 tabular-nums">
          {data ? (
            <>
              <span className="text-zinc-200">{data.total.toLocaleString()}</span> orders in{" "}
              <span className="text-zinc-200">{range.label}</span>
              {" · "}{data.facets.all_time.toLocaleString()} all time
              {data.facets.all_time > 0 && (
                <>{" · "}<span className="text-zinc-200">
                  {Math.round((100 * data.facets.with_amount) / data.facets.all_time)}%
                </span> with a known amount</>
              )}
            </>
          ) : "—"}
        </div>
      </div>

      {/* THE HERO. Twenty-one years of buying, and the date filter, are the
          same object: the strip shows where the active range sits in the whole
          run, and a bar is how you reach 2016. A spend total would be the
          template answer here and would also be a lie — 16% of orders have no
          amount. */}
      <div className="border-y border-zinc-800 py-3.5 mb-5">
        <div className="flex items-baseline justify-between mb-3">
          <span className="font-mono text-[10.5px] uppercase tracking-[0.12em] text-zinc-500">
            Ordered — {range.label}
          </span>
          <div className="flex gap-1 flex-wrap">
            {RANGES.map((r) => (
              <button
                key={r.id}
                onClick={() => set(setRangeKey)(r.id)}
                aria-pressed={rangeKey === r.id}
                className={`font-mono text-[10.5px] tracking-wide px-2.5 py-1 rounded-sm border ${
                  rangeKey === r.id
                    ? "bg-indigo-600 border-indigo-600 text-zinc-950"
                    : "border-zinc-800 text-zinc-400 hover:border-zinc-600 hover:text-zinc-100"
                }`}
              >{r.label}</button>
            ))}
          </div>
        </div>
        <YearStrip
          years={data?.facets.years ?? []}
          activeFrom={range.from}
          activeTo={range.to}
          onPick={(y) => set(setRangeKey)(String(y) as RangeKey)}
        />
      </div>

      <div className="flex gap-0.5 flex-wrap mb-3" role="tablist" aria-label="Order lanes">
        {LANES.map((l) => (
          <button
            key={l.id}
            role="tab"
            aria-selected={lane === l.id}
            onClick={() => set(setLane)(l.id)}
            className={`flex items-baseline gap-2 px-3 py-1.5 text-[13px] border-b-2 ${
              lane === l.id
                ? "text-zinc-50 border-indigo-500"
                : "text-zinc-400 border-transparent hover:text-zinc-100"
            }`}
          >
            {l.label}
            <span className={`font-mono text-[11px] tabular-nums ${lane === l.id ? "text-indigo-400" : "text-zinc-500"}`}>
              {laneCount(l.id).toLocaleString()}
            </span>
          </button>
        ))}
      </div>

      <div className="flex gap-2.5 items-center flex-wrap py-3 border-b border-zinc-800">
        <input
          type="search"
          value={search}
          onChange={(e) => set(setSearch)(e.target.value)}
          placeholder="Search items, merchants, order references…"
          aria-label="Search orders"
          className="flex-1 min-w-[200px] bg-zinc-900 border border-zinc-800 rounded-sm px-3 py-1.5 text-[13px] text-zinc-100 placeholder:text-zinc-500 focus:border-indigo-600 focus:outline-none"
        />
        <select
          value={platform}
          onChange={(e) => set(setPlatform)(e.target.value)}
          aria-label="Platform"
          className="bg-zinc-900 border border-zinc-800 rounded-sm px-2 py-1.5 text-xs text-zinc-300"
        >
          <option value="">All platforms</option>
          {data?.facets.platforms.map((p) => (
            <option key={p.platform} value={p.platform}>{p.platform} ({p.n})</option>
          ))}
        </select>
        <select
          value={status}
          onChange={(e) => set(setStatus)(e.target.value)}
          aria-label="Status"
          className="bg-zinc-900 border border-zinc-800 rounded-sm px-2 py-1.5 text-xs text-zinc-300"
        >
          <option value="">Any status</option>
          {data?.facets.statuses.map((s) => (
            <option key={s.status} value={s.status}>{s.status.replace(/_/g, " ")} ({s.n})</option>
          ))}
        </select>
        <label className="flex items-center gap-2 text-xs text-zinc-400 cursor-pointer select-none"
               title="Rows with no amount, no order reference and one lifecycle event are not purchases — they are a second entity minted from a mail describing an order that already exists. 620 remain: the review-request mails have been retired and the ones whose parent could be identified have been joined to it, so what is left is mail whose parent is not in the corpus.">
          <input
            type="checkbox"
            checked={showLifecycle}
            onChange={(e) => set(setShowLifecycle)(e.target.checked)}
            className="accent-indigo-500 cursor-pointer"
          />
          Show lifecycle-only records
        </label>
      </div>

      <div className="overflow-x-auto">
        <table className="w-full border-collapse min-w-[720px]">
          <thead>
            <tr className="border-b border-zinc-800">
              {["Ordered", "Merchant and contents", "Status", "Arrived", "Amount"].map((h, i) => (
                <th key={h}
                    className={`p-3 pb-2 font-mono text-[10px] uppercase tracking-widest text-zinc-500 font-normal whitespace-nowrap ${i === 4 ? "text-right" : "text-left"}`}>
                  {h}
                </th>
              ))}
            </tr>
          </thead>
          <tbody>
            {isLoading && (
              <tr><td colSpan={5} className="p-10 text-center text-zinc-500 text-[13px]">Loading orders…</td></tr>
            )}
            {error && (
              <tr><td colSpan={5} className="p-10 text-center text-zinc-400 text-[13px]">
                Could not load orders. The spine views may not be migrated yet — apply migration 018.
              </td></tr>
            )}
            {data?.data.length === 0 && (
              <tr><td colSpan={5} className="p-10 text-zinc-500 text-[13px]">
                No orders in {range.label} match these filters. Widen the date range or clear the search.
              </td></tr>
            )}
            {data?.data.map((r) => (
              <Row
                key={r.entity_key}
                row={r}
                expanded={expanded.has(r.entity_key)}
                onToggle={() =>
                  setExpanded((prev) => {
                    const next = new Set(prev);
                    if (next.has(r.entity_key)) next.delete(r.entity_key);
                    else next.add(r.entity_key);
                    return next;
                  })
                }
              />
            ))}
          </tbody>
        </table>
      </div>

      {(data?.total ?? 0) > limit && (
        <div className="flex items-center justify-between gap-4 pt-4 font-mono text-[11.5px] text-zinc-400 tabular-nums">
          <button
            onClick={() => setOffset(Math.max(0, offset - limit))}
            disabled={offset === 0}
            className="px-3 py-1 border border-zinc-800 rounded-sm disabled:opacity-40 hover:border-zinc-600"
          >← Previous</button>
          <span>Page {page} of {pages}</span>
          <button
            onClick={() => setOffset(offset + limit)}
            disabled={page >= pages}
            className="px-3 py-1 border border-zinc-800 rounded-sm disabled:opacity-40 hover:border-zinc-600"
          >Next →</button>
        </div>
      )}
    </div>
  );
}

export default function OrdersPage() {
  return (
    <Suspense fallback={<div className="p-6 text-zinc-500 text-sm">Loading…</div>}>
      <OrdersContent />
    </Suspense>
  );
}
