import { useCallback, useEffect, useMemo, useState, type CSSProperties } from "react";
import { useNavigate } from "react-router-dom";
import {
  BarChart3,
  Check,
  ChevronDown,
  FlaskConical,
  LockKeyhole,
  PackageCheck,
  Send,
  ShieldCheck,
  type LucideIcon,
} from "lucide-react";
import type { ApiSettings } from "../../types";
import {
  buildVersion,
  getCampaignJobs,
  getCampaignJobDetail,
  getCampaignSummary,
  mockSendCampaign,
  sendCampaignNow,
  updateCampaignReviewState,
  validateVersion,
  type CampaignJobsResponse,
  type CampaignSummary,
  type CampaignVersionDetail,
} from "../../api/campaigns";
import { getMockMailboxMessage, type MockMailboxMessage } from "../../api/mail";
import Button from "../../components/Button";
import DataGrid, { type DataGridColumn, type DataGridListOption } from "../../components/table/DataGrid";
import DismissibleAlert from "../../components/DismissibleAlert";
import ConfirmDialog from "../../components/ConfirmDialog";
import LoadingFrame from "../../components/LoadingFrame";
import PageTitle from "../../components/PageTitle";
import StatusBadge from "../../components/StatusBadge";
import ToggleSwitch from "../../components/ToggleSwitch";
import InlineHelp from "../../components/help/InlineHelp";
import MessagePreviewOverlay, { type MessagePreviewAttachment } from "./components/MessagePreviewOverlay";
import LockedVersionNotice from "./components/LockedVersionNotice";
import VersionLine from "./components/VersionLine";
import { useCampaignWorkspaceData } from "./hooks/useCampaignWorkspaceData";
import {
  asArray,
  asRecord,
  formatDateTime,
  getCampaignJson,
  getDeliverySection,
  humanize,
  isFinalLockedVersion,
  isHistoricalCampaignVersion,
  isUserLockedVersion,
  isVersionReadyForDelivery,
  stringifyPreview,
} from "./utils/campaignView";
import { getBool, getText } from "./utils/draftEditor";
import { buildTemplatePreviewContext, renderTemplatePreviewText } from "./utils/templatePlaceholders";

type FlowState =
  | "complete"
  | "warning"
  | "danger"
  | "active"
  | "locked"
  | "running"
  | "partial"
  | "stale"
  | "pending";

type FlowStageDefinition = {
  id: string;
  title: string;
  shortTitle: string;
  description: string;
  icon: LucideIcon;
  state: FlowState;
  stateLabel: string;
  lockReason?: string;
};

type WorkflowBusy = "validate" | "build" | "inspect" | "mock" | "mailbox" | "send" | "";

const stateColors: Record<FlowState, string> = {
  complete: "var(--green)",
  warning: "var(--amber)",
  danger: "var(--red)",
  active: "var(--blue)",
  locked: "var(--line-dark)",
  running: "var(--blue)",
  partial: "#9b86c7",
  stale: "#9b86c7",
  pending: "var(--muted)",
};

const MESSAGE_VALIDATION_OPTIONS: DataGridListOption[] = [
  { value: "ready", label: "Ready" },
  { value: "warning", label: "Warning" },
  { value: "needs_review", label: "Needs review" },
  { value: "blocked", label: "Blocked" },
  { value: "excluded", label: "Excluded" },
];

const MESSAGE_REVIEW_ISSUE_STATUSES = ["warning", "needs_review", "blocked", "excluded"];

