"use client";

import { useState, useCallback, useRef, useEffect, Suspense, Fragment } from "react";
import { useSearchParams } from "next/navigation";
import { useTransactions, useBanks, useUpdateTransaction, useBulkAction, useTags, useStatement, useCreateRule, useParticipants, useRecordPayment, useCurrentUser, useTrips, useAssignTransactionsToTrip, useRules } from "@/lib/hooks";
import { CATEGORIES, formatCategory } from "@/lib/categories";
import { SplitModal } from "@/components/split-modal";
import { TagPicker } from "@/components/tag-picker";
import { AddTransactionModal } from "@/components/add-transaction-modal";
import { EditTransactionModal } from "@/components/edit-transaction-modal";
import { CsvImportModal } from "@/components/csv-import-modal";
import type { TransactionRow, RoutePointRow } from "@/lib/queries";
import { OrderDetails } from "@/components/order-details";
import type { RuleRow } from "@/lib/hooks";

function formatDate(d: string) {
  return new Date(d).toLocaleDateString("en-AU", {
    day: "2-digit",
    month: "short",
    year: "numeric",
  });
}

const SPEND_TYPES = new Set(["debit", "fee", "interest"]);

// `amount` is in the statement's native currency; pass the currency to label it
// correctly. Callers showing a headline figure should pass amount_aud, which is
// what every analytics query totals.
function formatAmount(amount: number, type: string, currency = "AUD") {
  const formatted = new Intl.NumberFormat("en-AU", {
    style: "currency",
    currency,
  }).format(amount);
  return SPEND_TYPES.has(type) ? formatted : `+${formatted}`;
}

const TYPE_COLORS: Record<string, string> = {
  debit: "bg-red-900/30 text-red-400",
  credit: "bg-green-900/30 text-green-400",
  payment: "bg-blue-900/30 text-blue-400",
  refund: "bg-emerald-900/30 text-emerald-400",
  fee: "bg-yellow-900/30 text-yellow-400",
  interest: "bg-orange-900/30 text-orange-400",
  transfer: "bg-zinc-800 text-zinc-400",
};

const TYPE_OPTIONS = [
  "debit", "credit", "payment", "refund", "fee", "interest", "transfer",
].map((t) => ({ value: t, label: t }));

/** Tooltip text for a quick-action button: what the rule will actually do. */
function describeActions(
  actions: RuleRow["actions"],
  tags: { id: number; name: string }[] = [],
  participants: { id: number; name: string }[] = []
): string {
  const parts: string[] = [];
  if (actions.set_category) parts.push(`category → ${formatCategory(actions.set_category)}`);
  if (actions.set_merchant) parts.push(`merchant → ${actions.set_merchant}`);
  if (actions.add_tag_ids?.length) {
    const names = actions.add_tag_ids.map((id) => tags.find((t) => t.id === id)?.name ?? `tag#${id}`);
    parts.push(`tag: ${names.join(", ")}`);
  }
  if (actions.apply_split?.length) {
    const shares = actions.apply_split.map(
      (s) => `${participants.find((p) => p.id === s.participant_id)?.name ?? `#${s.participant_id}`} ${s.share_percent}%`
    );
    parts.push(`split: ${shares.join(" / ")}`);
  }
  return parts.join(" · ") || "no actions";
}

function TypeBadge({ type }: { type: string }) {
  return (
    <span className={`px-2 py-0.5 rounded text-xs font-medium ${TYPE_COLORS[type] || "bg-zinc-800 text-zinc-400"}`}>
      {type}
    </span>
  );
}

function EditableTypeBadge({ type, onSave }: { type: string; onSave: (t: string) => void }) {
  const [editing, setEditing] = useState(false);
  if (!editing) {
    return (
      <button onClick={() => setEditing(true)} title="Click to change type">
        <TypeBadge type={type} />
      </button>
    );
  }
  return (
    <select
      autoFocus
      defaultValue={type}
      onBlur={(e) => { onSave(e.target.value); setEditing(false); }}
      onChange={(e) => { onSave(e.target.value); setEditing(false); }}
      className="bg-zinc-800 border border-zinc-600 rounded px-1 py-0.5 text-xs"
    >
      {TYPE_OPTIONS.map((o) => (
        <option key={o.value} value={o.value}>{o.label}</option>
      ))}
    </select>
  );
}

// Prompt shown after a merchant/category edit — offers to save it as a rule
function SaveAsRulePrompt({
  tx,
  field,
  newValue,
  onDone,
}: {
  tx: { id: number; effective_merchant: string; description: string; bank_name: string };
  field: "category" | "merchant";
  newValue: string;
  onDone: () => void;
}) {
  const createRule = useCreateRule();
  const [saving, setSaving] = useState(false);

  // Build a sensible default rule from the transaction context.
  // Prefer merchant_normalized (full name, exact match) over a partial description word.
  const hasMerchant = !!tx.effective_merchant;
  const conditionField = hasMerchant ? "merchant_normalized" : "description";
  const conditionOperator = hasMerchant ? "equals" : "contains";
  const conditionValue = hasMerchant ? tx.effective_merchant : tx.description;

  const defaultName =
    field === "category"
      ? `${conditionValue} → ${formatCategory(newValue)}`
      : `Rename ${conditionValue} → ${newValue}`;

  const conditions = [{ field: conditionField, operator: conditionOperator, value: conditionValue }];
  const actions =
    field === "category"
      ? { set_category: newValue }
      : { set_merchant: newValue };

  async function save() {
    setSaving(true);
    await createRule.mutateAsync({ name: defaultName, conditions, actions, enabled: true, priority: 0 });
    onDone();
  }

  return (
    <div className="fixed bottom-4 right-4 z-50 bg-zinc-800 border border-zinc-600 rounded-xl shadow-2xl p-4 w-80 text-sm">
      <p className="text-zinc-200 font-medium mb-1">Save as rule?</p>
      <p className="text-zinc-400 text-xs mb-3">
        Automatically apply this {field === "category" ? "category" : "merchant name"} to future matching transactions.
      </p>
      <div className="bg-zinc-900 rounded-lg px-3 py-2 text-xs text-zinc-300 mb-3 space-y-1">
        <p><span className="text-zinc-500">If</span> {conditionField === "merchant_normalized" ? "merchant" : "description"} {conditionOperator} <span className="text-white">"{conditionValue}"</span></p>
        <p>
          <span className="text-zinc-500">Then</span>{" "}
          {field === "category"
            ? <>set category → <span className="text-white">{formatCategory(newValue)}</span></>
            : <>set merchant → <span className="text-white">{newValue}</span></>}
        </p>
      </div>
      <div className="flex gap-2">
        <button
          onClick={save}
          disabled={saving}
          className="flex-1 bg-blue-600 hover:bg-blue-500 disabled:opacity-50 text-white rounded-lg py-1.5 font-medium transition-colors"
        >
          {saving ? "Saving…" : "Save rule"}
        </button>
        <button
          onClick={onDone}
          className="flex-1 bg-zinc-700 hover:bg-zinc-600 text-zinc-200 rounded-lg py-1.5 transition-colors"
        >
          Dismiss
        </button>
      </div>
    </div>
  );
}

