'use client';

import { useCallback, useMemo, useState } from 'react';
import Link from 'next/link';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import {
  AlertTriangle,
  ArrowDown,
  CheckCircle2,
  Download,
  Eye,
  ExternalLink,
  FileSignature,
  GitBranch,
  Loader2,
  Mail,
  RefreshCw,
  Upload,
  XCircle,
} from 'lucide-react';
import { toast } from 'sonner';

import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Skeleton } from '@/components/ui/skeleton';
import { EoiCancelDialog } from '@/components/documents/eoi-cancel-dialog';
import { EoiGenerateDialog } from '@/components/documents/eoi-generate-dialog';
import { ExternalEoiUploadDialog } from '@/components/interests/external-eoi-upload-dialog';
import { MarkExternallySignedDialog } from '@/components/interests/mark-externally-signed-dialog';
import { SigningProgress } from '@/components/documents/signing-progress';
import { UploadForSigningDialog } from '@/components/documents/upload-for-signing-dialog';
import { FilePreviewDialog } from '@/components/files/file-preview-dialog';
import { PermissionGate } from '@/components/shared/permission-gate';
import { apiFetch } from '@/lib/api/client';
import { toastError } from '@/lib/api/toast-error';
import { useConfirmation } from '@/hooks/use-confirmation';
import { useRealtimeInvalidation } from '@/hooks/use-realtime-invalidation';
import {
  DOCUMENT_STATUS_ACTIVE,
  DOCUMENT_STATUS_LABELS,
  type DocumentStatus,
} from '@/lib/labels/document-status';
import { cn } from '@/lib/utils';
import { triggerUrlDownload } from '@/lib/utils/download';
import { useUIStore } from '@/stores/ui-store';

interface InterestEoiTabProps {
  interestId: string;
  /** Used by the generate dialog to deep-link to the client's record. */
  clientId: string | null;
}

interface DocumentRow {
  id: string;
  documentType: string;
  title: string;
  status: DocumentStatus;
  createdAt: string;
  signers?: Array<{ status: string }>;
  /** Null while the EOI is in flight; populated by the completion webhook
   *  once the fully-signed PDF has been downloaded from Documenso and
   *  stored in MinIO/filesystem. Drives the "Download signed PDF" CTA. */
  signedFileId?: string | null;
  /** Automate Signing mode — drives the banner + button visibility on
   *  ActiveEoiCard. `manual` (default) hides the banner and shows the
   *  "Automate signing" button; the auto modes hide the button and
   *  show the "Automating · N of M" banner + "Pause" CTA. */
  automationMode?: 'manual' | 'sequential_auto' | 'concurrent_auto';
  /** Documenso envelope id — required for the Automate Signing
   *  button to enable (we don't auto-create envelopes mid-flow). */
  documensoId?: string | null;
}

interface DocumentSigner {
  id: string;
  signerName: string;
  signerEmail: string;
  signerRole: string;
  signingOrder: number;
  status: string;
  signedAt?: string | null;
}

const STATUS_LABELS = DOCUMENT_STATUS_LABELS;

const STATUS_TONES: Record<DocumentStatus, string> = {
  draft: 'bg-slate-100 text-slate-700',
  sent: 'bg-blue-100 text-blue-700',
  partially_signed: 'bg-amber-100 text-amber-800',
  completed: 'bg-emerald-100 text-emerald-700',
  expired: 'bg-rose-100 text-rose-700',
  cancelled: 'bg-slate-200 text-slate-600',
  rejected: 'bg-rose-100 text-rose-700',
  declined: 'bg-rose-100 text-rose-700',
};

const ACTIVE_STATUSES = DOCUMENT_STATUS_ACTIVE;

/**
 * Dedicated EOI workspace tab. The user's "where do I generate / track
 * the EOI for this deal" surface, separate from the generic Documents
 * tab (which is the long-tail history of every document the interest
 * has accumulated, including signed past EOIs).
 *
 * Layout:
 *   - In-flight EOI hero (signing progress + reminders) when an active
 *     EOI document exists for the interest
 *   - "Generate EOI" CTA when none is in flight
 *   - History strip of past completed/cancelled EOIs
 *
 * The actual generate flow opens `EoiGenerateDialog` which now shows
 * the resolved EoiContext (real values that will be filled) rather
 * than just a checklist of which fields exist.
 */
