// ===== App.jsx — root: private code-gated trips, header, people, charges, panels =====

const EMPTY = [];
const MY_KEY = "tally_mytrips_v1"; // [{ id, code, owner }] — which trips this browser can open

const loadMine = () => { try { const a = JSON.parse(localStorage.getItem(MY_KEY)); return Array.isArray(a) ? a : []; } catch (e) { return []; } };
const saveMine = (trips) => { try { localStorage.setItem(MY_KEY, JSON.stringify(trips.map((t) => ({ id: t.id, code: t.code, owner: !!t.owner })))); } catch (e) {} };

// ---- admin key: host-level access to every trip (set on the server via TALLY_ADMIN_KEY) ----
const ADMIN_KEY_LS = "tally_admin_v1";
let adminKey = (() => { try { return localStorage.getItem(ADMIN_KEY_LS) || null; } catch (e) { return null; } })();
const setAdminKey = (k) => { adminKey = k || null; try { if (k) localStorage.setItem(ADMIN_KEY_LS, k); else localStorage.removeItem(ADMIN_KEY_LS); } catch (e) {} };
const tripHeaders = (code, extra) => { const h = { "X-Trip-Code": code || "", ...(extra || {}) }; if (adminKey) h["X-Admin-Key"] = adminKey; return h; };

// ---- API. Trip requests carry the share code (plus the admin key when signed in) ----
const api = {
  async getConfig() { try { const r = await fetch("/api/config"); return r.ok ? r.json() : { adminEnabled: false }; } catch (e) { return { adminEnabled: false }; } },
  async getTrip(id, code) {
    const r = await fetch("/api/trips/" + id, { headers: tripHeaders(code) });
    if (!r.ok) throw r;
    return r.json();
  },
  async createTrip(name) {
    const r = await fetch("/api/trips", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name }) });
    if (!r.ok) throw r;
    return r.json();
  },
  async saveTrip(trip, code) {
    const r = await fetch("/api/trips/" + trip.id, { method: "PUT", headers: tripHeaders(code, { "Content-Type": "application/json" }), body: JSON.stringify(trip) });
    if (!r.ok) throw r;
    return r.json();
  },
  async deleteTrip(id, code) {
    await fetch("/api/trips/" + id, { method: "DELETE", headers: tripHeaders(code) });
  },
  async join(code) {
    const r = await fetch("/api/join", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ code }) });
    if (!r.ok) throw r;
    return r.json();
  },
  async adminTrips(key) {
    const r = await fetch("/api/admin/trips", { headers: { "X-Admin-Key": key || adminKey || "" } });
    if (!r.ok) throw r;
    return r.json();
  },
};

const splitModeLabel = (m) => ({ equal: "Split equally", exact: "Custom amounts", percent: "By percentage", shares: "By shares" }[m] || "Split equally");

const SORTS = [
  { id: "date_desc", label: "Newest first" },
  { id: "date_asc", label: "Oldest first" },
  { id: "amt_desc", label: "Highest amount" },
  { id: "amt_asc", label: "Lowest amount" },
];

// ---------- Splash ----------
function Splash({ text, sub, children }) {
  return (
    <div style={{ minHeight: "100vh", display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 14, padding: 24, textAlign: "center" }}>
      <div style={{ width: 46, height: 46, borderRadius: 13, background: "var(--accent)", display: "flex", alignItems: "center", justifyContent: "center", boxShadow: "0 6px 18px oklch(0.71 0.132 44 / 0.4)" }}>
        <span className="display" style={{ color: "var(--accent-ink)", fontWeight: 700, fontSize: 24 }}>T</span>
      </div>
      <div className="display" style={{ fontSize: 16, color: "var(--muted)" }}>{text}</div>
      {sub && <div style={{ fontSize: 13.5, color: "var(--faint)" }}>{sub}</div>}
      {children}
    </div>
  );
}

// ---------- Join form (enter a code) ----------
function JoinForm({ onJoin }) {
  const [code, setCode] = useState("");
  const [err, setErr] = useState("");
  const [busy, setBusy] = useState(false);
  const submit = async () => {
    const c = code.trim();
    if (!c || busy) return;
    setBusy(true); setErr("");
    try { await onJoin(c); }
    catch (e) { setErr("That code didn't match any trip."); setBusy(false); }
  };
  return (
    <div>
      <div style={{ display: "flex", gap: 10 }}>
        <input value={code} autoCapitalize="characters" autoCorrect="off" spellCheck={false}
          onChange={(e) => { setCode(e.target.value.toUpperCase()); setErr(""); }}
          onKeyDown={(e) => e.key === "Enter" && submit()}
          placeholder="Enter trip code" className="mono" style={{ flex: 1, letterSpacing: "0.12em", textTransform: "uppercase" }} />
        <button className="btn btn-accent" onClick={submit} disabled={!code.trim() || busy} style={{ flex: "none" }}>
          <Icon name="arrow" size={17} /> Join
        </button>
      </div>
      {err && <div className="hint bad" style={{ marginTop: 8, fontSize: 13 }}>{err}</div>}
    </div>
  );
}