export default function ReviewSendPage({ settings, campaignId }: { settings: ApiSettings; campaignId: string }) {
  const navigate = useNavigate();
  const { data, loading, error, reload, setError } = useCampaignWorkspaceData(settings, campaignId, { includeSummary: true });
  const [liveSummary, setLiveSummary] = useState<CampaignSummary | null>(null);
  const [queueStatusLoading, setQueueStatusLoading] = useState(false);
  const version = data.currentVersion;
  const campaignJson = useMemo(() => getCampaignJson(version), [version]);
  const inlineEntries = useMemo(
    () => asArray(asRecord(campaignJson.entries).inline).map(asRecord),
    [campaignJson],
  );
  const validation = asRecord(version?.validation_summary);
  const build = asRecord(version?.build_summary);
  const summary = liveSummary ?? data.summary;
  const cards = summary?.cards;
  const attachmentSummary = asRecord(summary?.attachments);
  const delivery = getDeliverySection(version);
  const rateLimit = asRecord(delivery.rate_limit);
  const imapAppend = asRecord(delivery.imap_append_sent);

  const [busy, setBusy] = useState<WorkflowBusy>("");
  const [message, setMessage] = useState("");
  const [messageReviewComplete, setMessageReviewComplete] = useState(false);
  const [builtReviewRows, setBuiltReviewRows] = useState<Record<string, unknown>[]>([]);
  const [reviewJobs, setReviewJobs] = useState<CampaignJobsResponse>(() => emptyCampaignJobsResponse());
  const [reviewPage, setReviewPage] = useState(1);
  const [showAllReviewJobs, setShowAllReviewJobs] = useState(false);
  const [jobsLoadedKey, setJobsLoadedKey] = useState("");
  const [reviewedMessageKeys, setReviewedMessageKeys] = useState<Set<string>>(() => new Set());
  const [newlyReviewedRequiredKeys, setNewlyReviewedRequiredKeys] = useState<Set<string>>(() => new Set());
  const [selectedBuiltIndex, setSelectedBuiltIndex] = useState<number | null>(null);
  const [mockResult, setMockResult] = useState<Record<string, unknown> | null>(null);
  const [mockClearFirst, setMockClearFirst] = useState(true);
  const [mockAppendSent, setMockAppendSent] = useState(true);
  const [selectedMockMessage, setSelectedMockMessage] = useState<MockMailboxMessage | null>(null);
  const [reviewConfirmOpen, setReviewConfirmOpen] = useState(false);
  const [dryRun, setDryRun] = useState(false);
  const [sendConfirmOpen, setSendConfirmOpen] = useState(false);
  const [sendResult, setSendResult] = useState<Record<string, unknown> | null>(null);
  const persistedReview = storedMessageReviewState(version);
  const persistedReviewKey = `${persistedReview.buildToken}|${persistedReview.inspectionComplete ? "1" : "0"}|${persistedReview.reviewedMessageKeys.join(",")}`;

  useEffect(() => {
    setBuiltReviewRows([]);
    setReviewJobs(emptyCampaignJobsResponse());
    setReviewPage(1);
    setJobsLoadedKey("");
    setNewlyReviewedRequiredKeys(new Set());
    setSelectedBuiltIndex(null);
    setMockResult(null);
    setSelectedMockMessage(null);
    setSendResult(null);
    setSendConfirmOpen(false);
    setLiveSummary(null);
  }, [version?.id]);

  useEffect(() => {
    setMessageReviewComplete(persistedReview.inspectionComplete);
    setReviewedMessageKeys(new Set(persistedReview.reviewedMessageKeys));
  }, [version?.id, persistedReviewKey]);

  const validationPresent = Object.keys(validation).length > 0;
  const validationOk = validation.ok === true;
  const validationErrors = numberFrom(validation, ["error_count", "errors", "blocked"]);
  const validationWarnings = numberFrom(validation, ["warning_count", "warnings"]);
  const readyForDelivery = isVersionReadyForDelivery(version);
  const validationStale = validationOk && !readyForDelivery;

  const buildPresent = Object.keys(build).length > 0;
  const builtCount = numberFrom(build, ["built_count", "ready_count", "built", "messages_built"]);
  const buildBlocked = numberFrom(build, ["blocked_count", "blocked"]);
  const buildNeedsReview = numberFrom(build, ["needs_review_count", "needs_review"]);
  const buildWarnings = numberFrom(build, ["warning_count", "warnings"]);
  const hasBuild = buildPresent && (builtCount > 0 || version?.workflow_state === "built");

  useEffect(() => {
    if (!version?.id || !hasBuild) return;
    const expectedKey = `${version.id}:${reviewPage}:${showAllReviewJobs ? "all" : "issues"}`;
    if (jobsLoadedKey === expectedKey) return;
    void loadBuiltMessages(true, reviewPage);
  }, [version?.id, hasBuild, reviewPage, showAllReviewJobs, jobsLoadedKey]);

  const statusCounts = asRecord(summary?.status_counts);
  const sendStatusCounts = asRecord(statusCounts.send);
  const attempts = asRecord(summary?.attempts);
  const summaryDelivery = asRecord(summary?.delivery);
  const queuedSendCount = numberFrom(sendStatusCounts, ["queued"]);
  const claimedSendCount = numberFrom(sendStatusCounts, ["claimed"]);
  const sendingSendCount = numberFrom(sendStatusCounts, ["sending"]);
  const activeSendCount = claimedSendCount + sendingSendCount;
  const queuedOrActiveCount = cards?.queued_or_active ?? queuedSendCount + activeSendCount;
  const notQueuedCount = cards?.not_attempted ?? numberFrom(sendStatusCounts, ["not_queued"]);
  const cancelledCount = cards?.cancelled ?? numberFrom(sendStatusCounts, ["cancelled"]);
  const outcomeUnknownCount = cards?.outcome_unknown ?? numberFrom(sendStatusCounts, ["outcome_unknown"]);
  const sendAttemptCount = numberFrom(attempts, ["send_attempts"]);
  const backgroundWorkersEnabled = summaryDelivery.background_workers_enabled === true || summaryDelivery.celery_enabled === true;
  const backgroundWorkersDisabled = summaryDelivery.background_workers_enabled === false || summaryDelivery.celery_enabled === false;
  const recentFailures = asArray(summary?.recent_failures).map(asRecord).slice(0, 5);
  const jobsTotal = cards?.jobs_total ?? inlineEntries.filter((entry) => entry.active !== false).length;
  const sentCount = cards?.sent ?? 0;
  const failedCount = cards?.failed ?? 0;
  const imapAppended = cards?.imap_appended ?? 0;
  const imapFailed = cards?.imap_failed ?? 0;
  const deliveryHasTerminalOutcome = sentCount + failedCount + outcomeUnknownCount + cancelledCount > 0;
  const currentWorkflowState = (version?.workflow_state ?? "").toLowerCase();
  const deliverySending = activeSendCount > 0 || (currentWorkflowState === "sending" && queuedOrActiveCount > 0);
  const deliveryQueued = queuedOrActiveCount > 0 || (currentWorkflowState === "queued" && !deliveryHasTerminalOutcome);
  const deliveryStarted = deliverySending || deliveryHasTerminalOutcome || ["sent", "completed", "partially_completed", "outcome_unknown", "failed"].includes(currentWorkflowState);
  const queuedWithoutWorker = backgroundWorkersDisabled && queuedSendCount > 0 && activeSendCount === 0;
  const directQueuedSendAllowed = queuedWithoutWorker && !deliveryStarted;
  const selectedDryRun = dryRun && !directQueuedSendAllowed;
  const deliveryPartial = cards?.partially_completed === true || ["partially_sent", "failed_partial", "partially_completed"].includes(currentWorkflowState);
  const deliveryComplete = queuedOrActiveCount === 0
    && sentCount > 0
    && failedCount === 0
    && outcomeUnknownCount === 0
    && cancelledCount === 0
    && cards?.partially_completed !== true;
  const deliveryDanger = failedCount > 0 || outcomeUnknownCount > 0 || ["outcome_unknown", "failed"].includes(currentWorkflowState);
  const deliveryDisplayStatus = deliverySending
    ? "sending"
    : deliveryQueued
      ? "queued"
      : deliveryPartial
        ? "partially_completed"
        : deliveryComplete
          ? "completed"
          : deliveryDanger
            ? failedCount > 0 ? "failed" : "outcome_unknown"
            : data.campaign?.status ?? version?.workflow_state ?? "not_started";
  const queueStatusNote = queuedWithoutWorker
    ? "Queued jobs are waiting in the database, but background delivery workers are disabled on this server. Use Send queued now for a small dev run, or start Redis/Celery with CELERY_ENABLED=true."
    : deliverySending
      ? "Delivery is being processed. This page refreshes queue counters without reloading the whole campaign workspace."
      : deliveryQueued && backgroundWorkersEnabled
        ? "Queued jobs are waiting for a background delivery worker. If the counts do not change, check the worker process and queue logs."
        : deliveryQueued
          ? "Queued jobs are waiting. If the counts do not change, check whether background delivery workers are enabled and running."
          : "";
  const queueStatusTone = queuedWithoutWorker ? "is-warning" : deliveryQueued || deliverySending ? "is-stale" : "";
  const historicalVersion = isHistoricalCampaignVersion(version, data.campaign?.current_version_id);
  const finalVersion = isFinalLockedVersion(version);
  const userLockedVersion = isUserLockedVersion(version);
  const readOnlyVersion = historicalVersion || userLockedVersion || finalVersion;

  const refreshQueueStatus = useCallback(async (silent = true) => {
    if (!version?.id) return;
    setQueueStatusLoading(true);
    if (!silent) setMessage("Refreshing queue status…");
    setError("");
    try {
      const result = await getCampaignSummary(settings, campaignId, version.id);
      setLiveSummary(result);
      if (!silent) setMessage("Queue status refreshed.");
    } catch (err) {
      if (!silent) setMessage("");
      setError(err instanceof Error ? err.message : String(err));
    } finally {
      setQueueStatusLoading(false);
    }
  }, [campaignId, setError, settings, version?.id]);

  useEffect(() => {
    if (!(deliveryQueued || deliverySending) || loading || queueStatusLoading || busy === "send") return;
    const handle = window.setTimeout(() => { void refreshQueueStatus(true); }, 3000);
    return () => window.clearTimeout(handle);
  }, [deliveryQueued, deliverySending, loading, queueStatusLoading, busy, refreshQueueStatus]);

  const selectedBuiltMessage = selectedBuiltIndex === null ? null : builtReviewRows[selectedBuiltIndex] ?? null;
  const reviewMetadata = reviewJobs.review ?? {};
  const blockingReviewCount = Number(reviewMetadata.blocking_count ?? 0);
  const explicitReviewCount = Number(reviewMetadata.required_count ?? 0);
  const bulkAcceptableCount = Number(reviewMetadata.bulk_acceptable_count ?? 0);
  const reviewedExplicitCount = Math.min(
    explicitReviewCount,
    Number(reviewMetadata.reviewed_required_count ?? 0) + newlyReviewedRequiredKeys.size,
  );
  const reviewRequiredCount = explicitReviewCount + bulkAcceptableCount;
  const reviewedRequiredCount = reviewedExplicitCount;
  const automaticInspectionComplete = reviewJobs.total_unfiltered > 0
    && blockingReviewCount === 0
    && reviewRequiredCount === 0;

  const mockSend = asRecord(mockResult?.send);
  const mockSent = numberFrom(mockSend, ["sent_count", "attempted_count"]);
  const mockFailed = numberFrom(mockSend, ["failed_count"]);
  const mockSkipped = numberFrom(mockSend, ["skipped_count"]);
  const mockComplete = Boolean(mockResult) && mockSent > 0 && mockFailed === 0 && mockSkipped === 0;
  const mockPartial = Boolean(mockResult) && mockSent > 0 && (mockFailed > 0 || mockSkipped > 0);
  const mockRows = asArray(mockSend.results).map(asRecord);
  const mockMailbox = asRecord(mockResult?.mailbox);
  const mockMailboxMessages = asArray(mockMailbox.messages).map(asRecord);
  const sendResultRows = asArray(sendResult?.results).map(asRecord);

  const validationReviewState: FlowState = busy === "validate"
    ? "running"
    : validationStale
      ? "stale"
      : validationPresent && !validationOk
        ? "danger"
        : readyForDelivery && (validationWarnings > 0 || (cards?.needs_attention ?? 0) > 0)
          ? "warning"
          : readyForDelivery
            ? "complete"
            : "active";

  const downstreamDeliveryActivity = deliveryQueued || deliveryStarted;
  const inspectionSatisfied = automaticInspectionComplete || messageReviewComplete || downstreamDeliveryActivity;

  const buildReviewState: FlowState = !readyForDelivery
    ? "locked"
    : busy === "build" || busy === "inspect"
      ? "running"
      : hasBuild && buildBlocked > 0
        ? "danger"
        : hasBuild && (buildNeedsReview > 0 || buildWarnings > 0 || !inspectionSatisfied)
          ? "warning"
          : hasBuild && inspectionSatisfied
            ? "complete"
            : "active";

  const mockState: FlowState = !inspectionSatisfied
    ? "locked"
    : busy === "mock" || busy === "mailbox"
      ? "running"
      : mockPartial
        ? "partial"
        : mockFailed > 0
          ? "danger"
          : mockComplete
            ? "complete"
            : downstreamDeliveryActivity
              ? "warning"
              : "active";

  const mockGateSatisfied = mockComplete || downstreamDeliveryActivity;
  const sendState: FlowState = !mockGateSatisfied
    ? "locked"
    : busy === "send" || deliverySending
      ? "running"
      : queuedWithoutWorker
        ? "warning"
        : deliveryQueued
          ? "running"
          : deliveryPartial
            ? "partial"
            : deliveryComplete || ["sent", "completed"].includes(currentWorkflowState)
              ? "complete"
              : deliveryDanger
                ? "danger"
                : "active";

  const resultState: FlowState = !deliveryStarted
    ? "locked"
    : deliverySending
      ? "running"
      : deliveryPartial
        ? "partial"
        : deliveryComplete || ["sent", "completed"].includes(currentWorkflowState)
          ? "complete"
          : deliveryDanger
            ? "danger"
            : "active";

  const stages: FlowStageDefinition[] = useMemo(() => [
    {
      id: "workflow-validate-review",
      title: "Validate and inspect",
      shortTitle: "Validate",
      description: "Lock and validate the campaign, then inspect blocking errors, warnings and attachment exceptions in the same step.",
      icon: ShieldCheck,
      state: validationReviewState,
      stateLabel: stateLabel(validationReviewState),
    },
    {
      id: "workflow-build-review",
      title: "Build and review",
      shortTitle: "Build",
      description: "Build the exact queue, resolve recipient values and managed files, then review only messages that need attention.",
      icon: PackageCheck,
      state: buildReviewState,
      stateLabel: stateLabel(buildReviewState),
      lockReason: validationErrors > 0
        ? `Resolve ${validationErrors} blocking validation issue${validationErrors === 1 ? "" : "s"} first.`
        : "Lock and validate the current working version first.",
    },
    {
      id: "workflow-mock-verify",
      title: "Mock send and verify",
      shortTitle: "Mock send",
      description: "Exercise the delivery path and verify recipient outcomes and captured MIME messages without contacting the real servers.",
      icon: FlaskConical,
      state: mockState,
      stateLabel: stateLabel(mockState),
      lockReason: "Build and complete the required message review first.",
    },
    {
      id: "workflow-send",
      title: "Confirm and send",
      shortTitle: "Send",
      description: "Review the final execution summary, optionally run a dry run, and send the exact built queue.",
      icon: Send,
      state: sendState,
      stateLabel: stateLabel(sendState),
      lockReason: "Complete a successful mock delivery first.",
    },
    {
      id: "workflow-results",
      title: "Delivery results",
      shortTitle: "Results",
      description: "Review SMTP outcomes, IMAP append results, partial delivery and failures.",
      icon: BarChart3,
      state: resultState,
      stateLabel: stateLabel(resultState),
      lockReason: "Delivery results become available after the real send starts.",
    },
  ], [
    validationReviewState,
    buildReviewState,
    mockState,
    sendState,
    resultState,
    validationErrors,
  ]);

  async function runValidation() {
    if (!version || busy || readOnlyVersion || readyForDelivery) return;
    setBusy("validate");
    setMessage("Validating the campaign, including managed attachment matches…");
    setError("");
    try {
      const result = await validateVersion(settings, version.id, true);
      setMessage(result.ok ? "Validation passed." : "Validation finished with issues. Review the exceptions below.");
      resetDownstreamReview();
      await reload();
    } catch (err) {
      setMessage("");
      setError(err instanceof Error ? err.message : String(err));
    } finally {
      setBusy("");
    }
  }

  async function runBuild() {
    if (!version || busy || readOnlyVersion || !readyForDelivery || deliveryQueued || deliveryStarted) return;
    setBusy("build");
    setMessage("Building exact messages and resolving managed attachment versions…");
    setError("");
    try {
      const result = await buildVersion(settings, version.id, true);
      const review = await getCampaignJobs(settings, campaignId, {
        versionId: version.id,
        page: 1,
        pageSize: 50,
        validationStatus: showAllReviewJobs ? undefined : MESSAGE_REVIEW_ISSUE_STATUSES,
      });
      setReviewPage(1);
      setReviewJobs(review);
      setBuiltReviewRows(review.jobs.map(asRecord));
      setJobsLoadedKey(`${version.id}:1:${showAllReviewJobs ? "all" : "issues"}`);
      setMessage(`Build finished. Built ${String(result.built_count ?? result.ready_count ?? "—")} message(s). The message review is ready below.`);
      setMessageReviewComplete(false);
      setReviewedMessageKeys(new Set());
      setMockResult(null);
      setSelectedMockMessage(null);
      await reload();
    } catch (err) {
      setMessage("");
      setError(err instanceof Error ? err.message : String(err));
    } finally {
      setBusy("");
    }
  }

  async function loadBuiltMessages(silent = false, targetPage = reviewPage) {
    if (!version || busy || !hasBuild) return;
    setBusy("inspect");
    if (!silent) setMessage("Loading the built-message review…");
    setError("");
    try {
      const result = await getCampaignJobs(settings, campaignId, {
        versionId: version.id,
        page: targetPage,
        pageSize: 50,
        validationStatus: showAllReviewJobs ? undefined : MESSAGE_REVIEW_ISSUE_STATUSES,
      });
      const jobs = result.jobs.map(asRecord);
      setReviewJobs(result);
      setBuiltReviewRows(jobs);
      setJobsLoadedKey(`${version.id}:${targetPage}:${showAllReviewJobs ? "all" : "issues"}`);
      if (result.pages > 0 && targetPage > result.pages) setReviewPage(result.pages);
      if (!silent) setMessage(`Loaded ${jobs.length} message(s) on page ${result.page} of ${result.pages || 1}.`);
    } catch (err) {
      if (!silent) setMessage("");
      setError(err instanceof Error ? err.message : String(err));
    } finally {
      setBusy("");
    }
  }

  async function runMockSend() {
    if (!version || busy || readOnlyVersion || !inspectionSatisfied || deliveryQueued || deliveryStarted) return;
    setBusy("mock");
    setMessage("Running the complete mock-delivery flow…");
    setError("");
    setSelectedMockMessage(null);
    try {
      const response = await mockSendCampaign(settings, campaignId, {
        version_id: version.id,
        send: true,
        include_warnings: true,
        include_needs_review: false,
        append_sent: mockAppendSent,
        clear_mailbox: mockClearFirst,
        check_files: true,
      });
      const result = asRecord(response.result ?? response);
      setMockResult(result);
      const sendResult = asRecord(result.send);
      setMessage(`Mock delivery finished. Captured ${String(sendResult.sent_count ?? 0)} message(s), failed ${String(sendResult.failed_count ?? 0)}, skipped ${String(sendResult.skipped_count ?? 0)}.`);
    } catch (err) {
      setMessage("");
      setError(err instanceof Error ? err.message : String(err));
    } finally {
      setBusy("");
    }
  }

  async function runSendNow() {
    if (!version || busy || readOnlyVersion || !readyForDelivery || !hasBuild || !mockGateSatisfied || (deliveryQueued && !directQueuedSendAllowed) || deliveryStarted) return;
    const effectiveDryRun = dryRun && !directQueuedSendAllowed;
    setBusy("send");
    setMessage(effectiveDryRun
      ? "Checking the built queue without sending…"
      : directQueuedSendAllowed
        ? "Sending the queued jobs synchronously…"
        : "Sending the locked campaign version…");
    setSendResult(null);
    setError("");
    try {
      const response = await sendCampaignNow(settings, campaignId, {
        version_id: version.id,
        include_warnings: true,
        check_files: false,
        validate_before_send: false,
        build_before_send: false,
        dry_run: effectiveDryRun,
        use_rate_limit: true,
        enqueue_imap_task: false,
      });
      const result = asRecord(response.result ?? response);
      setSendResult(result);
      const sent = result.sent_count ?? 0;
      const failed = result.failed_count ?? 0;
      const unknown = result.outcome_unknown_count ?? 0;
      setMessage(
        effectiveDryRun
          ? "Dry run finished. No message was sent."
          : `Send finished. SMTP accepted ${String(sent)} message(s), failed ${String(failed)}, outcome unknown ${String(unknown)}.`,
      );
      setSendConfirmOpen(false);
      await reload();
    } catch (err) {
      setMessage("");
      setError(err instanceof Error ? err.message : String(err));
    } finally {
      setBusy("");
    }
  }

  async function completeInspection(_acceptBulk = false) {
    if (!version || busy || readOnlyVersion || automaticInspectionComplete || !canCompleteInspection || downstreamDeliveryActivity) return;
    setBusy("inspect");
    setError("");
    setMessage("Recording the completed message review…");
    try {
      const reviewed = new Set(reviewedMessageKeys);
      await updateCampaignReviewState(settings, campaignId, version.id, {
        inspection_complete: true,
        reviewed_message_keys: [...reviewed],
      });
      setReviewedMessageKeys(reviewed);
      setNewlyReviewedRequiredKeys(new Set());
      setMessageReviewComplete(true);
      setReviewJobs((current) => ({
        ...current,
        review: {
          ...current.review,
          inspection_complete: true,
          reviewed_required_count: explicitReviewCount,
        },
      }));
      setReviewConfirmOpen(false);
      setMessage("Message review completed and recorded for this build.");
      await reload();
    } catch (err) {
      setMessage("");
      setError(err instanceof Error ? err.message : String(err));
    } finally {
      setBusy("");
    }
  }

  async function openMockMessage(id: string) {
    if (!id || busy === "mailbox") return;
    setBusy("mailbox");
    setError("");
    try {
      const response = await getMockMailboxMessage(settings, id);
      setSelectedMockMessage(response.message);
    } catch (err) {
      setError(err instanceof Error ? err.message : String(err));
    } finally {
      setBusy("");
    }
  }

  async function openBuiltMessage(index: number) {
    const row = builtReviewRows[index];
    if (!row) return;
    const jobId = String(row.id ?? "");
    const reviewKey = String(row.review_key ?? builtMessageKey(row, index));
    setReviewedMessageKeys((current) => {
      const next = new Set(current);
      next.add(reviewKey);
      return next;
    });
    if (messageNeedsExplicitReview(row) && row.reviewed !== true) {
      setNewlyReviewedRequiredKeys((current) => new Set(current).add(reviewKey));
    }
    if (!jobId || row.resolved_recipients || row.attachments || row.issues) {
      setSelectedBuiltIndex(index);
      return;
    }
    setBusy("inspect");
    setError("");
    try {
      const detail = await getCampaignJobDetail(settings, campaignId, jobId);
      setBuiltReviewRows((current) => current.map((item, itemIndex) => itemIndex === index
        ? { ...item, ...detail.job, review_key: item.review_key, reviewed: item.reviewed, attempts: detail.attempts }
        : item));
      setSelectedBuiltIndex(index);
    } catch (err) {
      setError(err instanceof Error ? err.message : String(err));
    } finally {
      setBusy("");
    }
  }

  function resetDownstreamReview() {
    setMessageReviewComplete(false);
    setBuiltReviewRows([]);
    setReviewJobs(emptyCampaignJobsResponse());
    setReviewPage(1);
    setJobsLoadedKey("");
    setReviewedMessageKeys(new Set());
    setNewlyReviewedRequiredKeys(new Set());
    setSelectedBuiltIndex(null);
    setMockResult(null);
    setSelectedMockMessage(null);
  }

  function scrollToStage(id: string) {
    document.getElementById(id)?.scrollIntoView({ behavior: "smooth", block: "start" });
  }

  const matchedAttachments = numberFrom(attachmentSummary, ["total_matched_files"]);
  const missingAttachments = numberFrom(attachmentSummary, ["missing_configs"]);
  const ambiguousAttachments = numberFrom(attachmentSummary, ["ambiguous_configs"]);
  const messagesPerMinute = numberFrom(rateLimit, ["messages_per_minute"]);
  const estimatedMinutes = messagesPerMinute > 0 && jobsTotal > 0 ? Math.ceil(jobsTotal / messagesPerMinute) : null;
  const canCompleteInspection = blockingReviewCount === 0
    && reviewRequiredCount > 0
    && reviewedExplicitCount === explicitReviewCount;

  return (
    <div className="content-pad workspace-data-page review-send-page">
      <div className="page-heading split workspace-heading">
        <div>
          <PageTitle loading={loading || Boolean(busy)}>Review &amp; Send</PageTitle>
          <VersionLine version={version} versions={data.versions} loadedAt={version?.updated_at} />
        </div>
        <div className="button-row compact-actions">
          <Button onClick={() => void refreshQueueStatus(false)} disabled={!version || queueStatusLoading || Boolean(busy)}>
            {queueStatusLoading ? "Refreshing status…" : "Refresh status"}
          </Button>
          <Button onClick={reload} disabled={loading || Boolean(busy)}>Reload</Button>
        </div>
      </div>

      {error && <DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>}
      {message && <DismissibleAlert tone="info" resetKey={message} floating>{message}</DismissibleAlert>}


      {version && (historicalVersion || readyForDelivery || userLockedVersion || finalVersion) && (
        <LockedVersionNotice
          settings={settings}
          campaignId={campaignId}
          version={version}
          currentVersionId={data.campaign?.current_version_id}
          reload={reload}
          message="This workflow is read-only for the selected version."
        />
      )}

      <LoadingFrame loading={loading} label="Loading workflow state…">
        <WorkflowNavigation stages={stages} onSelect={scrollToStage} />

        <div className="review-flow-timeline" aria-label="Campaign review and sending workflow">
          <WorkflowStage stage={stages[0]} nextState={stages[1].state}>
            <div className="review-flow-fact-grid">
              <WorkflowFact label="Status" value={validationPresent ? (validationOk ? "Passed" : "Needs attention") : "Not run"} />
              <WorkflowFact label="Blocking" value={validationPresent ? validationErrors : "—"} />
              <WorkflowFact label="Warnings" value={validationPresent ? validationWarnings : "—"} />
              <WorkflowFact label="Jobs needing attention" value={cards?.needs_attention ?? "—"} />
            </div>
            {validationStale && <p className="review-flow-inline-note is-stale">The stored validation result is no longer an active delivery lock. Run validation again.</p>}
            {validationPresent && validationErrors === 0 && (
              <p className="review-flow-inline-note is-complete"><Check size={17} aria-hidden="true" /> No blocking validation exceptions remain.</p>
            )}
            {validationErrors > 0 && (
              <p className="review-flow-inline-note is-danger">Resolve the blocking entries, then validate again.</p>
            )}
            <div className="button-row compact-actions review-flow-stage-actions">
              <Button
                variant="primary"
                onClick={() => void runValidation()}
                disabled={!version || Boolean(busy) || readOnlyVersion || readyForDelivery}
              >
                {busy === "validate"
                  ? "Validating…"
                  : readyForDelivery
                    ? "Locked and validated"
                    : "Lock and validate"}
              </Button>
              <Button onClick={() => navigate("../files")}>Review attachment rules</Button>
            </div>
          </WorkflowStage>

          <WorkflowStage stage={stages[1]} nextState={stages[2].state}>
            <div className="review-flow-fact-grid">
              <WorkflowFact label="Built" value={hasBuild ? builtCount : "—"} />
              <WorkflowFact label="Blocked" value={hasBuild ? buildBlocked : "—"} />
              <WorkflowFact label="Need review" value={hasBuild ? buildNeedsReview : "—"} />
              <WorkflowFact label="Reviewed" value={hasBuild ? reviewedRequiredCount : "—"} />
              <WorkflowFact label="Review candidates" value={reviewJobs.total || "—"} />
              <WorkflowFact label="Attachment issues" value={missingAttachments + ambiguousAttachments} />
            </div>
            <p className="muted">Building freezes the current recipients, rendered content and exact managed-file versions into the queue reviewed below.</p>
            <div className="button-row compact-actions review-flow-stage-actions">
              <Button variant="primary" onClick={() => void runBuild()} disabled={!version || Boolean(busy) || readOnlyVersion || !readyForDelivery || deliveryQueued || deliveryStarted}>
                {busy === "build" ? "Building…" : hasBuild ? "Build again" : "Build exact messages"}
              </Button>
              <Button onClick={() => void loadBuiltMessages(false, reviewPage)} disabled={!hasBuild || Boolean(busy)}>
                {busy === "inspect" ? "Loading messages…" : builtReviewRows.length > 0 ? "Reload page" : "Load review"}
              </Button>
              <Button onClick={() => navigate("../template")}>Open template editor</Button>
              {reviewRequiredCount > 0 && (
                <Button
                  variant="primary"
                  onClick={() => bulkAcceptableCount > 0 ? setReviewConfirmOpen(true) : void completeInspection(false)}
                  disabled={readOnlyVersion || messageReviewComplete || !canCompleteInspection || downstreamDeliveryActivity || Boolean(busy)}
                >
                  {messageReviewComplete ? "Review completed" : "Complete review"}
                </Button>
              )}
            </div>
            {automaticInspectionComplete && (
              <p className="review-flow-inline-note is-complete"><Check size={17} aria-hidden="true" /> All built messages are ready. No manual review acknowledgement is required.</p>
            )}
            {blockingReviewCount > 0 && (
              <p className="review-flow-inline-note is-danger">{blockingReviewCount} blocked or failed message(s) must be resolved before review can be completed.</p>
            )}
            {hasBuild && (
              <div className="review-flow-data-section">
                <div className="page-heading split">
                  <div className="button-row compact-actions">
                    <Button onClick={() => { setShowAllReviewJobs((value) => !value); setReviewPage(1); setJobsLoadedKey(""); }} disabled={busy === "inspect"}>
                      {showAllReviewJobs ? "Show review candidates only" : "Show all messages"}
                    </Button>
                  </div>
                  <span className="muted">{reviewJobs.total} matching of {reviewJobs.total_unfiltered} built job(s)</span>
                </div>
                <DataGrid
                  id={`campaign-${campaignId}-workflow-built-messages`}
                  rows={builtReviewRows}
                  columns={builtMessageColumns(openBuiltMessage, reviewedMessageKeys)}
                  getRowKey={builtMessageKey}
                  emptyText="No built messages are available."
                  filteredEmptyText="No messages match the active filters."
                  className="data-table-wrap data-table compact-table"
                />
                <div className="button-row compact-actions">
                  <Button onClick={() => { setReviewPage((value) => Math.max(1, value - 1)); setJobsLoadedKey(""); }} disabled={reviewPage <= 1 || busy === "inspect"}>Previous</Button>
                  <span>Page {reviewJobs.pages === 0 ? 0 : reviewJobs.page} of {reviewJobs.pages}</span>
                  <Button onClick={() => { setReviewPage((value) => Math.min(reviewJobs.pages || 1, value + 1)); setJobsLoadedKey(""); }} disabled={reviewPage >= reviewJobs.pages || busy === "inspect"}>Next</Button>
                </div>
                <p className="muted small-note">Ready messages are hidden initially. Messages marked Needs review require an explicit open/review step. Warnings and exclusions are acceptable conditions and can be acknowledged together when completing review.</p>
              </div>
            )}
          </WorkflowStage>

          <WorkflowStage stage={stages[2]} nextState={stages[3].state}>
            <div className="review-flow-fact-grid">
              <WorkflowFact label="Captured SMTP" value={mockResult ? mockSent : "—"} />
              <WorkflowFact label="Mock failures" value={mockResult ? mockFailed : "—"} />
              <WorkflowFact label="Skipped" value={mockResult ? mockSkipped : "—"} />
              <WorkflowFact label="Captured messages" value={mockResult ? mockMailboxMessages.length : "—"} />
            </div>
            <div className="button-row compact-actions review-flow-stage-actions">
              <Button variant="primary" onClick={() => void runMockSend()} disabled={!version || Boolean(busy) || readOnlyVersion || !inspectionSatisfied || deliveryQueued || deliveryStarted}>
                {busy === "mock" ? "Running mock delivery…" : mockResult ? "Run mock delivery again" : "Run mock delivery"}
              </Button>
              <Button onClick={() => navigate("../mail-settings")}>Review server settings</Button>
            </div>
            <div className="toggle-row mock-send-options">
              <ToggleSwitch label="Clear mock mailbox first" checked={mockClearFirst} disabled={Boolean(busy)} onChange={setMockClearFirst} />
              <ToggleSwitch label="Append mock Sent copy" checked={mockAppendSent} disabled={Boolean(busy)} onChange={setMockAppendSent} />
            </div>
            {mockResult && (
              <div className="review-flow-data-stack">
                <section className="review-flow-data-section" aria-labelledby="workflow-mock-results-title">
                  <h3 id="workflow-mock-results-title">Recipient outcomes</h3>
                  <DataGrid
                    id={`campaign-${campaignId}-workflow-mock-results`}
                    rows={mockRows}
                    columns={mockSendResultColumns()}
                    getRowKey={(row, index) => String(row.entry_id ?? row.entry_index ?? index)}
                    emptyText="No mock delivery results were returned."
                    className="data-table-wrap data-table compact-table"
                  />
                </section>
                <section className="review-flow-data-section" aria-labelledby="workflow-mock-mailbox-title">
                  <h3 id="workflow-mock-mailbox-title">Captured mock messages</h3>
                  <DataGrid
                    id={`campaign-${campaignId}-workflow-mock-mailbox`}
                    rows={mockMailboxMessages}
                    columns={mockMailboxColumns(openMockMessage)}
                    getRowKey={(row, index) => String(row.id ?? index)}
                    emptyText="No mock messages were captured in this run."
                    className="data-table-wrap data-table compact-table"
                  />
                </section>
              </div>
            )}
          </WorkflowStage>

          <WorkflowStage stage={stages[3]} nextState={stages[4].state}>
            <div className="review-flow-execution-summary">
              <div><span>Recipients</span><strong>{jobsTotal || "—"}</strong></div>
              <div><span>Messages to send</span><strong>{builtCount || jobsTotal || "—"}</strong></div>
              <div><span>Matched attachments</span><strong>{matchedAttachments || "—"}</strong></div>
              <div><span>Missing / ambiguous</span><strong>{missingAttachments} / {ambiguousAttachments}</strong></div>
              <div><span>Rate limit</span><strong>{messagesPerMinute > 0 ? `${messagesPerMinute}/min` : "Not set"}</strong></div>
              <div><span>Minimum duration</span><strong>{estimatedMinutes ? `about ${estimatedMinutes} min` : "—"}</strong></div>
              <div><span>IMAP append</span><strong>{Boolean(imapAppend.enabled) ? "Enabled" : "Disabled"}</strong></div>
              <div><span>Version</span><strong>{version ? `v${version.version_number}` : "—"}</strong></div>
            </div>
            <div className="review-flow-fact-grid">
              <WorkflowFact label="Queued" value={queuedSendCount} />
              <WorkflowFact label="Claimed / sending" value={activeSendCount} />
              <WorkflowFact label="Not queued" value={notQueuedCount} />
              <WorkflowFact label="Send attempts" value={sendAttemptCount} />
            </div>
            {queueStatusNote && (
              <p className={`review-flow-inline-note ${queueStatusTone}`.trim()}>{queueStatusNote}</p>
            )}
            <div className="review-send-controls">
              <ToggleSwitch
                label="Dry run"
                checked={dryRun}
                disabled={Boolean(busy) || deliveryQueued || deliveryStarted}
                onChange={setDryRun}
              />
              <span className="muted small-note">A dry run checks the frozen queue and delivery configuration without contacting SMTP or IMAP.</span>
            </div>
            <div className="button-row compact-actions">
              <Button
                variant="primary"
                onClick={() => selectedDryRun ? void runSendNow() : setSendConfirmOpen(true)}
                disabled={!version || Boolean(busy) || readOnlyVersion || !readyForDelivery || !hasBuild || !mockGateSatisfied || (deliveryQueued && !directQueuedSendAllowed) || deliveryStarted}
              >
                {busy === "send"
                  ? (selectedDryRun ? "Running dry run…" : "Sending…")
                  : directQueuedSendAllowed
                    ? "Send queued now"
                    : selectedDryRun
                      ? "Run dry run"
                      : "Send now"}
              </Button>
            </div>
            {sendResult && (
              <div className="review-flow-data-section">
                <p className="muted small-note">Attempted {String(sendResult.attempted_count ?? "—")}, SMTP accepted {String(sendResult.sent_count ?? "—")}, failed {String(sendResult.failed_count ?? "—")}, outcome unknown {String(sendResult.outcome_unknown_count ?? 0)}, skipped {String(sendResult.skipped_count ?? "—")}.</p>
                {sendResultRows.length > 0 && (
                  <DataGrid
                    id={`campaign-${campaignId}-workflow-send-results`}
                    rows={sendResultRows}
                    columns={sendResultColumns()}
                    getRowKey={(_row, index) => `send-result-${index}`}
                    emptyText="No send results returned."
                    className="data-table-wrap data-table compact-table"
                  />
                )}
              </div>
            )}
          </WorkflowStage>

          <WorkflowStage stage={stages[4]}>
            <div className="review-flow-fact-grid">
              <WorkflowFact label="SMTP accepted" value={sentCount} />
              <WorkflowFact label="SMTP failed" value={failedCount} />
              <WorkflowFact label="Queued / active" value={queuedOrActiveCount} />
              <WorkflowFact label="Outcome unknown" value={outcomeUnknownCount} />
              <WorkflowFact label="IMAP appended" value={imapAppended} />
              <WorkflowFact label="IMAP failed" value={imapFailed} />
            </div>
            <div className="review-flow-result-line">
              <StatusBadge status={deliveryDisplayStatus} />
              <span>{deliveryStarted ? "Delivery activity is available in the report and audit views." : "No real delivery has started for this campaign version."}</span>
            </div>
            {recentFailures.length > 0 && (
              <div className="review-flow-data-section">
                <h3>Recent delivery failures</h3>
                <dl className="detail-list">
                  {recentFailures.map((failure, index) => (
                    <div key={String(failure.job_id ?? index)}>
                      <dt>{String(failure.recipient_email ?? failure.entry_id ?? `#${String(failure.entry_index ?? index + 1)}`)}</dt>
                      <dd>
                        <strong>{humanize(String(failure.send_status ?? failure.imap_status ?? "failed"))}</strong>
                        {failure.last_error ? `: ${String(failure.last_error)}` : ""}
                        {failure.updated_at ? <span className="muted"> · {formatDateTime(String(failure.updated_at))}</span> : null}
                      </dd>
                    </div>
                  ))}
                </dl>
              </div>
            )}
            <div className="button-row compact-actions">
              <Button onClick={() => navigate("../report")}>Open report</Button>
              <Button onClick={() => navigate("../audit")}>Open audit log</Button>
            </div>
          </WorkflowStage>
        </div>
      </LoadingFrame>

      {selectedBuiltMessage && selectedBuiltIndex !== null && (
        <BuiltMessagePreview
          campaignJson={campaignJson}
          entries={inlineEntries}
          rows={builtReviewRows}
          index={selectedBuiltIndex}
          onSelect={openBuiltMessage}
          onClose={() => setSelectedBuiltIndex(null)}
        />
      )}

      <ConfirmDialog
        open={reviewConfirmOpen}
        title="Accept non-blocking review conditions?"
        message={`${bulkAcceptableCount} message(s) contain warnings or exclusions but no blocking condition. Completing review will acknowledge these conditions together without opening each message.`}
        confirmLabel="Accept and complete review"
        cancelLabel="Cancel"
        onConfirm={() => void completeInspection(true)}
        onCancel={() => setReviewConfirmOpen(false)}
      />

      <ConfirmDialog
        open={sendConfirmOpen}
        title="Send this version now?"
        message={`This sends the frozen execution snapshot for version ${String(version?.version_number ?? "—")}: ${jobsTotal} planned job(s), ${builtCount} built, ${buildBlocked} blocked, ${String(attachmentSummary.total_matched_files ?? 0)} resolved file(s), rate limit ${String(rateLimit.messages_per_minute ?? "not set")}/minute, IMAP append ${imapAppend.enabled === true ? "enabled" : "disabled"}, snapshot ${version?.execution_snapshot_hash ? `${version.execution_snapshot_hash.slice(0, 12)}…` : "missing"}. SMTP-accepted or uncertain jobs will never be resent automatically.`}
        confirmLabel={directQueuedSendAllowed ? "Send queued now" : "Send now"}
        tone="danger"
        busy={busy === "send"}
        onCancel={() => setSendConfirmOpen(false)}
        onConfirm={() => void runSendNow()}
      />

      {selectedMockMessage && (
        <MessagePreviewOverlay
          title="Captured mock mail"
          subject={selectedMockMessage.subject || "Mock message"}
          bodyMode="text"
          text={selectedMockMessage.body_preview || ""}
          recipientLabel={selectedMockMessage.kind === "imap_append" ? "Mock IMAP append" : "Mock SMTP delivery"}
          recipientNote={selectedMockMessage.created_at ? formatDateTime(selectedMockMessage.created_at) : undefined}
          metaItems={mockMessageMetaItems(selectedMockMessage)}
          attachments={mockMessageAttachments(selectedMockMessage)}
          raw={selectedMockMessage.raw_eml}
          rawLabel="Raw MIME"
          onClose={() => setSelectedMockMessage(null)}
        />
      )}
    </div>
  );
}

