/* Meridian — routing and every commit in the product.
   The reversibility tiers are decided here, once: consequential and reversible
   actions apply immediately and raise an undo toast, irreversible ones open a
   confirm dialog, and the one action that needs a form (submitting dispute
   evidence) opens a staged dialog from an instant screen.

   Auth renders outside the shell (AGENTS §7.1): the sign-in and signed-out
   screens have no navigation owner, so they are returned before MeridianShell
   is reached rather than nested inside it. */
function App(props) {
  const options = props || {};
  const [path, setPath] = React.useState(options.startPath || "/");
  const [period, setPeriod] = React.useState("30");
  const [dark, setDark] = React.useState(options.theme === "dark");
  const [read, setRead] = React.useState(false);
  const [toasts, setToasts] = React.useState([]);
  const [confirm, setConfirm] = React.useState(null);
  const [form, setForm] = React.useState(null);
  const [rawData, setData] = React.useState({ payments: PAYMENTS, payouts: PAYOUTS, disputes: DISPUTES, expenses: EXPENSES, cards: CARDS, members: MEMBERS, comments: COMMENTS, customers: CUSTOMERS, balance: BALANCE, policy: MERIDIAN_POLICY_DEFAULTS });
  const [workspace, setWorkspace] = React.useState(MERIDIAN_WORKSPACE);
  /* The signed-in user is a member record, not a second literal: the profile
     screen and the team screen edit the same entry. */
  const palette = useCommandPalette();
  const now = MERIDIAN_NOW;
  const profile = rawData.members.find((m) => m.id === MERIDIAN_SIGNED_IN);
  /* The signed-in approver travels with the data, so every surface that asks
     "can I approve this" is handed the same person (AGENTS §2). Derived, never
     stored: setData writes into rawData. */
  const data = Object.assign({}, rawData, { approver: profile });

  React.useEffect(() => { if (options.startPath) setPath(options.startPath); }, [options.startPath]);
  React.useEffect(() => { setDark(options.theme === "dark"); }, [options.theme]);
  React.useEffect(() => {
    document.documentElement.classList.toggle("dark", dark);
    document.body.classList.toggle("dark", dark);
  }, [dark]);

  const dismiss = (id) => setToasts((list) => list.filter((t) => t.id !== id));
  const pushToast = (toast) => {
    const id = "t" + Date.now() + Math.random().toString(36).slice(2, 6);
    setToasts((list) => list.concat([Object.assign({ id: id }, toast)]));
    setTimeout(() => dismiss(id), 7000);
  };

  const patch = (key, id, fields) => setData((d) => Object.assign({}, d, { [key]: d[key].map((r) => (r.id === id ? Object.assign({}, r, fields) : r)) }));
  const restore = (key, snapshot) => setData((d) => Object.assign({}, d, { [key]: d[key].map((r) => (r.id === snapshot.id ? snapshot : r)) }));
  const memberName = (id) => (data.members.find((m) => m.id === id) || { name: "somebody who has left" }).name;

  /* ── Payouts ───────────────────────────────────────────────────────────── */
  const approvePayout = (payout) => {
    patch("payouts", payout.id, { status: "approved", approvedAt: now, approvedBy: profile.name });
    pushToast({
      variant: "success",
      title: "Payout approved",
      description: formatMoney(payout.amount) + " to " + payout.destination + " · " + payout.id,
      onUndo: () => restore("payouts", payout),
    });
  };

  const rejectPayout = (payout) => setConfirm({
    title: "Reject " + formatMoney(payout.amount) + " to " + payout.destination + "?",
    description: "The payout is closed and the money stays in your balance. " + payout.destination + " is notified, and a new payout has to be raised to pay them.",
    confirmLabel: "Reject payout",
    run: () => {
      patch("payouts", payout.id, { status: "rejected", rejectedAt: now, rejectedBy: profile.name, rejectReason: "Rejected in review" });
      pushToast({ title: "Payout rejected", description: payout.id + " · " + payout.destination });
    },
  });

  /* The bulk bar and the row call the same two functions; nothing about a
     selection changes what a payout may do. */
  const bulkApprove = (payouts) => {
    const approvable = payouts.filter((p) => PAYOUT_FLAGS.isApprovable(p, profile));
    const snapshot = rawData;
    setData((d) => Object.assign({}, d, {
      payouts: d.payouts.map((p) => (approvable.some((a) => a.id === p.id) ? Object.assign({}, p, { status: "approved", approvedAt: now, approvedBy: profile.name }) : p)),
    }));
    pushToast({
      variant: "success",
      title: approvable.length + " payouts approved",
      description: formatMoney(sumBy(approvable, "amount")) + " leaves the balance on the next run.",
      onUndo: () => setData(snapshot),
    });
  };

  const onPayoutTransition = (payout, transition) => {
    if (transition.id === "approve") approvePayout(payout);
    if (transition.id === "reject") rejectPayout(payout);
  };

  /* ── Payments ──────────────────────────────────────────────────────────── */
  const capturePayment = (payment) => {
    patch("payments", payment.id, { status: "succeeded" });
    pushToast({ variant: "success", title: "Payment captured", description: formatMoney(payment.amount) + " from " + payment.customer, onUndo: () => restore("payments", payment) });
  };

  const retryPayment = (payment) => {
    patch("payments", payment.id, { status: "pending", failureReason: undefined });
    pushToast({ title: "Retrying payment", description: payment.id + " · " + payment.customer, onUndo: () => restore("payments", payment) });
  };

  const refundPayment = (payment) => setConfirm({
    title: "Refund " + formatMoney(payment.amount) + " to " + payment.customer + "?",
    description: "The money leaves your available balance today and the payment cannot be captured again. " + payment.id + " stays on the customer's record as refunded.",
    confirmLabel: "Refund payment",
    run: () => {
      patch("payments", payment.id, { status: "refunded", refundedAt: now });
      setData((d) => Object.assign({}, d, { balance: Object.assign({}, d.balance, { availableCents: d.balance.availableCents - payment.amount }) }));
      pushToast({ title: "Refund sent", description: formatMoney(payment.amount) + " to " + payment.customer + " · " + payment.id });
    },
  });

  const onPaymentTransition = (payment, transition) => {
    if (transition.id === "capture") capturePayment(payment);
    if (transition.id === "retry") retryPayment(payment);
    if (transition.id === "refund") refundPayment(payment);
  };

  /* ── Disputes ──────────────────────────────────────────────────────────── */
  const onDisputeTransition = (dispute, transition) => {
    const snapshot = data;
    if (transition.id === "submit_evidence") {
      setForm({
        title: "Send evidence for " + dispute.id,
        description: dispute.customer + " · " + dispute.reason + " · " + formatMoney(dispute.amount) + ". Due " + formatCountdown(dispute.evidenceDueAt, now) + ".",
        submitLabel: "Submit evidence",
        initial: { documents: [] },
        fields: [
          { id: "documents", kind: "checks", label: "What we are sending", options: [
            { value: "receipt", label: "Receipt and order confirmation" },
            { value: "delivery", label: "Proof of delivery" },
            { value: "terms", label: "Terms the customer accepted" },
            { value: "comms", label: "Correspondence with the customer" },
          ] },
          { id: "note", kind: "text", label: "What happened", placeholder: "In your own words — the network reads this before the documents.", hint: "At least a sentence." },
        ],
        validate: (v) => {
          const errors = {};
          if (!v.documents || v.documents.length === 0) errors.documents = "Pick at least one document to send.";
          if (!v.note || v.note.trim().length < 20) errors.note = "Write out what happened — at least a sentence.";
          return errors;
        },
        run: (v) => {
          patch("disputes", dispute.id, { status: "under_review", evidenceSubmittedAt: now, evidenceCount: v.documents.length, evidenceNote: v.note.trim() });
          pushToast({
            variant: "success",
            title: "Evidence sent",
            description: v.documents.length + " documents on " + dispute.id + " · the clock is the network's now",
            onUndo: () => restore("disputes", dispute),
          });
        },
      });
      return;
    }
    if (transition.id === "accept_dispute") {
      setConfirm({
        title: "Accept the " + formatMoney(dispute.amount) + " dispute from " + dispute.customer + "?",
        description: "The money goes back to the customer and the case closes as lost. It cannot be reopened, and the reserve against your balance is released to pay it.",
        confirmLabel: "Accept dispute",
        run: () => {
          patch("disputes", dispute.id, { status: "lost", closedAt: now, evidenceDueAt: null });
          setData((d) => Object.assign({}, d, { balance: Object.assign({}, d.balance, { availableCents: d.balance.availableCents - dispute.amount }) }));
          pushToast({ title: "Dispute accepted", description: formatMoney(dispute.amount) + " returned to " + dispute.customer + " · " + dispute.id });
        },
      });
    }
  };

  /* ── Expenses ──────────────────────────────────────────────────────────── */
  const approveExpense = (expense) => {
    patch("expenses", expense.id, { status: "approved", reviewedAt: now, reviewedBy: profile.name });
    pushToast({
      variant: "success",
      title: "Expense approved",
      description: formatMoney(expense.amount) + " at " + expense.merchant + " · " + expense.id,
      onUndo: () => restore("expenses", expense),
    });
  };

  const bulkApproveExpenses = (expenses) => {
    const snapshot = data;
    const ids = expenses.map((e) => e.id);
    setData((d) => Object.assign({}, d, {
      expenses: d.expenses.map((e) => (ids.indexOf(e.id) > -1 ? Object.assign({}, e, { status: "approved", reviewedAt: now, reviewedBy: profile.name }) : e)),
    }));
    pushToast({
      variant: "success",
      title: expenses.length + " expenses approved",
      description: formatMoney(sumBy(expenses, "amount")) + " signed off across " + new Set(expenses.map((e) => e.cardId)).size + " cards.",
      onUndo: () => setData(snapshot),
    });
  };

  const queryExpense = (expense) => setForm({
    title: "Query " + formatMoney(expense.amount) + " at " + expense.merchant + "?",
    description: "It goes back to " + memberName(expense.spenderId) + " with your question and leaves your queue until they answer.",
    submitLabel: "Query expense",
    initial: { note: "" },
    fields: [{ id: "note", kind: "text", label: "What do you need to know", placeholder: "They see this exactly as you write it.", hint: "A sentence is enough." }],
    validate: (v) => (!v.note || v.note.trim().length < 12 ? { note: "Say what you need from them — at least a few words." } : {}),
    run: (v) => {
      patch("expenses", expense.id, { status: "queried", queriedAt: now, queriedBy: profile.name, queryNote: v.note.trim() });
      pushToast({ title: "Query sent", description: expense.id + " · waiting on " + memberName(expense.spenderId), onUndo: () => restore("expenses", expense) });
    },
  });

  const denyExpense = (expense) => setConfirm({
    title: "Deny " + formatMoney(expense.amount) + " at " + expense.merchant + "?",
    description: "The expense closes against " + memberName(expense.spenderId) + " and the money is recovered through payroll. It cannot be reopened — a new expense would have to be raised.",
    confirmLabel: "Deny expense",
    run: () => {
      patch("expenses", expense.id, { status: "denied", deniedAt: now, deniedBy: profile.name, denyReason: "Denied in review" });
      pushToast({ title: "Expense denied", description: expense.id + " · " + expense.merchant });
    },
  });

  const onExpenseTransition = (expense, transition) => {
    if (transition.id === "approve_expense") approveExpense(expense);
    if (transition.id === "query_expense") queryExpense(expense);
    if (transition.id === "deny_expense") denyExpense(expense);
  };

  /* ── Cards ─────────────────────────────────────────────────────────────── */
  const freezeCard = (card) => {
    patch("cards", card.id, { status: "frozen", frozenAt: now, frozenBy: profile.name, frozenReason: "Frozen in review" });
    pushToast({ title: "Card frozen", description: card.label + " · •••• " + card.last4 + " — authorisations are declined from now", onUndo: () => restore("cards", card) });
  };
  const unfreezeCard = (card) => {
    patch("cards", card.id, { status: "active", frozenAt: undefined, frozenBy: undefined, frozenReason: undefined });
    pushToast({ variant: "success", title: "Card unfrozen", description: card.label + " · •••• " + card.last4 + " can be spent on again", onUndo: () => restore("cards", card) });
  };
  const cancelCard = (card) => {
    const use = cardUtilisation(card, data.expenses, now, data.policy);
    setConfirm({
      title: "Cancel the " + card.label + " card?",
      description: "•••• " + card.last4 + " stops working immediately and cannot be reissued — " + memberName(card.holderId) + " would need a new card. The " + formatMoney(use.spent) + " already spent on it still has to be reviewed.",
      confirmLabel: "Cancel card",
      run: () => {
        patch("cards", card.id, { status: "cancelled", cancelledAt: now, cancelledBy: profile.name, cancelReason: "Cancelled in review" });
        pushToast({ title: "Card cancelled", description: card.label + " · •••• " + card.last4 });
      },
    });
  };

  const onCardTransition = (card, transition) => {
    if (transition.id === "freeze_card") freezeCard(card);
    if (transition.id === "unfreeze_card") unfreezeCard(card);
    if (transition.id === "cancel_card") cancelCard(card);
  };

  /* Issuing a card needs four fields, so this instant screen opens a staged
     dialog. The default limit is the workspace's own spend-control policy. */
  const issueCard = () => setForm({
    title: "New card",
    description: "It arrives inactive: the holder activates it before anything can be spent on it.",
    submitLabel: "New card",
    initial: { label: "", holder: profile.id, type: "virtual", limit: String(data.policy.cardDefaultLimitCents / 100) },
    fields: [
      { id: "label", kind: "input", label: "What the card is for", placeholder: "Cloud vendors", hint: "Shown on every expense put on it." },
      { id: "holder", kind: "radio", label: "Who holds it", options: data.members.map((m) => ({ value: m.id, label: m.name + " — " + m.role })) },
      { id: "type", kind: "radio", label: "Kind of card", options: CARD_TYPES.map((t) => ({ value: t.value, label: t.label + (t.value === "virtual" ? " — usable online straight away" : " — posted to the holder") })) },
      { id: "limit", kind: "input", label: "Limit per 30 days, in dollars", hint: "The default comes from spend controls." },
    ],
    validate: (v) => {
      const errors = {};
      if (!v.label || v.label.trim().length < 3) errors.label = "Name the card — at least three characters.";
      if (!(Math.round(Number(String(v.limit).replace(/[^0-9.]/g, "")) * 100) >= 10000)) errors.limit = "At least $100, or the card cannot buy anything.";
      return errors;
    },
    run: (v) => {
      const digits = String(Math.floor(1000 + Math.random() * 9000));
      const card = {
        id: "crd_" + digits,
        label: v.label.trim(),
        last4: digits,
        holderId: v.holder,
        type: v.type,
        status: "pending_activation",
        spendLimit: Math.round(Number(String(v.limit).replace(/[^0-9.]/g, "")) * 100),
        openedAt: now,
        expiresAt: now + 1095 * 24 * 3600e3,
      };
      setData((d) => Object.assign({}, d, { cards: d.cards.concat([card]) }));
      pushToast({
        variant: "success",
        title: "Card issued",
        description: card.label + " · •••• " + card.last4 + " — waiting on " + memberName(card.holderId) + " to activate it",
        onUndo: () => setData((d) => Object.assign({}, d, { cards: d.cards.filter((c) => c.id !== card.id) })),
      });
    },
  });

  /* ── Customers ──────────────────────────────────────────────────────── */
  const onCustomerTransition = (customer, transition) => {
    if (transition.id === "hold_customer") {
      patch("customers", customer.id, { status: "on_hold" });
      pushToast({ title: "Payouts paused", description: "Nothing leaves for " + customer.name + " until somebody resumes it. Money still arrives.", onUndo: () => restore("customers", customer) });
    }
    if (transition.id === "resume_customer") {
      patch("customers", customer.id, { status: "live" });
      pushToast({ variant: "success", title: "Payouts resumed", description: customer.name + " can be paid again · " + (customer.payoutAccount || "no bank account on file yet"), onUndo: () => restore("customers", customer) });
    }
    if (transition.id === "close_customer") {
      const open = openDisputes(disputesFor(data.disputes, customer));
      const pending = pendingPayouts(payoutsFor(data.payouts, customer));
      setConfirm({
        title: "Close the " + customer.name + " account?",
        description: "Nothing more can be taken from them or sent to them, and a closed account cannot be reopened — onboarding a new one is the only way back. "
          + (open.length ? open.length + " open " + (open.length === 1 ? "dispute" : "disputes") + " worth " + formatMoney(customerReserved(data.disputes, customer)) + " keep running against the balance. " : "")
          + (pending.length ? pending.length + " " + (pending.length === 1 ? "payout" : "payouts") + " already in flight still land." : "Nothing of theirs is in flight."),
        confirmLabel: "Close account",
        run: () => {
          patch("customers", customer.id, { status: "closed", closedAt: now });
          pushToast({ title: "Account closed", description: customer.name + " · " + formatMoney(customerVolume(data.payments, customer)) + " of volume stays on the ledger" });
        },
      });
    }
  };

  /* A create writes into the fixture, so every count it feeds moves. */
  const createCustomer = (draft) => {
    const customer = {
      id: "cus_" + draft.name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "").slice(0, 10) + Math.random().toString(36).slice(2, 5),
      name: draft.name.trim(),
      ownerId: draft.ownerId,
      status: "live",
      country: draft.country.trim() || "United States",
      currency: data.balance.currency,
      since: now,
      payoutAccount: draft.payoutAccount.trim() ? "•••• " + draft.payoutAccount.trim() : null,
      contact: { name: draft.contactName.trim(), email: draft.contactEmail.trim() },
    };
    setData((d) => Object.assign({}, d, { customers: d.customers.concat([customer]) }));
    setPath(recordRoute({ type: "customer", id: customer.id }));
    pushToast({
      variant: "success",
      title: "Customer created",
      description: customer.name + " · owned by " + memberName(customer.ownerId) + " · " + (customer.payoutAccount ? "can be charged and paid" : "can be charged, not paid yet"),
      onUndo: () => { setPath("/customers"); setData((d) => Object.assign({}, d, { customers: d.customers.filter((c) => c.id !== customer.id) })); },
    });
  };

  const createPayment = (draft) => {
    const payment = {
      id: "pay_" + Math.random().toString(36).slice(2, 7).toUpperCase(),
      customer: draft.customer,
      method: draft.method,
      amount: draft.amount,
      status: "pending",
      createdAt: Date.now(),
      reference: draft.reference || undefined,
    };
    setData((d) => Object.assign({}, d, { payments: d.payments.concat([payment]) }));
    setPath("/payments");
    pushToast({
      variant: "success",
      title: "Payment created",
      description: formatMoney(payment.amount) + " from " + payment.customer + " · " + formatMoney(methodFee(payment.method, payment.amount)) + " of that is fees",
      onUndo: () => setData((d) => Object.assign({}, d, { payments: d.payments.filter((p) => p.id !== payment.id) })),
    });
  };

  const createPayout = (draft) => {
    const payout = {
      id: "po_" + Math.floor(7750 + Math.random() * 200),
      destination: draft.destination,
      account: draft.account,
      amount: draft.amount,
      status: "awaiting_approval",
      requestedAt: Date.now(),
      expectedAt: Date.now() + 30 * 3600e3,
      reference: draft.reference || undefined,
    };
    setData((d) => Object.assign({}, d, { payouts: d.payouts.concat([payout]) }));
    setPath("/payouts");
    pushToast({
      variant: "success",
      title: "Payout raised",
      description: formatMoney(payout.amount) + " to " + payout.destination + " · waiting on approval, " + approvablePayouts(data.payouts.concat([payout]), profile).length + " in the queue now",
      onUndo: () => setData((d) => Object.assign({}, d, { payouts: d.payouts.filter((p) => p.id !== payout.id) })),
    });
  };

  /* Three fields, so inviting somebody is a staged dialog rather than a route. */
  const invitePerson = () => setForm({
    title: "Invite teammate",
    description: "They get an email to set a password. Until they accept, they hold no cards and can approve nothing.",
    submitLabel: "Invite teammate",
    initial: { name: "", email: "", teamRole: "member" },
    fields: [
      { id: "name", kind: "input", label: "Name", placeholder: "Rosa Iyer" },
      { id: "email", kind: "input", label: "Work email", placeholder: "rosa@northwind.example" },
      { id: "teamRole", kind: "radio", label: "What they can do", options: TEAM_ROLES.map((r) => ({ value: r.value, label: r.label + " — " + r.note })) },
    ],
    validate: (v) => {
      const errors = {};
      if (!v.name || v.name.trim().length < 3) errors.name = "Enter their name.";
      if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(v.email || "")) errors.email = "Enter a valid work email.";
      else if (data.members.some((m) => m.email.toLowerCase() === v.email.trim().toLowerCase())) errors.email = "Somebody with that address is already here.";
      return errors;
    },
    run: (v) => {
      const member = {
        id: "usr_" + v.name.trim().toLowerCase().replace(/[^a-z]+/g, "").slice(0, 8),
        name: v.name.trim(),
        role: labelOf(TEAM_ROLES, v.teamRole),
        teamRole: v.teamRole,
        email: v.email.trim(),
        avatar: null,
        approvalLimit: 0,
        canIssueCards: false,
        notify: [],
      };
      setData((d) => Object.assign({}, d, { members: d.members.concat([member]) }));
      pushToast({
        variant: "success",
        title: "Invite sent",
        description: member.name + " · " + member.email + " · no approval limit until somebody gives them one",
        onUndo: () => setData((d) => Object.assign({}, d, { members: d.members.filter((m) => m.id !== member.id) })),
      });
    },
  });

  /* ── Notes ─────────────────────────────────────────────────────────────── */
  /* Consequential and reversible, so it applies immediately and raises an undo
     toast. Mentions are read out of the text rather than collected separately —
     one source for who was named, and the queue predicate reads the same array. */
  const addComment = (record, body) => {
    const mentions = (body.match(/@usr_[a-z0-9_]+/g) || []).map((t) => t.slice(1)).filter((id, i, all) => all.indexOf(id) === i && data.members.some((m) => m.id === id));
    const comment = { id: "cmt_" + Math.random().toString(36).slice(2, 7), record: record, authorId: profile.id, at: Date.now(), mentions: mentions, body: body };
    setData((d) => Object.assign({}, d, { comments: d.comments.concat([comment]) }));
    pushToast({
      title: "Note posted",
      description: mentions.length
        ? "It is in " + mentions.map(memberName).join(" and ") + "'s queue until they answer on " + record.id
        : "On " + record.id + " · nobody was mentioned, so nobody was notified",
      onUndo: () => setData((d) => Object.assign({}, d, { comments: d.comments.filter((c) => c.id !== comment.id) })),
    });
  };

  /* ── Settings, staged ──────────────────────────────────────────────────── */
  const saveProfile = (draft) => {
    const snapshot = profile;
    patch("members", profile.id, draft);
    pushToast({ variant: "success", title: "Profile saved", description: draft.name + " · " + draft.role + (draft.avatar !== (snapshot.avatar || null) ? (draft.avatar ? " · new photo" : " · photo removed") : ""), onUndo: () => restore("members", snapshot) });
  };
  const saveWorkspace = (draft) => {
    const snapshot = workspace;
    setWorkspace(Object.assign({}, workspace, draft));
    pushToast({
      variant: "success",
      title: "Workspace saved",
      description: draft.name + " · statements read " + draft.descriptor + " · payouts " + labelOf(PAYOUT_SCHEDULES, draft.schedule).toLowerCase(),
      onUndo: () => setWorkspace(snapshot),
    });
  };
  const saveNotifications = (draft) => {
    const snapshot = profile;
    patch("members", profile.id, { notify: draft.notify });
    pushToast({
      variant: "success",
      title: "Notification preferences saved",
      description: draft.notify.length ? draft.notify.length + " kinds of alert will be emailed to " + profile.email : "Nothing will be emailed to you",
      onUndo: () => restore("members", snapshot),
    });
  };

  /* Policy is what the derived flags read, so saving it recounts the sidebar
     badge, the tiles and every row's action — nothing is stored twice. */
  const savePolicy = (drafted) => {
    const snapshot = data.policy;
    setData((d) => Object.assign({}, d, { policy: drafted }));
    pushToast({
      variant: "success",
      title: "Spend controls saved",
      description: "Receipts required over " + formatMoney(drafted.receiptThresholdCents) + " · " + reviewableExpenses(data.expenses, drafted).length + " expenses can be approved now",
      onUndo: () => setData((d) => Object.assign({}, d, { policy: snapshot })),
    });
  };

  const saveTeam = (rows) => {
    const snapshot = data.members;
    setData((d) => Object.assign({}, d, {
      members: d.members.map((m) => {
        const row = rows.find((r) => r.id === m.id);
        return row ? Object.assign({}, m, { teamRole: row.teamRole, approvalLimit: row.approvalLimit, canIssueCards: row.canIssueCards }) : m;
      }),
    }));
    pushToast({
      variant: "success",
      title: "Team saved",
      description: rows.filter((r) => r.teamRole !== "member").length + " of " + rows.length + " people can approve something",
      onUndo: () => setData((d) => Object.assign({}, d, { members: snapshot })),
    });
  };

  /* ── Auth, outside the shell ───────────────────────────────────────────── */
  /* Harness chrome, not product chrome: the floating page menu is how a reader
     reaches every registered screen, including the auth ones the shell cannot
     render. It sits outside the shell so it is on those screens too. */
  const preview = (
    <TemplateMenu product="Meridian" path={path} onNavigate={setPath} groups={MERIDIAN_PAGES(data)}
      dark={dark} onToggleTheme={() => setDark(!dark)} />
  );

  if (MERIDIAN_AUTH_PATHS.indexOf(path) > -1) {
    const auth = {
      "/signin": <SignInScreen workspace={workspace} profile={profile} onNavigate={setPath} />,
      "/two-factor": <TwoFactorScreen profile={profile} onNavigate={setPath} onSignIn={() => setPath("/")} />,
      "/verify-code": <VerifyCodeScreen profile={profile} onNavigate={setPath} onSignIn={() => setPath("/")} />,
      "/forgot-password": <ForgotPasswordScreen profile={profile} onNavigate={setPath} />,
      "/reset-password": <ResetPasswordScreen profile={profile} onNavigate={setPath} />,
      "/signout": <SignedOutScreen profile={profile} onNavigate={setPath} />,
    };
    return (
      <>
        {auth[path]}
        {preview}
      </>
    );
  }

  const disputeMatch = path.match(/^\/disputes\/([^/]+)$/);
  const selectedDispute = disputeMatch && data.disputes.some((d) => d.id === disputeMatch[1]) ? disputeMatch[1] : null;
  const cardMatch = path.match(/^\/cards\/([^/]+)$/);
  const selectedCard = cardMatch && data.cards.some((c) => c.id === cardMatch[1]) ? cardMatch[1] : null;
  const customerMatch = path.match(/^\/customers\/([^/]+)$/);
  const selectedCustomer = customerMatch && data.customers.some((c) => c.id === customerMatch[1]) ? customerMatch[1] : null;
  const personMatch = path.match(/^\/people\/([^/]+)$/);
  const selectedPerson = personMatch && data.members.some((m) => m.id === personMatch[1]) ? personMatch[1] : null;

  const screen = () => {
    if (path === "/") return <DashboardScreen data={data} pushToast={pushToast} onNavigate={setPath} period={period} onPeriodChange={setPeriod} commits={{ approvePayout, rejectPayout, refundPayment, capturePayment, retryPayment, approveExpense, onExpenseTransition }} />;
    if (path === "/payments") return <PaymentsScreen data={data} now={now} onNavigate={setPath} onPaymentTransition={onPaymentTransition} pushToast={pushToast} />;
    if (path === "/payments/new") return <NewPaymentScreen data={data} now={now} onCreate={createPayment} onNavigate={setPath} />;
    if (path === "/payouts/new") return <NewPayoutScreen data={data} now={now} onCreate={createPayout} onNavigate={setPath} />;
    if (path === "/customers/new") return <NewCustomerScreen data={data} onCreate={createCustomer} onNavigate={setPath} />;
    if (selectedCustomer) return <CustomerScreen data={data} now={now} customerId={selectedCustomer} onNavigate={setPath} onCustomerTransition={onCustomerTransition} onPaymentTransition={onPaymentTransition} onPostComment={addComment} />;
    if (path === "/customers") return <CustomersScreen data={data} now={now} onNavigate={setPath} onCustomerTransition={onCustomerTransition} />;
    if (selectedPerson) return <PersonScreen data={data} now={now} memberId={selectedPerson} onNavigate={setPath} onExpenseTransition={onExpenseTransition} onPostComment={addComment} />;
    if (path === "/people") return <PeopleScreen data={data} now={now} onNavigate={setPath} onInvite={invitePerson} />;
    if (path === "/activity") return <ActivityScreen data={data} now={now} onNavigate={setPath} />;
    if (path === "/payouts") return <PayoutsScreen data={data} now={now} onNavigate={setPath} onPayoutTransition={onPayoutTransition} onBulkApprove={bulkApprove} pushToast={pushToast} />;
    if (path === "/expenses") return <ExpensesScreen data={data} now={now} period={period} onPeriodChange={setPeriod} onExpenseTransition={onExpenseTransition} onBulkApprove={bulkApproveExpenses} pushToast={pushToast} />;
    if (selectedCard) return <CardScreen data={data} now={now} cardId={selectedCard} onNavigate={setPath} onCardTransition={onCardTransition} onExpenseTransition={onExpenseTransition} onPostComment={addComment} />;
    if (path === "/cards") return <CardsScreen data={data} now={now} onNavigate={setPath} onCardTransition={onCardTransition} onIssueCard={issueCard} issuer={profile} />;
    if (path === "/disputes" || selectedDispute) return <DisputesScreen data={data} now={now} selectedId={selectedDispute} onNavigate={setPath} onDisputeTransition={onDisputeTransition} onPostComment={addComment} />;
    if (path === "/balances") return <BalancesScreen data={data} now={now} onNavigate={setPath} />;
    if (path === "/reports") return <ReportsScreen data={data} now={now} period={period} onPeriodChange={setPeriod} onNavigate={setPath} pushToast={pushToast} />;
    if (path === "/settings") return <WorkspaceSettingsScreen workspace={workspace} payouts={data.payouts} onSave={saveWorkspace} />;
    if (path === "/settings/billing") return <BillingScreen data={data} now={now} workspace={workspace} onNavigate={setPath} />;
    if (path === "/settings/profile") return <ProfileScreen profile={profile} onSave={saveProfile} />;
    if (path === "/settings/notifications") return <NotificationsScreen profile={profile} data={data} onSave={saveNotifications} />;
    if (path === "/settings/spend-controls") return <SpendControlsScreen policy={data.policy} data={data} now={now} onSave={savePolicy} />;
    if (path === "/settings/team") return <TeamScreen members={data.members} signedInId={MERIDIAN_SIGNED_IN} data={data} onSave={saveTeam} />;
    return <RouteNotBuilt path={path} onNavigate={setPath} />;
  };

  const recordGroups = [
    { label: "Payouts awaiting approval", items: approvablePayouts(data.payouts, profile).map((p) => ({ value: p.id, label: p.id + " · " + p.destination + " · " + formatMoney(p.amount), route: "/payouts" })) },
    { label: "Expenses you can approve", items: reviewableExpenses(data.expenses, data.policy).map((e) => ({ value: e.id, label: e.id + " · " + e.merchant + " · " + formatMoney(e.amount), route: "/expenses" })) },
    { label: "Disputes needing evidence", items: disputesNeedingEvidence(data.disputes).map((d) => ({ value: d.id, label: d.id + " · " + d.customer + " · " + formatMoney(d.amount), route: "/disputes/" + d.id })) },
    { label: "Notes waiting on your reply", items: mentionsAwaiting(data.comments, MERIDIAN_SIGNED_IN).map((c) => ({ value: c.id, label: memberName(c.authorId) + " on " + recordTitle(data, c.record) + " · " + c.record.id, route: recordRoute(c.record) })) },
    { label: "Customers", items: customersByVolume(data.customers, data.payments).map((c) => ({ value: c.id, label: c.name + " · " + formatMoney(customerVolume(data.payments, c)) + " · " + labelOf(CUSTOMER_STATUS, c.status), route: recordRoute({ type: "customer", id: c.id }) })) },
    { label: "People", items: data.members.map((m) => ({ value: m.id, label: m.name + " · " + m.role, route: recordRoute({ type: "member", id: m.id }) })) },
    { label: "Cards", items: data.cards.map((c) => ({ value: c.id, label: c.label + " · •••• " + c.last4 + " · " + labelOf(CARD_STATUS, c.status), route: "/cards/" + c.id })) },
    { label: "Payments needing attention", items: data.payments.filter((p) => PAYMENT_FLAGS.needsAttention(p, data.disputes)).map((p) => ({ value: p.id, label: p.id + " · " + p.customer + " · " + formatMoney(p.amount), route: "/payments" })) },
  ];

  return (
    <MeridianShell
      path={path}
      onNavigate={setPath}
      dark={dark}
      onToggleTheme={() => setDark(!dark)}
      data={data}
      profile={profile}
      workspace={workspace}
      notifications={NOTIFICATIONS}
      unread={read ? 0 : NOTIFICATIONS.length}
      onRead={() => setRead(true)}
      palette={palette}
      toasts={toasts}
      onDismissToast={dismiss}
    >
      {screen()}
      <CommandPalette
        open={palette.open}
        onClose={() => palette.setOpen(false)}
        routes={MERIDIAN_ROUTES}
        recordGroups={recordGroups}
        onNavigate={setPath}
        placeholder="Search payments, payouts, customers, people…"
      />
      <ConfirmDialog request={confirm} onCancel={() => setConfirm(null)} onConfirm={() => { confirm.run(); setConfirm(null); }} />
      <FormDialog request={form} onCancel={() => setForm(null)} onSubmit={(values) => { form.run(values); setForm(null); }} />
      {preview}
    </MeridianShell>
  );
}
window.__meridianReal = App;
if (!window.MeridianApp) window.MeridianApp = App;