// ---------- Welcome (no trips yet) ----------
function Welcome({ onCreate, onJoin, adminEnabled, onAdmin }) {
  return (
    <div style={{ minHeight: "100vh", display: "flex", alignItems: "center", justifyContent: "center", padding: 24 }}>
      <div className="card rise" style={{ width: "100%", maxWidth: 440, padding: 28 }}>
        <div style={{ display: "flex", alignItems: "center", gap: 13, marginBottom: 20 }}>
          <div style={{ width: 46, height: 46, borderRadius: 13, background: "var(--accent)", display: "flex", alignItems: "center", justifyContent: "center", flex: "none", boxShadow: "0 6px 18px oklch(0.71 0.132 44 / 0.4)" }}>
            <span className="display" style={{ color: "var(--accent-ink)", fontWeight: 700, fontSize: 24 }}>T</span>
          </div>
          <div>
            <div className="display" style={{ fontSize: 22, fontWeight: 700, letterSpacing: "-0.02em" }}>Tally</div>
            <div style={{ fontSize: 13, color: "var(--faint)" }}>Split trip costs with friends</div>
          </div>
        </div>

        <button className="btn btn-accent" style={{ width: "100%", height: 46 }} onClick={onCreate}>
          <Icon name="plus" size={18} /> Start a new trip
        </button>

        <div style={{ display: "flex", alignItems: "center", gap: 12, margin: "20px 0 16px" }}>
          <hr className="divider" style={{ flex: 1 }} />
          <span style={{ fontSize: 12, color: "var(--faint)", fontWeight: 600 }}>OR JOIN ONE</span>
          <hr className="divider" style={{ flex: 1 }} />
        </div>

        <div className="label" style={{ marginBottom: 9 }}>Got a code from a friend?</div>
        <JoinForm onJoin={onJoin} />

        {adminEnabled && (
          <div style={{ textAlign: "center", marginTop: 18 }}>
            <button className="btn btn-quiet btn-sm" onClick={onAdmin} style={{ color: "var(--faint)" }}>Admin sign-in</button>
          </div>
        )}
      </div>
    </div>
  );
}

// ---------- Join modal (when you already have trips) ----------
function JoinModal({ onJoin, onClose }) {
  useEffect(() => {
    const h = (e) => { if (e.key === "Escape") onClose(); };
    window.addEventListener("keydown", h);
    return () => window.removeEventListener("keydown", h);
  }, []);
  return (
    <div className="modal-backdrop" onMouseDown={(e) => { if (e.target === e.currentTarget) onClose(); }}>
      <div className="modal" style={{ maxWidth: 440 }}>
        <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", padding: "20px 24px" }}>
          <div style={{ display: "flex", alignItems: "center", gap: 12 }}>
            <span style={{ color: "var(--accent)", display: "inline-flex" }}><Icon name="arrow" size={20} /></span>
            <h2 className="display" style={{ fontSize: 19, fontWeight: 600 }}>Join a trip</h2>
          </div>
          <button className="btn btn-icon btn-quiet" onClick={onClose} aria-label="Close"><Icon name="close" size={18} /></button>
        </div>
        <hr className="divider" />
        <div style={{ padding: 24 }}>
          <div style={{ fontSize: 13.5, color: "var(--muted)", marginBottom: 14 }}>Enter the code a friend shared with you to open their trip.</div>
          <JoinForm onJoin={async (c) => { await onJoin(c); onClose(); }} />
        </div>
      </div>
    </div>
  );
}

// ---------- Admin sign-in modal ----------
function AdminModal({ onLogin, onClose }) {
  const [key, setKey] = useState("");
  const [err, setErr] = useState("");
  const [busy, setBusy] = useState(false);
  useEffect(() => {
    const h = (e) => { if (e.key === "Escape") onClose(); };
    window.addEventListener("keydown", h);
    return () => window.removeEventListener("keydown", h);
  }, []);
  const submit = async () => {
    const k = key.trim();
    if (!k || busy) return;
    setBusy(true); setErr("");
    try { await onLogin(k); }
    catch (e) { setErr("That admin key wasn't accepted."); setBusy(false); }
  };
  return (
    <div className="modal-backdrop" onMouseDown={(e) => { if (e.target === e.currentTarget) onClose(); }}>
      <div className="modal" style={{ maxWidth: 440 }}>
        <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", padding: "20px 24px" }}>
          <div style={{ display: "flex", alignItems: "center", gap: 12 }}>
            <span style={{ color: "var(--accent)", display: "inline-flex" }}><Icon name="wallet" size={20} /></span>
            <h2 className="display" style={{ fontSize: 19, fontWeight: 600 }}>Admin sign-in</h2>
          </div>
          <button className="btn btn-icon btn-quiet" onClick={onClose} aria-label="Close"><Icon name="close" size={18} /></button>
        </div>
        <hr className="divider" />
        <div style={{ padding: 24 }}>
          <div style={{ fontSize: 13.5, color: "var(--muted)", marginBottom: 14 }}>Enter the admin key to see and edit every trip on this server.</div>
          <div style={{ display: "flex", gap: 10 }}>
            <input type="password" value={key} autoFocus onChange={(e) => { setKey(e.target.value); setErr(""); }} onKeyDown={(e) => e.key === "Enter" && submit()} placeholder="Admin key" style={{ flex: 1 }} />
            <button className="btn btn-accent" onClick={submit} disabled={!key.trim() || busy} style={{ flex: "none" }}><Icon name="arrow" size={17} /> Sign in</button>
          </div>
          {err && <div className="hint bad" style={{ marginTop: 8, fontSize: 13 }}>{err}</div>}
        </div>
      </div>
    </div>
  );
}

