// ---------------------------------------------------------------------------
// inbox/InboxPage.tsx — WAVE 32 · T20 (owner item 16, ruling R7, contract C3).
//
// Owner, item 16: *"Alerts becomes an Inbox that looks like email."* R7: it is a
// FULL MODULE and the pop-up pane is deleted — the bell navigates here, there is
// no dropdown.
//
// So this is a LIST + a READING PANE, which is the only structural thing an email
// client actually is. The pane it replaces was a 520px drawer with one column, and
// that shape is why the feature never read like mail: a drawer has no room for a
// subject line above a body, so subject and body were one field, and a
// notification could only ever be a sentence with no sender.
//
// ⛔ NOT A REGISTRY MODULE — a CHROME route, like Home and Connectors. Every
// provisioned tenant carries a restricted `modules` list (`['analyst','automation']`
// on all three), and a registry key outside it is silently OMITTED from the rail.
// Registering Inbox would make it invisible in every tenant while every gate stayed
// green. `alerts` was already chrome; Inbox inherits the right shape. The MOUNT is
// A's (`shell/Shell.tsx`, W32-T04); this file only exports the surface.
//
// ⚠ STYLING REUSES THE `.alerts-*` RULES ON PURPOSE. They are the same feature's
// classes, they already exist, and `src/index.css` is in no lane's fence this wave
// (D-186's shape, second occurrence). Page-frame geometry that has no class yet is
// inline rather than invented: a class name with no rule behind it looks identical
// to a rule that failed to load.
// ---------------------------------------------------------------------------

import { useCallback, useEffect, useMemo, useState } from "react";
import {
  deleteAlert,
  fetchAlerts,
  fetchInbox,
  markRead,
  runAlert,
} from "../alerts/alertsApi";
import {
  NOTIF_KIND_AUTOMATION,
  applyRead,
  canMarkAll,
  paneView,
  stampText,
} from "../alerts/alertsModel";
import type { Alert, Inbox, Notification, NotificationTarget } from "../alerts/alertsModel";
import type { InboxSections } from "./inboxModel";
import {
  UNOPENABLE_NOTE,
  canOpen,
  inboxSections,
  kindLabel,
  previewOf,
  subjectOf,
  targetOf,
} from "./inboxModel";
import { BellIcon } from "../ui/icons";
// ⭐ WAVE 27 C5, CARRIED FORWARD — the ONE generated loop mark, consumed rather than redrawn.
// ⛔ It is here because W32-T21 DELETES `AlertsPane`, and a rename ticket must not quietly drop a
// shipped visual: an automation row wore this mark, and losing it in the move would be a silent
// regression behind a green gate. ⛔ NOT A STATUS DOT (W26 R14 deleted the whole status
// vocabulary from the automation module) and never a hand-copied path — `shell/Brand.tsx`'s own
// header records a redrawn copy silently painting LAST WAVE'S BRAND while a comment claimed parity.
import { Mark } from "../shell/Brand";

/** The page frame. `.shell-home`'s geometry, without borrowing Home's class name for a
 *  surface that is not Home — the rule is six declarations and copying them is cheaper than
 *  a shared class two modules would then have to agree about. */
const PAGE: React.CSSProperties = {
  height: "100%",
  overflow: "hidden",
  padding: "34px 44px 28px",
  boxSizing: "border-box",
  background: "var(--lp-wash)",
  display: "flex",
  flexDirection: "column",
};

const SPLIT: React.CSSProperties = {
  flex: "1 1 auto",
  minHeight: 0,
  display: "flex",
  gap: 20,
  alignItems: "stretch",
};

const CARD: React.CSSProperties = {
  background: "var(--lp-surface)",
  border: "1px solid var(--lp-line)",
  borderRadius: "var(--lp-r-lg)",
  overflow: "auto",
  minHeight: 0,
};

