// ===== util.jsx — money helpers, share calc, settlement, categories, atoms =====
const { useState, useEffect, useRef, useMemo } = React;

const uid = () => Math.random().toString(36).slice(2, 10);

// ---- money: integer cents to avoid float drift ----
const toCents = (n) => Math.round((Number(n) || 0) * 100);
const fromCents = (c) => c / 100;

// percentage of a whole, to 1 decimal place (splits can be fractional)
const pctOf = (part, whole) => (whole > 0 ? (part / whole) * 100 : 0).toFixed(1);

function money(n, { sign = false, dash = true } = {}) {
  const c = Math.round((Number(n) || 0) * 100);
  if (c === 0 && dash) return "$0.00";
  const abs = Math.abs(c) / 100;
  const s = "$" + abs.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
  if (sign) return (c < 0 ? "−" : "+") + s;
  return (c < 0 ? "−" : "") + s;
}

const todayISO = () => {
  const d = new Date();  // local date, not UTC (toISOString rolls over in the evening west of UTC)
  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
};
function fmtDate(iso) {
  if (!iso) return "";
  const [y, m, d] = iso.split("-").map(Number);
  const dt = new Date(y, m - 1, d);
  return dt.toLocaleDateString("en-US", { month: "short", day: "numeric" });
}

// ---- categories ----
const CATEGORIES = [
  { id: "lodging", label: "Lodging", icon: "bed", hue: 255 },
  { id: "food", label: "Food & drink", icon: "food", hue: 44 },
  { id: "transport", label: "Transport", icon: "car", hue: 162 },
  { id: "activities", label: "Activities", icon: "star", hue: 320 },
  { id: "groceries", label: "Groceries", icon: "cart", hue: 130 },
  { id: "shopping", label: "Shopping", icon: "bag", hue: 205 },
  { id: "fees", label: "Fees & misc", icon: "dots", hue: 20 },
];
const catOf = (id) => CATEGORIES.find((c) => c.id === id) || CATEGORIES[CATEGORIES.length - 1];
const catColor = (id) => `oklch(0.73 0.115 ${catOf(id).hue})`;

function categoryTotals(txns) {
  const map = {};
  txns.forEach((t) => {
    const id = t.category || "fees";
    map[id] = (map[id] || 0) + toCents(t.amount);
  });
  return Object.entries(map)
    .map(([id, c]) => ({ id, label: catOf(id).label, icon: catOf(id).icon, value: fromCents(c), color: catColor(id) }))
    .filter((x) => x.value > 0)
    .sort((a, b) => b.value - a.value);
}

// Avatar palette
const AV_HUES = [44, 162, 250, 320, 96, 200, 18, 285, 130, 0];
const personColor = (i) => `oklch(0.74 0.118 ${AV_HUES[i % AV_HUES.length]})`;

// ---- per-transaction shares, returns {personId: cents} ----
function distributeRemainder(raw, rem) {
  // raw: [{id, c}] sorted desc by c; nudge by ±1 to absorb rounding
  let k = 0;
  while (rem !== 0 && raw.length) {
    raw[k % raw.length].c += rem > 0 ? 1 : -1;
    rem += rem > 0 ? -1 : 1;
    k++;
  }
}

function computeShares(txn, people) {
  const parts = txn.participants.filter((id) => people.some((p) => p.id === id));
  const total = toCents(txn.amount);
  const out = {};
  if (parts.length === 0 || total === 0) {
    parts.forEach((id) => (out[id] = 0));
    return out;
  }
  if (txn.splitMode === "equal") {
    const base = Math.floor(total / parts.length);
    let rem = total - base * parts.length;
    parts.forEach((id, i) => (out[id] = base + (i < rem ? 1 : 0)));
  } else if (txn.splitMode === "exact") {
    parts.forEach((id) => (out[id] = toCents(txn.splits?.[id]) || 0));
  } else if (txn.splitMode === "percent") {
    let allocated = 0;
    const raw = parts.map((id) => {
      const c = Math.round((total * (Number(txn.splits?.[id]) || 0)) / 100);
      allocated += c;
      return { id, c };
    });
    raw.sort((a, b) => b.c - a.c);
    distributeRemainder(raw, total - allocated);
    raw.forEach((r) => (out[r.id] = r.c));
  } else if (txn.splitMode === "shares") {
    const sh = parts.map((id) => ({ id, sh: Math.max(0, Number(txn.splits?.[id]) || 0) }));
    const totalSh = sh.reduce((s, x) => s + x.sh, 0);
    if (totalSh <= 0) {
      const base = Math.floor(total / parts.length);
      let rem = total - base * parts.length;
      parts.forEach((id, i) => (out[id] = base + (i < rem ? 1 : 0)));
    } else {
      let allocated = 0;
      const raw = sh.map((x) => {
        const c = Math.round((total * x.sh) / totalSh);
        allocated += c;
        return { id: x.id, c };
      });
      raw.sort((a, b) => b.c - a.c);
      distributeRemainder(raw, total - allocated);
      raw.forEach((r) => (out[r.id] = r.c));
    }
  }
  return out;
}

