// =================================================================
// pw-screens-op-verification.jsx — Operator · Verification review (PKG-15)
//
//   Decision D7: `pending` holds a publisher's payouts for 90 days,
//   exactly like `unverified`. So a publisher who has done everything
//   asked of them sees NO change until an operator reviews it, and
//   review latency converts directly into publisher-visible delay.
//
//   That is why this screen leads with the queue's age rather than its
//   contents. A slow queue is an operations defect, and §10.2 already
//   says a low week-one verification rate is an onboarding defect
//   rather than a policy outcome — the same logic applies here.
// =================================================================

const VERIF_TONE = { verified: "good", pending: "warn", rejected: "bad", unverified: "neutral" };

// Evidence is stored write-only: the API returns whether a field is present, never its value.
// Rendering it would put a bank account number on an operator's screen for no reason — the
// operator decides on the *presence* of evidence, and inspects the underlying document elsewhere.
const EVIDENCE = [
  { key: "business_registration", label: "Business registration" },
  { key: "tax_identity", label: "Tax identity" },
  { key: "bank_account", label: "Bank account" },
];

const QueueHealth = ({ queue }) => {
  const oldest = queue?.oldest_pending_days ?? null;
  const stale = queue?.stale_pending_count ?? 0;
  const threshold = queue?.stale_threshold_days ?? 3;
  const pending = (queue?.entries || []).filter((e) => e.status === "pending").length;

  // Tone follows the oldest submission, not the count. One publisher waiting three weeks is a
  // worse failure than twenty waiting a day, and an average would hide exactly that.
  const tone = oldest === null ? "neutral" : oldest > threshold ? "bad" : oldest > 1 ? "warn" : "good";

  return (
    <Card
      title="Queue health"
      subtitle="Review latency is publisher-visible delay — payouts stay held at 90 days until this clears."
    >
      <div style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: 14 }}>
        <div>
          <div style={{ fontSize: 13, opacity: 0.7 }}>Waiting for review</div>
          <div style={{ fontSize: 28, fontWeight: 600 }}>{pending}</div>
        </div>
        <div>
          <div style={{ fontSize: 13, opacity: 0.7 }}>Oldest submission</div>
          <div style={{ fontSize: 28, fontWeight: 600 }}>
            {oldest === null ? "—" : `${oldest}d`}
          </div>
        </div>
        <div>
          <div style={{ fontSize: 13, opacity: 0.7 }}>Older than {threshold} days</div>
          <div style={{ fontSize: 28, fontWeight: 600 }}>{stale}</div>
        </div>
      </div>
      {oldest !== null && oldest > threshold && (
        <div style={{ marginTop: 12, padding: "10px 12px", borderRadius: 8, background: "rgba(220,80,80,0.12)", fontSize: 13, lineHeight: 1.5 }}>
          <strong>A publisher has been waiting {oldest} days.</strong> Their payouts are held at the
          90-day unverified window for every one of those days, and nothing they can do changes it.
          This is a queue problem, not a policy outcome.
        </div>
      )}
      <div style={{ fontSize: 12, opacity: 0.6, marginTop: 12 }}>
        Tone follows the oldest submission rather than the count — one publisher waiting three weeks
        is a worse failure than twenty waiting a day, and an average would hide it.
      </div>
    </Card>
  );
};