export function InterestEoiTab({ interestId, clientId }: InterestEoiTabProps) {
  const portSlug = useUIStore((s) => s.currentPortSlug);
  const [generateOpen, setGenerateOpen] = useState(false);
  const [uploadSignedOpen, setUploadSignedOpen] = useState(false);
  const [uploadForSigningOpen, setUploadForSigningOpen] = useState(false);
  const [markSignedOpen, setMarkSignedOpen] = useState(false);
  // Lifted preview state so the View button on every signed-PDF row opens
  // the in-app preview dialog rather than navigating to a presigned URL
  // (which the storage backend serves with Content-Disposition=attachment,
  // forcing a download even when the rep just wants to inspect the PDF).
  const [previewFile, setPreviewFile] = useState<{ id: string; name?: string } | null>(null);
  const { confirm, dialog: confirmDialog } = useConfirmation();

  // Manually (re)send the finalized signed PDF to the deal's client.
  // Lifted to the parent (like the preview dialog) so every row + the
  // signed-EOI hero share one confirm + handler. Guarded by a confirm so
  // a stray click can't fire a real client email.
  const handleSendCopy = useCallback(
    async (documentId: string) => {
      const ok = await confirm({
        title: 'Send signed copy to client?',
        description: 'Emails the deal’s client the finalized signed PDF as an attachment.',
        confirmLabel: 'Send copy',
      });
      if (!ok) return;
      try {
        const res = await apiFetch<{ data: { recipientEmail: string } }>(
          `/api/v1/documents/${documentId}/send-signed-copy`,
          { method: 'POST' },
        );
        toast.success(`Signed copy sent to ${res.data.recipientEmail}.`);
      } catch (err) {
        toastError(err, 'Failed to send signed copy');
      }
    },
    [confirm],
  );

  const { data: docsRes, isLoading: docsLoading } = useQuery<{ data: DocumentRow[] }>({
    queryKey: ['documents', { interestId, documentType: 'eoi' }],
    queryFn: () =>
      apiFetch<{ data: DocumentRow[] }>(
        `/api/v1/documents?interestId=${interestId}&documentType=eoi`,
      ),
  });

  const docs = docsRes?.data ?? [];
  const activeDoc = useMemo(() => docs.find((d) => ACTIVE_STATUSES.has(d.status)) ?? null, [docs]);
  const completedDocs = useMemo(() => docs.filter((d) => !ACTIVE_STATUSES.has(d.status)), [docs]);
  // Most-recent fully-signed EOI. When no EOI is in flight, this becomes
  // the hero (instead of the generate/upload empty state) so a deal whose
  // EOI is already done leads with the signed document, per UAT 2026-06-03.
  const latestSignedDoc = useMemo(() => {
    return (
      docs
        .filter((d) => d.status === 'completed')
        .sort((a, b) => +new Date(b.createdAt) - +new Date(a.createdAt))[0] ?? null
    );
  }, [docs]);
  // History strip excludes whichever signed doc is shown as the hero so it
  // isn't listed twice.
  const historyDocs = useMemo(
    () => completedDocs.filter((d) => d.id !== latestSignedDoc?.id),
    [completedDocs, latestSignedDoc],
  );

  // Pulled at the parent so we can thread the active EOI's signers into the
  // ExternalEoiUploadDialog as a prefill seed. ActiveEoiCard hits the same
  // query key — react-query dedupes the actual fetch.
  const { data: activeSignersRes } = useQuery<{ data: DocumentSigner[] }>({
    queryKey: ['documents', activeDoc?.id, 'signers'],
    queryFn: () =>
      apiFetch<{ data: DocumentSigner[] }>(`/api/v1/documents/${activeDoc!.id}/signers`),
    enabled: !!activeDoc,
    staleTime: 30_000,
  });

  const externalUploadPrefill = useMemo(() => {
    const rows = activeSignersRes?.data ?? [];
    if (rows.length === 0) return undefined;
    const ROLE_MAP: Record<string, 'client' | 'developer' | 'rep' | 'witness' | 'cc'> = {
      client: 'client',
      developer: 'developer',
      approver: 'developer',
      rep: 'rep',
      witness: 'witness',
      cc: 'cc',
      viewer: 'cc',
      other: 'witness',
    };
    return rows.map((s) => ({
      name: s.signerName,
      email: s.signerEmail,
      role: ROLE_MAP[(s.signerRole ?? '').toLowerCase()] ?? ('witness' as const),
    }));
  }, [activeSignersRes]);

  return (
    <div className="space-y-5">
      {docsLoading ? (
        <Skeleton className="h-44 w-full rounded-lg" aria-hidden />
      ) : activeDoc ? (
        <ActiveEoiCard
          doc={activeDoc}
          portSlug={portSlug ?? null}
          onUploadSigned={() => setUploadSignedOpen(true)}
          onView={(id, name) => setPreviewFile({ id, name })}
          onSendCopy={handleSendCopy}
        />
      ) : latestSignedDoc ? (
        <SignedEoiCard
          doc={latestSignedDoc}
          portSlug={portSlug ?? null}
          onView={(id, name) => setPreviewFile({ id, name })}
          onSendCopy={handleSendCopy}
          onGenerateNew={() => setGenerateOpen(true)}
        />
      ) : (
        <EmptyEoiState
          onGenerate={() => setGenerateOpen(true)}
          onUploadForSigning={() => setUploadForSigningOpen(true)}
          onUploadSigned={() => setUploadSignedOpen(true)}
          onMarkSigned={() => setMarkSignedOpen(true)}
        />
      )}

      {/* History strip  -  completed + cancelled EOIs from earlier in the
          deal's life. Quiet and skimmable; the active document above
          carries the day-to-day attention. */}
      {historyDocs.length > 0 && (
        <section className="rounded-lg border bg-background">
          <header className="flex items-center justify-between border-b px-4 py-2.5">
            <h3 className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
              EOI history
            </h3>
            <span className="text-xs text-muted-foreground">
              {historyDocs.length} {historyDocs.length === 1 ? 'document' : 'documents'}
            </span>
          </header>
          <ul className="divide-y">
            {historyDocs.map((d) => (
              <li key={d.id} className="flex items-center gap-3 px-4 py-2.5 text-sm">
                <StatusBadge status={d.status} />
                <span className="flex-1 truncate font-medium">{d.title}</span>
                <span className="text-xs text-muted-foreground">
                  {new Date(d.createdAt).toLocaleDateString()}
                </span>
                {d.signedFileId ? (
                  <SignedPdfActions
                    fileId={d.signedFileId}
                    documentId={d.id}
                    isSignedCopySendable={d.status === 'completed'}
                    title={d.title}
                    onView={(id, name) => setPreviewFile({ id, name })}
                    onSendCopy={handleSendCopy}
                  />
                ) : null}
                {portSlug && (
                  <Link
                    // eslint-disable-next-line @typescript-eslint/no-explicit-any
                    href={`/${portSlug}/documents/${d.id}` as any}
                    className="text-xs text-primary hover:underline inline-flex items-center gap-1"
                  >
                    Open in Documents
                    <ExternalLink className="size-3" aria-hidden />
                  </Link>
                )}
              </li>
            ))}
          </ul>
        </section>
      )}

      <EoiGenerateDialog
        interestId={interestId}
        clientId={clientId}
        open={generateOpen}
        onOpenChange={setGenerateOpen}
      />

      <ExternalEoiUploadDialog
        open={uploadSignedOpen}
        onOpenChange={setUploadSignedOpen}
        interestId={interestId}
        prefillSignatories={externalUploadPrefill}
      />

      {/* Phase 4 parity - same upload-PDF + place-fields wizard as
          Contract/Reservation, scoped to documentType="eoi". The
          template-driven generate flow lives on `EoiGenerateDialog`
          above; this branch handles the custom-draft path. */}
      {uploadForSigningOpen && (
        <UploadForSigningDialog
          open={uploadForSigningOpen}
          onOpenChange={setUploadForSigningOpen}
          interestId={interestId}
          documentType="eoi"
        />
      )}

      <MarkExternallySignedDialog
        open={markSignedOpen}
        onOpenChange={setMarkSignedOpen}
        interestId={interestId}
        docType="eoi"
      />

      <FilePreviewDialog
        open={!!previewFile}
        onOpenChange={(o) => {
          if (!o) setPreviewFile(null);
        }}
        fileId={previewFile?.id}
        fileName={previewFile?.name}
      />
      {confirmDialog}
    </div>
  );
}