export default function InboxPage({
  onOpenTarget,
  onInbox,
  onToast,
  seed,
}: {
  /**
   * ⛔ REQUIRED, NOT OPTIONAL, and this is the lesson written into a type rather than a comment.
   * An optional callback the frame forgot to pass degrades to "clicking an Inbox item does
   * nothing" — indistinguishable from "the feature was never built", and red in no gate. A
   * required prop fails `tsc` the moment it is unmounted. `AlertsPane` carried the same rule on
   * `onOpenAutomation` and `seed`; C3 carries it forward as the wave's cross-fence contract.
   *
   * The FRAME owns routing — it holds the router and knows which module surfaces exist. This
   * module owns the QUESTION (`inboxModel.routeForTarget` turns a wire target into a surface +
   * what it needs) so the frame's dispatcher is a switch over two literals rather than a second
   * copy of the topic→route table.
   */
  onOpenTarget: (t: NotificationTarget) => void;
  /** Hand the freshly-read inbox back so the rail badge and this module agree. */
  onInbox: (inbox: Inbox) => void;
  onToast: (message: string) => void;
  /**
   * ⛔ REQUIRED, for the reason `onOpenTarget` is (W31-T23's shipped bug, kept as a type). The
   * frame has ALREADY fetched this — it is what paints the rail badge the user just clicked — so
   * the module opens knowing the account's count, and "Mark all read" is live from the first
   * frame and stays live if this module's own read fails.
   */
  seed: Inbox;
}) {
  const [inbox, setInbox] = useState<Inbox>(seed);
  const [alerts, setAlerts] = useState<Alert[]>([]);
  const [error, setError] = useState("");
  const [busy, setBusy] = useState(false);
  const [selectedId, setSelectedId] = useState("");
  /** ⛔ THE STATE THE PANE DID NOT HAVE (W31-T23). Without it "no rows yet" and "no rows, ever"
   *  are the same value, so an in-flight read — 3,280 ms live on tenant #0 — printed a confident
   *  "Nothing new." over an inbox nobody had looked at yet. */
  const [phase, setPhase] = useState<"pending" | "ready" | "error">("pending");

  const publish = useCallback(
    (next: Inbox) => {
      setInbox(next);
      onInbox(next);
    },
    [onInbox]
  );

  const load = useCallback(async () => {
    const [inboxRes, alertRes] = await Promise.all([fetchInbox(), fetchAlerts()]);
    if (inboxRes.ok) {
      publish(inboxRes.value);
      setError("");
      setPhase("ready");
    } else {
      // ⚠ THE SEEDED INBOX SURVIVES A FAILED READ. Blanking it would take the count — and with it
      // the Mark-all control — away from a reader who can see the badge. The failure is REPORTED
      // and nothing the frame already knew is discarded.
      setError(inboxRes.message);
      setPhase("error");
    }
    if (alertRes.ok) setAlerts(alertRes.value);
  }, [publish]);

  useEffect(() => {
    void load();
  }, [load]);

  const sections = useMemo(() => inboxSections(inbox.items), [inbox.items]);
  const rowCount = sections.unread.length + sections.read.length;
  const view = paneView(phase, rowCount, seed.unread);
  const selected = useMemo(
    () => inbox.items.find((n) => n.id === selectedId) ?? null,
    [inbox.items, selectedId]
  );

  const setRead = useCallback(
    (n: Notification, read: boolean) => {
      if (n.read === read) return;
      // Optimistic, with the server's own convention (`ids`, `read`) sent explicitly — and
      // reverted OUT LOUD if refused, because a badge that silently disagrees with the list is
      // how a reader learns to stop trusting it.
      const before = inbox;
      publish(applyRead(inbox, [n.id], read));
      void markRead([n.id], read).then((r) => {
        if (!r.ok) {
          publish(before);
          onToast(r.message);
        }
      });
    },
    [inbox, publish, onToast]
  );

  const markAll = useCallback(() => {
    const before = inbox;
    // `applyRead(…, null, true)` zeroes the count from the ITEMS, which is right here because the
    // request below clears the whole ACCOUNT (`ids: null`) — 0 is what the server is about to
    // make true. The revert on failure is what keeps it honest.
    publish({ ...applyRead(inbox, null, true), unread: 0 });
    void markRead(null, true).then((r) => {
      if (!r.ok) {
        publish(before);
        onToast(r.message);
      }
    });
  }, [inbox, publish, onToast]);

  /**
   * ⭐ SELECTING IS READING. Opening a mail item marks it read — that is what the metaphor
   * promises, and a reader who has to press a second button to clear a badge they have plainly
   * seen concludes the badge is decorative.
   *
   * ⛔ IT DOES NOT NAVIGATE. Item 19 (T29) is the separate act: the reading pane offers "Open …",
   * which is where `onOpenTarget` fires. The old pane conflated them, so there was no way to READ
   * a notification without leaving the surface you were reading it on.
   */
  const select = useCallback(
    (n: Notification) => {
      setSelectedId(n.id);
      setRead(n, true);
    },
    [setRead]
  );

  const open = useCallback(
    (n: Notification) => {
      const t = targetOf(n);
      if (!t) return onToast(UNOPENABLE_NOTE);
      setRead(n, true);
      onOpenTarget(t);
    },
    [onOpenTarget, onToast, setRead]
  );

  const runAlertNow = useCallback(
    (a: Alert) => {
      setBusy(true);
      void runAlert(a.id).then((r) => {
        setBusy(false);
        if (!r.ok) return onToast(r.message);
        const v = r.value as { new?: unknown[]; skipped?: string };
        onToast(
          v?.skipped
            ? `Skipped: ${v.skipped}`
            : `${(v?.new ?? []).length} new since the last check.`
        );
        void load();
      });
    },
    [load, onToast]
  );

  const deleteAlertNow = useCallback(
    (a: Alert) => {
      setBusy(true);
      void deleteAlert(a.id).then((r) => {
        setBusy(false);
        if (!r.ok) return onToast(r.message);
        setAlerts((cur) => cur.filter((x) => x.id !== a.id));
        void load();
      });
    },
    [load, onToast]
  );

  return (
    <InboxSurface
      alerts={alerts}
      error={error}
      busy={busy}
      view={view}
      sections={sections}
      selected={selected}
      selectedId={selectedId}
      onSelect={select}
      onOpen={open}
      onSetRead={setRead}
      onMarkAll={markAll}
      onRunAlert={runAlertNow}
      onDeleteAlert={deleteAlertNow}
      canMarkAllNow={canMarkAll(inbox.unread, busy)}
    />
  );
}