function WorkflowNavigation({ stages, onSelect }: { stages: FlowStageDefinition[]; onSelect: (id: string) => void }) {
  return (
    <nav className="review-flow-navigation" aria-label="Review and send workflow steps">
      <div className="review-flow-navigation-track">
        {stages.map((stage, index) => {
          const Icon = stage.icon;
          const nextStage = stages[index + 1];
          const style = {
            "--review-nav-color": stateColors[stage.state],
            "--review-nav-next-color": stateColors[nextStage?.state ?? stage.state],
          } as CSSProperties;
          const showSecondaryState = !["active", "locked"].includes(stage.state);
          return (
            <div className="review-flow-navigation-group" key={stage.id} style={style}>
              <button
                type="button"
                className="review-flow-navigation-item"
                data-state={stage.state}
                onClick={() => onSelect(stage.id)}
                title={`${stage.title}: ${stage.stateLabel}`}
              >
                <span className="review-flow-navigation-icon"><Icon size={17} strokeWidth={1.8} aria-hidden="true" /></span>
                <span className="review-flow-navigation-copy">
                  <strong>
                    <span>{stage.shortTitle}</span>
                    {stage.state === "locked" && <LockKeyhole size={12} aria-label="Locked" />}
                  </strong>
                  {showSecondaryState && <small>{stage.stateLabel}</small>}
                </span>
              </button>
              {nextStage && <span className="review-flow-navigation-line" aria-hidden="true" />}
            </div>
          );
        })}
      </div>
    </nav>
  );
}