const ReviewRow = ({ entry, onReviewed, notify }) => {
  const [open, setOpen] = React.useState(false);
  const [reviewer, setReviewer] = React.useState("");
  const [reason, setReason] = React.useState("");
  const [busy, setBusy] = React.useState(false);

  const decide = async (decision) => {
    if (!reviewer.trim()) { notify("Who is reviewing? Enter a name first.", "bad"); return; }
    if (decision === "rejected" && !reason.trim()) {
      // The contract refuses this too. Caught here so the operator gets a sentence rather than a
      // 400 — a publisher told only "rejected" has nothing to act on.
      notify("A rejection needs a reason. The publisher sees it.", "bad");
      return;
    }
    setBusy(true);
    try {
      await window.__api.write("reviewVerification", { publisher_id: entry.publisher_id }, {
        body: {
          decision,
          reviewed_by: reviewer.trim(),
          ...(decision === "rejected" ? { rejection_reason: reason.trim() } : {}),
        },
      });
      notify(decision === "verified" ? "Verified. Their hold drops to 60 days at the next sweep." : "Rejected.");
      setOpen(false);
      onReviewed();
    } catch (e) {
      notify(e.message || "Could not record the decision.", "bad");
    } finally {
      setBusy(false);
    }
  };

  const missing = EVIDENCE.filter((f) => !entry[f.key]);

  return (
    <div style={{ borderTop: "1px solid rgba(127,127,127,0.2)", padding: "12px 10px" }}>
      <div style={{ display: "flex", alignItems: "center", gap: 12 }}>
        <div style={{ flex: 1 }}>
          <div style={{ fontWeight: 600 }}>{entry.publisher_name}</div>
          <div style={{ fontSize: 12, opacity: 0.65 }}>
            {entry.status === "pending" && entry.pending_days !== null
              ? `Waiting ${entry.pending_days} day${entry.pending_days === 1 ? "" : "s"}`
              : entry.status === "verified" && entry.verified_at
                ? `Verified ${String(entry.verified_at).slice(0, 10)}`
                : entry.status}
          </div>
        </div>
        <StatusPill status={entry.status} tone={VERIF_TONE[entry.status]} />
        {entry.status === "pending" && (
          <button className="pw-btn" onClick={() => setOpen(!open)}>{open ? "Close" : "Review"}</button>
        )}
      </div>

      {open && (
        <div style={{ marginTop: 12, display: "flex", flexDirection: "column", gap: 10 }}>
          <div style={{ fontSize: 13 }}>
            {EVIDENCE.map((f) => (
              <span key={f.key} style={{ marginRight: 14, opacity: entry[f.key] ? 1 : 0.45 }}>
                {entry[f.key] ? "✓" : "—"} {f.label}
              </span>
            ))}
          </div>
          {/* Presence only. The API never returns the stored values, and putting a bank account
              number on an operator's screen would be a disclosure with no decision attached. */}
          <div style={{ fontSize: 12, opacity: 0.6 }}>
            Evidence is stored write-only — this shows what is on file, never its contents.
            {missing.length > 0 && ` Missing: ${missing.map((m) => m.label.toLowerCase()).join(", ")}.`}
          </div>

          <input
            className="pw-input"
            placeholder="Your name (recorded against the decision)"
            value={reviewer}
            onChange={(e) => setReviewer(e.target.value)}
          />
          <textarea
            className="pw-input"
            rows={2}
            placeholder="Reason — required to reject, and shown to the publisher"
            value={reason}
            onChange={(e) => setReason(e.target.value)}
          />
          <div style={{ display: "flex", gap: 8 }}>
            <button className="pw-btn pw-btn-primary" disabled={busy} onClick={() => decide("verified")}>
              Verify
            </button>
            <button className="pw-btn" disabled={busy} onClick={() => decide("rejected")}>
              Reject
            </button>
          </div>
          {/* C7 is a real gap and the operator should know the audit trail is self-reported. */}
          <div style={{ fontSize: 11, opacity: 0.55 }}>
            There is one shared admin token, so this name is not verified against an identity — it is
            a label on the record, not proof of who decided.
          </div>
        </div>
      )}
    </div>
  );
};

const OpVerificationScreen = ({ data, setData }) => {
  const notify = (msg, tone) => window.__toast ? window.__toast(msg, tone) : null;
  const reload = () => window.__api.read("adminVerificationQueue").then((q) => setData((d) => ({ ...d, verificationQueue: q })));

  const queue = data?.verificationQueue || null;
  const entries = queue?.entries || [];
  // Pending first, oldest first within that: the queue exists to be worked down, and sorting by
  // name would bury the publisher who has waited longest.
  const ordered = [...entries].sort((a, b) => {
    if (a.status === "pending" && b.status !== "pending") return -1;
    if (b.status === "pending" && a.status !== "pending") return 1;
    return (b.pending_days ?? -1) - (a.pending_days ?? -1);
  });

  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
      <QueueHealth queue={queue} />
      <Card title="Submissions" subtitle="Oldest pending first." padded={false}>
        {ordered.length === 0 ? (
          <div style={{ padding: 20, opacity: 0.6, fontSize: 13 }}>Nothing submitted yet.</div>
        ) : (
          ordered.map((e) => (
            <ReviewRow key={e.publisher_id} entry={e} onReviewed={reload} notify={notify} />
          ))
        )}
      </Card>
    </div>
  );
};