// ---------- Copy button ----------
function CopyBtn({ text, label = "Copy", small }) {
  const [done, setDone] = useState(false);
  const copy = async () => {
    try { await navigator.clipboard.writeText(text); }
    catch (e) {
      const ta = document.createElement("textarea"); ta.value = text; ta.style.position = "fixed"; ta.style.opacity = "0";
      document.body.appendChild(ta); ta.select(); try { document.execCommand("copy"); } catch (e2) {} document.body.removeChild(ta);
    }
    setDone(true); setTimeout(() => setDone(false), 1500);
  };
  return (
    <button className={"btn btn-ghost" + (small ? " btn-sm" : "")} onClick={copy} style={{ flex: "none" }}>
      <Icon name={done ? "check" : "copy"} size={15} /> {done ? "Copied" : label}
    </button>
  );
}

// ---------- Share modal (codes + links) ----------
function ShareModal({ trip, onClose }) {
  useEffect(() => {
    const h = (e) => { if (e.key === "Escape") onClose(); };
    window.addEventListener("keydown", h);
    return () => window.removeEventListener("keydown", h);
  }, []);
  const base = location.origin + location.pathname;
  const link = (code) => base + "?join=" + code;

  const Block = ({ accent, icon, title, blurb, code }) => (
    <div style={{ border: "1px solid var(--border)", borderRadius: 14, padding: 16, background: "var(--bg-2)" }}>
      <div style={{ display: "flex", alignItems: "center", gap: 9, marginBottom: 4 }}>
        <span style={{ color: accent, display: "inline-flex" }}><Icon name={icon} size={17} /></span>
        <span style={{ fontWeight: 600, fontSize: 14.5 }}>{title}</span>
      </div>
      <div style={{ fontSize: 12.5, color: "var(--faint)", marginBottom: 12 }}>{blurb}</div>
      <div style={{ display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap" }}>
        <div className="mono" style={{ flex: "1 1 120px", fontSize: 24, fontWeight: 700, letterSpacing: "0.22em", color: "var(--text)", background: "var(--bg)", border: "1px solid var(--border)", borderRadius: 10, padding: "10px 14px", textAlign: "center" }}>{code}</div>
        <CopyBtn text={code} label="Copy code" />
        <CopyBtn text={link(code)} label="Copy invite link" />
      </div>
    </div>
  );

  return (
    <div className="modal-backdrop" onMouseDown={(e) => { if (e.target === e.currentTarget) onClose(); }}>
      <div className="modal" style={{ maxWidth: 540 }}>
        <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", padding: "20px 24px" }}>
          <div style={{ display: "flex", alignItems: "center", gap: 12 }}>
            <span style={{ color: "var(--accent)", display: "inline-flex" }}><Icon name="share" size={20} /></span>
            <h2 className="display" style={{ fontSize: 19, fontWeight: 600 }}>Invite to {trip.name}</h2>
          </div>
          <button className="btn btn-icon btn-quiet" onClick={onClose} aria-label="Close"><Icon name="close" size={18} /></button>
        </div>
        <hr className="divider" />
        <div style={{ padding: 24, display: "flex", flexDirection: "column", gap: 16 }}>
          <div style={{ fontSize: 13.5, color: "var(--muted)" }}>
            Send your friends the site link, then a code below. They open the link, tap <b style={{ color: "var(--text)" }}>Join a trip</b>, and enter it. (Or just send the one-tap invite link.)
          </div>
          <Block accent="var(--accent)" icon="edit" title="Full access" blurb="Can add and edit charges, people, and payments." code={trip.editCode} />
          <Block accent="var(--pos)" icon="receipt" title="View only" blurb="Can see balances and the settle-up plan, but can't change anything." code={trip.viewCode} />
          <div style={{ display: "flex", alignItems: "center", gap: 10, paddingTop: 2 }}>
            <span style={{ fontSize: 13, color: "var(--faint)", flex: 1 }}>Plain site link (they'll enter a code)</span>
            <CopyBtn text={base} label="Copy link" small />
          </div>
        </div>
      </div>
    </div>
  );
}

// ---------- People bar ----------
function PeopleBar({ people, setPeople, txns, canEdit }) {
  const [name, setName] = useState("");
  const add = () => { const n = name.trim(); if (!n) return; setPeople((ps) => [...ps, { id: uid(), name: n }]); setName(""); };
  const remove = (id) => {
    const used = txns.some((t) => t.payers.some((p) => p.personId === id) || t.participants.includes(id));
    if (used && !confirm("This person is in one or more charges. Remove them anyway? Their charges will stay but may need fixing.")) return;
    setPeople((ps) => ps.filter((p) => p.id !== id));
  };
  const rename = (id, v) => setPeople((ps) => ps.map((p) => (p.id === id ? { ...p, name: v } : p)));

  return (
    <div className="card" style={{ padding: "18px 20px" }}>
      <div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 14 }}>
        <span style={{ color: "var(--muted)", display: "inline-flex" }}><Icon name="user" size={17} /></span>
        <span className="label" style={{ color: "var(--muted)" }}>Who's on the trip</span>
        <span className="tag mono" style={{ marginLeft: 2 }}>{people.length}</span>
      </div>

      {people.length > 0 && (
        <div style={{ display: "flex", flexWrap: "wrap", gap: 8, marginBottom: canEdit ? 14 : 0 }}>
          {people.map((p, i) => (
            <div key={p.id} className="rise" style={{ display: "flex", alignItems: "center", gap: 8, height: 40, padding: canEdit ? "0 6px" : "0 14px 0 6px", background: "var(--bg-2)", border: "1px solid var(--border)", borderRadius: 99 }}>
              <Avatar person={p} idx={i} size={28} />
              {canEdit ? (
                <input value={p.name} onChange={(e) => rename(p.id, e.target.value)}
                  style={{ background: "transparent", border: "none", height: "auto", width: Math.max(60, p.name.length * 8.5 + 8), padding: 0, color: "var(--text)", fontWeight: 500, fontSize: 14.5 }} />
              ) : (
                <span style={{ color: "var(--text)", fontWeight: 500, fontSize: 14.5 }}>{p.name}</span>
              )}
              {canEdit && <button className="btn btn-icon btn-quiet" style={{ width: 28, height: 28 }} onClick={() => remove(p.id)} aria-label="Remove"><Icon name="close" size={15} /></button>}
            </div>
          ))}
        </div>
      )}

      {canEdit && (
        <div style={{ display: "flex", alignItems: "center", gap: 16, flexWrap: "wrap" }}>
          <div style={{ display: "flex", gap: 10, flex: "0 1 400px", minWidth: 250 }}>
            <input style={{ flex: 1 }} value={name} onChange={(e) => setName(e.target.value)} onKeyDown={(e) => e.key === "Enter" && add()} placeholder={people.length ? "Add another person…" : "Add the first person…"} />
            <button className="btn btn-ghost" onClick={add} disabled={!name.trim()}><Icon name="plus" size={17} /> Add</button>
          </div>
          {people.length < 2 && (
            <span style={{ display: "inline-flex", alignItems: "center", gap: 8, fontSize: 13, color: "var(--faint)", lineHeight: 1.4 }}>
              <span style={{ color: "var(--accent)", display: "inline-flex", flex: "none" }}><Icon name="split" size={15} /></span>
              Add everyone splitting costs — you can rename or remove anyone later.
            </span>
          )}
        </div>
      )}
    </div>
  );
}

// ---------- Expense row ----------
function ExpenseRow({ txn, people, onEdit, onDelete, onOpen, canEdit }) {
  const payerNames = txn.payers.map((p) => people.find((x) => x.id === p.personId)).filter(Boolean);
  const multi = txn.payers.length > 1;
  return (
    <div className="rise tx-row" onClick={() => onOpen(txn)} title="View charge details" style={{ display: "flex", alignItems: "center", gap: 15, padding: "14px 18px", borderBottom: "1px solid var(--border)" }}>
      <span style={{ width: 40, height: 40, borderRadius: 11, background: "var(--bg-2)", border: "1px solid var(--border)", display: "flex", alignItems: "center", justifyContent: "center", flex: "none" }}>
        <CatIcon id={txn.category} size={19} />
      </span>
      <div style={{ flex: 1, minWidth: 0 }}>
        <div style={{ display: "flex", alignItems: "center", gap: 9, marginBottom: 4 }}>
          <span className="display tx-name" style={{ fontSize: 16, fontWeight: 600, flex: "1 1 auto", minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{txn.description}</span>
          <span className="mono tx-date" style={{ fontSize: 12, color: "var(--faint)", flex: "none" }}>{fmtDate(txn.date)}</span>
        </div>
        <div style={{ display: "flex", alignItems: "center", gap: 9, flexWrap: "wrap", fontSize: 13, color: "var(--muted)" }}>
          <span style={{ display: "inline-flex", alignItems: "center", gap: 6 }}>
            {multi ? <AvatarStack ids={txn.payers.map((p) => p.personId)} people={people} size={21} /> : (payerNames[0] && <Avatar person={payerNames[0]} idx={people.findIndex((x) => x.id === payerNames[0].id)} size={21} />)}
            <span style={{ color: "var(--text)", fontWeight: 500 }}>{multi ? `${txn.payers.length} paid` : (payerNames[0]?.name || "—") + " paid"}</span>
          </span>
          <span style={{ color: "var(--border-strong)" }}>•</span>
          <span style={{ display: "inline-flex", alignItems: "center", gap: 7 }}>
            <AvatarStack ids={txn.participants} people={people} size={21} />
            <span>{splitModeLabel(txn.splitMode)}</span>
          </span>
        </div>
        {txn.note && (
          <div style={{ display: "flex", alignItems: "center", gap: 6, marginTop: 5, fontSize: 12.5, color: "var(--faint)", minWidth: 0 }}>
            <span style={{ color: "var(--accent)", display: "inline-flex", flex: "none" }}><Icon name="receipt" size={12} /></span>
            <span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{txn.note}</span>
          </div>
        )}
      </div>
      <div className="mono display tx-amt" style={{ fontSize: 18, fontWeight: 600, letterSpacing: "-0.01em" }}>{money(txn.amount)}</div>
      {canEdit && (
        <div style={{ display: "flex", gap: 4 }}>
          <button className="btn btn-icon btn-quiet" onClick={(e) => { e.stopPropagation(); onEdit(txn); }} aria-label="Edit"><Icon name="edit" size={17} /></button>
          <button className="btn btn-icon btn-quiet" onClick={(e) => { e.stopPropagation(); onDelete(txn.id); }} aria-label="Delete"><Icon name="trash" size={17} /></button>
        </div>
      )}
    </div>
  );
}

// ---------- Charges toolbar ----------
function ChargesToolbar({ search, setSearch, sort, setSort, filterCat, setFilterCat, txns }) {
  const present = useMemo(() => new Set(txns.map((t) => t.category)), [txns]);
  return (
    <div style={{ display: "flex", alignItems: "center", gap: 10, padding: "12px 16px", flexWrap: "wrap" }}>
      <div style={{ position: "relative", flex: "1 1 200px", minWidth: 160 }}>
        <span style={{ position: "absolute", left: 12, top: "50%", transform: "translateY(-50%)", color: "var(--faint)", display: "inline-flex" }}><Icon name="search" size={16} /></span>
        <input value={search} onChange={(e) => setSearch(e.target.value)} placeholder="Search charges…" style={{ height: 38, paddingLeft: 36, fontSize: 14 }} />
        {search && <button className="btn btn-icon btn-quiet" style={{ position: "absolute", right: 4, top: 4, width: 30, height: 30 }} onClick={() => setSearch("")}><Icon name="close" size={14} /></button>}
      </div>

      <Dropdown width={190}
        trigger={(open, toggle) => (
          <button className="btn btn-ghost btn-sm" onClick={toggle} style={{ height: 38 }}>
            <Icon name="folder" size={15} /> {filterCat === "all" ? "All categories" : catOf(filterCat).label}
            <span style={{ display: "inline-flex", color: "var(--faint)" }}><Icon name="chevron" size={14} /></span>
          </button>
        )}>
        {(close) => (
          <>
            <button className="menu-item" style={{ background: filterCat === "all" ? "var(--surface-2)" : "transparent" }} onClick={() => { setFilterCat("all"); close(); }}>
              <span style={{ width: 20 }} /> All categories {filterCat === "all" && <span style={{ marginLeft: "auto", color: "var(--accent)" }}><Icon name="check" size={15} /></span>}
            </button>
            {CATEGORIES.filter((c) => present.has(c.id)).map((c) => (
              <button key={c.id} className="menu-item" style={{ background: filterCat === c.id ? "var(--surface-2)" : "transparent" }} onClick={() => { setFilterCat(c.id); close(); }}>
                <CatIcon id={c.id} size={17} /> {c.label} {filterCat === c.id && <span style={{ marginLeft: "auto", color: "var(--accent)" }}><Icon name="check" size={15} /></span>}
              </button>
            ))}
          </>
        )}
      </Dropdown>

      <Dropdown width={180}
        trigger={(open, toggle) => (
          <button className="btn btn-ghost btn-sm" onClick={toggle} style={{ height: 38 }}>
            <Icon name="sort" size={15} /> {SORTS.find((s) => s.id === sort).label}
            <span style={{ display: "inline-flex", color: "var(--faint)" }}><Icon name="chevron" size={14} /></span>
          </button>
        )}>
        {(close) => SORTS.map((s) => (
          <button key={s.id} className="menu-item" style={{ background: sort === s.id ? "var(--surface-2)" : "transparent" }} onClick={() => { setSort(s.id); close(); }}>
            <span style={{ width: 20 }} /> {s.label} {sort === s.id && <span style={{ marginLeft: "auto", color: "var(--accent)" }}><Icon name="check" size={15} /></span>}
          </button>
        ))}
      </Dropdown>
    </div>
  );
}

// ---------- App ----------
function App() {
  const [store, setStore] = useState({ status: "loading", trips: [], activeId: null, admin: false, adminEnabled: false });
  const storeRef = useRef(store);
  useEffect(() => { storeRef.current = store; });

  const [modal, setModal] = useState(null);
  const [personId, setPersonId] = useState(null);
  const [chargeId, setChargeId] = useState(null);
  const [showExport, setShowExport] = useState(false);
  const [showShare, setShowShare] = useState(false);
  const [showJoin, setShowJoin] = useState(false);
  const [showAdmin, setShowAdmin] = useState(false);
  const [search, setSearch] = useState("");
  const [sort, setSort] = useState("date_desc");
  const [filterCat, setFilterCat] = useState("all");

  const saveTimer = useRef(null);
  const pendingSave = useRef(false);

  const active = store.trips.find((t) => t.id === store.activeId);
  const canEdit = !!active && active.role !== "view";

  const syncUrl = (id) => { try { const u = new URL(location.href); u.searchParams.set("trip", id); u.searchParams.delete("join"); history.replaceState(null, "", u); } catch (e) {} };

  // add/replace a trip in the store + persist membership
  const upsertTrip = (t, code, owner) => {
    setStore((s) => {
      const exists = s.trips.find((x) => x.id === t.id);
      const merged = { ...t, code, owner: exists ? exists.owner : owner, admin: s.admin };
      const trips = exists ? s.trips.map((x) => (x.id === t.id ? merged : x)) : [...s.trips, merged];
      if (!s.admin) saveMine(trips);
      return { ...s, status: "ready", trips, activeId: t.id };
    });
    syncUrl(t.id);
  };

  const doJoin = async (code) => {
    const c = code.trim().toUpperCase();
    const t = await api.join(c);            // throws on bad code
    upsertTrip(t, c, false);
    setSearch(""); setFilterCat("all");
    return t;
  };

  // initial load
  useEffect(() => {
    let alive = true;
    (async () => {
      const cfg = await api.getConfig();
      const adminEnabled = !!cfg.adminEnabled;

      // admin mode: load every trip
      if (adminKey) {
        try {
          const all = await api.adminTrips();
          if (!alive) return;
          const trips = all.map((t) => ({ ...t, code: t.editCode, owner: true, admin: true }));
          const urlTrip = new URLSearchParams(location.search).get("trip");
          const activeId = (urlTrip && trips.find((t) => t.id === urlTrip)) ? urlTrip : (trips[0] ? trips[0].id : null);
          setStore({ status: "ready", admin: true, adminEnabled, trips, activeId });
          if (activeId) syncUrl(activeId);
          return;
        } catch (e) { setAdminKey(null); /* bad/old admin key → fall back to normal */ }
      }

      const mine = loadMine();
      const trips = [];
      for (const e of mine) {
        try { const t = await api.getTrip(e.id, e.code); trips.push({ ...t, code: e.code, owner: !!e.owner }); }
        catch (err) { /* code revoked or trip gone — drop it */ }
      }
      if (!alive) return;
      const params = new URLSearchParams(location.search);
      const joinCode = params.get("join");
      const urlTrip = params.get("trip");

      if (joinCode) {
        const existing = trips.find((t) => t.code === joinCode.toUpperCase());
        if (existing) { setStore({ status: "ready", admin: false, adminEnabled, trips, activeId: existing.id }); syncUrl(existing.id); return; }
        try {
          const t = await api.join(joinCode.toUpperCase());
          const merged = { ...t, code: joinCode.toUpperCase(), owner: false };
          const all = [...trips, merged];
          setStore({ status: "ready", admin: false, adminEnabled, trips: all, activeId: t.id }); saveMine(all); syncUrl(t.id);
          return;
        } catch (e) { /* invalid link code — fall through */ }
      }

      const activeId = (urlTrip && trips.find((t) => t.id === urlTrip)) ? urlTrip : (trips[0] ? trips[0].id : null);
      setStore({ status: "ready", admin: false, adminEnabled, trips, activeId });
      if (activeId) syncUrl(activeId);
      // deep link to a trip we can't open yet → prompt for the code
      if (urlTrip && !trips.find((t) => t.id === urlTrip)) setShowJoin(true);
    })();
    return () => { alive = false; };
  }, []);

  const scheduleSave = (tripId) => {
    pendingSave.current = true;
    clearTimeout(saveTimer.current);
    saveTimer.current = setTimeout(async () => {
      const t = storeRef.current.trips.find((x) => x.id === tripId);
      if (!t || t.role === "view") { pendingSave.current = false; return; }
      try {
        const res = await api.saveTrip(t, t.code);
        setStore((s) => ({ ...s, trips: s.trips.map((x) => (x.id === tripId ? { ...x, rev: res.rev } : x)) }));
      } catch (e) {}
      pendingSave.current = false;
    }, 450);
  };

  const patchTrip = (patch) => {
    const cur = storeRef.current.trips.find((t) => t.id === storeRef.current.activeId);
    if (!cur || cur.role === "view") return;
    const id = storeRef.current.activeId;
    setStore((s) => ({ ...s, trips: s.trips.map((t) => (t.id === s.activeId ? { ...t, ...(typeof patch === "function" ? patch(t) : patch) } : t)) }));
    scheduleSave(id);
  };
  const setPeople = (fn) => patchTrip((t) => ({ people: typeof fn === "function" ? fn(t.people) : fn }));
  const setTxns = (fn) => patchTrip((t) => ({ txns: typeof fn === "function" ? fn(t.txns) : fn }));
  const setTripName = (v) => patchTrip({ name: v });

  const saveTxn = (t) => {
    setTxns((cur) => { const i = cur.findIndex((x) => x.id === t.id); if (i >= 0) { const n = [...cur]; n[i] = t; return n; } return [t, ...cur]; });
    setModal(null);
  };
  const deleteTxn = (id) => setTxns((cur) => cur.filter((x) => x.id !== id));
  const addPayment = (pm) => patchTrip((t) => ({ payments: [...(t.payments || []), { id: uid(), ...pm, ts: Date.now() }] }));
  const removePayment = (id) => patchTrip((t) => ({ payments: (t.payments || []).filter((p) => p.id !== id) }));

  // trips
  const switchTrip = (id) => { setStore((s) => ({ ...s, activeId: id })); setSearch(""); setFilterCat("all"); syncUrl(id); };
  const createTrip = async () => {
    const name = prompt("Name this trip", "New trip");
    if (name === null) return;
    try { const t = await api.createTrip(name.trim() || "New trip"); upsertTrip(t, t.editCode, true); setSearch(""); setFilterCat("all"); }
    catch (e) { alert("Couldn't create the trip — check your connection and try again."); }
  };
  const removeFromMine = (id) => {
    setStore((s) => {
      const trips = s.trips.filter((t) => t.id !== id);
      const activeId = id === s.activeId ? (trips[0] ? trips[0].id : null) : s.activeId;
      if (!s.admin) saveMine(trips);
      if (activeId) syncUrl(activeId);
      return { ...s, trips, activeId };
    });
  };
  const deleteTrip = async (id) => {
    const t = store.trips.find((x) => x.id === id);
    try { if (t && t.owner) await api.deleteTrip(id, t.code); } catch (e) {}
    removeFromMine(id);
  };
  const leaveTrip = (id) => removeFromMine(id);

  // admin
  const adminLogin = async (key) => {
    const all = await api.adminTrips(key);   // throws on bad key
    setAdminKey(key);
    const trips = all.map((t) => ({ ...t, code: t.editCode, owner: true, admin: true }));
    setStore((s) => ({ ...s, status: "ready", admin: true, trips, activeId: trips[0] ? trips[0].id : null }));
    if (trips[0]) syncUrl(trips[0].id);
    setSearch(""); setFilterCat("all"); setShowAdmin(false);
  };
  const adminLogout = () => { setAdminKey(null); location.reload(); };

  // live sync — pull others' edits without clobbering active typing or a pending save
  useEffect(() => {
    if (store.status !== "ready" || !store.activeId) return;
    const iv = setInterval(async () => {
      if (pendingSave.current) return;
      if (document.querySelector(".modal-backdrop")) return;
      const ae = document.activeElement;
      if (ae && (ae.tagName === "INPUT" || ae.tagName === "TEXTAREA")) return;
      const cur = storeRef.current.trips.find((t) => t.id === storeRef.current.activeId);
      if (!cur) return;
      try {
        const fresh = await api.getTrip(cur.id, cur.code);
        setStore((s) => {
          const c2 = s.trips.find((t) => t.id === fresh.id);
          if (!c2 || c2.rev === fresh.rev) return s;
          return { ...s, trips: s.trips.map((t) => (t.id === fresh.id ? { ...fresh, code: t.code, owner: t.owner } : t)) };
        });
      } catch (e) {}
    }, 5000);
    return () => clearInterval(iv);
  }, [store.status, store.activeId]);

  // filtered + sorted charges (hook must run every render)
  const txnsForCalc = active ? active.txns : EMPTY;
  const shown = useMemo(() => {
    const list = txnsForCalc.filter((t) => (filterCat === "all" || t.category === filterCat) && (!search.trim() || t.description.toLowerCase().includes(search.trim().toLowerCase())));
    const by = {
      date_desc: (a, b) => (b.date || "").localeCompare(a.date || "") || b.createdAt - a.createdAt,
      date_asc: (a, b) => (a.date || "").localeCompare(b.date || "") || a.createdAt - b.createdAt,
      amt_desc: (a, b) => b.amount - a.amount,
      amt_asc: (a, b) => a.amount - b.amount,
    }[sort];
    return [...list].sort(by);
  }, [txnsForCalc, filterCat, search, sort]);

  if (store.status === "loading") return <Splash text="Loading your trips…" />;

  if (!active) {
    return (
      <>
        <Welcome onCreate={createTrip} onJoin={doJoin} adminEnabled={store.adminEnabled} onAdmin={() => setShowAdmin(true)} />
        {showJoin && <JoinModal onJoin={doJoin} onClose={() => setShowJoin(false)} />}
        {showAdmin && <AdminModal onLogin={adminLogin} onClose={() => setShowAdmin(false)} />}
      </>
    );
  }

  const { people, txns, payments } = active;
  const canAdd = canEdit && people.length >= 2;
  const toolbarVisible = txns.length > 2;

  return (
    <div className="app-shell" style={{ maxWidth: 1180, margin: "0 auto", padding: "30px 28px 80px" }}>
      {/* Header */}
      <header className="app-header" style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 26, flexWrap: "wrap", gap: 16 }}>
        <div style={{ display: "flex", alignItems: "center", gap: 14 }}>
          <div style={{ width: 42, height: 42, borderRadius: 12, background: "var(--accent)", display: "flex", alignItems: "center", justifyContent: "center", boxShadow: "0 6px 18px oklch(0.71 0.132 44 / 0.4)" }}>
            <span className="display" style={{ color: "var(--accent-ink)", fontWeight: 700, fontSize: 22 }}>T</span>
          </div>
          <div>
            <div style={{ display: "flex", alignItems: "center", gap: 9, flexWrap: "wrap" }}>
              <input value={active.name} onChange={(e) => setTripName(e.target.value)} className="display trip-title" readOnly={!canEdit}
                style={{ background: "transparent", border: "none", height: "auto", padding: 0, color: "var(--text)", fontSize: 26, fontWeight: 700, letterSpacing: "-0.02em", width: Math.max(120, active.name.length * 15 + 20), cursor: canEdit ? "text" : "default" }} />
              {store.admin && <span className="tag" style={{ color: "var(--accent)", borderColor: "var(--accent-line)", background: "var(--accent-quiet)" }}>Admin</span>}
              {!canEdit && <span className="tag" style={{ color: "var(--pos)", borderColor: "var(--pos-quiet)", background: "var(--pos-quiet)" }}>View only</span>}
            </div>
            <div style={{ fontSize: 13, color: "var(--faint)", marginTop: -1 }}>Trip cost splitter</div>
          </div>
        </div>
        <div className="header-actions" style={{ display: "flex", alignItems: "center", gap: 10 }}>
          {canEdit && (
            <button className="btn btn-ghost" onClick={() => setShowShare(true)} title="Share this trip with a code">
              <Icon name="share" size={16} /> Share
            </button>
          )}
          <TripMenu trips={store.trips} activeId={store.activeId} onSwitch={switchTrip} onNew={createTrip} onJoin={() => setShowJoin(true)} onDelete={deleteTrip} onLeave={leaveTrip}
            admin={store.admin} adminEnabled={store.adminEnabled} onAdmin={() => setShowAdmin(true)} onExitAdmin={adminLogout} />
          {canEdit && (
            <button className="btn btn-accent" onClick={() => setModal({})} disabled={people.length < 2} title={people.length >= 2 ? "" : "Add at least 2 people first"}>
              <Icon name="plus" size={18} /> Add a charge
            </button>
          )}
        </div>
      </header>

      <div style={{ marginBottom: 18 }}>
        <PeopleBar people={people} setPeople={setPeople} txns={txns} canEdit={canEdit} />
      </div>

      {/* Main grid */}
      <div className={"grid-main" + (canEdit ? "" : " viewer")} style={{ display: "grid", gridTemplateColumns: "minmax(0, 1fr) 396px", gap: 18, alignItems: "start" }}>
        <div className="card charges-card" style={{ overflow: "hidden" }}>
          <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", padding: "18px 18px 16px" }}>
            <div style={{ display: "flex", alignItems: "center", gap: 9 }}>
              <span style={{ color: "var(--muted)", display: "inline-flex" }}><Icon name="receipt" size={18} /></span>
              <span className="label" style={{ color: "var(--muted)" }}>Charges</span>
              {txns.length > 0 && <span className="tag mono">{txns.length}</span>}
            </div>
            {txns.length > 0 && canAdd && <button className="btn btn-sm btn-ghost" onClick={() => setModal({})}><Icon name="plus" size={15} /> Add</button>}
          </div>
          <hr className="divider" />

          {!canEdit && txns.length === 0 ? (
            <div style={{ padding: 22 }}><div className="empty"><div className="display" style={{ fontSize: 16, color: "var(--muted)", marginBottom: 6 }}>No charges yet</div><div style={{ fontSize: 13.5 }}>Nothing has been logged for this trip.</div></div></div>
          ) : !canEdit ? null : people.length < 2 ? (
            <div style={{ padding: 22 }}>
              <div className="empty">
                <div className="display" style={{ fontSize: 16, color: "var(--muted)", marginBottom: 6 }}>Add your travelers first</div>
                <div style={{ fontSize: 13.5 }}>You need at least 2 people before logging charges.</div>
              </div>
            </div>
          ) : txns.length === 0 ? (
            <div style={{ padding: 22 }}>
              <div className="empty">
                <div style={{ color: "var(--accent)", display: "inline-flex", marginBottom: 10 }}><Icon name="receipt" size={28} /></div>
                <div className="display" style={{ fontSize: 16, color: "var(--muted)", marginBottom: 6 }}>No charges yet</div>
                <div style={{ fontSize: 13.5, marginBottom: 16 }}>Log every cab, dinner, and hotel — split however you like.</div>
                <button className="btn btn-accent btn-sm" onClick={() => setModal({})}><Icon name="plus" size={15} /> Add the first charge</button>
              </div>
            </div>
          ) : null}

          {txns.length > 0 && (
            <div>
              {toolbarVisible && (
                <>
                  <ChargesToolbar search={search} setSearch={setSearch} sort={sort} setSort={setSort} filterCat={filterCat} setFilterCat={setFilterCat} txns={txns} />
                  <hr className="divider" />
                </>
              )}
              {shown.length === 0 ? (
                <div style={{ padding: "40px 24px", textAlign: "center", color: "var(--faint)", fontSize: 14 }}>
                  No charges match <b style={{ color: "var(--text)" }}>{search ? `"${search}"` : "this filter"}</b>.
                </div>
              ) : shown.map((t) => <ExpenseRow key={t.id} txn={t} people={people} onEdit={setModal} onDelete={deleteTxn} onOpen={(x) => setChargeId(x.id)} canEdit={canEdit} />)}
            </div>
          )}
        </div>

        {/* Right column */}
        <div className="settle-col" style={{ position: "sticky", top: 24, display: "flex", flexDirection: "column", gap: 16 }}>
          <Breakdown txns={txns} people={people} />
          <SettlePanel people={people} txns={txns} payments={payments} canEdit={canEdit}
            onAddPayment={addPayment} onRemovePayment={removePayment}
            onOpenPerson={setPersonId} onExport={() => setShowExport(true)} />
        </div>
      </div>

      {modal !== null && canEdit && (
        <ExpenseModal people={people} initial={modal.id ? modal : null} onSave={saveTxn} onClose={() => setModal(null)} />
      )}
      {personId && <PersonDetail personId={personId} people={people} txns={txns} payments={payments} onClose={() => setPersonId(null)} />}
      {chargeId && txns.find((t) => t.id === chargeId) && <ChargeDetail txn={txns.find((t) => t.id === chargeId)} people={people} onClose={() => setChargeId(null)} />}
      {showExport && <ExportModal trip={active} onClose={() => setShowExport(false)} />}
      {showShare && canEdit && <ShareModal trip={active} onClose={() => setShowShare(false)} />}
      {showJoin && <JoinModal onJoin={doJoin} onClose={() => setShowJoin(false)} />}
      {showAdmin && <AdminModal onLogin={adminLogin} onClose={() => setShowAdmin(false)} />}
    </div>
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(<App />);