function WorkflowStage({
  stage,
  nextState,
  children,
}: {
  stage: FlowStageDefinition;
  nextState?: FlowState;
  children: React.ReactNode;
}) {
  const Icon = stage.icon;
  const locked = stage.state === "locked";
  const [collapsed, setCollapsed] = useState(false);
  const style = {
    "--review-stage-color": stateColors[stage.state],
    "--review-next-stage-color": stateColors[nextState ?? stage.state],
  } as CSSProperties;

  return (
    <section id={stage.id} className="review-flow-stage" data-state={stage.state} style={style}>
      <div className="review-flow-stage-marker" aria-hidden="true">
        <div className="review-flow-stage-node"><Icon size={20} strokeWidth={1.8} /></div>
        {nextState && <div className="review-flow-stage-line" />}
      </div>
      <article className={`card card-collapsible review-flow-stage-card${locked ? " is-locked" : ""}${collapsed ? " is-collapsed" : ""}`} aria-disabled={locked || undefined}>
        <header className="card-header review-flow-stage-header">
          <h2>
            <span>{stage.title}</span>
            {locked && <LockKeyhole className="review-flow-title-lock" size={15} aria-label="Locked" />}
            {!locked && (
              <span
                className="review-flow-state-badge"
                data-state={stage.state}
                aria-label={stage.stateLabel}
                title={stage.stateLabel}
              >
                {stage.stateLabel}
              </span>
            )}
            <InlineHelp>{stage.description}</InlineHelp>
          </h2>
          <div className="review-flow-stage-header-actions">
            <button
              type="button"
              className="card-collapse-toggle"
              aria-expanded={!collapsed}
              aria-label={collapsed ? `Expand ${stage.title}` : `Collapse ${stage.title}`}
              title={collapsed ? "Show content" : "Show header only"}
              onClick={() => setCollapsed((value) => !value)}
            >
              <ChevronDown size={18} strokeWidth={2.4} aria-hidden="true" />
            </button>
          </div>
        </header>
        {!collapsed && (
          <>
            <div className="card-body review-flow-stage-content">{children}</div>
            {locked && (
              <div className="review-flow-lock-message">
                <span className="review-flow-lock-icon"><LockKeyhole size={20} aria-hidden="true" /></span>
                <span>{stage.lockReason}</span>
              </div>
            )}
          </>
        )}
      </article>
    </section>
  );
}

