/* Marketing Desk — 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; the one irreversible action
   — stopping a journey — opens a confirm dialog; and the two things that need a
   form (writing a new broadcast, sending one back with a note) open staged
   dialogs from these instant screens.

   Auth renders outside the shell (AGENTS §7.1): sign-in has no navigation owner,
   so it is returned before MdShell 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);
  /* The address the reset flow is working on. The request screen sets it and the
     confirmation states it, so the confirmation never claims an address the user
     did not type. */
  const [authEmail, setAuthEmail] = React.useState(MD_RESET_LINK.email);
  const [data, setData] = React.useState({
    journeys: JOURNEYS, broadcasts: BROADCASTS, audiences: AUDIENCES, content: CONTENT,
    domains: SENDING_DOMAINS, members: MEMBERS, contacts: SAMPLE_CONTACTS,
    /* The social layer and the settings records live in the same object as the
       entities, so a note posted here re-renders every count that reads it. */
    comments: COMMENTS, activity: ACTIVITY, channels: CONNECTED_CHANNELS, keys: API_KEYS, webhooks: WEBHOOKS,
  });
  const [workspace, setWorkspace] = React.useState(MD_WORKSPACE);
  const [billing] = React.useState(BILLING);
  const [prefs, setPrefs] = React.useState(NOTIFICATION_PREFS);
  const palette = useCommandPalette();
  const now = MD_NOW;
  /* The signed-in user is a member record, not a second literal: the team screen
     edits the same entry the approval predicate reads. */
  const profile = data.members.find((m) => m.id === MD_SIGNED_IN);

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

  /* ── Journeys ──────────────────────────────────────────────────────────── */
  const publishJourney = (journey) => {
    patch("journeys", journey.id, { status: "live", publishedAt: now });
    pushToast({
      variant: "success",
      title: "Journey published",
      description: journey.name + " · anyone matching “" + journey.trigger.toLowerCase() + "” enters it from now",
      onUndo: () => restore("journeys", journey),
    });
  };

  const pauseJourney = (journey) => {
    patch("journeys", journey.id, { status: "paused", pausedAt: now, pausedBy: profile.name });
    pushToast({
      title: "Journey paused",
      description: formatCount(journeyEnrolled(journey)) + " people are holding where they were in " + journey.name,
      onUndo: () => restore("journeys", journey),
    });
  };

  const resumeJourney = (journey) => {
    patch("journeys", journey.id, { status: "live", pausedAt: undefined, pausedBy: undefined });
    pushToast({
      variant: "success",
      title: "Journey resumed",
      description: formatCount(journeyEnrolled(journey)) + " people carry on from where they stopped in " + journey.name,
      onUndo: () => restore("journeys", journey),
    });
  };

  /* Stopping empties the journey, so the commit moves those people out of `at`
     and into `exited` — the step arithmetic stays closed and every figure that
     reads it moves with the commit. */
  const stopJourney = (journey) => setConfirm({
    title: "Stop “" + journey.name + "”?",
    description: "The " + formatCount(journeyEnrolled(journey)) + " people enrolled are taken out of it and cannot be put back — a new journey would have to be published to reach them. Everything it has already sent stays on their record.",
    confirmLabel: "Stop journey",
    run: () => {
      patch("journeys", journey.id, {
        status: "stopped",
        stoppedAt: now,
        stoppedBy: profile.name,
        steps: journey.steps.map((s) => Object.assign({}, s, { at: 0, exited: s.exited + s.at })),
      });
      pushToast({ title: "Journey stopped", description: journey.name + " · " + formatCount(journeyEnrolled(journey)) + " people were taken out of it" });
    },
  });

  const onJourneyTransition = (journey, transition) => {
    if (transition.id === "publish") publishJourney(journey);
    if (transition.id === "pause") pauseJourney(journey);
    if (transition.id === "resume") resumeJourney(journey);
    if (transition.id === "stop") stopJourney(journey);
  };

  /* Creating a journey writes a real record with real zeroes: nothing has
     entered it, so every count is 0 and the figures on its own screen say so
     rather than showing a plausible history it does not have. */
  const createJourney = (draft, steps) => {
    const id = "jny_" + Math.random().toString(36).slice(2, 7);
    const journey = {
      id: id,
      name: draft.name.trim(),
      status: "draft",
      channel: "email",
      trigger: draft.trigger,
      audienceId: draft.audienceId,
      ownerId: profile.id,
      publishedAt: null,
      lastSendAt: null,
      steps: steps.map((step, index) => Object.assign({}, step, {
        id: "s" + (index + 1),
        entered: 0, completed: 0, at: 0, failed: 0, exited: 0,
      })),
    };
    setData((d) => Object.assign({}, d, { journeys: d.journeys.concat([journey]) }));
    setPath("/journeys/" + id);
    const plan = journeyPlanSummary(journey.steps);
    pushToast({
      variant: "success",
      title: "Journey created",
      description: journey.name + " · " + plan.emails + (plan.emails === 1 ? " email" : " emails") + " over " + (plan.days || 0) + " days · draft, so nothing sends yet",
      onUndo: () => { setData((d) => Object.assign({}, d, { journeys: d.journeys.filter((j) => j.id !== id) })); setPath("/journeys"); },
    });
  };

  const saveJourneySteps = (journey, steps) => {
    const snapshot = journey;
    patch("journeys", journey.id, {
      steps: steps.map((step, index) => Object.assign({}, step, {
        id: step.id.indexOf("s_new_") === 0 ? "s" + (index + 1) : step.id,
        entered: step.entered || 0, completed: step.completed || 0, at: step.at || 0, failed: step.failed || 0, exited: step.exited || 0,
      })),
    });
    const plan = journeyPlanSummary(steps);
    pushToast({
      variant: "success",
      title: "Steps saved",
      description: plan.steps + (plan.steps === 1 ? " step" : " steps") + " · " + plan.emails + (plan.emails === 1 ? " email" : " emails") + " over " + (plan.days || 0) + " days",
      onUndo: () => restore("journeys", snapshot),
    });
  };

  /* ── Audiences ─────────────────────────────────────────────────────────── */
  /* The size the builder computed is an extrapolation from the sample, and the
     record is marked as such until the engine evaluates the full list. */
  const createAudience = (built) => {
    const id = "aud_" + Math.random().toString(36).slice(2, 7);
    const audience = {
      id: id,
      name: built.name,
      kind: "segment",
      channel: "email",
      rule: describeSegment(built.rules),
      size: built.estimate,
      joined30: built.joined30,
      evaluatedAt: now,
      pendingEvaluation: true,
      sample: built.matched.slice(0, 5).map((contact) => ({
        name: contact.name,
        email: contact.email,
        note: labelOf(LIFECYCLES, contact.lifecycle) + " · " + (contact.lastOpenAt ? "opened " + formatWhen(contact.lastOpenAt, now).toLowerCase() : "never opened"),
      })),
    };
    setData((d) => Object.assign({}, d, { audiences: d.audiences.concat([audience]) }));
    setPath("/audiences/" + id);
    pushToast({
      variant: "success",
      title: "Audience created",
      description: audience.name + " · about " + formatCount(audience.size) + " people, estimated from " + built.matched.length + " of " + data.contacts.length + " sampled",
      onUndo: () => { setData((d) => Object.assign({}, d, { audiences: d.audiences.filter((a) => a.id !== id) })); setPath("/audiences"); },
    });
  };

  /* ── Broadcasts ────────────────────────────────────────────────────────── */
  const approveBroadcast = (broadcast) => {
    const recipients = broadcastRecipients(broadcast, data.audiences);
    patch("broadcasts", broadcast.id, { status: "scheduled", approvedById: profile.id, approvedAt: now });
    pushToast({
      variant: "success",
      title: "Broadcast scheduled",
      description: broadcast.name + " · " + formatCount(recipients.count) + " recipients · " + formatCountdown(broadcast.scheduledAt, now),
      onUndo: () => restore("broadcasts", broadcast),
    });
  };

  /* The bulk bar and the row call the same function, and the bar was given only
     the rows the act-ability predicate allows. */
  const bulkApprove = (broadcasts) => {
    const snapshot = data;
    const ids = broadcasts.map((b) => b.id);
    setData((d) => Object.assign({}, d, {
      broadcasts: d.broadcasts.map((b) => (ids.indexOf(b.id) > -1 ? Object.assign({}, b, { status: "scheduled", approvedById: profile.id, approvedAt: now }) : b)),
    }));
    pushToast({
      variant: "success",
      title: broadcasts.length + (broadcasts.length === 1 ? " broadcast scheduled" : " broadcasts scheduled"),
      description: formatCount(broadcasts.reduce((n, b) => n + broadcastRecipients(b, data.audiences).count, 0)) + " recipients across them",
      onUndo: () => setData(snapshot),
    });
  };

  const sendBackBroadcast = (broadcast) => setForm({
    title: "Send “" + broadcast.name + "” back?",
    description: "It goes back to " + memberName(data.members, broadcast.submittedById) + " as a draft, with your note, and leaves the review queue.",
    submitLabel: "Send back",
    initial: { note: "" },
    fields: [{ id: "note", kind: "text", label: "What needs changing", 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 needs changing — at least a few words." } : {}),
    run: (v) => {
      patch("broadcasts", broadcast.id, { status: "draft", reviewNote: v.note.trim(), sentBackAt: now, sentBackBy: profile.name });
      pushToast({
        title: "Sent back",
        description: broadcast.name + " · waiting on " + memberName(data.members, broadcast.submittedById),
        onUndo: () => restore("broadcasts", broadcast),
      });
    },
  });

  const submitForReview = (broadcast) => {
    patch("broadcasts", broadcast.id, { status: "in_review", submittedById: profile.id, submittedAt: now });
    pushToast({
      title: "Sent for review",
      description: broadcast.name + " · somebody other than you has to approve it",
      onUndo: () => restore("broadcasts", broadcast),
    });
  };

  const cancelSchedule = (broadcast) => {
    patch("broadcasts", broadcast.id, { status: "draft", approvedById: undefined, approvedAt: undefined });
    pushToast({
      title: "Schedule cancelled",
      description: broadcast.name + " is a draft again and will not send " + formatWhen(broadcast.scheduledAt, now).toLowerCase(),
      onUndo: () => restore("broadcasts", broadcast),
    });
  };

  const retryBroadcast = (broadcast) => {
    const missed = broadcast.sends - broadcast.delivered;
    patch("broadcasts", broadcast.id, { status: "sending", retriedAt: now, failureReason: undefined });
    pushToast({
      title: "Retrying",
      description: formatCount(missed) + " messages that were never handed off are going out again",
      onUndo: () => restore("broadcasts", broadcast),
    });
  };

  const onBroadcastTransition = (broadcast, transition) => {
    if (transition.id === "approve") approveBroadcast(broadcast);
    if (transition.id === "send_back") sendBackBroadcast(broadcast);
    if (transition.id === "send_for_review") submitForReview(broadcast);
    if (transition.id === "cancel_schedule") cancelSchedule(broadcast);
    if (transition.id === "retry") retryBroadcast(broadcast);
  };

  /* ── Content ───────────────────────────────────────────────────────────── */
  const onContentTransition = (item, transition) => {
    if (transition.id === "publish_content") {
      patch("content", item.id, { status: "published", updatedById: profile.id, updatedAt: now });
      pushToast({ variant: "success", title: "Template published", description: item.name + " can be sent from now", onUndo: () => restore("content", item) });
    }
    if (transition.id === "archive_content") {
      patch("content", item.id, { status: "archived", updatedById: profile.id, updatedAt: now });
      pushToast({ title: "Template archived", description: item.name + " is kept for the record and cannot be sent", onUndo: () => restore("content", item) });
    }
    if (transition.id === "restore_content") {
      patch("content", item.id, { status: "published", updatedById: profile.id, updatedAt: now });
      pushToast({ variant: "success", title: "Template restored", description: item.name + " can be sent again", onUndo: () => restore("content", item) });
    }
  };

  /* ── Deliverability ────────────────────────────────────────────────────── */
  /* Instant and harmless: it re-reads DNS and says what it found. */
  const recheckDns = () => {
    setData((d) => Object.assign({}, d, { domains: d.domains.map((domain) => Object.assign({}, domain, { checkedAt: now })) }));
    const passing = authenticatedDomains(data.domains).length;
    pushToast({
      title: "DNS rechecked",
      description: passing + " of " + data.domains.length + " domains pass all three checks — nothing has changed since the last check.",
    });
  };

  /* A new domain arrives with nothing published, which is why its three checks
     are all `missing` — and why creating one immediately moves the nav badge and
     the authenticated figure. Nothing is invented on the user's behalf. */
  const addDomain = () => setForm({
    title: "Add a sending domain",
    description: "It cannot send anything until SPF, DKIM and DMARC are published and pass. Marketing Desk checks them every hour.",
    submitLabel: "Add domain",
    initial: { domain: "", purpose: "" },
    fields: [
      { id: "domain", kind: "input", label: "Domain", placeholder: "send.northwind.studio", hint: "A subdomain you control. Sending from the root domain risks the whole domain's reputation." },
      { id: "purpose", kind: "input", label: "What it is for", placeholder: "Transactional only", hint: "Shown beside it on the deliverability matrix." },
    ],
    validate: (v) => {
      const errors = {};
      if (!/^[a-z0-9.-]+\.[a-z]{2,}$/i.test((v.domain || "").trim())) errors.domain = "Enter a domain, like send.northwind.studio.";
      else if (data.domains.some((d) => d.domain.toLowerCase() === v.domain.trim().toLowerCase())) errors.domain = "That domain is already on this workspace.";
      if (!v.purpose || v.purpose.trim().length < 3) errors.purpose = "Say what it is for — a few words is enough.";
      return errors;
    },
    run: (v) => {
      const id = "dom_" + Math.random().toString(36).slice(2, 6);
      const domain = { id: id, domain: v.domain.trim().toLowerCase(), purpose: v.purpose.trim(), spf: "missing", dkim: "missing", dmarc: "missing", checkedAt: now };
      setData((d) => Object.assign({}, d, { domains: d.domains.concat([domain]) }));
      pushToast({
        title: "Domain added",
        description: domain.domain + " · all three checks are unset, so it counts against the deliverability badge until the records are published",
        onUndo: () => setData((d) => Object.assign({}, d, { domains: d.domains.filter((x) => x.id !== id) })),
      });
    },
  });

  /* A template arrives as a draft with the subject the author typed. Nothing can
     send it until it is published, which is the transition the row then offers. */
  const createTemplate = () => setForm({
    title: "New template",
    description: "It starts as a draft. A journey step or a broadcast can only pick a published template.",
    submitLabel: "New template",
    initial: { name: "", subject: "" },
    fields: [
      { id: "name", kind: "input", label: "What the template is called", placeholder: "Welcome and set-up checklist", hint: "Only your team sees this." },
      { id: "subject", kind: "input", label: "Subject line", placeholder: "Welcome to Marketing Desk — start here", hint: "Leave it blank and it cannot be published." },
    ],
    validate: (v) => {
      const errors = {};
      if (!v.name || v.name.trim().length < 3) errors.name = "Name it — at least three characters.";
      else if (data.content.some((c) => c.name.toLowerCase() === v.name.trim().toLowerCase())) errors.name = "A template with that name already exists.";
      return errors;
    },
    run: (v) => {
      const id = "cnt_" + Math.random().toString(36).slice(2, 7);
      const item = { id: id, name: v.name.trim(), subject: (v.subject || "").trim(), status: "draft", kind: "email", updatedById: profile.id, updatedAt: now };
      setData((d) => Object.assign({}, d, { content: [item].concat(d.content) }));
      pushToast({
        variant: "success",
        title: "Template created",
        description: item.name + " · " + (item.subject ? "draft, ready to publish" : "draft with no subject line, so it cannot be published yet"),
        onUndo: () => setData((d) => Object.assign({}, d, { content: d.content.filter((c) => c.id !== id) })),
      });
    },
  });

  /* An invited person holds a role but cannot use it until they accept, so they
     are not counted as somebody who can approve a send. */
  const inviteTeammate = () => setForm({
    title: "Invite a teammate",
    description: "They get an email with a link to set a password. Their role applies the moment they accept.",
    submitLabel: "Send invitation",
    initial: { name: "", email: "", teamRole: "editor" },
    fields: [
      { id: "name", kind: "input", label: "Name", placeholder: "Ines Guerrero" },
      { id: "email", kind: "input", label: "Work email", placeholder: "ines@northwind.studio" },
      { id: "teamRole", kind: "radio", label: "Role", options: TEAM_ROLES.map((role) => ({ value: role.value, label: role.label + " — " + (ROLE_CAN_APPROVE[role.value] ? "can approve a send" : ROLE_CAN_EDIT[role.value] ? "can write and submit, not approve" : "read only") })) },
    ],
    validate: (v) => {
      const errors = {};
      if (!v.name || v.name.trim().length < 2) errors.name = "Give them a name.";
      if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test((v.email || "").trim())) errors.email = "Enter a valid email address.";
      else if (data.members.some((m) => m.email.toLowerCase() === v.email.trim().toLowerCase())) errors.email = "Somebody with that address is already on this workspace.";
      return errors;
    },
    run: (v) => {
      const id = "mem_" + Math.random().toString(36).slice(2, 6);
      const member = {
        id: id, name: v.name.trim(), email: v.email.trim().toLowerCase(),
        role: labelOf(TEAM_ROLES, v.teamRole) + " · invited", teamRole: v.teamRole, invited: true, invitedAt: now,
      };
      setData((d) => Object.assign({}, d, { members: d.members.concat([member]) }));
      pushToast({
        variant: "success",
        title: "Invitation sent",
        description: member.name + " · " + member.email + " — " + (ROLE_CAN_APPROVE[v.teamRole] ? "they can approve sends once they accept" : "they cannot approve sends"),
        onUndo: () => setData((d) => Object.assign({}, d, { members: d.members.filter((m) => m.id !== id) })),
      });
    },
  });

  /* ── Settings, staged ──────────────────────────────────────────────────── */
  const saveWorkspace = (draft) => {
    const snapshot = workspace;
    setWorkspace(Object.assign({}, workspace, draft));
    pushToast({
      variant: "success",
      title: "Workspace saved",
      description: draft.name + " · sending from " + domainName(data.domains, draft.sendingDomainId) + " · replies to " + draft.replyTo,
      onUndo: () => setWorkspace(snapshot),
    });
  };

  /* Roles are what the approval predicate reads, so saving here recounts the
     sidebar badge, the overview section and every row's button at once. */
  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 }) : m;
      }),
    }));
    const approvers = rows.filter((r) => ROLE_CAN_APPROVE[r.teamRole]);
    pushToast({
      variant: "success",
      title: "Roles saved",
      description: approvers.length + (approvers.length === 1 ? " person" : " people") + " can approve a send: " + approvers.map((r) => memberName(data.members, r.id)).join(", "),
      onUndo: () => setData((d) => Object.assign({}, d, { members: snapshot })),
    });
  };

  /* Writing a broadcast needs three decisions, so this instant screen opens a
     staged dialog. It arrives as a draft: nothing sends until somebody else has
     approved it. */
  const newBroadcast = () => setForm({
    title: "New broadcast",
    description: "It starts as a draft. Sending it needs a review by somebody other than you.",
    submitLabel: "New broadcast",
    initial: { name: "", audience: data.audiences[0].id, days: "7" },
    fields: [
      { id: "name", kind: "input", label: "What it is called", placeholder: "September newsletter", hint: "Only your team sees this." },
      { id: "audience", kind: "select", label: "Who it goes to", options: data.audiences.map((a) => ({ value: a.id, label: a.name + " — " + formatCount(a.size) + " people" })) },
      { id: "days", kind: "input", label: "Days from now to send it", hint: "You can change this before it is approved." },
    ],
    validate: (v) => {
      const errors = {};
      if (!v.name || v.name.trim().length < 3) errors.name = "Name it — at least three characters.";
      const days = Number(v.days);
      if (!(days >= 1 && days <= 90)) errors.days = "Between 1 and 90 days from now.";
      return errors;
    },
    run: (v) => {
      const audience = audienceById(data.audiences, v.audience);
      const broadcast = {
        id: "bc_" + Math.floor(110 + Math.random() * 800),
        name: v.name.trim(),
        status: "draft",
        channel: "email",
        audienceId: v.audience,
        ownerId: profile.id,
        scheduledAt: now + Number(v.days) * DAY_MS,
      };
      setData((d) => Object.assign({}, d, { broadcasts: [broadcast].concat(d.broadcasts) }));
      pushToast({
        variant: "success",
        title: "Broadcast created",
        description: broadcast.name + " · " + formatCount(audience.size) + " in " + audience.name + " · " + formatCountdown(broadcast.scheduledAt, now),
        onUndo: () => setData((d) => Object.assign({}, d, { broadcasts: d.broadcasts.filter((b) => b.id !== broadcast.id) })),
      });
    },
  });

  /* ── The social layer, instant ─────────────────────────────────────────
     A note posts on its own; there is no Save on a screen that holds one. It is
     written into the fixture with its mentions parsed out of the body, so the
     inbox count, the nav badge and every "waiting on" line move with it — and an
     activity entry is written beside it, because that is what the feed reads. */
  const onPostNote = (record, body) => {
    const id = "cmt_" + Math.floor(900 + Math.random() * 90);
    const comment = {
      id, record, authorId: profile.id, at: now, body,
      mentions: (body.match(/@mem_\d+/g) || []).map((m) => m.slice(1)),
      reactions: {},
    };
    setData((d) => Object.assign({}, d, {
      comments: d.comments.concat([comment]),
      activity: [{ id: "act_" + id, actorId: profile.id, at: now, verb: "commented", record, note: null }].concat(d.activity),
    }));
    const named = comment.mentions.map((m) => memberName(data.members, m));
    pushToast({
      variant: "success",
      title: "Note posted",
      description: named.length
        ? "On " + recordTitle(data, record.type, record.id) + " · waiting on " + named.join(" and ") + " until they write on it"
        : "On " + recordTitle(data, record.type, record.id) + " · nobody was mentioned, so nothing is waiting on anyone",
      onUndo: () => setData((d) => Object.assign({}, d, {
        comments: d.comments.filter((c) => c.id !== id),
        activity: d.activity.filter((v) => v.id !== "act_" + id),
      })),
    });
  };

  /* A reaction is instant and harmless, so it is a bare control: no toast, no
     confirm. Clicking again takes yours off. */
  const onReactToNote = (comment, key) => setData((d) => Object.assign({}, d, {
    comments: d.comments.map((c) => {
      if (c.id !== comment.id) return c;
      const ids = c.reactions[key] || [];
      const next = ids.indexOf(profile.id) > -1 ? ids.filter((id) => id !== profile.id) : ids.concat([profile.id]);
      const reactions = Object.assign({}, c.reactions);
      if (next.length) reactions[key] = next; else delete reactions[key];
      return Object.assign({}, c, { reactions: reactions });
    }),
  }));

  /* ── Notifications: instant, one row at a time ─────────────────────────── */
  const onToggleNotification = (row, channel, value) => {
    setPrefs((p) => Object.assign({}, p, { rows: p.rows.map((r) => (r.id === row.id ? Object.assign({}, r, { [channel]: value }) : r)) }));
    const where = { email: "email", slack: "Slack", inApp: "the app" }[channel];
    pushToast({
      title: value ? "Turned on" : "Turned off",
      description: row.label + " — " + (value ? "you will be told in " + where : "you will no longer be told in " + where),
      onUndo: () => setPrefs((p) => Object.assign({}, p, { rows: p.rows.map((r) => (r.id === row.id ? Object.assign({}, r, { [channel]: !value }) : r)) })),
    });
  };

  const onQuietHours = () => setForm({
    title: "Quiet hours",
    description: "Nothing is emailed or posted to Slack between these times. In-app notifications still arrive.",
    submitLabel: "Save quiet hours",
    initial: { from: prefs.quietHours.from, to: prefs.quietHours.to },
    fields: [
      { id: "from", kind: "input", label: "From", hint: "24-hour clock, in " + profile.timezone + "." },
      { id: "to", kind: "input", label: "Until" },
    ],
    validate: (v) => {
      const errors = {};
      if (!/^\d\d:\d\d$/.test(v.from || "")) errors.from = "Use HH:MM.";
      if (!/^\d\d:\d\d$/.test(v.to || "")) errors.to = "Use HH:MM.";
      return errors;
    },
    run: (v) => {
      const before = prefs.quietHours;
      setPrefs((p) => Object.assign({}, p, { quietHours: { from: v.from, to: v.to } }));
      pushToast({ variant: "success", title: "Quiet hours saved", description: v.from + " to " + v.to + ", " + profile.timezone, onUndo: () => setPrefs((p) => Object.assign({}, p, { quietHours: before })) });
    },
  });

  /* ── Channels: connect and reconnect instantly, disconnect confirms ────── */
  const onChannelAction = (channel, action) => {
    if (action === "disconnect") {
      setConfirm({
        title: "Disconnect " + channel.name + "?",
        description: channel.detail + ". The token is revoked immediately and cannot be restored — reconnecting means authorising " + channel.name + " again from scratch.",
        confirmLabel: "Disconnect " + channel.name,
        run: () => {
          patch("channels", channel.id, { state: "available", connectedAt: null, byId: null, detail: "Not connected — reconnect to start it again" });
          pushToast({ title: channel.name + " disconnected", description: "Nothing in the product uses it now." });
        },
      });
      return;
    }
    const detail = action === "connect" ? "Connected just now by " + profile.name : channel.detail;
    patch("channels", channel.id, { state: "connected", connectedAt: now, byId: profile.id, detail: detail });
    pushToast({
      variant: "success",
      title: action === "connect" ? channel.name + " connected" : channel.name + " reconnected",
      description: action === "connect" ? channel.kind + " · it starts working on the next event" : "Its token is good again; the exports that stopped will run tonight",
      onUndo: () => restore("channels", channel),
    });
  };

  /* ── API keys ──────────────────────────────────────────────────────────── */
  const onCreateKey = () => setForm({
    title: "New API key",
    description: "The key is shown once, here, and never again. Give it the narrowest scope that does the job.",
    submitLabel: "Create key",
    initial: { label: "", scope: "read" },
    fields: [
      { id: "label", kind: "input", label: "What it is for", placeholder: "Website signup form" },
      { id: "scope", kind: "radio", label: "Scope", options: KEY_SCOPES.map((s) => ({ value: s.value, label: s.label })) },
    ],
    validate: (v) => (!v.label || v.label.trim().length < 3 ? { label: "Name it — at least three characters." } : {}),
    run: (v) => {
      const id = "key_" + Math.random().toString(36).slice(2, 6);
      const key = {
        id, label: v.label.trim(), scope: v.scope,
        prefix: "nw_" + (v.scope === "write" ? "live" : "read") + "_" + Math.random().toString(36).slice(2, 6),
        createdAt: now, byId: profile.id, lastUsedAt: now,
      };
      setData((d) => Object.assign({}, d, { keys: [key].concat(d.keys) }));
      pushToast({
        variant: "success", title: "Key created",
        description: key.prefix + "··· · " + labelOf(KEY_SCOPES, key.scope) + " · copy it now, it is not shown again",
        onUndo: () => setData((d) => Object.assign({}, d, { keys: d.keys.filter((k) => k.id !== id) })),
      });
    },
  });

  const onRevokeKey = (key) => setConfirm({
    title: "Revoke " + key.label + "?",
    description: "Anything using " + key.prefix + "··· stops working the moment you revoke it. It was last used " + formatWhen(key.lastUsedAt, now).toLowerCase() + ". This cannot be undone.",
    confirmLabel: "Revoke key",
    run: () => {
      setData((d) => Object.assign({}, d, { keys: d.keys.filter((k) => k.id !== key.id) }));
      pushToast({ title: "Key revoked", description: key.label + " · " + key.prefix + "··· no longer authenticates." });
    },
  });

  /* ── Billing ───────────────────────────────────────────────────────────── */
  const onChangePlan = () => pushToast({
    title: "Plans are handled by the billing portal",
    description: "This workspace is on " + billing.plan + " at " + formatMoney(billing.priceMinor, billing.currency) + " a month, renewing " + formatAbsolute(billing.renewsAt).split(",")[0] + ".",
  });
  const onUpdateCard = () => pushToast({
    title: "Card details are handled by the billing portal",
    description: "The card on file is " + billing.card.brand + " ···· " + billing.card.last4 + ", expiring " + billing.card.expiry + ".",
  });

  /* ── Your own profile, staged ──────────────────────────────────────────── */
  const onSaveProfile = (draft) => {
    const before = profile;
    patch("members", profile.id, draft);
    pushToast({
      variant: "success",
      title: "Profile saved",
      description: draft.name + " · " + draft.role + " · every note you have written and the account menu now read this",
      onUndo: () => restore("members", before),
    });
  };

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

  if (MD_AUTH_PATHS.indexOf(path) > -1) {
    const authScreen = () => {
      if (path === "/forgot-password") return <ForgotPasswordScreen workspace={workspace} email={authEmail} onSubmit={(value) => { setAuthEmail(value); setPath("/forgot-password/sent"); }} onNavigate={setPath} />;
      if (path === "/forgot-password/sent") return <ResetLinkSentScreen workspace={workspace} email={authEmail} onNavigate={setPath} />;
      if (path === "/reset-password") return <SetNewPasswordScreen workspace={workspace} link={MD_RESET_LINK} now={now} onSubmit={() => setPath("/reset-password/done")} onNavigate={setPath} />;
      if (path === "/reset-password/expired") return <ResetLinkExpiredScreen workspace={workspace} onNavigate={setPath} />;
      if (path === "/reset-password/done") return <PasswordResetDoneScreen workspace={workspace} email={MD_RESET_LINK.email} onNavigate={setPath} />;
      return <SignInScreen workspace={workspace} onSignIn={() => setPath("/")} onNavigate={setPath} />;
    };
    return (
      <>
        {authScreen()}
        {preview}
      </>
    );
  }

  const journeyMatch = path.match(/^\/journeys\/([^/]+)$/);
  const selectedJourney = journeyMatch && data.journeys.some((j) => j.id === journeyMatch[1]) ? journeyMatch[1] : null;
  const stepsMatch = path.match(/^\/journeys\/([^/]+)\/steps$/);
  const stepsJourney = stepsMatch ? data.journeys.find((j) => j.id === stepsMatch[1] && j.status === "draft") : null;
  const broadcastMatch = path.match(/^\/broadcasts\/([^/]+)$/);
  const selectedBroadcast = broadcastMatch && data.broadcasts.some((b) => b.id === broadcastMatch[1]) ? broadcastMatch[1] : null;
  const personMatch = path.match(/^\/people\/([^/]+)$/);
  const selectedPerson = personMatch && data.members.some((m) => m.id === personMatch[1]) ? personMatch[1] : null;
  const contactMatch = path.match(/^\/contacts\/([^/]+)$/);
  const selectedContact = contactMatch && data.contacts.some((c) => c.id === contactMatch[1]) ? contactMatch[1] : null;
  const audienceMatch = path.match(/^\/audiences\/([^/]+)$/);
  const selectedAudience = audienceMatch && data.audiences.some((a) => a.id === audienceMatch[1]) ? audienceMatch[1] : null;

  const screen = () => {
    if (path === "/") {
      return (
        <OverviewScreen
          data={data} now={now} period={period} onPeriodChange={setPeriod} onNavigate={setPath}
          viewer={profile} onJourneyTransition={onJourneyTransition} onBroadcastTransition={onBroadcastTransition}
          onReactToNote={onReactToNote}
        />
      );
    }
    if (path === "/journeys/new") return <NewJourneyScreen data={data} onCreate={createJourney} onNavigate={setPath} />;
    if (stepsJourney) return <JourneyStepsScreen journey={stepsJourney} data={data} onSave={(steps) => { saveJourneySteps(stepsJourney, steps); setPath("/journeys/" + stepsJourney.id); }} onNavigate={setPath} />;
    if (path === "/journeys" || selectedJourney) {
      return (
        <JourneysScreen
          data={data} now={now} path={selectedJourney ? path : "/journeys"} selectedId={selectedJourney}
          onNavigate={setPath} onJourneyTransition={onJourneyTransition}
        />
      );
    }    if (selectedBroadcast) {
      return (
        <BroadcastScreen
          data={data} now={now} broadcastId={selectedBroadcast} viewer={profile}
          onNavigate={setPath} onBroadcastTransition={onBroadcastTransition}
        />
      );
    }
    if (path === "/broadcasts") {
      return (
        <BroadcastsScreen
          data={data} now={now} period={period} onPeriodChange={setPeriod} viewer={profile}
          onNavigate={setPath} onBroadcastTransition={onBroadcastTransition} onBulkApprove={bulkApprove}
          onNewBroadcast={newBroadcast} pushToast={pushToast}
        />
      );
    }
    if (path === "/audiences/new") return <NewAudienceScreen data={data} workspace={workspace} now={now} onCreate={createAudience} />;
    if (selectedAudience) {
      return <AudienceScreen data={data} workspace={workspace} now={now} audienceId={selectedAudience} onNavigate={setPath} pushToast={pushToast} />;
    }
    if (path === "/audiences") return <AudiencesScreen data={data} workspace={workspace} onNavigate={setPath} pushToast={pushToast} />;
    if (path === "/content") return <ContentScreen data={data} now={now} onNavigate={setPath} onContentTransition={onContentTransition} onNewTemplate={createTemplate} pushToast={pushToast} />;
    if (path === "/deliverability") return <DeliverabilityScreen data={data} now={now} onNavigate={setPath} onRecheck={recheckDns} onAddDomain={addDomain} />;
    if (path === "/calendar") return <CalendarScreen data={data} now={now} viewer={profile} onNavigate={setPath} onNewBroadcast={newBroadcast} />;
    if (path === "/approvals") return <ApprovalsScreen data={data} now={now} viewer={profile} onNavigate={setPath} onBroadcastTransition={onBroadcastTransition} onBulkApprove={bulkApprove} />;
    if (path === "/people") return <PeopleScreen data={data} now={now} viewer={profile} onNavigate={setPath} onInvite={inviteTeammate} />;
    if (selectedPerson) return <PersonScreen data={data} now={now} memberId={selectedPerson} viewer={profile} onNavigate={setPath} onBroadcastTransition={onBroadcastTransition} onReactToNote={onReactToNote} />;
    if (path === "/contacts") return <ContactsScreen data={data} workspace={workspace} now={now} onNavigate={setPath} />;
    if (selectedContact) return <ContactScreen data={data} workspace={workspace} now={now} contactId={selectedContact} onNavigate={setPath} />;
    if (path === "/inbox") return <InboxScreen data={data} now={now} viewer={profile} onNavigate={setPath} onPostNote={onPostNote} onReactToNote={onReactToNote} />;
    if (path === "/activity") return <ActivityScreen data={data} now={now} onNavigate={setPath} />;
    if (path === "/reports") return <ReportsScreen data={data} now={now} period={period} onPeriodChange={setPeriod} onNavigate={setPath} />;
    if (path === "/me/profile") return <MyProfileScreen data={data} viewer={profile} now={now} prefs={prefs} onSave={onSaveProfile} onNavigate={setPath} />;
    if (path === "/settings/channels") return <ChannelsSettingsScreen data={data} now={now} onChannelAction={onChannelAction} />;
    if (path === "/settings/sending") return <SendingSettingsScreen data={data} workspace={workspace} now={now} onNavigate={setPath} onRecheck={recheckDns} onAddDomain={addDomain} />;
    if (path === "/settings/notifications") return <NotificationsSettingsScreen data={data} prefs={prefs} viewer={profile} onToggle={onToggleNotification} onQuietHours={onQuietHours} />;
    if (path === "/settings/billing") return <BillingScreen data={data} workspace={workspace} billing={billing} now={now} onChangePlan={onChangePlan} onUpdateCard={onUpdateCard} />;
    if (path === "/settings/api") return <ApiSettingsScreen data={data} now={now} onCreateKey={onCreateKey} onRevokeKey={onRevokeKey} />;
    if (path === "/settings") return <WorkspaceSettingsScreen workspace={workspace} data={data} onSave={saveWorkspace} />;
    if (path === "/settings/team") return <TeamScreen data={data} viewer={profile} onSave={saveTeam} onInvite={inviteTeammate} />;
    return <RouteNotBuilt path={path} onNavigate={setPath} />;
  };

  const recordGroups = [
    { label: "Journeys needing attention", items: journeysNeedingAttention(data.journeys, now).map((j) => ({ value: j.id, label: j.name + " · " + journeyConcern(j, now), route: "/journeys/" + j.id })) },
    { label: "Broadcasts waiting on you", items: broadcastsToReview(data.broadcasts, profile).map((b) => ({ value: b.id, label: b.name + " · " + formatCountdown(b.scheduledAt, now), route: "/broadcasts/" + b.id })) },
    { label: "Journeys", items: data.journeys.map((j) => ({ value: j.id, label: j.name + " · " + statusValue(JOURNEY_STATUS, j.status).label, route: "/journeys/" + j.id })) },
    { label: "Broadcasts", items: data.broadcasts.map((b) => ({ value: b.id, label: b.name + " · " + statusValue(BROADCAST_STATUS, b.status).label, route: "/broadcasts/" + b.id })) },
    { label: "Audiences", items: data.audiences.map((a) => ({ value: a.id, label: a.name + " · " + formatCount(a.size) + " people", route: "/audiences/" + a.id })) },
    { label: "People", items: data.members.map((m) => ({ value: m.id, label: m.name + " · " + m.role, route: "/people/" + m.id })) },
    { label: "Contacts", items: data.contacts.slice(0, 12).map((c) => ({ value: c.id, label: c.name + " · " + c.email, route: "/contacts/" + c.id })) },
    { label: "Templates", items: data.content.map((c) => ({ value: c.id, label: c.name + " · " + statusValue(CONTENT_STATUS, c.status).label, route: "/content" })) },
  ];

  return (
    <MdShell
      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={MD_ROUTES}
        recordGroups={recordGroups}
        onNavigate={setPath}
        placeholder="Search journeys, broadcasts, audiences, 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}
    </MdShell>
  );
}
window.__mdReal = App;
if (!window.MdApp) window.MdApp = App;
