// ---------------------------------------------------------------------------
// settings / StatementsPane.tsx — the statement-of-account sender (EXIT-6).
//
// Ported off `app.py::_collections_statements` when Streamlit was deleted. Owner
// ruling wave-17 item 15 retired the Collections DASHBOARD (it is the shared
// "Collections" view on the Customer grid) but explicitly left this workflow
// untouched — so it is the last live surface that had nowhere else to go.
//
// ⛔ THIS IS THE ONE SANCTIONED ODOO WRITER. Queueing these emails is the only
// write the product performs, so the original's shape is preserved deliberately
// rather than modernised: review → tick → preview → test → CONFIRM → send. The
// confirm step is a product requirement, not a formality, and the send button is
// disabled while SAFE_MODE is on exactly as the Streamlit page had it.
//
// ⚠ THE GUARDRAIL IS THE SERVER'S. Everything this file does with `safeMode` is
// presentation: disabling a control and explaining why. `collections_send`
// refuses an out-of-allow-list recipient in the data layer, which is what makes
// the guarantee real. Never read the disabled button as the enforcement.
// ---------------------------------------------------------------------------
import { useCallback, useEffect, useMemo, useState } from "react";

import {
  loadStatements,
  previewStatement,
  sendStatements,
  type SendOutcome,
  type StatementRow,
  type StatementTemplates,
  type StatementsPayload,
} from "./statementsApi";

const money = (n: number) =>
  `$${Math.round(Number(n) || 0).toLocaleString("en-US")}`;

/** The tiers the original defaulted ON. "Monitor" = open receivable but nothing
 *  overdue, and the Streamlit page excluded it by default for that reason. */
const DEFAULT_TIERS = ["A-Urgent", "B-Active", "C-Light"];