function BuiltMessagePreview({
  campaignJson,
  entries,
  rows,
  index,
  onSelect,
  onClose,
}: {
  campaignJson: Record<string, unknown>;
  entries: Record<string, unknown>[];
  rows: Record<string, unknown>[];
  index: number;
  onSelect: (index: number) => void;
  onClose: () => void;
}) {
  const row = rows[index] ?? {};
  const entryIndex = Math.max(0, numberFrom(row, ["entry_index"]) - 1);
  const entry = entries[entryIndex] ?? {};
  const template = asRecord(campaignJson.template);
  const context = buildTemplatePreviewContext(campaignJson, entry);
  const ignoreEmptyFields = getBool(asRecord(campaignJson.validation_policy), "ignore_empty_fields", false);
  const html = renderTemplatePreviewText(getText(template, "html"), context, ignoreEmptyFields);
  const text = renderTemplatePreviewText(getText(template, "text"), context, ignoreEmptyFields);
  const subject = String(row.subject || renderTemplatePreviewText(getText(template, "subject"), context, ignoreEmptyFields) || "No subject");
  const issues = asArray(row.issues).map(asRecord);
  const resolvedRecipients = asRecord(row.resolved_recipients);

  return (
    <MessagePreviewOverlay
      title="Built message review"
      subject={subject}
      bodyMode={html.trim() ? "html" : "text"}
      text={text}
      html={html}
      recipientLabel={formatAddressList(resolvedRecipients.to) || String(row.recipient_email ?? `Message ${index + 1}`)}
      recipientNote={issues.length > 0 ? `${issues.length} issue${issues.length === 1 ? "" : "s"}: ${issues.map((issue) => String(issue.message ?? issue.code ?? "Issue")).join(" · ")}` : "Built without reported issues."}
      metaItems={builtMessageMetaItems(row)}
      attachments={builtMessageAttachments(row)}
      navigation={{
        index,
        total: rows.length,
        onFirst: () => onSelect(0),
        onPrevious: () => onSelect(Math.max(0, index - 1)),
        onNext: () => onSelect(Math.min(rows.length - 1, index + 1)),
        onLast: () => onSelect(rows.length - 1),
      }}
      onClose={onClose}
    />
  );
}

