"use client";

import { useState, useRef, useEffect } from "react";
import {
  useSharedTransactions,
  useParticipantBalances,
  useParticipants,
  useCreateParticipant,
  useRecordPayment,
  usePaymentHistory,
  useDeletePayment,
  useCurrentUser,
  useTags,
  useTrips,
  type SplitPayment,
} from "@/lib/hooks";
import type { SharedTransactionRow } from "@/lib/queries";
import { EditTransactionModal } from "@/components/edit-transaction-modal";
import { formatCategory } from "@/lib/categories";
import { CATEGORY_COLORS } from "@/lib/category-colors";

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

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

function formatAmount(n: number, type?: string, currency?: string) {
  // A bare "$" on a non-AUD row was the visible half of the problem: the row
  // read as dollars while the participant balances converted to AUD, so the
  // two disagreed on screen with nothing to explain why.
  const value = Number(n).toFixed(2);
  const formatted = !currency || currency === "AUD" ? `$${value}` : `${currency} ${value}`;
  return type && !SPEND_TYPES.has(type) ? `+${formatted}` : formatted;
}

// ── Tag multi-select ──────────────────────────────────────────────────────────
function TagFilter({ value, onChange }: { value: string[]; onChange: (v: string[]) => void }) {
  const { data: tags = [] } = useTags();
  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 = (id: string) => {
    let next: string[];
    if (value.includes(id)) {
      next = value.filter((x) => x !== id);
    } else if (id === "untagged") {
      next = ["untagged"];
    } else {
      next = [...value.filter((x) => x !== "untagged"), id];
    }
    onChange(next);
  };

  const label = value.length === 0 ? "All Tags"
    : value.includes("untagged") ? "No tags"
    : value.length === 1 ? (tags.find((t) => String(t.id) === value[0])?.name ?? "1 tag")
    : `${value.length} tags`;

  return (
    <div ref={ref} className="relative">
      <button
        type="button"
        onClick={() => setOpen((v) => !v)}
        className={`border rounded px-3 py-1.5 text-sm flex items-center gap-2 min-w-[120px] bg-zinc-900 ${value.length > 0 ? "border-indigo-500 text-white" : "border-zinc-700 text-zinc-400"}`}
      >
        <span className="flex-1 text-left">{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-56 overflow-y-auto">
          <label className="flex items-center gap-2 px-3 py-1.5 hover:bg-zinc-800 cursor-pointer text-sm border-b border-zinc-800">
            <input type="checkbox" checked={value.includes("untagged")} onChange={() => toggle("untagged")}
              className="accent-indigo-500 flex-shrink-0" />
            <span className="text-zinc-400 italic">No tags</span>
          </label>
          {tags.map((t) => (
            <label key={t.id} 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(String(t.id))} onChange={() => toggle(String(t.id))}
                className="accent-indigo-500 flex-shrink-0" />
              <span className="w-2 h-2 rounded-full flex-shrink-0" style={{ backgroundColor: t.color }} />
              {t.name}
            </label>
          ))}
        </div>
      )}
    </div>
  );
}