export function StatementsPane() {
  const [data, setData] = useState<StatementsPayload | null>(null);
  const [err, setErr] = useState("");
  const [busy, setBusy] = useState(false);
  const [tiers, setTiers] = useState<string[]>(DEFAULT_TIERS);
  const [emailOnly, setEmailOnly] = useState(true);
  const [search, setSearch] = useState("");
  const [picked, setPicked] = useState<Set<string>>(new Set());
  const [tpl, setTpl] = useState<StatementTemplates | null>(null);
  const [showTpl, setShowTpl] = useState(false);
  const [previewOf, setPreviewOf] = useState("");
  const [preview, setPreview] = useState<{ html: string; to: string; subject: string } | null>(null);
  const [testTo, setTestTo] = useState("");
  const [confirming, setConfirming] = useState(false);
  const [result, setResult] = useState<SendOutcome | null>(null);

  const reload = useCallback(async (refresh = false) => {
    setBusy(true);
    const r = await loadStatements(refresh);
    setBusy(false);
    if (!r.ok) { setErr(r.message); return; }
    setErr("");
    setData(r.data);
    setTpl((t) => t ?? r.data.templates);
    setTestTo((v) => v || (r.data.safeMode ? (r.data.safeRecipients[0] ?? "") : ""));
  }, []);

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

  const view = useMemo(() => {
    if (!data) return [] as StatementRow[];
    const s = search.trim().toLowerCase();
    return data.rows.filter(
      (r) =>
        tiers.includes(r.Tier) &&
        (!emailOnly || !!r.Email) &&
        (!s || r.Customer.toLowerCase().includes(s)),
    );
  }, [data, tiers, emailOnly, search]);

  // The selection follows the FILTER: a customer ticked and then filtered out
  // must not ride along invisibly into a send. Intersecting here (rather than
  // pruning on every filter change) keeps that true without fighting the user's
  // ticks when they widen the filter again.
  const selected = useMemo(
    () => view.filter((r) => picked.has(r.Customer)),
    [view, picked],
  );
  const sendable = useMemo(() => selected.filter((r) => r.Email), [selected]);
  const noEmail = useMemo(() => selected.filter((r) => !r.Email), [selected]);

  const previewRow = previewOf || selected[0]?.Customer || view[0]?.Customer || "";

  useEffect(() => {
    if (!previewRow || !tpl) { setPreview(null); return; }
    let live = true;
    void previewStatement(previewRow, tpl).then((r) => {
      if (live) setPreview(r.ok ? r.data : null);
    });
    return () => { live = false; };
  }, [previewRow, tpl]);

  const toggle = (name: string) =>
    setPicked((p) => {
      const n = new Set(p);
      if (n.has(name)) n.delete(name); else n.add(name);
      return n;
    });

  const doSend = async (customers: string[], overrideTo?: string) => {
    if (!tpl || !customers.length) return;
    setBusy(true);
    const r = await sendStatements(customers, tpl, overrideTo);
    setBusy(false);
    setConfirming(false);
    if (!r.ok) { setErr(r.message); return; }
    setErr("");
    setResult(r.data);
    if (!overrideTo) setPicked(new Set());
  };

  if (!data && !err) return <p className="set-help">Loading the collection list…</p>;

  return (
    <div className="set-pane">
      <h3 className="set-h">Statements</h3>
      <p className="set-pane-intro set-help">
        Queue per-customer statement-of-account emails. The list is the Odoo follow-up filter
        (Reminders = Automatic, receivable over $1, GIFTWARE excluded), tiered by urgency.
        Statements send as <b>{data?.sender.name}</b> through the Odoo mail queue (Office 365
        relay, ~15 min); every send is logged on the customer chatter in Odoo. Queueing these
        emails is the only write this app can perform, and nothing sends without the confirm step.
      </p>

      {data?.safeMode ? (
        <div className="stmt-warn">
          <b>Testing guardrail is ON</b> — email can only go to{" "}
          {data.safeRecipients.join(", ")}. Bulk send to customers is disabled. This is enforced
          in the data layer, not by this screen; set the Space secret <code>SAFE_MODE=0</code> to
          go live.
        </div>
      ) : null}
      {err ? <div className="set-error">{err}</div> : null}

      {data ? (
        <>
          <div className="stmt-row">
            {data.tiers.map((t) => (
              <label key={t} className="set-chip">
                <input
                  type="checkbox"
                  checked={tiers.includes(t)}
                  onChange={() =>
                    setTiers((cur) =>
                      cur.includes(t) ? cur.filter((x) => x !== t) : [...cur, t],
                    )
                  }
                />
                {t}
              </label>
            ))}
            <label className="set-chip">
              <input
                type="checkbox"
                checked={emailOnly}
                onChange={(e) => setEmailOnly(e.currentTarget.checked)}
              />
              Has email only
            </label>
            <input
              className="set-input"
              placeholder="Search customer"
              value={search}
              onChange={(e) => setSearch(e.currentTarget.value)}
            />
            <button className="set-secondary" disabled={busy} onClick={() => void reload(true)}>
              Refresh from Odoo
            </button>
          </div>
          <p className="set-help stmt-small">
            Data as of {data.loadedAt}. “Monitor” customers have open receivables but nothing
            overdue — usually excluded from monthly statements.
          </p>

          <button className="stmt-link" onClick={() => setShowTpl((v) => !v)}>
            {showTpl ? "Hide" : "Show"} statement template (applies to every send below)
          </button>
          {showTpl && tpl ? (
            <div className="stmt-stack">
              <label className="set-label">
                Subject <span className="set-help">— placeholders: {"{customer} {company} {month}"}</span>
                <input
                  className="set-input stmt-wide"
                  value={tpl.subject}
                  onChange={(e) => setTpl({ ...tpl, subject: e.currentTarget.value })}
                />
              </label>
              <label className="set-label">
                Intro (HTML ok)
                <textarea
                  className="stmt-textarea"
                  rows={4}
                  value={tpl.intro}
                  onChange={(e) => setTpl({ ...tpl, intro: e.currentTarget.value })}
                />
              </label>
              <label className="set-label">
                Footer (HTML ok)
                <textarea
                  className="stmt-textarea"
                  rows={4}
                  value={tpl.footer}
                  onChange={(e) => setTpl({ ...tpl, footer: e.currentTarget.value })}
                />
              </label>
            </div>
          ) : null}

          <div className="stmt-tablewrap">
            <table className="stmt-table">
              <thead>
                <tr>
                  <th style={{ width: 44 }}>Send</th>
                  <th>Customer</th>
                  <th>Email</th>
                  <th>Tier</th>
                  <th className="stmt-num">Overdue</th>
                  <th className="stmt-num">Receivable</th>
                </tr>
              </thead>
              <tbody>
                {view.map((r) => (
                  <tr key={r.Customer} className={picked.has(r.Customer) ? "is-picked" : undefined}>
                    <td>
                      <input
                        type="checkbox"
                        aria-label={`Send to ${r.Customer}`}
                        checked={picked.has(r.Customer)}
                        onChange={() => toggle(r.Customer)}
                      />
                    </td>
                    <td>{r.Customer}</td>
                    <td className={r.Email ? undefined : "set-muted"}>{r.Email || "(no email)"}</td>
                    <td>{r.Tier}</td>
                    <td className="stmt-num">{money(r.Overdue)}</td>
                    <td className="stmt-num">{money(r.Receivable)}</td>
                  </tr>
                ))}
              </tbody>
            </table>
            {/* Owner rule [[no-unverifiable-aggregates]]: state the denominator, always. */}
            <p className="set-help stmt-small">
              Showing {view.length} of {data.rows.length} customers · {selected.length} selected ·
              combined overdue {money(selected.reduce((a, r) => a + (Number(r.Overdue) || 0), 0))}.
            </p>
          </div>

          {view.length ? (
            <div className="stmt-stack">
              <label className="set-label">
                Preview customer
                <select
                  className="set-input"
                  value={previewRow}
                  onChange={(e) => setPreviewOf(e.currentTarget.value)}
                >
                  {(selected.length ? selected : view).map((r) => (
                    <option key={r.Customer} value={r.Customer}>{r.Customer}</option>
                  ))}
                </select>
              </label>
              {preview ? (
                <>
                  <p className="set-help stmt-small">
                    From: {data.sender.name} &lt;{data.sender.email}&gt; · reply-to{" "}
                    {data.sender.replyTo} · To: {preview.to || "(no email)"} · Subject:{" "}
                    {preview.subject}
                  </p>
                  <details className="stmt-details">
                    <summary>Preview statement</summary>
                    {/* Server-rendered from our own template + our own Odoo rows — the same
                        HTML the send path posts. */}
                    <div
                      className="stmt-preview"
                      dangerouslySetInnerHTML={{ __html: preview.html }}
                    />
                  </details>
                </>
              ) : null}

              <div className="stmt-row">
                <label className="set-label">
                  Test address (sends the preview customer’s statement here)
                  <input
                    className="set-input stmt-wide"
                    value={testTo}
                    disabled={data.safeMode}
                    title={data.safeMode ? "Locked to the guardrail address while testing." : undefined}
                    onChange={(e) => setTestTo(e.currentTarget.value)}
                  />
                </label>
                <button
                  className="set-secondary"
                  disabled={busy || !previewRow || !testTo.includes("@")}
                  onClick={() => void doSend([previewRow], testTo)}
                >
                  Send test email
                </button>
              </div>
            </div>
          ) : (
            <p className="set-help">No customers match the current filters.</p>
          )}

          {noEmail.length ? (
            <div className="stmt-warn">
              {noEmail.length} selected {noEmail.length === 1 ? "customer has" : "customers have"} no
              email and will be skipped: {noEmail.slice(0, 5).map((r) => r.Customer).join(", ")}
              {noEmail.length > 5 ? "…" : ""}
            </div>
          ) : null}

          {result ? (
            <div className={result.failed.length ? "set-warn" : "set-ok"}>
              <b>
                {result.sent.length} statement{result.sent.length === 1 ? "" : "s"} queued
                {result.test ? " (test)" : ""} in Odoo
              </b>{" "}
              — delivery within ~15 min via the Office 365 relay; each send is logged on the
              customer record.
              {result.skipped.length ? (
                <div>Skipped: {result.skipped.map((s) => `${s.customer} (${s.reason})`).join(" · ")}</div>
              ) : null}
              {result.failed.length ? (
                <div>Failed: {result.failed.map((f) => `${f.customer}: ${f.error}`).join(" | ")}</div>
              ) : null}
            </div>
          ) : null}

          {/* ── the confirm flow ─────────────────────────────────────────── */}
          {!confirming ? (
            <button
              className="set-primary"
              disabled={busy || !sendable.length || data.safeMode}
              title={data.safeMode ? "Disabled while the testing guardrail is on." : undefined}
              onClick={() => { setResult(null); setConfirming(true); }}
            >
              Send {sendable.length} statement{sendable.length === 1 ? "" : "s"}…
            </button>
          ) : (
            <div className="stmt-confirm">
              <div className="stmt-warn">
                About to queue <b>{sendable.length}</b> statements as {data.sender.name} (
                {data.sender.email}), replies to {data.sender.replyTo} — combined overdue{" "}
                {money(sendable.reduce((a, r) => a + (Number(r.Overdue) || 0), 0))}.{" "}
                <b>This cannot be undone from here.</b>
              </div>
              <div className="stmt-tablewrap stmt-tablewrap--short">
                <table className="stmt-table">
                  <thead>
                    <tr><th>Customer</th><th>Email</th><th className="stmt-num">Overdue</th></tr>
                  </thead>
                  <tbody>
                    {sendable.map((r) => (
                      <tr key={r.Customer}>
                        <td>{r.Customer}</td>
                        <td>{r.Email}</td>
                        <td className="stmt-num">{money(r.Overdue)}</td>
                      </tr>
                    ))}
                  </tbody>
                </table>
              </div>
              <div className="stmt-row">
                <button
                  className="set-primary"
                  disabled={busy}
                  onClick={() => void doSend(sendable.map((r) => r.Customer))}
                >
                  {busy ? "Queueing…" : "Confirm and send"}
                </button>
                <button className="set-secondary" disabled={busy} onClick={() => setConfirming(false)}>
                  Cancel
                </button>
              </div>
            </div>
          )}
        </>
      ) : null}
    </div>
  );
}

export default StatementsPane;