function splitStatus(txn) {
  const total = toCents(txn.amount);
  const parts = txn.participants;
  if (txn.splitMode === "equal") return { ok: true };
  if (txn.splitMode === "shares") {
    const totalSh = parts.reduce((s, id) => s + Math.max(0, Number(txn.splits?.[id]) || 0), 0);
    return { ok: totalSh > 0, totalShares: totalSh };
  }
  if (txn.splitMode === "exact") {
    const sum = parts.reduce((s, id) => s + (toCents(txn.splits?.[id]) || 0), 0);
    return { ok: sum === total, sum: fromCents(sum), target: fromCents(total), remaining: fromCents(total - sum) };
  }
  if (txn.splitMode === "percent") {
    const sum = parts.reduce((s, id) => s + (Number(txn.splits?.[id]) || 0), 0);
    return { ok: Math.abs(sum - 100) < 0.005, sum, target: 100, remaining: Math.round((100 - sum) * 100) / 100 };
  }
  return { ok: true };
}

function payerStatus(txn) {
  const total = toCents(txn.amount);
  const sum = txn.payers.reduce((s, p) => s + (toCents(p.amount) || 0), 0);
  return { ok: sum === total, sum: fromCents(sum), remaining: fromCents(total - sum) };
}

// ---- net balances (cents). +ve => is owed money. Includes settle payments ----
function computeBalances(people, txns, payments = []) {
  const bal = {};
  people.forEach((p) => (bal[p.id] = 0));
  txns.forEach((t) => {
    t.payers.forEach((pay) => { if (bal[pay.personId] != null) bal[pay.personId] += toCents(pay.amount) || 0; });
    const shares = computeShares(t, people);
    Object.entries(shares).forEach(([pid, c]) => { if (bal[pid] != null) bal[pid] -= c; });
  });
  // a settle payment: `from` hands cash to `to` → reduces from's debt, reduces to's credit
  payments.forEach((pm) => {
    if (bal[pm.from] != null) bal[pm.from] += toCents(pm.amt) || 0;
    if (bal[pm.to] != null) bal[pm.to] -= toCents(pm.amt) || 0;
  });
  return bal;
}

// ---- minimal-transfer settlement ----
function settle(balCents) {
  const creditors = [], debtors = [];
  Object.entries(balCents).forEach(([id, c]) => {
    if (c > 0) creditors.push({ id, amt: c });
    else if (c < 0) debtors.push({ id, amt: -c });
  });
  creditors.sort((a, b) => b.amt - a.amt);
  debtors.sort((a, b) => b.amt - a.amt);
  const transfers = [];
  let ci = 0, di = 0, guard = 0;
  while (ci < creditors.length && di < debtors.length && guard < 10000) {
    guard++;
    const c = creditors[ci], d = debtors[di];
    const m = Math.min(c.amt, d.amt);
    if (m > 0) transfers.push({ from: d.id, to: c.id, amt: fromCents(m) });
    c.amt -= m; d.amt -= m;
    if (c.amt === 0) ci++;
    if (d.amt === 0) di++;
  }
  return transfers;
}

