"use client";

import { useState, useCallback, useRef, useEffect, Suspense } 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 } from "@/lib/queries";
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>
  );
}

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") || "";

  const [filters, setFilters] = useState({
    from: "",
    to: "",
    categories: [] as string[],
    bank_names: [] as string[],
    tag_ids: [] as string[],
    transaction_types: [] as string[],
    search: "",
    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("");
  const [queryTokens, setQueryTokens] = useState<QueryToken[]>([]);

  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);
  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, offset: 0 }))}
          placeholder="All Categories"
        />
        <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) => (
                <tr
                  key={t.id}
                  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">
                    <p className="truncate" title={t.description}>{t.description}</p>
                    {t.notes && (
                      <p className="truncate text-xs text-zinc-500 italic mt-0.5" title={t.notes}>{t.notes}</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"
                  }`}>
                    {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>
              ))
            )}
          </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>
  );
}