// ─── In-flight EOI hero ──────────────────────────────────────────────────────

function ActiveEoiCard({
  doc,
  portSlug,
  onUploadSigned,
  onView,
  onSendCopy,
}: {
  doc: DocumentRow;
  portSlug: string | null;
  onUploadSigned: () => void;
  onView: (fileId: string, fileName?: string) => void;
  onSendCopy: (documentId: string) => void;
}) {
  const queryClient = useQueryClient();
  const { confirm, dialog: confirmDialog } = useConfirmation();
  const [cancelOpen, setCancelOpen] = useState(false);

  const { data: signersRes, isLoading: signersLoading } = useQuery<{ data: DocumentSigner[] }>({
    queryKey: ['documents', doc.id, 'signers'],
    queryFn: () => apiFetch<{ data: DocumentSigner[] }>(`/api/v1/documents/${doc.id}/signers`),
    // Polling backstop in case a webhook event misses the open browser
    // (transient socket drop, user in a different tab when the event
    // fires, cloudflared tunnel hiccup). Primary update path is
    // socket-driven via `useRealtimeInvalidation` below - this just
    // bounds the worst-case staleness to ~5s.
    refetchInterval: 5_000,
  });

  // Surface the per-port signing-order preference (Sequential vs Concurrent
  // = Parallel in Documenso parlance) so the team knows what order recipients
  // will receive the signing chain in.
  const { data: signingDefaultsRes } = useQuery<{
    data: { signingOrder: 'PARALLEL' | 'SEQUENTIAL' };
  }>({
    queryKey: ['documents', 'signing-defaults'],
    queryFn: () =>
      apiFetch<{ data: { signingOrder: 'PARALLEL' | 'SEQUENTIAL' } }>(
        '/api/v1/documents/signing-defaults',
      ),
    staleTime: 60_000,
  });
  const signingOrder = signingDefaultsRes?.data?.signingOrder ?? 'PARALLEL';

  const signers = signersRes?.data ?? [];
  const signedCount = signers.filter((s) => s.status === 'signed').length;
  const totalCount = signers.length;
  const allSigned = totalCount > 0 && signedCount === totalCount;

  // Treat "all signers complete" as the finalised UX even when the
  // DOCUMENT_COMPLETED webhook hasn't landed yet - defends against the
  // gap between the last per-recipient sign event and the document-level
  // completion event. The badge below flips to "Finalising" so the rep
  // sees the in-flight state rather than a stale PARTIALLY_SIGNED chip.
  const effectivelyCompleted = doc.status === 'completed' || allSigned;
  const isAwaitingFinalisation = allSigned && doc.status !== 'completed';

  // Real-time push: invalidate the signers query the moment a webhook
  // fires `document:signer:*` so the card flips state without waiting
  // for the 30s refetch interval. Same for `document:completed` so the
  // "all signed" footer chip appears as soon as the last signer finishes.
  useRealtimeInvalidation({
    'document:signer:signed': [['documents', doc.id, 'signers'], ['documents']],
    'document:signer:opened': [['documents', doc.id, 'signers']],
    'document:completed': [['documents', doc.id, 'signers'], ['documents']],
    'document:signer:rejected': [['documents', doc.id, 'signers'], ['documents']],
    'document:rejected': [['documents', doc.id, 'signers'], ['documents']],
  });

  // §4.13: surface the rejection callout in a high-visibility banner -
  // status pill alone doesn't communicate that the doc is dead and the
  // rep must take action.
  const isRejected = doc.status === 'rejected' || doc.status === 'declined';
  const rejectedSigner = isRejected
    ? signers.find((s) => s.status === 'declined' || s.status === 'rejected')
    : null;

  // Fetch the rejection event so the banner can surface the free-text
  // reason the signer typed into Documenso (plumbed end-to-end in the
  // 2026-05-26 Documenso reliability round — webhook receiver coalesces
  // v2 rejectionReason / v1 declineReason and persists to
  // document_events.eventData.rejectionReason). Skipped when the doc
  // isn't rejected, so we don't pay the round-trip on the happy path.
  const { data: eventsRes } = useQuery<{ data: Array<{ eventType: string; eventData: unknown }> }>({
    queryKey: ['documents', doc.id, 'events'],
    queryFn: () => apiFetch(`/api/v1/documents/${doc.id}/events`),
    enabled: isRejected,
    staleTime: 60_000,
  });
  const rejectionReason: string | null = (() => {
    if (!isRejected) return null;
    const rejectionEvent = eventsRes?.data?.find((e) => e.eventType === 'rejected');
    const data = rejectionEvent?.eventData as { rejectionReason?: string | null } | undefined;
    const reason = data?.rejectionReason;
    if (typeof reason !== 'string') return null;
    const trimmed = reason.trim();
    return trimmed.length > 0 ? trimmed : null;
  })();

  const remindAllMutation = useMutation({
    mutationFn: () => apiFetch(`/api/v1/documents/${doc.id}/remind`, { method: 'POST', body: {} }),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['documents', doc.id, 'signers'] });
      toast.success('Reminder sent.');
    },
    onError: (err) => toastError(err),
  });

  // Automate Signing — single button that fires the first signer's
  // invitation (sequential envelopes) OR all signers in parallel
  // (concurrent envelopes). The existing webhook cascade handles
  // sequential follow-ups; the completion broadcast handles the
  // "signed PDF to everyone" email regardless of mode. See
  // src/lib/services/signing-automation.service.ts.
  const automateMutation = useMutation({
    mutationFn: () =>
      apiFetch<{ data: { mode: string; invitedCount: number } }>(
        `/api/v1/documents/${doc.id}/automate`,
        { method: 'POST' },
      ),
    onSuccess: (res) => {
      queryClient.invalidateQueries({ queryKey: ['documents', doc.id, 'signers'] });
      queryClient.invalidateQueries({ predicate: (q) => q.queryKey[0] === 'documents' });
      const modeLabel =
        res.data.mode === 'sequential_auto' ? 'Automating sequentially' : 'Automating concurrently';
      toast.success(`${modeLabel} - ${res.data.invitedCount} invitation(s) sent.`);
    },
    onError: (err) => toastError(err),
  });
  const pauseAutomationMutation = useMutation({
    mutationFn: () => apiFetch(`/api/v1/documents/${doc.id}/automate`, { method: 'DELETE' }),
    onSuccess: () => {
      queryClient.invalidateQueries({ predicate: (q) => q.queryKey[0] === 'documents' });
      toast.success('Automation paused. Reverted to manual.');
    },
    onError: (err) => toastError(err),
  });

  const automationMode = doc.automationMode ?? 'manual';
  const automationActive =
    automationMode === 'sequential_auto' || automationMode === 'concurrent_auto';
  const canAutomate =
    !automationActive &&
    !effectivelyCompleted &&
    !isRejected &&
    totalCount >= 2 &&
    Boolean(doc.documensoId);

  return (
    <section
      className={
        isRejected
          ? 'rounded-xl border border-rose-300 bg-rose-50 p-5 shadow-xs'
          : 'rounded-xl border bg-gradient-brand-soft p-5 shadow-xs'
      }
    >
      {isRejected ? (
        <div className="mb-4 flex items-start gap-3 rounded-md border border-rose-300 bg-white p-3">
          <AlertTriangle className="size-5 shrink-0 text-rose-600" aria-hidden />
          <div className="min-w-0 flex-1 text-sm">
            <p className="font-semibold text-rose-900">
              EOI declined
              {rejectedSigner
                ? ` by ${rejectedSigner.signerName ?? rejectedSigner.signerEmail}`
                : ''}
            </p>
            {rejectionReason ? (
              <blockquote className="mt-1 rounded border-l-2 border-rose-400 bg-rose-50 px-2 py-1 text-rose-900 italic">
                &ldquo;{rejectionReason}&rdquo;
              </blockquote>
            ) : null}
            <p className="mt-0.5 text-rose-800">
              The document is no longer valid. Cancel and regenerate, or reach out to the signer
              before re-sending.
            </p>
          </div>
        </div>
      ) : null}
      <header className="flex items-start justify-between gap-3">
        <div className="min-w-0 flex-1 space-y-1">
          <div className="flex items-center gap-2 flex-wrap">
            <FileSignature className="size-4 text-foreground" aria-hidden />
            <h2 className="truncate text-base font-semibold text-foreground">{doc.title}</h2>
            {isAwaitingFinalisation ? (
              <Badge variant="outline" className="border-sky-300 bg-sky-50 text-sky-800">
                <Loader2 className="mr-1 size-3 animate-spin" aria-hidden /> Finalising…
              </Badge>
            ) : (
              <StatusBadge status={doc.status} />
            )}
          </div>
          <div className="flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-muted-foreground">
            <span>
              Created {new Date(doc.createdAt).toLocaleDateString()} ·{' '}
              {totalCount > 0 ? `${signedCount} of ${totalCount} signed` : 'No signers loaded'}
            </span>
            {/* Signing-order badge  -  tells the team whether recipients
                must sign in order or can sign concurrently. Drives off
                the per-port setting; for v2 templates the template's
                stored order wins server-side and we still surface our
                local preference here so the UI matches what was sent. */}
            <span
              className={cn(
                'inline-flex items-center gap-1 rounded-full border px-1.5 py-0.5 text-xs font-medium uppercase tracking-wide',
                signingOrder === 'SEQUENTIAL'
                  ? 'border-indigo-200 bg-indigo-50 text-indigo-800'
                  : 'border-sky-200 bg-sky-50 text-sky-800',
              )}
              title={
                signingOrder === 'SEQUENTIAL'
                  ? 'Signers receive the invite chain one at a time  -  each must sign before the next is emailed.'
                  : 'All signers receive the invite at once and can sign in any order.'
              }
            >
              {signingOrder === 'SEQUENTIAL' ? (
                <ArrowDown className="size-2.5" aria-hidden />
              ) : (
                <GitBranch className="size-2.5" aria-hidden />
              )}
              {signingOrder === 'SEQUENTIAL' ? 'Sequential' : 'Concurrent'}
            </span>
          </div>
        </div>
        <div className="flex shrink-0 items-center gap-1">
          {portSlug && (
            <Button asChild variant="outline" size="sm" className="gap-1.5 [&_svg]:size-3.5">
              <Link
                // eslint-disable-next-line @typescript-eslint/no-explicit-any
                href={`/${portSlug}/documents/${doc.id}` as any}
              >
                Open
                <ExternalLink />
              </Link>
            </Button>
          )}
          {/* Automate signing — fires the first signer's invitation
              (sequential envelopes) or all signers in parallel
              (concurrent envelopes) in one click. Hidden when
              automation is already active or when there are fewer
              than two signers. */}
          {canAutomate && (
            <Button
              variant="default"
              size="sm"
              disabled={automateMutation.isPending}
              onClick={() => automateMutation.mutate()}
              className="gap-1.5 [&_svg]:size-3.5"
              title="Fire the first signer's invitation; webhook handles the cascade. Sends the completed PDF to everyone when all signers finish."
            >
              {automateMutation.isPending ? (
                <Loader2 className="animate-spin" aria-hidden />
              ) : (
                <FileSignature />
              )}
              Automate signing
            </Button>
          )}
          {/* Remind all hides once every signer is signed  -  no-one to nudge. */}
          {!effectivelyCompleted && (
            <Button
              variant="outline"
              size="sm"
              disabled={remindAllMutation.isPending}
              onClick={() => remindAllMutation.mutate()}
              className="gap-1.5 [&_svg]:size-3.5"
            >
              {remindAllMutation.isPending ? (
                <Loader2 className="animate-spin" aria-hidden />
              ) : (
                <RefreshCw />
              )}
              Remind all
            </Button>
          )}
        </div>
      </header>

      {/* Automation banner — replaces the manual "Send invitation"
          friction with a single status indicator. Manual override
          buttons in SigningProgress stay visible (per decision
          locked 2026-05-26) so reps retain individual control. */}
      {automationActive && !effectivelyCompleted && !isRejected ? (
        <div className="mt-3 flex items-center justify-between gap-3 rounded-md border border-emerald-300 bg-emerald-50 px-3 py-2 text-xs text-emerald-900">
          <span className="flex items-center gap-2">
            <FileSignature className="size-3.5" aria-hidden />
            <span className="font-semibold">
              {automationMode === 'sequential_auto'
                ? `Automating sequentially - ${signedCount} of ${totalCount} signed`
                : `Automating concurrently - ${signedCount} of ${totalCount} signed`}
            </span>
            <span className="text-emerald-800">
              {automationMode === 'sequential_auto'
                ? 'Next signer auto-fires when the current one completes.'
                : 'All signers invited; waiting on completion.'}
            </span>
          </span>
          <Button
            variant="outline"
            size="sm"
            className="h-7 gap-1.5 border-emerald-300 bg-white text-emerald-900 [&_svg]:size-3"
            disabled={pauseAutomationMutation.isPending}
            onClick={() => pauseAutomationMutation.mutate()}
            title="Stop the cascade and revert to manual control. Doesn't undo invitations already sent."
          >
            {pauseAutomationMutation.isPending ? (
              <Loader2 className="animate-spin" aria-hidden />
            ) : null}
            Pause
          </Button>
        </div>
      ) : null}

      <div className="mt-4 rounded-lg border bg-background p-4">
        <h3 className="mb-3 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
          Signing progress
        </h3>
        {signersLoading ? (
          <div className="flex items-center gap-2 text-sm text-muted-foreground">
            <Loader2 className="size-3.5 animate-spin" aria-hidden /> Loading signers…
          </div>
        ) : signers.length === 0 ? (
          <p className="text-sm text-muted-foreground italic">
            The signing service hasn&apos;t reported signers yet - check back in a moment.
          </p>
        ) : (
          <SigningProgress documentId={doc.id} signers={signers} />
        )}
      </div>

      {/* Signed-PDF inline preview, shown once the completion webhook has
          downloaded + stored the final signed file. Defends in two ways:
          (a) status === 'completed' (the ideal path), (b) doc reports a
          signedFileId even when status hasn't flipped yet. */}
      {doc.signedFileId ? (
        <div className="mt-3 rounded-lg border bg-background p-4">
          <div className="mb-3 flex items-center justify-between gap-2">
            <h3 className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
              Signed document
            </h3>
            <SignedPdfActions
              fileId={doc.signedFileId}
              documentId={doc.id}
              isSignedCopySendable={doc.status === 'completed'}
              title={doc.title}
              onView={onView}
              onSendCopy={onSendCopy}
            />
          </div>
          <SignedPdfPreview fileId={doc.signedFileId} />
        </div>
      ) : null}

      {/* Footer hides once every signer is signed: Cancel + Remind reminder
          stop making sense, and the rep's natural next action is to view
          the signed PDF (rendered above) or open the linked document
          detail page. Upload-paper-signed-copy stays available  -  useful
          for in-person sign-out workflows even after the digital flow. */}
      {!effectivelyCompleted ? (
        <footer className="mt-3 flex flex-wrap items-center justify-between gap-2 text-xs text-muted-foreground">
          <p className="flex items-center gap-1.5">
            <AlertTriangle className="size-3 text-amber-600" aria-hidden />
            Manual reminders are rate-limited by Documenso (max once per 7 days per signer).
            Automatic follow-ups run on the configured cadence and are not throttled by us.
          </p>
          <div className="flex items-center gap-1">
            <Button
              type="button"
              variant="ghost"
              size="sm"
              onClick={onUploadSigned}
              className="h-7 gap-1.5 text-xs [&_svg]:size-3"
            >
              <Upload />
              Upload paper-signed copy
            </Button>
            {/* Regenerate is only safe when no one has signed yet  -  once
                signatures are on the doc, the rep must go through the
                cancel-with-notify path so collaborators learn about the
                discard. Wrapped in PermissionGate so reps without the
                cancel perm don't see the action at all (matches the
                Cancel EOI button gating below). */}
            {signedCount === 0 ? (
              <PermissionGate resource="documents" action="edit">
                <Button
                  type="button"
                  variant="ghost"
                  size="sm"
                  onClick={async () => {
                    const ok = await confirm({
                      title: 'Regenerate this EOI?',
                      description:
                        'The current envelope will be voided silently  -  no recipients will be notified  -  and the generate dialog will re-open so you can rebuild.',
                      confirmLabel: 'Regenerate',
                    });
                    if (ok) {
                      try {
                        await apiFetch(`/api/v1/documents/${doc.id}/cancel`, {
                          method: 'POST',
                          body: { reason: 'regenerated', notifyRecipients: [] },
                        });
                        queryClient.invalidateQueries({
                          predicate: (q) => q.queryKey[0] === 'documents',
                        });
                        toast.success('EOI voided. Regenerate now.');
                      } catch (err) {
                        toastError(err);
                      }
                    }
                  }}
                  className="h-7 gap-1.5 text-xs [&_svg]:size-3"
                  title="Void the current envelope (no notifications) and rebuild from scratch."
                >
                  <RefreshCw />
                  Regenerate
                </Button>
              </PermissionGate>
            ) : null}
            <PermissionGate resource="documents" action="edit">
              <Button
                type="button"
                variant="ghost"
                size="sm"
                onClick={() => setCancelOpen(true)}
                className="h-7 gap-1.5 text-xs text-destructive hover:text-destructive [&_svg]:size-3"
              >
                <XCircle />
                Cancel EOI
              </Button>
            </PermissionGate>
          </div>
        </footer>
      ) : null}
      {confirmDialog}
      <EoiCancelDialog
        documentId={doc.id}
        signers={signers}
        open={cancelOpen}
        onOpenChange={setCancelOpen}
      />
    </section>
  );
}