function InlineEdit({
  value,
  onSave,
  type = "text",
  options,
}: {
  value: string;
  onSave: (val: string) => void;
  type?: "text" | "select";
  options?: { value: string; label: string }[];
}) {
  const [editing, setEditing] = useState(false);
  const [draft, setDraft] = useState(value);

  if (!editing) {
    return (
      <button
        onClick={() => { setDraft(value); setEditing(true); }}
        className="text-left hover:bg-zinc-800 px-1 -mx-1 rounded cursor-pointer transition-colors w-full truncate"
        title="Click to edit"
      >
        {value || <span className="text-zinc-600 italic">unset</span>}
      </button>
    );
  }

  const commit = () => {
    if (draft !== value) onSave(draft);
    setEditing(false);
  };

  if (type === "select" && options) {
    return (
      <select
        autoFocus
        value={draft}
        onChange={(e) => { setDraft(e.target.value); }}
        onBlur={commit}
        className="bg-zinc-800 border border-zinc-600 rounded px-1 py-0.5 text-sm w-full"
      >
        {options.map((o) => (
          <option key={o.value} value={o.value}>{o.label}</option>
        ))}
      </select>
    );
  }

  return (
    <input
      autoFocus
      value={draft}
      onChange={(e) => setDraft(e.target.value)}
      onBlur={commit}
      onKeyDown={(e) => { if (e.key === "Enter") commit(); if (e.key === "Escape") setEditing(false); }}
      className="bg-zinc-800 border border-zinc-600 rounded px-1 py-0.5 text-sm w-full"
    />
  );
}

// ── Mark as Payment modal ─────────────────────────────────────────────────────
function MarkAsPaymentModal({
  transaction,
  onClose,
}: {
  transaction: TransactionRow;
  onClose: () => void;
}) {
  const { data: participants = [] } = useParticipants();
  const { data: me } = useCurrentUser();
  const record = useRecordPayment();

  const others = participants.filter((p) => p.id !== me?.id);

  const [participantId, setParticipantId] = useState<number | "">(others[0]?.id ?? "");

  useEffect(() => {
    if (participantId === "" && others.length > 0) {
      setParticipantId(others[0].id);
    }
  }, [others]);
  // For credits/refunds the default direction is "they paid me"
  const [direction, setDirection] = useState<"received" | "sent">(
    SPEND_TYPES.has(transaction.transaction_type) ? "sent" : "received"
  );
  const [amount, setAmount] = useState(Number(transaction.amount).toFixed(2));
  const [date, setDate] = useState(transaction.transaction_date.slice(0, 10));
  const [notes, setNotes] = useState("");
  const [error, setError] = useState("");

  const selectedParticipant = others.find((p) => p.id === participantId);

  async function handleSave() {
    setError("");
    if (!participantId || !me) { setError("Select a participant"); return; }
    const amt = parseFloat(amount);
    if (!amt || amt <= 0) { setError("Enter a valid amount"); return; }
    try {
      await record.mutateAsync({
        from_participant_id: direction === "received" ? participantId : me.id,
        to_participant_id: direction === "received" ? me.id : participantId,
        amount: amt,
        payment_date: date,
        notes: notes || undefined,
        linked_transaction_id: transaction.id,
      });
      onClose();
    } catch (e) {
      setError(e instanceof Error ? e.message : "Failed to record");
    }
  }

  return (
    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60" onClick={onClose}>
      <div
        className="bg-zinc-900 border border-zinc-700 rounded-xl w-full max-w-sm mx-4 shadow-2xl p-6 space-y-4"
        onClick={(e) => e.stopPropagation()}
      >
        <div>
          <h3 className="font-semibold text-sm text-zinc-300">Record as Debt Payment</h3>
          <p className="text-xs text-zinc-500 mt-0.5 truncate">{transaction.description}</p>
        </div>

        {/* Participant */}
        <div>
          <label className="block text-xs text-zinc-500 mb-1">Participant</label>
          <select
            value={participantId}
            onChange={(e) => setParticipantId(Number(e.target.value))}
            className="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1.5 text-sm"
          >
            {others.map((p) => (
              <option key={p.id} value={p.id}>{p.name}</option>
            ))}
          </select>
        </div>

        {/* Direction */}
        <div className="flex rounded-lg overflow-hidden border border-zinc-700 text-sm">
          <button
            type="button"
            onClick={() => setDirection("received")}
            className={`flex-1 py-1.5 transition-colors ${direction === "received" ? "bg-emerald-700 text-white" : "bg-zinc-800 text-zinc-400 hover:bg-zinc-700"}`}
          >
            {selectedParticipant?.name ?? "They"} paid me
          </button>
          <button
            type="button"
            onClick={() => setDirection("sent")}
            className={`flex-1 py-1.5 transition-colors ${direction === "sent" ? "bg-blue-700 text-white" : "bg-zinc-800 text-zinc-400 hover:bg-zinc-700"}`}
          >
            I paid {selectedParticipant?.name ?? "them"}
          </button>
        </div>

        <div className="grid grid-cols-2 gap-3">
          <div>
            <label className="block text-xs text-zinc-500 mb-1">Amount</label>
            <div className="relative">
              <span className="absolute left-2.5 top-1/2 -translate-y-1/2 text-zinc-500 text-sm">$</span>
              <input
                type="number" step="0.01" min="0.01" value={amount}
                onChange={(e) => setAmount(e.target.value)}
                className="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1.5 text-sm pl-6"
              />
            </div>
          </div>
          <div>
            <label className="block text-xs text-zinc-500 mb-1">Date</label>
            <input
              type="date" value={date} onChange={(e) => setDate(e.target.value)}
              className="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1.5 text-sm"
            />
          </div>
        </div>

        <div>
          <label className="block text-xs text-zinc-500 mb-1">Notes (optional)</label>
          <input
            value={notes} onChange={(e) => setNotes(e.target.value)}
            placeholder="e.g. bank transfer reference"
            className="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1.5 text-sm"
          />
        </div>

        {error && <p className="text-red-400 text-xs">{error}</p>}

        <div className="flex gap-2">
          <button type="button" onClick={onClose}
            className="flex-1 px-4 py-2 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 rounded-lg text-sm">
            Cancel
          </button>
          <button type="button" onClick={handleSave} disabled={record.isPending}
            className="flex-1 px-4 py-2 bg-indigo-600 hover:bg-indigo-500 disabled:opacity-50 text-white rounded-lg text-sm font-medium">
            {record.isPending ? "Saving…" : "Record Payment"}
          </button>
        </div>
      </div>
    </div>
  );
}