function WorkflowFact({ label, value }: { label: string; value: React.ReactNode }) {
  return (
    <div className="review-flow-fact">
      <span>{label}</span>
      <strong>{value}</strong>
    </div>
  );
}

function builtMessageColumns(
  openMessage: (index: number) => void,
  reviewedKeys: Set<string>,
): DataGridColumn<Record<string, unknown>>[] {
  return [
    { id: "number", header: "#", width: 70, sortable: true, filterType: "integer", sticky: "start", value: (row, index) => Number(row.entry_index ?? index + 1) },
    { id: "recipient", header: "Recipient", width: 250, resizable: true, sortable: true, filterable: true, value: (row) => formatAddressList(asRecord(row.resolved_recipients).to) || String(row.recipient_email ?? "—") },
    { id: "subject", header: "Subject", width: "minmax(260px, 1fr)", resizable: true, sortable: true, filterable: true, value: (row) => String(row.subject ?? "—") },
    {
      id: "validation",
      header: "Validation",
      width: 145,
      sortable: true,
      filterable: true,
      columnType: "from-list",
      list: { options: MESSAGE_VALIDATION_OPTIONS, display: "pill" },
      value: (row) => String(row.validation_status ?? "unknown"),
    },
    { id: "attachments", header: "Attachments", width: 125, sortable: true, filterable: true, filterType: "integer", align: "right", value: (row) => Number(row.attachment_count ?? countResolvedAttachments(row.attachments)) },
    { id: "reviewed", header: "Reviewed", width: 110, sortable: true, filterable: true, columnType: "from-list", list: { options: [{ value: "yes", label: "Reviewed" }, { value: "no", label: "Not reviewed" }] }, render: (row, index) => (row.reviewed === true || reviewedKeys.has(String(row.review_key ?? builtMessageKey(row, index)))) ? <Check size={17} aria-label="Reviewed" /> : <span className="muted">—</span>, value: (row, index) => (row.reviewed === true || reviewedKeys.has(String(row.review_key ?? builtMessageKey(row, index)))) ? "yes" : "no" },
    { id: "actions", header: "Actions", width: 110, sticky: "end", render: (_row, index) => <Button onClick={() => openMessage(index)}>Review</Button> },
  ];
}