// ─── Signed EOI hero (no active EOI, but one is already signed) ───────────────

/**
 * Shown when the deal has a fully-signed EOI and nothing is in flight. Leads
 * with the signed document (preview + download + send-to-client) instead of
 * the generate/upload empty state — a deal whose EOI is done shouldn't open
 * on a big "Generate EOI" CTA. A quiet "Generate new EOI" remains for the
 * re-issue case.
 */
function SignedEoiCard({
  doc,
  portSlug,
  onView,
  onSendCopy,
  onGenerateNew,
}: {
  doc: DocumentRow;
  portSlug: string | null;
  onView: (fileId: string, fileName?: string) => void;
  onSendCopy: (documentId: string) => void;
  onGenerateNew: () => void;
}) {
  return (
    <section className="rounded-xl border bg-gradient-brand-soft p-5 shadow-xs">
      <header className="flex items-start justify-between gap-3">
        <div className="min-w-0 flex-1 space-y-1">
          <div className="flex flex-wrap items-center gap-2">
            <CheckCircle2 className="size-4 text-emerald-600" aria-hidden />
            <h2 className="truncate text-base font-semibold text-foreground">{doc.title}</h2>
            <StatusBadge status={doc.status} />
          </div>
          <p className="text-xs text-muted-foreground">
            Signed · {new Date(doc.createdAt).toLocaleDateString()}
          </p>
        </div>
        <div className="flex shrink-0 flex-wrap items-center justify-end gap-x-3 gap-y-1">
          {doc.signedFileId ? (
            <SignedPdfActions
              fileId={doc.signedFileId}
              documentId={doc.id}
              isSignedCopySendable={doc.status === 'completed'}
              title={doc.title}
              onView={onView}
              onSendCopy={onSendCopy}
            />
          ) : null}
          {portSlug && (
            <Link
              // eslint-disable-next-line @typescript-eslint/no-explicit-any
              href={`/${portSlug}/documents/${doc.id}` as any}
              className="inline-flex items-center gap-1 text-xs text-primary hover:underline"
            >
              Open in Documents
              <ExternalLink className="size-3" aria-hidden />
            </Link>
          )}
        </div>
      </header>

      {doc.signedFileId ? (
        <div className="mt-4 rounded-lg border bg-background p-4">
          <SignedPdfPreview fileId={doc.signedFileId} />
        </div>
      ) : (
        <p className="mt-4 rounded-md border border-dashed bg-background p-3 text-xs text-muted-foreground">
          The signed PDF isn&apos;t linked to this EOI yet, so inline preview, download, and send
          aren&apos;t available. Open it in Documents — this lights up once migrated EOIs are
          reconciled to their signed files.
        </p>
      )}

      <footer className="mt-3 flex items-center justify-between gap-2">
        <p className="text-xs text-muted-foreground">
          This deal&apos;s EOI is signed. Generate a new one only if you need to re-issue it.
        </p>
        <Button variant="ghost" size="sm" onClick={onGenerateNew} className="gap-1.5">
          <FileSignature className="size-4" aria-hidden />
          Generate new EOI
        </Button>
      </footer>
    </section>
  );
}