// ── Query bar parser ──────────────────────────────────────────────────────────
interface QueryToken { key: string; label: string }
interface ParsedQuery { text: string; amountMin?: number; amountMax?: number; tokens: QueryToken[] }

function parseQuery(input: string): ParsedQuery {
  let text = input;
  let amountMin: number | undefined;
  let amountMax: number | undefined;
  const tokens: QueryToken[] = [];

  // Range shorthand: 500-1500
  text = text.replace(/\b(\d+(?:\.\d+)?)\s*-\s*(\d+(?:\.\d+)?)\b/g, (_, a, b) => {
    amountMin = parseFloat(a);
    amountMax = parseFloat(b);
    tokens.push({ key: "range", label: `$${a}–$${b}` });
    return "";
  });

  // Operators: >=500 <=500 >500 <500
  text = text.replace(/(>=|<=|>|<)\s*(\d+(?:\.\d+)?)/g, (_, op, num) => {
    const val = parseFloat(num);
    if (op === ">") amountMin = val + 0.005;
    else if (op === ">=") amountMin = val;
    else if (op === "<") amountMax = Math.max(0, val - 0.005);
    else if (op === "<=") amountMax = val;
    const display = op === ">=" ? "≥" : op === "<=" ? "≤" : op;
    tokens.push({ key: `amt_${op}`, label: `${display} $${num}` });
    return "";
  });

  return { text: text.trim(), amountMin, amountMax, tokens };
}

// ── MultiSelect dropdown ──────────────────────────────────────────────────────
function MultiSelect({
  options,
  value,
  onChange,
  placeholder,
}: {
  options: { value: string; label: string }[];
  value: string[];
  onChange: (v: string[]) => void;
  placeholder: string;
}) {
  const [open, setOpen] = useState(false);
  const ref = useRef<HTMLDivElement>(null);

  useEffect(() => {
    function handler(e: MouseEvent) {
      if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
    }
    document.addEventListener("mousedown", handler);
    return () => document.removeEventListener("mousedown", handler);
  }, []);

  const toggle = (v: string) =>
    onChange(value.includes(v) ? value.filter((x) => x !== v) : [...value, v]);

  const label =
    value.length === 0
      ? placeholder
      : value.length === 1
      ? (options.find((o) => o.value === value[0])?.label ?? value[0])
      : `${value.length} selected`;

  return (
    <div ref={ref} className="relative">
      <button
        type="button"
        onClick={() => setOpen((v) => !v)}
        className={`bg-zinc-900 border rounded px-3 py-1.5 text-sm flex items-center gap-2 min-w-[140px] whitespace-nowrap ${
          value.length > 0 ? "border-indigo-500 text-white" : "border-zinc-700 text-zinc-400"
        }`}
      >
        <span className="flex-1 text-left truncate">{label}</span>
        <span className="text-zinc-500 text-xs">▾</span>
      </button>
      {open && (
        <div className="absolute top-full mt-1 z-20 bg-zinc-900 border border-zinc-700 rounded-lg shadow-xl min-w-[160px] max-h-64 overflow-y-auto">
          {options.map((o) => (
            <label
              key={o.value}
              className="flex items-center gap-2 px-3 py-1.5 hover:bg-zinc-800 cursor-pointer text-sm"
            >
              <input
                type="checkbox"
                checked={value.includes(o.value)}
                onChange={() => toggle(o.value)}
                className="accent-indigo-500 flex-shrink-0"
              />
              {o.label}
            </label>
          ))}
        </div>
      )}
    </div>
  );
}

/**
 * "Melbourne Airport (MEL) → Wyndham Vale VIC 3024" from the two stops on an
 * Uber *trip* receipt. Deliveries are excluded by the caller: their merchant
 * already identifies them, so the restaurant's street address would be clutter
 * on every food order.
 *
 * Keeps the first two comma-segments of each address — a truncation, not a
 * guess about geography. The venue or street comes first in Uber's format and
 * is the identifying part; the full text stays in the title attribute.
 */
function routeSummary(route: RoutePointRow[] | null | undefined): string | null {
  if (!route || route.length < 2) return null;
  const short = (a: string) => a.split(",").slice(0, 2).join(",").trim();
  const from = short(route[0].address);
  const to = short(route[route.length - 1].address);
  if (!from || !to) return null;
  return `${from} → ${to}`;
}

export default function TransactionsPage() {
  return (
    <Suspense fallback={<p className="text-zinc-500 text-sm">Loading...</p>}>
      <TransactionsContent />
    </Suspense>
  );
}