// ---- per-person detail rollup ----
function personRollup(personId, people, txns, payments = []) {
  let paid = 0, owed = 0;
  const inCharges = [];
  txns.forEach((t) => {
    const shares = computeShares(t, people);
    const myShare = shares[personId] || 0;
    const myPay = t.payers.filter((p) => p.personId === personId).reduce((s, p) => s + (toCents(p.amount) || 0), 0);
    if (myShare > 0 || myPay > 0) {
      paid += myPay; owed += myShare;
      inCharges.push({ txn: t, share: fromCents(myShare), paid: fromCents(myPay) });
    }
  });
  inCharges.sort((a, b) => (b.txn.date || "").localeCompare(a.txn.date || ""));
  return { paid: fromCents(paid), owed: fromCents(owed), inCharges };
}

// ---- export summary text ----
function buildSummary(trip) {
  const { name, people, txns, payments = [] } = trip;
  const bal = computeBalances(people, txns, payments);
  const transfers = settle(bal);
  const total = txns.reduce((s, t) => s + (Number(t.amount) || 0), 0);
  const L = [];
  L.push(name);
  L.push("─".repeat(Math.max(8, name.length)));
  L.push(`Total spend: ${money(total)}   ·   ${txns.length} charges   ·   ${people.length} people`);
  L.push("");
  L.push("NET BALANCES");
  people.forEach((p) => {
    const b = fromCents(bal[p.id] || 0);
    L.push(`  • ${p.name}: ${Math.abs(b) < 0.005 ? "settled up" : money(b, { sign: true })}`);
  });
  L.push("");
  L.push("WHO PAYS WHOM");
  if (!transfers.length) L.push("  ✓ All settled — nobody owes anything.");
  transfers.forEach((t) => {
    const f = people.find((p) => p.id === t.from), to = people.find((p) => p.id === t.to);
    L.push(`  ${f?.name}  →  ${to?.name}:  ${money(t.amt)}`);
  });
  if (payments.length) {
    L.push("");
    L.push("ALREADY PAID");
    payments.forEach((pm) => {
      const f = people.find((p) => p.id === pm.from), to = people.find((p) => p.id === pm.to);
      L.push(`  ✓ ${f?.name}  →  ${to?.name}:  ${money(pm.amt)}`);
    });
  }
  // per-person breakdown (cost + % of total spend)
  L.push("");
  L.push("PER PERSON");
  people.forEach((p) => {
    const roll = personRollup(p.id, people, txns, payments);
    const pct = pctOf(roll.owed, total);
    const b = fromCents(bal[p.id] || 0);
    const net = Math.abs(b) < 0.005 ? "settled up" : money(b, { sign: true });
    L.push(`  ${p.name} — paid ${money(roll.paid)}, share ${money(roll.owed)} (${pct}%), net ${net}`);
  });

  const cats = categoryTotals(txns);
  if (cats.length) {
    L.push("");
    L.push("BY CATEGORY");
    cats.forEach((c) => L.push(`  ${c.label}: ${money(c.value)}  (${pctOf(c.value, total)}%)`));
  }

  // itemized charges with each person's share
  if (txns.length) {
    L.push("");
    L.push("CHARGES (DETAIL)");
    [...txns].sort((a, b) => (a.date || "").localeCompare(b.date || "")).forEach((t) => {
      const shares = computeShares(t, people);
      const payer = t.payers.length === 1
        ? (people.find((p) => p.id === t.payers[0].personId)?.name || "?")
        : t.payers.map((p) => `${people.find((x) => x.id === p.personId)?.name || "?"} ${money(p.amount)}`).join(", ");
      L.push("");
      L.push(`  ${fmtDate(t.date)} · ${t.description} — ${money(t.amount)} [${catOf(t.category).label}]`);
      L.push(`    paid by ${payer}`);
      if (t.note) L.push(`    note: ${t.note}`);
      Object.entries(shares).forEach(([pid, c]) => {
        L.push(`    ${people.find((p) => p.id === pid)?.name || "?"}: ${money(fromCents(c))}`);
      });
    });
  }

  L.push("");
  L.push("Made with Tally");
  return L.join("\n");
}

// ---------- atoms ----------
function Avatar({ person, size = 30, idx = 0, ring = false }) {
  const initials = (person?.name || "?").trim().split(/\s+/).slice(0, 2).map((w) => w[0]).join("").toUpperCase() || "?";
  return (
    <span className="av" title={person?.name}
      style={{ "--c": personColor(idx), width: size, height: size, fontSize: size * 0.42,
        boxShadow: ring ? "inset 0 0 0 1px rgba(255,255,255,0.12), 0 0 0 3px var(--bg)" : undefined }}>
      {initials}
    </span>
  );
}