/**
 * ⭐ THE SURFACE, WITH NO FETCH IN IT — every value arrives as a prop.
 *
 * ⛔ SPLIT FOR ONE REASON, AND IT IS THE `done-when`: *"observed by a person"*. `InboxPage` reads
 * on mount, and `renderToStaticMarkup` never runs an effect — so a shot of the page can only ever
 * photograph its spinner. `_inbox_shot.tsx` renders THIS with rows that came through the real
 * server derivation (`routes_alerts.inbox_view`), so what a reviewer looks at is the shipped
 * markup over the shipped payload rather than a picture of a fixture. It is the same division
 * `connectors/_conn_shot.tsx` already relies on.
 */
export function InboxSurface({
  alerts,
  error,
  busy,
  view,
  sections,
  selected,
  selectedId,
  onSelect,
  onOpen,
  onSetRead,
  onMarkAll,
  onRunAlert,
  onDeleteAlert,
  canMarkAllNow,
}: {
  alerts: Alert[];
  error: string;
  busy: boolean;
  view: ReturnType<typeof paneView>;
  sections: InboxSections;
  selected: Notification | null;
  selectedId: string;
  onSelect: (n: Notification) => void;
  onOpen: (n: Notification) => void;
  onSetRead: (n: Notification, read: boolean) => void;
  onMarkAll: () => void;
  onRunAlert: (a: Alert) => void;
  onDeleteAlert: (a: Alert) => void;
  canMarkAllNow: boolean;
}) {
  const row = (n: Notification) => {
    const preview = previewOf(n);
    return (
      <li key={n.id} className={"alerts-row" + (n.read ? "" : " is-unread")}>
        <button
          type="button"
          className="alerts-row-main"
          aria-current={n.id === selectedId ? "true" : undefined}
          style={n.id === selectedId ? { background: "var(--lp-wash)" } : undefined}
          onClick={() => onSelect(n)}
          title={subjectOf(n)}
        >
          {/* THE SUBJECT, on its own line and first — the header this feature never had. */}
          <span className="alerts-row-label">
            {n.kind === NOTIF_KIND_AUTOMATION ? (
              // ⚠ INSIDE the label span, not beside it. `.alerts-row-main` is a flex COLUMN, so a
              // sibling here becomes a third ROW stacked above the text
              // ([[wrong-parent-not-broken-control]]); inline, it sits on the line it annotates
              // and rides the same ellipsis.
              <span className="alerts-row-mark">
                <Mark size={12} />
              </span>
            ) : null}
            {subjectOf(n)}
          </span>
          <span className="alerts-row-meta">
            {kindLabel(n.kind)}
            {preview ? ` · ${preview}` : ""}
            {" · "}
            {/* Readable, never re-derived: `stampText` is string surgery over the server's own
                UTC-with-offset stamp (D-18). Parsing it into a browser Date is how a tenant a
                day ahead gets told an event happened tomorrow. */}
            {stampText(n.at)}
          </span>
        </button>
        <button
          type="button"
          className="alerts-row-toggle"
          aria-label={n.read ? `Mark ${subjectOf(n)} unread` : `Mark ${subjectOf(n)} read`}
          title={n.read ? "Mark unread" : "Mark read"}
          onClick={() => onSetRead(n, !n.read)}
        >
          {/* The ACTION, not the state: "Read" beside an unread row reads as a label for the
              row itself. */}
          {n.read ? "Mark unread" : "Mark read"}
        </button>
      </li>
    );
  };

  return (
    <div className="inbox-page" style={PAGE}>
      <header className="alerts-head" style={{ padding: "0 0 14px" }}>
        <h2>
          <BellIcon size={18} className="alerts-bell" /> Inbox
        </h2>
        <button
          type="button"
          className="alerts-markall"
          // ⭐⭐ W31-T23 (R6), KEPT: the question is about the ACCOUNT, so it is asked of the
          // count the frame already holds. `inbox.unread === 0` was this surface's OWN state —
          // zero until its fetch landed and zero for ever if it failed — so the one verb that
          // clears the badge was disabled in exactly the two situations the owner reported.
          disabled={!canMarkAllNow}
          onClick={onMarkAll}
        >
          Mark all read
        </button>
      </header>

      {error ? <p className="shell-newdb-err">{error}</p> : null}

      <div style={SPLIT}>
        <div style={{ ...CARD, flex: "1 1 42%", padding: "4px 16px 16px" }}>
          {view === "pending" ? (
            /* R6: the mark, never the word. `verify_icons` asserts this product ships no load
               TEXT — and the sentence that used to occupy this space was worse than a word, it
               was an ANSWER ("Nothing new.") to a question nobody had asked yet. */
            <p className="alerts-empty" aria-busy="true">
              <span className="lp-spin" role="status" aria-label="Loading the inbox" />
            </p>
          ) : view === "error" ? (
            <p className="alerts-empty">
              These notifications could not be loaded. The count beside the bell is still this
              account&rsquo;s — &ldquo;Mark all read&rdquo; above clears it.
            </p>
          ) : view === "empty" ? (
            <p className="alerts-empty">
              Nothing new. An alert watches ONE view and tells you when a record it had never
              matched arrives in it — make one from a view&rsquo;s menu.
            </p>
          ) : (
            <>
              {/* ⛔ THE UNREAD HEADING RENDERS AT ZERO TOO. Hiding it would make "I have read
                  everything" and "the unread half failed to render" the same picture. */}
              <h3 className="home-section-title" style={{ margin: "14px 0 6px" }}>
                Unread{sections.unread.length ? ` (${sections.unread.length})` : ""}
              </h3>
              {sections.unread.length ? (
                <ul className="alerts-list">{sections.unread.map(row)}</ul>
              ) : (
                <p className="alerts-empty" style={{ margin: "4px 0 10px" }}>
                  Nothing unread.
                </p>
              )}
              {sections.read.length ? (
                <>
                  <h3 className="home-section-title" style={{ margin: "18px 0 6px" }}>
                    Earlier
                  </h3>
                  <ul className="alerts-list">{sections.read.map(row)}</ul>
                </>
              ) : null}
            </>
          )}

          {alerts.length > 0 ? (
            <section className="alerts-watching">
              <h3 className="home-section-title">Watching</h3>
              {alerts.map((a) => (
                <div key={a.id} className="alerts-watch-row">
                  <span className="alerts-watch-label">{a.label}</span>
                  <span className="alerts-watch-meta">
                    {/* The remembered set's SIZE, which is what "no news" means here: an alert
                        with 40 matches and nothing new is working. */}
                    {a.matched.toLocaleString()} matched
                    {a.lastError ? ` · ${a.lastError}` : ""}
                  </span>
                  <button
                    type="button"
                    className="alerts-watch-run"
                    disabled={busy}
                    onClick={() => onRunAlert(a)}
                  >
                    Check now
                  </button>
                  <button
                    type="button"
                    className="alerts-watch-del"
                    disabled={busy}
                    aria-label={`Delete the alert ${a.label}`}
                    onClick={() => onDeleteAlert(a)}
                  >
                    Delete
                  </button>
                </div>
              ))}
            </section>
          ) : null}
        </div>

        {/* THE READING PANE. Always present, so the layout does not jump when a row is picked —
            and so the surface reads as mail before anything is selected. */}
        <div className="inbox-detail" style={{ ...CARD, flex: "1 1 58%", padding: "20px 24px" }}>
          {selected === null ? (
            <p className="alerts-empty">Select a notification to read it.</p>
          ) : (
            <>
              <h3
                style={{
                  margin: "0 0 6px",
                  fontSize: "var(--lp-fs-sm)",
                  fontWeight: 650,
                  color: "var(--lp-ink)",
                }}
              >
                {subjectOf(selected)}
              </h3>
              <p className="alerts-row-meta" style={{ margin: "0 0 16px" }}>
                {kindLabel(selected.kind)} · {stampText(selected.at)}
              </p>
              {previewOf(selected) ? (
                <p
                  style={{
                    margin: "0 0 18px",
                    fontSize: "var(--lp-fs-xs)",
                    lineHeight: 1.55,
                    color: "var(--lp-ink)",
                  }}
                >
                  {previewOf(selected)}
                </p>
              ) : null}
              {canOpen(selected) ? (
                <button
                  type="button"
                  className="login-submit"
                  onClick={() => onOpen(selected)}
                >
                  {/* Item 19 (T29): the verb names the DESTINATION rather than promising
                      "open" generically — two kinds go two places. */}
                  {kindLabel(selected.kind) === "Automation"
                    ? "Open the automation"
                    : "Open the database"}
                </button>
              ) : (
                /* ⛔ SAID ON THE ITEM, NOT ON THE CLICK. The pane raised this as a toast AFTER
                   the reader pressed a button that looked live — "you pressed this and were
                   told off" rather than "this is unavailable". */
                <p className="alerts-empty" style={{ margin: 0 }}>
                  {UNOPENABLE_NOTE}
                </p>
              )}
              <button
                type="button"
                className="alerts-row-toggle"
                style={{ marginLeft: 10 }}
                onClick={() => onSetRead(selected, !selected.read)}
              >
                {selected.read ? "Mark unread" : "Mark read"}
              </button>
            </>
          )}
        </div>
      </div>
    </div>
  );
}