function TransactionsContent() {
  const searchParams = useSearchParams();
  const initialStatementId = searchParams.get("statement_id") || "";

  // `?q=` lands the view on a specific row. The Slack order nudge links here,
  // and without it the link drops you at the top of an unfiltered ledger and
  // the merchant has to be found by hand — which is how a nudge stops getting
  // opened.
  const initialQuery = searchParams.get("q") || "";
  const initialParsed = parseQuery(initialQuery);

  const [filters, setFilters] = useState({
    from: "",
    to: "",
    categories: [] as string[],
    // Transfers move money between your own accounts — they are not spending,
    // and at ~380 rows they crowd out everything that is. Hidden by default,
    // with a visible toggle: a filter you cannot see is one you forget is on.
    //
    // Off when the view is scoped to a statement. That is a reconciliation
    // view — the row count has to match the statement, and a credit-card
    // payment is exactly the row you are there to check.
    exclude_categories: (initialStatementId ? [] : ["transfers"]) as string[],
    bank_names: [] as string[],
    tag_ids: [] as string[],
    transaction_types: [] as string[],
    search: initialParsed.text,
    statement_id: initialStatementId,
    sort_by: "transaction_date",
    sort_dir: "desc",
    limit: 50,
    offset: 0,
    amount_min: undefined as number | undefined,
    amount_max: undefined as number | undefined,
    has_split: "" as string,
    trip_id: "" as string,
  });
  const [queryInput, setQueryInput] = useState(initialQuery);
  const [queryTokens, setQueryTokens] = useState<QueryToken[]>(initialParsed.tokens);

  function handleQueryChange(val: string) {
    setQueryInput(val);
    const parsed = parseQuery(val);
    setQueryTokens(parsed.tokens);
    setFilters((f) => ({
      ...f,
      search: parsed.text,
      amount_min: parsed.amountMin,
      amount_max: parsed.amountMax,
      offset: 0,
    }));
  }

  function clearQueryToken(key: string) {
    // Rebuild input without the token's contribution by re-running parse on cleared input
    // Simplest: just clear the whole query bar
    const next = queryInput
      .replace(/(>=|<=|>|<)\s*\d+(?:\.\d+)?/g, "")
      .replace(/\b\d+(?:\.\d+)?\s*-\s*\d+(?:\.\d+)?\b/g, "")
      .trim();
    handleQueryChange(next);
  }
  const [selected, setSelected] = useState<Set<number>>(new Set());
  const [bulkCategory, setBulkCategory] = useState("");
  const [bulkTagId, setBulkTagId] = useState("");
  const [bulkTripId, setBulkTripId] = useState("");
  const [splitModal, setSplitModal] = useState<{ transactionId?: number; transactionIds?: number[]; amount?: number; description: string; merchant?: string } | null>(null);
  const [addModal, setAddModal] = useState<{ prefill?: Parameters<typeof AddTransactionModal>[0]["prefill"]; title?: string } | null>(null);
  const [editModal, setEditModal] = useState<TransactionRow | null>(null);
  // Rows expanded to show the ingested receipt. A set, not a single id: the
  // point is comparing several orders without losing your place.
  const [expanded, setExpanded] = useState<Set<number>>(new Set());
  const [showImportModal, setShowImportModal] = useState(false);
  const [paymentModal, setPaymentModal] = useState<TransactionRow | null>(null);
  const [rulePrompt, setRulePrompt] = useState<{
    tx: { id: number; effective_merchant: string; description: string; bank_name: string };
    field: "category" | "merchant";
    newValue: string;
  } | null>(null);

  const { data, isLoading } = useTransactions(filters);
  const { data: banks } = useBanks();
  const { data: tags } = useTags();
  const { data: me } = useCurrentUser();
  const { data: statementInfo } = useStatement(parseInt(filters.statement_id) || 0);
  const updateTxn = useUpdateTransaction();
  const bulkAction = useBulkAction();
  const { data: trips = [] } = useTrips();
  const assignToTrip = useAssignTransactionsToTrip();
  const { data: allRules = [] } = useRules();
  const { data: participants = [] } = useParticipants();
  const quickActions = allRules.filter((r) => r.manual_only);
  const [quickResult, setQuickResult] = useState<string | null>(null);

  const toggleSelect = useCallback((id: number) => {
    setSelected((prev) => {
      const next = new Set(prev);
      if (next.has(id)) next.delete(id);
      else next.add(id);
      return next;
    });
  }, []);

  const toggleAll = useCallback(() => {
    if (!data?.data) return;
    setSelected((prev) => {
      if (prev.size === data.data.length) return new Set();
      return new Set(data.data.map((t) => t.id));
    });
  }, [data]);

  const setPage = (newOffset: number) => {
    setFilters((f) => ({ ...f, offset: newOffset }));
    setSelected(new Set());
  };

  const toggleSort = (col: string) => {
    setFilters((f) => ({
      ...f,
      sort_by: col,
      sort_dir: f.sort_by === col && f.sort_dir === "desc" ? "asc" : "desc",
      offset: 0,
    }));
  };

  const categoryOptions = CATEGORIES.map((c) => ({ value: c, label: formatCategory(c) }));

  const totalPages = data ? Math.ceil(data.total / filters.limit) : 0;
  const currentPage = Math.floor(filters.offset / filters.limit) + 1;

  return (
    <div>
      <div className="flex items-center justify-between mb-4">
        <h2 className="text-2xl font-display">Transactions</h2>
        <div className="flex gap-2">
          <button
            onClick={() => setShowImportModal(true)}
            className="px-4 py-2 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 rounded-lg text-sm font-medium"
          >
            Import CSV
          </button>
          <button
            onClick={() => setAddModal({})}
            className="px-4 py-2 bg-indigo-600 hover:bg-indigo-500 text-white rounded-lg text-sm font-medium"
          >
            + Add Transaction
          </button>
        </div>
      </div>

      {/* Statement context banner */}
      {filters.statement_id && statementInfo && (
        <div className="flex items-center gap-3 mb-4 px-3 py-2 bg-indigo-950/40 border border-indigo-800/50 rounded-lg text-sm">
          <span className="text-indigo-300 font-medium">{statementInfo.bank_name}</span>
          {statementInfo.billing_start_date && statementInfo.billing_end_date && (
            <span className="text-zinc-400">
              {new Date(statementInfo.billing_start_date).toLocaleDateString("en-AU", { day: "2-digit", month: "short" })}
              {" – "}
              {new Date(statementInfo.billing_end_date).toLocaleDateString("en-AU", { day: "2-digit", month: "short", year: "numeric" })}
            </span>
          )}
          <span className="text-zinc-500 text-xs">{statementInfo.transaction_count} transactions</span>
          <button
            onClick={() => setFilters((f) => ({ ...f, statement_id: "", offset: 0 }))}
            className="ml-auto text-zinc-500 hover:text-zinc-200 text-xs px-2 py-0.5 rounded hover:bg-zinc-800 transition-colors"
          >
            × Clear filter
          </button>
        </div>
      )}

      {/* Filter bar */}
      <div className="flex flex-wrap gap-3 mb-2">
        {/* Smart query bar */}
        <div className="flex flex-col gap-1">
          <input
            type="text"
            value={queryInput}
            onChange={(e) => handleQueryChange(e.target.value)}
            placeholder="Search… or >500  <=1500  200-800"
            className="bg-zinc-900 border border-zinc-700 rounded px-3 py-1.5 text-sm w-full sm:w-64 font-mono placeholder:font-sans placeholder:text-zinc-600"
          />
          {queryTokens.length > 0 && (
            <div className="flex flex-wrap gap-1">
              {queryTokens.map((tok) => (
                <span
                  key={tok.key}
                  className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs bg-indigo-900/50 border border-indigo-700/50 text-indigo-300"
                >
                  {tok.label}
                  <button
                    type="button"
                    onClick={() => clearQueryToken(tok.key)}
                    className="text-indigo-400 hover:text-white leading-none"
                  >
                    ×
                  </button>
                </span>
              ))}
            </div>
          )}
        </div>

        {/* Date range */}
        <input
          type="date"
          value={filters.from}
          onChange={(e) => setFilters((f) => ({ ...f, from: e.target.value, offset: 0 }))}
          className="bg-zinc-900 border border-zinc-700 rounded px-3 py-1.5 text-sm"
        />
        <input
          type="date"
          value={filters.to}
          onChange={(e) => setFilters((f) => ({ ...f, to: e.target.value, offset: 0 }))}
          className="bg-zinc-900 border border-zinc-700 rounded px-3 py-1.5 text-sm"
        />

        {/* Multi-select dropdowns */}
        <MultiSelect
          options={CATEGORIES.map((c) => ({ value: c, label: formatCategory(c) }))}
          value={filters.categories}
          onChange={(v) =>
            setFilters((f) => ({
              ...f,
              categories: v,
              // Asking for a category you are also hiding is a contradiction the
              // server resolves in favour of the explicit pick; drop it here too
              // so the toggle does not claim to be hiding what is on screen.
              exclude_categories: f.exclude_categories.filter((c) => !v.includes(c)),
              offset: 0,
            }))
          }
          placeholder="All Categories"
        />
        <label className="flex items-center gap-1.5 px-3 py-1.5 bg-zinc-900 border border-zinc-700 rounded text-sm text-zinc-300 cursor-pointer select-none">
          <input
            type="checkbox"
            checked={filters.exclude_categories.includes("transfers")}
            onChange={(e) =>
              setFilters((f) => ({
                ...f,
                exclude_categories: e.target.checked
                  ? [...f.exclude_categories, "transfers"]
                  : f.exclude_categories.filter((c) => c !== "transfers"),
                categories: e.target.checked
                  ? f.categories.filter((c) => c !== "transfers")
                  : f.categories,
                offset: 0,
              }))
            }
            className="accent-indigo-500"
          />
          Hide transfers
        </label>
        <MultiSelect
          options={(banks ?? []).map((b) => ({ value: b, label: b }))}
          value={filters.bank_names}
          onChange={(v) => setFilters((f) => ({ ...f, bank_names: v, offset: 0 }))}
          placeholder="All Banks"
        />
        <MultiSelect
          options={[{ value: "untagged", label: "No tags" }, ...(tags ?? []).map((t) => ({ value: String(t.id), label: t.name }))]}
          value={filters.tag_ids}
          onChange={(v) => {
            let next = v;
            if (v.includes("untagged") && !filters.tag_ids.includes("untagged")) next = ["untagged"];
            else if (filters.tag_ids.includes("untagged") && v.length > 1) next = v.filter((x) => x !== "untagged");
            setFilters((f) => ({ ...f, tag_ids: next, offset: 0 }));
          }}
          placeholder="All Tags"
        />
        <MultiSelect
          options={TYPE_OPTIONS}
          value={filters.transaction_types}
          onChange={(v) => setFilters((f) => ({ ...f, transaction_types: v, offset: 0 }))}
          placeholder="All Types"
        />
        <select
          value={filters.has_split}
          onChange={(e) => setFilters((f) => ({ ...f, has_split: e.target.value, offset: 0 }))}
          className="bg-zinc-900 border border-zinc-700 rounded px-3 py-1.5 text-sm text-zinc-300"
        >
          <option value="">All Splits</option>
          <option value="yes">Split only</option>
          <option value="no">Unsplit only</option>
        </select>
        <select
          value={filters.trip_id}
          onChange={(e) => setFilters((f) => ({ ...f, trip_id: e.target.value, offset: 0 }))}
          className="bg-zinc-900 border border-zinc-700 rounded px-3 py-1.5 text-sm text-zinc-300"
        >
          <option value="">All Trips</option>
          <option value="unassigned">No Trip</option>
          {trips.map((t) => (
            <option key={t.id} value={String(t.id)}>{t.name}</option>
          ))}
        </select>
      </div>

      {/* Bulk action bar */}
      {selected.size > 0 && (
        <div className="flex items-center gap-3 mb-3 p-2 bg-zinc-900 border border-zinc-700 rounded">
          <span className="text-sm text-zinc-400">{selected.size} selected</span>
          <select
            value={bulkCategory}
            onChange={(e) => setBulkCategory(e.target.value)}
            className="bg-zinc-800 border border-zinc-600 rounded px-2 py-1 text-sm"
          >
            <option value="">Set category...</option>
            {CATEGORIES.map((c) => (
              <option key={c} value={c}>{formatCategory(c)}</option>
            ))}
          </select>
          <button
            disabled={!bulkCategory || bulkAction.isPending}
            onClick={() => {
              bulkAction.mutate(
                { action: "categorize", ids: Array.from(selected), category: bulkCategory },
                { onSuccess: () => { setSelected(new Set()); setBulkCategory(""); } }
              );
            }}
            className="px-3 py-1 bg-blue-600 hover:bg-blue-700 disabled:opacity-50 rounded text-sm"
          >
            Apply
          </button>
          <button
            onClick={() =>
              setSplitModal({
                transactionIds: Array.from(selected),
                description: `${selected.size} selected transaction${selected.size !== 1 ? "s" : ""}`,
              })
            }
            className="px-3 py-1 bg-zinc-700 hover:bg-zinc-600 rounded text-sm"
          >
            Split...
          </button>
          <select
            value={bulkTagId}
            onChange={(e) => setBulkTagId(e.target.value)}
            className="bg-zinc-800 border border-zinc-600 rounded px-2 py-1 text-sm"
          >
            <option value="">Tag as...</option>
            {tags?.map((t) => (
              <option key={t.id} value={t.id}>{t.name}</option>
            ))}
          </select>
          <button
            disabled={!bulkTagId || bulkAction.isPending}
            onClick={() => {
              bulkAction.mutate(
                { action: "tag", ids: Array.from(selected), tag_id: Number(bulkTagId) },
                { onSuccess: () => { setSelected(new Set()); setBulkTagId(""); } }
              );
            }}
            className="px-3 py-1 bg-indigo-600 hover:bg-indigo-700 disabled:opacity-50 rounded text-sm"
          >
            Tag
          </button>
          <select
            value={bulkTripId}
            onChange={(e) => setBulkTripId(e.target.value)}
            className="bg-zinc-800 border border-zinc-600 rounded px-2 py-1 text-sm"
          >
            <option value="">Assign trip…</option>
            <option value="remove">Remove from trip</option>
            {trips.filter((t) => !t.archived).map((t) => (
              <option key={t.id} value={String(t.id)}>{t.name}</option>
            ))}
          </select>
          <button
            disabled={!bulkTripId || assignToTrip.isPending}
            onClick={() => {
              const tripId = bulkTripId === "remove" ? null : Number(bulkTripId);
              assignToTrip.mutate(
                { tripId, transactionIds: Array.from(selected) },
                { onSuccess: () => { setSelected(new Set()); setBulkTripId(""); } }
              );
            }}
            className="px-3 py-1 bg-emerald-700 hover:bg-emerald-600 disabled:opacity-50 rounded text-sm"
          >
            {bulkTripId === "remove" ? "Remove" : "Assign"}
          </button>
          {quickActions.length > 0 && (
            <div className="flex items-center gap-2 pl-3 ml-1 border-l border-zinc-700">
              {quickActions.map((rule) => (
                <button
                  key={rule.id}
                  disabled={bulkAction.isPending}
                  title={describeActions(rule.actions, tags, participants)}
                  onClick={() => {
                    const count = selected.size;
                    bulkAction.mutate(
                      { action: "apply_rule", ids: Array.from(selected), rule_id: rule.id },
                      {
                        onSuccess: () => {
                          setSelected(new Set());
                          setQuickResult(`${rule.name} applied to ${count} transaction${count !== 1 ? "s" : ""}`);
                        },
                      }
                    );
                  }}
                  className="px-3 py-1 bg-amber-700/80 hover:bg-amber-600 disabled:opacity-50 rounded text-sm"
                >
                  {rule.name}
                </button>
              ))}
            </div>
          )}
          <button
            onClick={() => setSelected(new Set())}
            className="px-3 py-1 text-zinc-400 hover:text-white text-sm"
          >
            Clear
          </button>
        </div>
      )}

      {quickResult && (
        <div className="flex items-center gap-3 mb-3 px-3 py-2 bg-emerald-900/30 border border-emerald-700 rounded text-sm text-emerald-200">
          <span>{quickResult}</span>
          <a href="/rules" className="text-emerald-400 hover:text-emerald-300 underline">
            undo from Rules → Apply History
          </a>
          <button onClick={() => setQuickResult(null)} className="ml-auto text-zinc-400 hover:text-white">
            dismiss
          </button>
        </div>
      )}

      {/* Table */}
      <div className="overflow-x-auto border border-zinc-800 rounded-lg">
        <table className="w-full text-sm min-w-[900px]">
          <thead>
            <tr className="border-b border-zinc-800 bg-zinc-900/50">
              <th className="p-2 w-8 sticky left-0 z-10 bg-zinc-900">
                <input
                  type="checkbox"
                  checked={data?.data.length ? selected.size === data.data.length : false}
                  onChange={toggleAll}
                  className="accent-blue-600"
                />
              </th>
              <th
                className="p-2 text-left cursor-pointer hover:text-white sticky left-8 z-10 bg-zinc-900 border-r border-zinc-800/80 whitespace-nowrap"
                onClick={() => toggleSort("transaction_date")}
              >
                Date {filters.sort_by === "transaction_date" && (filters.sort_dir === "desc" ? "\u2193" : "\u2191")}
              </th>
              <th
                className="p-2 text-left text-zinc-500 cursor-pointer hover:text-white whitespace-nowrap"
                onClick={() => toggleSort("created_at")}
              >
                Imported {filters.sort_by === "created_at" && (filters.sort_dir === "desc" ? "\u2193" : "\u2191")}
              </th>
              <th className="p-2 text-left">Description</th>
              <th className="p-2 text-left">Merchant</th>
              <th
                className="p-2 text-right cursor-pointer hover:text-white"
                onClick={() => toggleSort("amount")}
              >
                Amount {filters.sort_by === "amount" && (filters.sort_dir === "desc" ? "\u2193" : "\u2191")}
              </th>
              <th className="p-2 text-left">Type</th>
              <th className="p-2 text-left">Category</th>
              <th className="p-2 text-left">Bank</th>
              <th className="p-2 text-left">Tags</th>
              <th className="p-2"></th>
            </tr>
          </thead>
          <tbody>
            {isLoading ? (
              <tr><td colSpan={11} className="p-8 text-center text-zinc-500">Loading...</td></tr>
            ) : !data?.data.length ? (
              <tr><td colSpan={11} className="p-8 text-center text-zinc-500">No transactions found</td></tr>
            ) : (
              data.data.map((t) => (
                <Fragment key={t.id}>
                <tr
                  className={`border-b border-zinc-800/50 hover:bg-zinc-900/30 ${
                    selected.has(t.id) ? "bg-zinc-800/40" : ""
                  }`}
                >
                  <td className={`p-2 sticky left-0 z-10 ${selected.has(t.id) ? "bg-zinc-800" : "bg-zinc-950"}`}>
                    <input
                      type="checkbox"
                      checked={selected.has(t.id)}
                      onChange={() => toggleSelect(t.id)}
                      className="accent-blue-600"
                    />
                  </td>
                  <td className={`p-2 whitespace-nowrap sticky left-8 z-10 border-r border-zinc-800/80 ${selected.has(t.id) ? "bg-zinc-800" : "bg-zinc-950"}`}>{formatDate(t.transaction_date)}</td>
                  <td className="p-2 whitespace-nowrap text-zinc-500 text-xs">{formatDate(t.created_at)}</td>
                  <td className="p-2 max-w-xs">
                    <div className="flex items-start gap-1.5">
                      {t.order_platform && (
                        // Only where there IS a receipt behind the row. A
                        // disclosure arrow on every transaction would promise
                        // detail that mostly does not exist.
                        <button
                          onClick={() => setExpanded((prev) => {
                            const next = new Set(prev);
                            if (next.has(t.id)) next.delete(t.id); else next.add(t.id);
                            return next;
                          })}
                          className="text-zinc-600 hover:text-zinc-300 leading-none mt-0.5 shrink-0"
                          title={expanded.has(t.id) ? "Hide receipt" : "Show the receipt this came from"}
                          aria-expanded={expanded.has(t.id)}
                        >
                          {expanded.has(t.id) ? "▾" : "▸"}
                        </button>
                      )}
                      <p className="truncate" title={t.description}>{t.description}</p>
                    </div>
                    {t.notes ? (
                      <p className="truncate text-xs text-zinc-500 italic mt-0.5" title={t.notes}>{t.notes}</p>
                    ) : t.order_leg_kind === "instalment" && t.order_name ? (
                      // Board 205. Four rows reading "Afterpay $399.75" name
                      // nothing — this is the surface where that was noticed.
                      // The order is what tells them apart, and "2 of 4" is
                      // what stops the same purchase reading as four.
                      <p className="truncate text-xs text-zinc-500 italic mt-0.5" title={t.order_name}>
                        {t.order_leg_index && t.order_leg_count
                          ? `${t.order_leg_index} of ${t.order_leg_count} · `
                          : ""}
                        {t.order_name}
                      </p>
                    ) : t.order_platform === "uber" && routeSummary(t.order_route) && (
                      // Five rows all reading "Order - Uber Trip" are
                      // indistinguishable. Where the trip went is what tells
                      // them apart, and it was already stored. A note the user
                      // wrote always wins — this only fills an empty line.
                      <p
                        className="truncate text-xs text-zinc-500 italic mt-0.5"
                        title={t.order_route!.map((r) => `${r.label}${r.time ? ` ${r.time}` : ""}: ${r.address}`).join("\n")}
                      >
                        {routeSummary(t.order_route)}
                      </p>
                    )}
                  </td>
                  <td className="p-2 max-w-[150px]">
                    <div className="relative">
                      <InlineEdit
                        value={t.effective_merchant || ""}
                        onSave={(val) => {
                          updateTxn.mutate({ id: t.id, merchant_normalized: val });
                          setRulePrompt({ tx: t, field: "merchant", newValue: val });
                        }}
                      />
                      {t.merchant_override && (
                        <span className="absolute -left-2 top-1/2 -translate-y-1/2 w-1.5 h-1.5 bg-blue-500 rounded-full" title="Manually overridden" />
                      )}
                    </div>
                  </td>
                  <td className={`p-2 text-right whitespace-nowrap font-mono ${
                    SPEND_TYPES.has(t.transaction_type) ? "text-red-400" : "text-green-400"
                  }`}>
                    {/*
                      A row with no AUD figure must NOT be printed as AUD.
                      `amount_aud ?? amount` formatted with the default currency
                      rendered a USD 10,782.00 Wise credit as "+$10,782.00" in a
                      column of AUD figures — the real value was about $15,500,
                      and the row directly above it (the same payment from the
                      statement) showed exactly that. The native figure is the
                      only true one here, so it becomes the headline and the
                      absence of a rate is stated rather than papered over.
                    */}
                    {t.amount_unconverted ? (
                      <>
                        {formatAmount(t.amount, t.transaction_type, t.currency)}
                        <div className="text-[10px] text-zinc-500 mt-0.5" title="No AUD figure exists for this row — it is excluded from AUD totals">
                          no AUD rate
                        </div>
                      </>
                    ) : (
                      <>
                        {formatAmount(t.amount_aud ?? t.amount, t.transaction_type)}
                        {t.currency && t.currency !== "AUD" && (
                          <div className="text-[10px] text-zinc-500 mt-0.5">
                            {formatAmount(t.amount, t.transaction_type, t.currency)}
                          </div>
                        )}
                      </>
                    )}
                  </td>
                  <td className="p-2">
                    <EditableTypeBadge
                      type={t.transaction_type}
                      onSave={(val) => updateTxn.mutate({ id: t.id, transaction_type: val })}
                    />
                  </td>
                  <td className="p-2 max-w-[140px]">
                    <div className="relative">
                      <InlineEdit
                        value={t.effective_category}
                        onSave={(val) => {
                          updateTxn.mutate({ id: t.id, category: val });
                          setRulePrompt({ tx: t, field: "category", newValue: val });
                        }}
                        type="select"
                        options={categoryOptions}
                      />
                      {t.category_override && (
                        <span className="absolute -left-2 top-1/2 -translate-y-1/2 w-1.5 h-1.5 bg-blue-500 rounded-full" title="Manually overridden" />
                      )}
                    </div>
                  </td>
                  <td className="p-2 text-zinc-400 whitespace-nowrap">{t.bank_name}</td>
                  <td className="p-2">
                    <div className="flex items-center gap-1 flex-wrap">
                      {t.tags?.map((tag) => (
                        <span
                          key={tag.id}
                          className="px-1.5 py-0.5 rounded text-xs font-medium text-white"
                          style={{ backgroundColor: tag.color + "99" }}
                        >
                          {tag.name}
                        </span>
                      ))}
                      <TagPicker transactionId={t.id} currentTags={t.tags ?? []} />
                    </div>
                  </td>
                  <td className="p-2 whitespace-nowrap">
                    <div className="flex items-center gap-1 flex-wrap">
                      {t.splits?.filter((s) => s.participant_id !== me?.id).map((s) => (
                        <span
                          key={s.participant_id}
                          className={`inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium ${
                            s.settled ? "bg-zinc-800 text-zinc-500" : "bg-amber-900/40 text-amber-300"
                          }`}
                          title={`${s.name}: ${s.share_percent}%${s.settled ? " (settled)" : ""}`}
                        >
                          {s.name} {s.share_percent}%
                        </span>
                      ))}
                      <button
                        onClick={() => setSplitModal({ transactionId: t.id, amount: t.amount_aud ?? t.amount, description: t.description, merchant: t.effective_merchant || undefined, transactionIds: undefined })}
                        className={`text-xs px-2 py-0.5 rounded transition-colors ${
                          t.splits?.some((s) => s.participant_id !== me?.id)
                            ? "text-amber-400 hover:text-amber-200 hover:bg-zinc-800"
                            : "text-zinc-500 hover:text-zinc-200 hover:bg-zinc-800"
                        }`}
                        title="Split this transaction"
                      >
                        Split
                      </button>
                    </div>
                    <button
                      onClick={() => setEditModal(t)}
                      className="text-xs text-zinc-500 hover:text-zinc-200 px-2 py-0.5 rounded hover:bg-zinc-800 transition-colors"
                      title="Edit this transaction"
                    >
                      Edit
                    </button>
                    {!SPEND_TYPES.has(t.transaction_type) && (
                      <button
                        onClick={() => setPaymentModal(t)}
                        className="text-xs text-emerald-600 hover:text-emerald-400 px-2 py-0.5 rounded hover:bg-zinc-800 transition-colors"
                        title="Record as debt payment"
                      >
                        Payment
                      </button>
                    )}
                    <button
                      onClick={() => setAddModal({
                        title: "Duplicate Transaction",
                        prefill: {
                          date: new Date().toISOString().slice(0, 10),
                          description: t.description,
                          // Duplicates become manual (AUD) transactions, so seed
                          // them with the converted figure, not the native one.
                          amount: t.amount_aud ?? t.amount,
                          transaction_type: t.transaction_type,
                          merchant_normalized: t.effective_merchant || undefined,
                          category: t.effective_category || undefined,
                        },
                      })}
                      className="text-xs text-zinc-500 hover:text-zinc-200 px-2 py-0.5 rounded hover:bg-zinc-800 transition-colors"
                      title="Duplicate this transaction"
                    >
                      Dupe
                    </button>
                  </td>
                </tr>
                {expanded.has(t.id) && (
                  <tr className="border-b border-zinc-800/50 bg-zinc-900/40">
                    <td />
                    <td colSpan={10} className="px-4 py-3">
                      <OrderDetails transactionId={t.id} currency={t.currency ?? null} bare />
                    </td>
                  </tr>
                )}
                </Fragment>
              ))
            )}
          </tbody>
        </table>
      </div>

      {/* Split modal */}
      {splitModal && (
        <SplitModal
          transactionId={splitModal.transactionId}
          transactionIds={splitModal.transactionIds}
          amount={splitModal.amount}
          description={splitModal.description}
          merchant={splitModal.merchant}
          onClose={() => { setSplitModal(null); if (splitModal.transactionIds) setSelected(new Set()); }}
        />
      )}

      {showImportModal && <CsvImportModal onClose={() => setShowImportModal(false)} />}

      {addModal && (
        <AddTransactionModal
          prefill={addModal.prefill}
          title={addModal.title}
          onClose={() => setAddModal(null)}
        />
      )}

      {editModal && (
        <EditTransactionModal
          transaction={editModal}
          onClose={() => setEditModal(null)}
        />
      )}

      {paymentModal && (
        <MarkAsPaymentModal
          transaction={paymentModal}
          onClose={() => setPaymentModal(null)}
        />
      )}

      {rulePrompt && (
        <SaveAsRulePrompt
          tx={rulePrompt.tx}
          field={rulePrompt.field}
          newValue={rulePrompt.newValue}
          onDone={() => setRulePrompt(null)}
        />
      )}

      {/* Pagination */}
      {data && data.total > filters.limit && (
        <div className="flex items-center justify-between mt-4">
          <span className="text-sm text-zinc-500">
            Showing {filters.offset + 1}-{Math.min(filters.offset + filters.limit, data.total)} of {data.total}
          </span>
          <div className="flex gap-2">
            <button
              disabled={currentPage <= 1}
              onClick={() => setPage(filters.offset - filters.limit)}
              className="px-3 py-1 bg-zinc-800 hover:bg-zinc-700 disabled:opacity-50 rounded text-sm"
            >
              Previous
            </button>
            <span className="px-3 py-1 text-sm text-zinc-400">
              Page {currentPage} of {totalPages}
            </span>
            <button
              disabled={currentPage >= totalPages}
              onClick={() => setPage(filters.offset + filters.limit)}
              className="px-3 py-1 bg-zinc-800 hover:bg-zinc-700 disabled:opacity-50 rounded text-sm"
            >
              Next
            </button>
          </div>
        </div>
      )}
    </div>
  );
}