function mockSendResultColumns(): DataGridColumn<Record<string, unknown>>[] {
  const options: DataGridListOption[] = [
    { value: "sent", label: "Sent" },
    { value: "failed", label: "Failed" },
    { value: "skipped", label: "Skipped" },
    { value: "ready", label: "Ready" },
  ];
  return [
    { id: "number", header: "#", width: 70, sortable: true, filterType: "integer", sticky: "start", value: (row, index) => Number(row.entry_index ?? index + 1) },
    { id: "status", header: "Status", width: 130, sortable: true, filterable: true, columnType: "from-list", list: { options, display: "pill" }, value: (row) => String(row.status ?? "info") },
    { id: "recipient", header: "Recipient", width: 250, resizable: true, sortable: true, filterable: true, value: (row) => formatAddressList(row.to) || asArray(row.envelope_recipients).join(", ") || "—" },
    { id: "smtp", header: "SMTP", width: 190, sortable: true, filterable: true, value: (row) => String(row.smtp_message_id ?? row.status ?? "—") },
    { id: "imap", header: "IMAP", width: 190, sortable: true, filterable: true, value: (row) => String(row.imap_message_id ?? row.imap_status ?? "—") },
    { id: "message", header: "Message", width: "minmax(260px, 1fr)", resizable: true, filterable: true, value: (row) => String(row.message ?? "—") },
  ];
}

function sendResultColumns(): DataGridColumn<Record<string, unknown>>[] {
  const statuses: DataGridListOption[] = [
    { value: "smtp_accepted", label: "SMTP accepted" },
    { value: "already_accepted", label: "Already accepted" },
    { value: "outcome_unknown", label: "Outcome unknown" },
    { value: "failed", label: "Failed" },
    { value: "cancelled", label: "Cancelled" },
    { value: "not_claimed", label: "Not claimed" },
    { value: "dry_run", label: "Dry run" },
  ];
  return [
    { id: "status", header: "Status", width: 160, sortable: true, filterable: true, sticky: "start", columnType: "from-list", list: { options: statuses, display: "pill" }, render: (row) => <StatusBadge status={String(row.status ?? "info")} />, value: (row) => String(row.status ?? "info") },
    { id: "job", header: "Job", width: 180, sortable: true, filterable: true, value: (row) => String(row.job_id ?? row.version_id ?? "—") },
    { id: "message", header: "Message", width: "minmax(320px, 1fr)", resizable: true, filterable: true, value: (row) => String(row.message ?? stringifyPreview(row, 180)) },
  ];
}