// ── Add Participant ───────────────────────────────────────────────────────────
function AddParticipantForm({ onDone }: { onDone: () => void }) {
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");
  const [error, setError] = useState("");
  const create = useCreateParticipant();

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    setError("");
    if (!name.trim()) { setError("Name is required"); return; }
    try {
      await create.mutateAsync({ name: name.trim(), email: email.trim() || undefined });
      onDone();
    } catch (err) {
      setError(err instanceof Error ? err.message : "Failed to create");
    }
  }

  return (
    <form onSubmit={handleSubmit} className="bg-zinc-900 border border-zinc-700 rounded-xl p-4 space-y-3">
      <p className="text-sm font-medium">Add Participant</p>
      <div className="flex gap-2">
        <input type="text" placeholder="Name" value={name} onChange={(e) => setName(e.target.value)}
          className="flex-1 bg-zinc-800 border border-zinc-700 rounded-lg px-3 py-1.5 text-sm focus:outline-none focus:border-zinc-500" />
        <input type="email" placeholder="Email (optional)" value={email} onChange={(e) => setEmail(e.target.value)}
          className="flex-1 bg-zinc-800 border border-zinc-700 rounded-lg px-3 py-1.5 text-sm focus:outline-none focus:border-zinc-500" />
        <button type="submit" disabled={create.isPending}
          className="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 disabled:opacity-50 text-white rounded-lg text-sm font-medium">
          {create.isPending ? "Adding..." : "Add"}
        </button>
        <button type="button" onClick={onDone}
          className="px-3 py-1.5 bg-zinc-800 hover:bg-zinc-700 text-zinc-400 rounded-lg text-sm">
          Cancel
        </button>
      </div>
      {error && <p className="text-red-400 text-xs">{error}</p>}
    </form>
  );
}

// ── Record Payment modal ──────────────────────────────────────────────────────
function RecordPaymentModal({
  participant,
  currentUserId,
  currentBalance,
  onClose,
}: {
  participant: { id: number; name: string };
  currentUserId: number;
  currentBalance: number; // positive = they owe me, negative = I owe them
  onClose: () => void;
}) {
  const record = useRecordPayment();
  const { data: trips = [] } = useTrips();
  const theyOweMe = currentBalance > 0;

  // Default direction matches the debt direction
  const [amount, setAmount] = useState(Math.abs(currentBalance).toFixed(2));
  const [date, setDate] = useState(new Date().toISOString().slice(0, 10));
  const [notes, setNotes] = useState("");
  // direction: "received" = they paid me, "sent" = I paid them
  const [direction, setDirection] = useState<"received" | "sent">(theyOweMe ? "received" : "sent");
  // Which tab this settles. "" = the ongoing household tab (trip_id NULL).
  const [tripId, setTripId] = useState("");
  const [error, setError] = useState("");

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

  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()}>
        <h3 className="font-semibold text-sm text-zinc-300">Record Payment</h3>

        {/* Direction toggle */}
        <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"}`}
          >
            {participant.name} 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 {participant.name}
          </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>

        {/* Scope. Until now every payment recorded here landed on the household
            tab, because the API dropped trip_id — so a $11k Europe settlement
            silently reduced the ongoing household balance instead. */}
        <div>
          <label className="block text-xs text-zinc-500 mb-1">Settles</label>
          <select value={tripId} onChange={(e) => setTripId(e.target.value)}
            className="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1.5 text-sm">
            <option value="">Household (ongoing)</option>
            {trips.filter((t) => !t.archived).map((t) => (
              <option key={t.id} value={t.id}>{t.name}</option>
            ))}
          </select>
          <p className="text-[11px] text-zinc-600 mt-1">
            Covering more than one tab? Record it once per tab — the parts add back
            up to the transfer.
          </p>
        </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, cash"
            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"}
          </button>
        </div>
      </div>
    </div>
  );
}

// ── Payment history inline ────────────────────────────────────────────────────
function PaymentHistory({ participantId, currentUserId }: { participantId: number; currentUserId: number }) {
  const { data: payments = [], isLoading } = usePaymentHistory(participantId);
  const deletePayment = useDeletePayment();

  if (isLoading) return <p className="text-xs text-zinc-600 mt-2">Loading payments…</p>;
  if (payments.length === 0) return <p className="text-xs text-zinc-600 italic mt-2">No payments recorded</p>;

  return (
    <div className="mt-3 space-y-1.5">
      <p className="text-xs text-zinc-500 font-medium">Payment history</p>
      {payments.map((p: SplitPayment) => {
        const theyPaidMe = p.to_participant_id === currentUserId;
        return (
          <div key={p.id} className="flex items-center gap-2 text-xs">
            <span className={`font-mono font-medium ${theyPaidMe ? "text-emerald-400" : "text-blue-400"}`}>
              {theyPaidMe ? "+" : "-"}${Number(p.amount).toFixed(2)}
            </span>
            <span className="text-zinc-500">{formatDate(p.payment_date)}</span>
            {/* Scope, so a grouped transfer stops looking like a duplicate: two
                rows of the same amount and date differ only by which tab they
                settle, and that was invisible until the API returned trip_id. */}
            <span className="text-[11px] px-1.5 py-0.5 rounded bg-zinc-800 text-zinc-400 flex-shrink-0">
              {p.trip_name ?? "Household"}
            </span>
            {p.notes && <span className="text-zinc-600 truncate flex-1">{p.notes}</span>}
            <button
              onClick={() => deletePayment.mutate(p.id)}
              className="text-zinc-600 hover:text-red-400 leading-none ml-auto flex-shrink-0"
              title="Delete payment"
            >
              ×
            </button>
          </div>
        );
      })}
    </div>
  );
}