/**
 * Inline iframe preview of a signed PDF. Fetches a short-lived presigned
 * URL from `/api/v1/files/[id]/download` and renders the browser's native
 * PDF viewer inside the EOI card. Constrained to a fixed max-height so a
 * tall multi-page document doesn't blow out the page; the rep can open
 * the file in a new tab via the alongside View button for full-screen.
 */
function SignedPdfPreview({ fileId }: { fileId: string }) {
  // Use the PREVIEW endpoint, not /download: /download presigns with the
  // filename so S3 returns `Content-Disposition: attachment`, which makes the
  // iframe trigger a file download (blank preview) instead of rendering. The
  // preview endpoint presigns WITHOUT a filename → inline disposition → the
  // browser's native PDF viewer renders it in the card.
  const { data, isLoading, isError } = useQuery<{ data: { url: string; mimeType: string } }>({
    queryKey: ['files', fileId, 'preview-url'],
    queryFn: () =>
      apiFetch<{ data: { url: string; mimeType: string } }>(`/api/v1/files/${fileId}/preview`),
    // Presigned URL TTLs vary per backend - refresh well before they
    // expire so a long-open card doesn't suddenly 403. 4 minutes is
    // comfortably below the 5-minute MinIO default.
    staleTime: 4 * 60_000,
  });

  if (isLoading) {
    return (
      <div className="flex h-64 items-center justify-center text-xs text-muted-foreground">
        <Loader2 className="mr-2 size-3 animate-spin" aria-hidden /> Loading preview…
      </div>
    );
  }
  if (isError || !data?.data.url) {
    return (
      <p className="text-xs italic text-muted-foreground">
        Preview unavailable - use the Download button to grab the signed PDF.
      </p>
    );
  }
  return (
    <iframe
      src={data.data.url}
      title="Signed EOI preview"
      className="h-[560px] w-full rounded border bg-white"
    />
  );
}