function AvatarStack({ ids, people, size = 26, max = 5 }) {
  const list = ids.map((id) => people.findIndex((p) => p.id === id)).filter((i) => i >= 0);
  const shown = list.slice(0, max);
  const extra = list.length - shown.length;
  return (
    <span style={{ display: "inline-flex", alignItems: "center" }}>
      {shown.map((i, k) => (
        <span key={i} style={{ marginLeft: k === 0 ? 0 : -size * 0.34 }}>
          <Avatar person={people[i]} idx={i} size={size} ring />
        </span>
      ))}
      {extra > 0 && (
        <span className="av mono" style={{ marginLeft: -size * 0.34, width: size, height: size, fontSize: size * 0.38,
          background: "var(--surface-3)", color: "var(--muted)", boxShadow: "inset 0 0 0 1px rgba(255,255,255,0.08), 0 0 0 3px var(--bg)" }}>
          +{extra}
        </span>
      )}
    </span>
  );
}

function CatIcon({ id, size = 16 }) {
  return <span style={{ color: catColor(id), display: "inline-flex" }}><Icon name={catOf(id).icon} size={size} /></span>;
}

function Donut({ segments, total, size = 168, thickness = 20, centerLabel, centerSub }) {
  const r = (size - thickness) / 2;
  const c = 2 * Math.PI * r;
  const gap = segments.length > 1 ? 0.012 * c : 0;
  let offset = 0;
  return (
    <div style={{ position: "relative", width: size, height: size, flex: "none" }}>
      <svg width={size} height={size} style={{ transform: "rotate(-90deg)" }}>
        <circle cx={size / 2} cy={size / 2} r={r} fill="none" stroke="var(--bg-2)" strokeWidth={thickness} />
        {total > 0 && segments.map((s, i) => {
          const frac = s.value / total;
          const len = Math.max(0, frac * c - gap);
          const el = (
            <circle key={i} cx={size / 2} cy={size / 2} r={r} fill="none"
              stroke={s.color} strokeWidth={thickness} strokeLinecap="round"
              strokeDasharray={`${len} ${c - len}`} strokeDashoffset={-offset}
              style={{ transition: "stroke-dasharray 0.5s var(--ease), stroke-dashoffset 0.5s var(--ease)" }} />
          );
          offset += frac * c;
          return el;
        })}
      </svg>
      <div style={{ position: "absolute", inset: 0, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center" }}>
        <div className="mono display" style={{ fontSize: Math.max(18, Math.round(size * 0.135)), fontWeight: 700, letterSpacing: "-0.02em" }}>{centerLabel}</div>
        {centerSub && <div style={{ fontSize: 11.5, color: "var(--faint)", marginTop: 1 }}>{centerSub}</div>}
      </div>
    </div>
  );
}

function Icon({ name, size = 18, stroke = 1.8 }) {
  const p = {
    plus: "M12 5v14M5 12h14",
    trash: "M4 7h16M9 7V5a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2m2 0-.7 12a2 2 0 0 1-2 1.9H8.7a2 2 0 0 1-2-1.9L6 7",
    edit: "M4 20h4L19 9a2 2 0 0 0-3-3L5 17v3z M14 6l3 3",
    arrow: "M5 12h14M13 6l6 6-6 6",
    close: "M6 6l12 12M18 6 6 18",
    check: "M5 12.5 10 17l9-10",
    user: "M12 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8ZM4.5 20a7.5 7.5 0 0 1 15 0",
    receipt: "M5 3h14v18l-3-2-2 2-2-2-2 2-2-2-3 2V3Z M9 8h6M9 12h6",
    wallet: "M3 7h15a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7Zm0 0 1-3h12M16 13h.01",
    split: "M6 3v6a4 4 0 0 0 4 4h4a4 4 0 0 1 4 4v4M18 3v6",
    bed: "M2 17v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5M2 17h20M2 13h20M2 17v3M22 17v3M6 10V8a2 2 0 0 1 2-2h2v4",
    food: "M4 3v6a2 2 0 0 0 4 0V3M6 11v10M16 3c-1.5 0-2.5 2.2-2.5 5 0 2.3 1 3.8 2 4.3V21M16 3v18",
    car: "M5 13l1.5-4.6A2 2 0 0 1 8.4 7h7.2a2 2 0 0 1 1.9 1.4L19 13M4 13h16a1 1 0 0 1 1 1v3H3v-3a1 1 0 0 1 1-1ZM7 17v2M17 17v2M6.5 14h.01M17.5 14h.01",
    star: "M12 3.5l2.5 5.4 5.9.8-4.3 4.1 1.1 5.9L12 17l-5.2 2.8 1.1-5.9-4.3-4.1 5.9-.8L12 3.5Z",
    cart: "M3 4h2l2.2 11.2a1 1 0 0 0 1 .8h8.4a1 1 0 0 0 1-.8L20 7H6M9 20a1 1 0 1 0 0-2 1 1 0 0 0 0 2ZM17 20a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z",
    bag: "M6 8h12l-1 12.2a1 1 0 0 1-1 .8H8a1 1 0 0 1-1-.8L6 8ZM9 8V6a3 3 0 0 1 6 0v2",
    dots: "M5 12h.01M12 12h.01M19 12h.01",
    calendar: "M4 6.5A1.5 1.5 0 0 1 5.5 5h13A1.5 1.5 0 0 1 20 6.5v12A1.5 1.5 0 0 1 18.5 20h-13A1.5 1.5 0 0 1 4 18.5v-12ZM4 9.5h16M8 3.5v3M16 3.5v3",
    search: "M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16ZM21 21l-4.3-4.3",
    copy: "M9 9h9.5a1.5 1.5 0 0 1 1.5 1.5V20a1.5 1.5 0 0 1-1.5 1.5H9A1.5 1.5 0 0 1 7.5 20V10.5A1.5 1.5 0 0 1 9 9ZM5 15.5H4.5A1.5 1.5 0 0 1 3 14V4.5A1.5 1.5 0 0 1 4.5 3H14a1.5 1.5 0 0 1 1.5 1.5V5",
    download: "M12 4v11M7.5 11 12 15.5 16.5 11M5 19.5h14",
    share: "M4 12v7.5a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1V12M16 6l-4-4-4 4M12 2.5V15",
    chevron: "M6 9l6 6 6-6",
    chevronUp: "M6 15l6-6 6 6",
    scale: "M12 3.5v17M7.5 20.5h9M5 7.5h14M5 7.5 2.7 13a2.3 2.3 0 0 0 4.6 0L5 7.5ZM19 7.5 16.7 13a2.3 2.3 0 0 0 4.6 0L19 7.5ZM6 6 12 5l6 1",
    folder: "M3 7.5A1.5 1.5 0 0 1 4.5 6h4l2 2.2h7A1.5 1.5 0 0 1 19 9.7V17.5A1.5 1.5 0 0 1 17.5 19h-13A1.5 1.5 0 0 1 3 17.5V7.5Z",
    checkCircle: "M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18ZM8 12l2.8 2.8L16 9",
    undo: "M9 14.5 4.5 10 9 5.5M5 10h9.5a5 5 0 0 1 0 10H11",
    sort: "M4 7h16M7 12h10M10 17h4",
    minus: "M5 12h14",
    coins: "M8 9.5a5 2.5 0 1 0 0-5 5 2.5 0 0 0 0 5ZM3 7v5c0 1.4 2.2 2.5 5 2.5s5-1.1 5-2.5V7M16 9.5c2.6.2 4.5 1.2 4.5 2.5 0 1.4-2.2 2.5-5 2.5M11 17c0 1.4 2.2 2.5 5 2.5s5-1.1 5-2.5v-5",
  }[name];
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none"
      stroke="currentColor" strokeWidth={stroke} strokeLinecap="round" strokeLinejoin="round">
      <path d={p} />
    </svg>
  );
}

Object.assign(window, {
  uid, toCents, fromCents, pctOf, money, todayISO, fmtDate, personColor,
  CATEGORIES, catOf, catColor, categoryTotals,
  computeShares, splitStatus, payerStatus, computeBalances, settle, personRollup, buildSummary,
  Avatar, AvatarStack, CatIcon, Donut, Icon,
  useState, useEffect, useRef, useMemo,
});