// ── Main page ─────────────────────────────────────────────────────────────────
type SortCol = "transaction_date" | "created_at" | "amount";

export default function SharedPage() {
  const [tagIds, setTagIds] = useState<string[]>([]);
  const [participantId, setParticipantId] = useState<number | undefined>(undefined);
  const [sortCol, setSortCol] = useState<SortCol>("transaction_date");
  const [sortDir, setSortDir] = useState<"asc" | "desc">("desc");
  const [search, setSearch] = useState("");
  const realTagIds = tagIds.filter((id) => id !== "untagged");
  const { data: participants = [] } = useParticipants();
  const { data: rawTransactions = [], isLoading: txLoading } = useSharedTransactions(tagIds, participantId);

  // Filtered client-side, like the sort above and unlike the transactions page.
  // This endpoint returns every split row in one go (1,267 today) with no
  // pagination, so there is nothing for a server round-trip to narrow — and a
  // server search would have to be added to a query the balance cards share.
  //
  // Deliberately does NOT match participant names: the participant dropdown
  // already does that properly, and typing "sonu" matching every row she is
  // split on would make the box look broken. The payer IS matched, because
  // nothing else on the page filters by who paid.
  const transactions = [...rawTransactions]
    .filter((tx) => {
      const q = search.trim().toLowerCase();
      if (!q) return true;
      return [
        tx.description,
        tx.effective_merchant,
        tx.notes,
        tx.effective_category ? formatCategory(tx.effective_category) : null,
        tx.owner_name,
      ].some((f) => f?.toLowerCase().includes(q));
    })
    .sort((a, b) => {
      const av = sortCol === "amount" ? Number(a.amount) : new Date(a[sortCol]).getTime();
      const bv = sortCol === "amount" ? Number(b.amount) : new Date(b[sortCol]).getTime();
      return sortDir === "desc" ? bv - av : av - bv;
    });

  function toggleSort(col: SortCol) {
    if (sortCol === col) setSortDir((d) => (d === "desc" ? "asc" : "desc"));
    else { setSortCol(col); setSortDir("desc"); }
  }

  function SortIcon({ col }: { col: SortCol }) {
    if (sortCol !== col) return <span className="text-zinc-600 ml-0.5">↕</span>;
    return <span className="ml-0.5">{sortDir === "desc" ? "↓" : "↑"}</span>;
  }
  const { data: balances = [], isLoading: balLoading } = useParticipantBalances(realTagIds);
  const { data: allTags = [] } = useTags();
  const { data: me } = useCurrentUser();

  // Names the tag scope when one is active. Non-empty means the cards below are
  // split totals rather than payable balances.
  const tagScopeLabel =
    realTagIds.length === 0
      ? null
      : realTagIds.length === 1
        ? (allTags.find((t) => String(t.id) === realTagIds[0])?.name ?? "this tag")
        : `${realTagIds.length} tags`;
  const [addingParticipant, setAddingParticipant] = useState(false);
  const [paymentModal, setPaymentModal] = useState<{ id: number; name: string; balance: number } | null>(null);
  const [showHistory, setShowHistory] = useState<number | null>(null);
  const [editModal, setEditModal] = useState<SharedTransactionRow | null>(null);

  return (
    <div className="space-y-6">
      <div className="flex items-center justify-between gap-3">
        <h2 className="text-2xl font-display">Shared Expenses</h2>
        <div className="flex items-center gap-2 ml-auto flex-wrap">
          <div className="relative">
            <input
              type="search"
              value={search}
              onChange={(e) => setSearch(e.target.value)}
              placeholder="Search description, merchant, category, payer…"
              aria-label="Search split transactions"
              className="w-64 bg-zinc-800 border border-zinc-700 rounded-lg pl-8 pr-2 py-1.5 text-sm placeholder:text-zinc-600 focus:outline-none focus:border-zinc-500"
            />
            <span className="absolute left-2.5 top-1/2 -translate-y-1/2 text-zinc-500 text-sm pointer-events-none">⌕</span>
          </div>
          <select
            value={participantId ?? ""}
            onChange={(e) => setParticipantId(e.target.value ? Number(e.target.value) : undefined)}
            className={`border rounded px-3 py-1.5 text-sm bg-zinc-900 ${participantId ? "border-indigo-500 text-white" : "border-zinc-700 text-zinc-400"}`}
          >
            <option value="">All People</option>
            {participants.map((p: { id: number; name: string }) => (
              <option key={p.id} value={p.id}>{p.name}</option>
            ))}
          </select>
          <TagFilter value={tagIds} onChange={setTagIds} />
          {!addingParticipant && (
            <button onClick={() => setAddingParticipant(true)}
              className="text-sm px-3 py-1.5 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 rounded-lg whitespace-nowrap">
              + Add Participant
            </button>
          )}
        </div>
      </div>

      {addingParticipant && <AddParticipantForm onDone={() => setAddingParticipant(false)} />}

      {/* Balance cards */}
      {realTagIds.length === 0 && tagIds.includes("untagged") ? null : realTagIds.length > 0 && (
        <p className="text-xs text-zinc-500 mb-2">Showing split totals for selected tag — payments excluded (payments settle overall debt, not per-tag)</p>
      )}
      <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
        {balLoading ? (
          <p className="text-zinc-500 text-sm col-span-3">Loading balances...</p>
        ) : balances.length === 0 ? (
          <p className="text-zinc-500 text-sm col-span-3">No participants yet.</p>
        ) : (
          balances.map((b) => {
            const theyOweMe = b.total_owed > 0;
            const net = Math.abs(b.total_owed);
            const settled = net < 0.005;
            return (
              <div key={b.id} className="bg-zinc-900 border border-zinc-700 rounded-xl p-4">
                <div className="flex items-start justify-between mb-3">
                  <div>
                    <p className="font-medium">{b.name}</p>
                    <p className="text-xs text-zinc-500">
                      {/* With a tag filter on, payments are deliberately not
                          subtracted — so this is a split total, not a payable
                          balance, and must not claim to be one. */}
                      {tagScopeLabel
                        ? `split total in ${tagScopeLabel}`
                        : settled ? "all square" : theyOweMe ? "owes you" : "you owe"}
                    </p>
                  </div>
                  <div className="text-right">
                    <p className={`text-lg font-semibold ${tagScopeLabel ? "text-zinc-300" : settled ? "text-zinc-500" : theyOweMe ? "text-amber-400" : "text-blue-400"}`}>
                      ${net.toFixed(2)}
                    </p>
                    {b.unconverted_count > 0 && (
                      <p className="text-[11px] text-amber-500/80 mt-0.5">
                        approx · {b.unconverted_count} unconverted
                      </p>
                    )}
                  </div>
                </div>

                <div className="flex gap-2">
                  {/* Settling against a tag-scoped total would record a payment
                      for a figure that never was the debt. */}
                  {!tagScopeLabel && (
                    <button
                      onClick={() => setPaymentModal({ id: b.id, name: b.name, balance: b.total_owed })}
                      className="flex-1 py-1.5 text-xs font-medium bg-zinc-800 hover:bg-zinc-700 text-zinc-300 rounded-lg"
                    >
                      Record Payment
                    </button>
                  )}
                  <button
                    onClick={() => setShowHistory(showHistory === b.id ? null : b.id)}
                    className={`px-3 py-1.5 text-xs rounded-lg ${showHistory === b.id ? "bg-zinc-700 text-white" : "bg-zinc-800 text-zinc-500 hover:text-zinc-300"}`}
                  >
                    History
                  </button>
                </div>

                {showHistory === b.id && me && (
                  <PaymentHistory participantId={b.id} currentUserId={me.id} />
                )}
              </div>
            );
          })
        )}
      </div>

      {/* Transaction list */}
      <div className="bg-zinc-900 border border-zinc-700 rounded-xl overflow-x-auto">
        <div className="px-4 py-3 border-b border-zinc-800 flex items-center gap-2">
          <h3 className="text-sm font-medium">Split Transactions</h3>
          {search.trim() && !txLoading && (
            <span className="text-xs text-zinc-500">
              {transactions.length} of {rawTransactions.length} match “{search.trim()}”
            </span>
          )}
        </div>
        {txLoading ? (
          <p className="text-zinc-500 text-sm px-4 py-6">Loading...</p>
        ) : transactions.length === 0 ? (
          // "None yet" is wrong when a search simply matched nothing, and it reads
          // as though the splits were lost.
          search.trim() ? (
            <p className="text-zinc-500 text-sm px-4 py-6">
              Nothing matches “{search.trim()}”.{" "}
              <button onClick={() => setSearch("")} className="text-zinc-400 hover:text-zinc-200 underline">
                Clear search
              </button>
            </p>
          ) : (
            <p className="text-zinc-500 text-sm px-4 py-6">
              No split transactions yet. Use the Split button on any transaction.
            </p>
          )
        ) : (
          <table className="w-full text-sm min-w-[760px]">
            {/* min-w raised from 520px with the Category and Paid-by columns: the
                wrapper scrolls horizontally, so a too-small minimum crushes cells
                rather than letting them scroll. */}
            <thead>
              <tr className="border-b border-zinc-800">
                <th
                  className="text-left px-4 py-2 text-xs text-zinc-500 font-medium cursor-pointer hover:text-white whitespace-nowrap"
                  onClick={() => toggleSort("transaction_date")}
                >
                  Date <SortIcon col="transaction_date" />
                </th>
                <th
                  className="text-left px-4 py-2 text-xs text-zinc-500 font-medium cursor-pointer hover:text-white whitespace-nowrap"
                  onClick={() => toggleSort("created_at")}
                >
                  Imported <SortIcon col="created_at" />
                </th>
                <th className="text-left px-4 py-2 text-xs text-zinc-500 font-medium sticky left-0 z-10 bg-zinc-900 border-r border-zinc-800/80">Description</th>
                <th className="text-left px-4 py-2 text-xs text-zinc-500 font-medium">Category</th>
                <th
                  className="text-right px-4 py-2 text-xs text-zinc-500 font-medium cursor-pointer hover:text-white"
                  onClick={() => toggleSort("amount")}
                >
                  Amount <SortIcon col="amount" />
                </th>
                {/* Paid by sits next to Splits deliberately: together they are the
                    two halves of the question this page exists to answer — whose
                    money went out, and whose share it was. */}
                <th className="text-left px-4 py-2 text-xs text-zinc-500 font-medium whitespace-nowrap">Paid by</th>
                <th className="text-left px-4 py-2 text-xs text-zinc-500 font-medium">Splits</th>
                <th className="px-4 py-2"></th>
              </tr>
            </thead>
            <tbody>
              {(transactions as SharedTransactionRow[]).map((tx) => {
                const splits = Array.isArray(tx.splits) ? tx.splits : [];
                return (
                  <tr key={tx.id} className="border-b border-zinc-800/50 hover:bg-zinc-800/30">
                    <td className="px-4 py-3 text-zinc-400 whitespace-nowrap">{formatDate(tx.transaction_date)}</td>
                    <td className="px-4 py-3 text-zinc-500 text-xs whitespace-nowrap">{formatDate(tx.created_at)}</td>
                    <td className="px-4 py-3 max-w-xs sticky left-0 z-10 bg-zinc-900 border-r border-zinc-800/80">
                      <p className="font-medium break-words">{tx.effective_merchant || tx.description}</p>
                      {tx.effective_merchant && (
                        <p className="text-xs text-zinc-500 break-words">{tx.description}</p>
                      )}
                      {tx.notes && (
                        <p className="text-xs text-zinc-500 italic mt-0.5 break-words">{tx.notes}</p>
                      )}
                    </td>
                    {/* Category is the effective one — the override wins over the
                        extracted value, the same COALESCE every other view uses,
                        so a correction made elsewhere shows up here too. */}
                    <td className="px-4 py-3 whitespace-nowrap">
                      {tx.effective_category ? (
                        <span
                          className="inline-flex items-center gap-1.5 text-xs text-zinc-300"
                          title={formatCategory(tx.effective_category)}
                        >
                          <span
                            className="w-1.5 h-1.5 rounded-full flex-shrink-0"
                            style={{ background: CATEGORY_COLORS[tx.effective_category] ?? "#71717a" }}
                          />
                          {formatCategory(tx.effective_category)}
                        </span>
                      ) : (
                        <span className="text-xs text-zinc-600 italic">uncategorised</span>
                      )}
                    </td>
                    <td className={`px-4 py-3 text-right font-medium tabular-nums ${SPEND_TYPES.has(tx.transaction_type) ? "" : "text-green-400"}`}>
                      {formatAmount(tx.amount, tx.transaction_type, tx.currency)}
                      {tx.currency !== "AUD" && (
                        // Splits settle on the AUD figure, so show it next to the
                        // native one rather than leaving the two to differ silently.
                        <span className="block text-xs font-normal text-zinc-500">
                          {tx.amount_unconverted
                            ? "AUD value unknown"
                            : `≈ ${formatAmount(Number(tx.amount_aud), tx.transaction_type, "AUD")} AUD`}
                        </span>
                      )}
                    </td>
                    {/* Whose money actually left. This is the effective owner —
                        COALESCE(t.owner_id, s.owner_id) — so it is the account the
                        spend came out of, which is what every balance on this page
                        is computed from. "Me" matches the split chips rather than
                        printing your own name twice in one row. */}
                    <td className="px-4 py-3 whitespace-nowrap">
                      <span className={`text-xs ${tx.owner_id === me?.id ? "text-zinc-400" : "text-indigo-300"}`}>
                        {tx.owner_id === me?.id ? "Me" : tx.owner_name}
                      </span>
                    </td>
                    <td className="px-4 py-3">
                      <div className="flex flex-wrap gap-1">
                        {splits.map((s) => (
                          <span key={s.participant_id}
                            className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs bg-zinc-800 text-zinc-300">
                            {s.participant_id === me?.id ? "Me" : s.name} {s.share_percent}%
                          </span>
                        ))}
                      </div>
                    </td>
                    <td className="px-4 py-3">
                      <button
                        onClick={() => setEditModal(tx)}
                        className="text-xs text-zinc-500 hover:text-zinc-200 px-2 py-0.5 rounded hover:bg-zinc-800 transition-colors"
                      >
                        Edit
                      </button>
                    </td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        )}
      </div>

      {/* Payment modal */}
      {paymentModal && me && (
        <RecordPaymentModal
          participant={{ id: paymentModal.id, name: paymentModal.name }}
          currentUserId={me.id}
          currentBalance={paymentModal.balance}
          onClose={() => setPaymentModal(null)}
        />
      )}

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