function mockMailboxColumns(openMessage: (id: string) => Promise<void>): DataGridColumn<Record<string, unknown>>[] {
  const options: DataGridListOption[] = [
    { value: "smtp", label: "SMTP" },
    { value: "imap_append", label: "IMAP append" },
  ];
  return [
    { id: "kind", header: "Kind", width: 125, sortable: true, filterable: true, sticky: "start", columnType: "from-list", list: { options, display: "pill" }, value: (row) => String(row.kind ?? "mock") },
    { id: "subject", header: "Subject", width: "minmax(260px, 1fr)", resizable: true, sortable: true, filterable: true, value: (row) => String(row.subject ?? "—") },
    { id: "envelope", header: "Envelope / folder", width: 300, resizable: true, filterable: true, value: (row) => `${String(row.envelope_from ?? row.folder ?? "—")} → ${asArray(row.envelope_recipients).join(", ") || String(row.folder ?? "—")}` },
    { id: "attachments", header: "Attachments", width: 125, sortable: true, filterable: true, filterType: "integer", align: "right", value: (row) => Number(row.attachment_count ?? 0) },
    { id: "actions", header: "Actions", width: 110, sticky: "end", render: (row) => <Button onClick={() => void openMessage(String(row.id ?? ""))}>Review</Button> },
  ];
}

function builtMessageMetaItems(row: Record<string, unknown>) {
  const recipients = asRecord(row.resolved_recipients);
  return [
    { label: "From", value: formatSingleAddress(recipients.from) || "—" },
    { label: "To", value: formatAddressList(recipients.to) || String(row.recipient_email ?? "—") },
    { label: "CC", value: formatAddressList(recipients.cc) || "—" },
    { label: "BCC", value: formatAddressList(recipients.bcc) || "—" },
    { label: "Validation", value: String(row.validation_status ?? "—") },
    { label: "MIME size", value: row.eml_size_bytes ? `${String(row.eml_size_bytes)} bytes` : "—" },
  ];
}

function builtMessageAttachments(row: Record<string, unknown>): MessagePreviewAttachment[] {
  return asArray(row.attachments).flatMap((value, index) => {
    const attachment = asRecord(value);
    const managedMatches = asArray(attachment.managed_matches).map(asRecord);
    if (managedMatches.length > 0) {
      return managedMatches.map((match, matchIndex) => ({
        filename: String(match.filename ?? match.display_path ?? `Attachment ${index + 1}.${matchIndex + 1}`),
        detail: String(match.display_path ?? match.relative_path ?? attachment.label ?? ""),
        contentType: stringOrUndefined(match.content_type),
        sizeBytes: numberOrUndefined(match.size_bytes),
        archiveGroup: stringOrUndefined(attachment.zip_filename),
        archiveLabel: stringOrUndefined(attachment.zip_filename),
      }));
    }
    const matches = asArray(attachment.matches).filter((match): match is string => typeof match === "string" && Boolean(match.trim()));
    if (matches.length > 0) {
      return matches.map((match) => ({
        filename: match.split(/[\\/]/).pop() || String(attachment.label ?? `Attachment ${index + 1}`),
        detail: match,
        contentType: stringOrUndefined(attachment.content_type),
        sizeBytes: numberOrUndefined(attachment.size_bytes),
        archiveGroup: stringOrUndefined(attachment.zip_filename),
        archiveLabel: stringOrUndefined(attachment.zip_filename),
      }));
    }
    return [{
      filename: String(attachment.filename ?? attachment.filename_used ?? attachment.display_path ?? attachment.label ?? `Attachment ${index + 1}`),
      detail: String(attachment.display_path ?? attachment.source_path ?? attachment.file_filter ?? ""),
      contentType: stringOrUndefined(attachment.content_type),
      sizeBytes: numberOrUndefined(attachment.size_bytes),
      archiveGroup: stringOrUndefined(attachment.zip_filename),
      archiveLabel: stringOrUndefined(attachment.zip_filename),
    }];
  });
}

function mockMessageMetaItems(message: MockMailboxMessage) {
  return [
    { label: "From", value: message.from_header || message.envelope_from || "—" },
    { label: "To", value: message.to_header || message.envelope_recipients?.join(", ") || "—" },
    { label: "Kind", value: message.kind || "—" },
    { label: "Folder", value: message.folder || "—" },
    { label: "Message-ID", value: message.message_id || "—" },
    { label: "Size", value: `${message.size_bytes || 0} bytes` },
  ];
}

function mockMessageAttachments(message: MockMailboxMessage): MessagePreviewAttachment[] {
  return (message.attachments ?? []).map((attachment, index) => ({
    filename: attachment.filename || `Attachment ${index + 1}`,
    contentType: attachment.content_type || undefined,
    sizeBytes: attachment.size_bytes ?? undefined,
  }));
}

function countResolvedAttachments(value: unknown): number {
  const archives = new Set<string>();
  let directCount = 0;
  for (const item of asArray(value)) {
    const attachment = asRecord(item);
    const zipFilename = String(attachment.zip_filename ?? "").trim();
    if (zipFilename) {
      archives.add(zipFilename);
      continue;
    }
    const managedCount = asArray(attachment.managed_matches).length;
    if (managedCount > 0) {
      directCount += managedCount;
      continue;
    }
    const matchCount = asArray(attachment.matches).length;
    directCount += matchCount > 0 ? matchCount : 1;
  }
  return directCount + archives.size;
}

function messageNeedsExplicitReview(row: Record<string, unknown>): boolean {
  return String(row.validation_status ?? "").toLowerCase() === "needs_review";
}

function emptyCampaignJobsResponse(): CampaignJobsResponse {
  return {
    jobs: [],
    page: 1,
    page_size: 50,
    total: 0,
    total_unfiltered: 0,
    pages: 0,
    counts: {},
    filtered_counts: {},
    review: {},
  };
}

function storedMessageReviewState(version: CampaignVersionDetail | null): {
  buildToken: string;
  inspectionComplete: boolean;
  reviewedMessageKeys: string[];
} {
  const build = asRecord(version?.build_summary);
  const buildToken = String(build.build_token ?? build.built_at ?? "");
  const review = asRecord(asRecord(version?.editor_state).review_send);
  if (!buildToken || String(review.build_token ?? "") !== buildToken) {
    return { buildToken, inspectionComplete: false, reviewedMessageKeys: [] };
  }
  return {
    buildToken,
    inspectionComplete: review.inspection_complete === true,
    reviewedMessageKeys: asArray(review.reviewed_message_keys).filter((value): value is string => typeof value === "string"),
  };
}

function builtMessageKey(row: Record<string, unknown>, index: number): string {
  return String(row.entry_id ?? row.entry_index ?? index);
}

function formatAddressList(value: unknown): string {
  return asArray(value).map(asRecord).map(formatSingleAddress).filter(Boolean).join(", ");
}

function formatSingleAddress(value: unknown): string {
  const address = asRecord(value);
  const email = String(address.email ?? "").trim();
  const name = String(address.name ?? "").trim();
  if (name && email) return `${name} <${email}>`;
  return email || name;
}

function numberFrom(record: Record<string, unknown>, keys: string[]): number {
  for (const key of keys) {
    const value = record[key];
    if (typeof value === "number" && Number.isFinite(value)) return value;
    if (typeof value === "string" && value.trim() && Number.isFinite(Number(value))) return Number(value);
  }
  return 0;
}

function numberOrUndefined(value: unknown): number | undefined {
  if (typeof value === "number" && Number.isFinite(value)) return value;
  if (typeof value === "string" && value.trim() && Number.isFinite(Number(value))) return Number(value);
  return undefined;
}

function stringOrUndefined(value: unknown): string | undefined {
  if (typeof value !== "string") return undefined;
  return value.trim() || undefined;
}

function stateLabel(state: FlowState): string {
  switch (state) {
    case "complete": return "Passed";
    case "warning": return "Warnings";
    case "danger": return "Blocked";
    case "active": return "Available";
    case "locked": return "Locked";
    case "running": return "Running";
    case "partial": return "Partial";
    case "stale": return "Stale";
    default: return humanize(state);
  }
}