// ─── Empty state ─────────────────────────────────────────────────────────────

function EmptyEoiState({
  onGenerate,
  onUploadForSigning,
  onUploadSigned,
  onMarkSigned,
}: {
  onGenerate: () => void;
  onUploadForSigning: () => void;
  onUploadSigned: () => void;
  onMarkSigned: () => void;
}) {
  return (
    <section className="rounded-xl border border-dashed bg-muted/20 p-8 text-center">
      <div className="mx-auto flex size-14 items-center justify-center rounded-full bg-background text-muted-foreground">
        <FileSignature className="size-6" aria-hidden />
      </div>
      <h2 className="mt-4 text-base font-semibold text-foreground">
        No EOI in flight for this interest
      </h2>
      <p className="mt-1 text-sm text-muted-foreground">
        Generate the EOI from the template, upload a custom draft and place signing fields, or
        upload a paper-signed copy if it was signed outside the system.
      </p>
      <div className="mt-5 flex flex-wrap items-center justify-center gap-2">
        <Button onClick={onGenerate} size="sm" className="gap-1.5">
          <FileSignature className="size-4" aria-hidden />
          Generate EOI
        </Button>
        <Button onClick={onUploadForSigning} variant="outline" size="sm" className="gap-1.5">
          <Upload className="size-4" aria-hidden />
          Upload draft for signing
        </Button>
        <Button onClick={onUploadSigned} variant="outline" size="sm" className="gap-1.5">
          <Upload className="size-4" aria-hidden />
          Upload paper-signed copy
        </Button>
        <Button onClick={onMarkSigned} variant="ghost" size="sm" className="gap-1.5">
          <CheckCircle2 className="size-4" aria-hidden />
          Mark signed without file
        </Button>
      </div>
    </section>
  );
}

function StatusBadge({ status }: { status: DocumentRow['status'] }) {
  return (
    <Badge
      variant="outline"
      className={cn(
        'border-transparent text-xs font-semibold uppercase tracking-wide',
        STATUS_TONES[status],
      )}
    >
      {status === 'completed' && <CheckCircle2 className="mr-1 size-3" aria-hidden />}
      {STATUS_LABELS[status]}
    </Badge>
  );
}

/**
 * View + Download buttons for a signed PDF. View opens the in-app
 * preview dialog (lifted to the parent so a single dialog instance
 * serves every row); Download fetches the presigned URL and triggers a
 * filename-preserving download via the shared helper.
 */
function SignedPdfActions({
  fileId,
  documentId,
  isSignedCopySendable = false,
  title,
  onView,
  onSendCopy,
}: {
  fileId: string;
  /** Document id — required for the "Send to client" action (which targets
   *  the document, not the raw file). */
  documentId?: string;
  /** Only show "Send to client" for a fully-completed document. */
  isSignedCopySendable?: boolean;
  title?: string;
  onView: (fileId: string, fileName?: string) => void;
  onSendCopy?: (documentId: string) => void;
}) {
  const handleDownload = async () => {
    try {
      const res = await apiFetch<{ data: { url: string; filename: string } }>(
        `/api/v1/files/${fileId}/download`,
      );
      triggerUrlDownload(res.data.url, res.data.filename);
    } catch (err) {
      toastError(err, 'Failed to fetch signed PDF');
    }
  };
  return (
    <>
      <button
        type="button"
        onClick={() => onView(fileId, title)}
        className="text-xs text-primary hover:underline inline-flex items-center gap-1"
      >
        <Eye className="size-3" aria-hidden /> View
      </button>
      <button
        type="button"
        onClick={handleDownload}
        className="text-xs text-primary hover:underline inline-flex items-center gap-1"
      >
        <Download className="size-3" aria-hidden /> Download
      </button>
      {onSendCopy && documentId && isSignedCopySendable ? (
        <button
          type="button"
          onClick={() => onSendCopy(documentId)}
          className="text-xs text-primary hover:underline inline-flex items-center gap-1"
        >
          <Mail className="size-3" aria-hidden /> Send to client
        </button>
      ) : null}
    </>
  );
}
