'use client';

import { useMemo, useState } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { Loader2, Plus, Trash2, Upload } from 'lucide-react';
import { toast } from 'sonner';

import { Button } from '@/components/ui/button';
import { DatePicker } from '@/components/ui/date-picker';
import { FileInputButton } from '@/components/ui/file-input-button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from '@/components/ui/select';
import { Textarea } from '@/components/ui/textarea';
import { apiFetch } from '@/lib/api/client';
import { toastError } from '@/lib/api/toast-error';
import { formatBerthRange } from '@/lib/templates/berth-range';

type SignatoryRole = 'client' | 'developer' | 'rep' | 'witness' | 'cc';

interface SignatoryRow {
  name: string;
  email: string;
  role: SignatoryRole;
}

const ROLE_LABELS: Record<SignatoryRole, string> = {
  client: 'Client',
  developer: 'Developer',
  rep: 'Rep',
  witness: 'Witness',
  cc: 'CC (no signing)',
};
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
} from '@/components/ui/dialog';

interface Props {
  open: boolean;
  onOpenChange: (next: boolean) => void;
  interestId: string;
  onSuccess?: () => void;
  /** When supplied, used as the initial signatory seed (typically derived
   *  from the active Documenso EOI's signers). Falls through to the
   *  client-only seed when omitted or empty. */
  prefillSignatories?: SignatoryRow[];
  /** Which signed document this upload is for. Defaults to 'eoi'; the
   *  reservation/contract tabs pass their own type so the file is filed +
   *  the deal advanced under the right kind (EOI-specific UI is hidden). */
  docType?: 'eoi' | 'reservation' | 'contract';
}

export function ExternalEoiUploadDialog({
  open,
  onOpenChange,
  interestId,
  onSuccess,
  prefillSignatories,
  docType = 'eoi',
}: Props) {
  const DOC_LABEL =
    docType === 'reservation'
      ? 'reservation agreement'
      : docType === 'contract'
        ? 'contract'
        : 'EOI';
  const isEoi = docType === 'eoi';
  const qc = useQueryClient();
  const [file, setFile] = useState<File | null>(null);
  const [title, setTitle] = useState('');
  const [signedAt, setSignedAt] = useState(() => new Date().toISOString().slice(0, 10));
  // `null` means "rep hasn't touched the list yet - show the
  // derived-from-interest seed". Once edited (add/remove/change),
  // the explicit array takes over. Avoids a setState-in-effect that
  // the React Compiler bans.
  const [signatoriesOverride, setSignatoriesOverride] = useState<SignatoryRow[] | null>(null);
  const [notes, setNotes] = useState('');
  // When a non-terminal generated EOI exists on the interest, this drives
  // the warning banner + the replace-vs-keep choice. 'replace' (default)
  // voids the generated envelope on Documenso + flips the local row to
  // cancelled before the new upload lands; 'keep' leaves the prior doc
  // alone (legacy behaviour). Hidden behind an Advanced disclosure since
  // the replace path is the right default for the vast majority of cases.
  const [replaceMode, setReplaceMode] = useState<'replace' | 'keep'>('replace');

  // Fetched on open to power the default title:
  // "External EOI - <Client> - <berth range> - YYYY-MM-DD". Without
  // this the file lands as just "External EOI - <date>" which is
  // unscannable in any list when a port has multiple deals closing on
  // the same day. Also drives auto-fill on signatory rows tagged
  // role=client.
  // Cache shape MUST match the parent <InterestDetail> consumer of the same
  // ['interests', interestId] key, which unwraps the envelope and caches the
  // inner row. Caching the wrapped { data: ... } here would clobber the
  // parent's cache and blank the detail page (the rep would see "Unknown
  // Client" + an empty tab body the moment this dialog opened — UAT
  // 2026-05-24).
  const { data: interestData } = useQuery<{
    clientName: string | null;
    clientPrimaryEmail: string | null;
  }>({
    queryKey: ['interests', interestId],
    queryFn: () =>
      apiFetch<{ data: { clientName: string | null; clientPrimaryEmail: string | null } }>(
        `/api/v1/interests/${interestId}`,
      ).then((r) => r.data),
    enabled: open,
    staleTime: 60_000,
  });

  // Compute the effective signatory list - when the rep hasn't touched
  // anything, seed from the interest's client. Once they edit, the
  // explicit override takes over.
  const signatories: SignatoryRow[] = useMemo(() => {
    if (signatoriesOverride !== null) return signatoriesOverride;
    if (prefillSignatories && prefillSignatories.length > 0) return prefillSignatories;
    if (!interestData) return [];
    return [
      {
        name: interestData.clientName ?? '',
        email: interestData.clientPrimaryEmail ?? '',
        role: 'client' as const,
      },
    ];
  }, [signatoriesOverride, prefillSignatories, interestData]);
  const { data: berthsData } = useQuery<{
    data: Array<{
      berthId: string;
      mooringNumber: string | null;
      isInEoiBundle: boolean;
      isSpecificInterest: boolean;
    }>;
  }>({
    queryKey: ['interests', interestId, 'berths'],
    queryFn: () =>
      apiFetch<{
        data: Array<{
          berthId: string;
          mooringNumber: string | null;
          isInEoiBundle: boolean;
          isSpecificInterest: boolean;
        }>;
      }>(`/api/v1/interests/${interestId}/berths`),
    enabled: open,
    staleTime: 60_000,
  });

  // Inheritance-driven default for the public-map flip: if ANY in-bundle
  // berth already has isSpecificInterest=true, default the checkbox ON
  // so an upstream EOI flow's "make this deal visible publicly" decision
  // is honoured automatically through the upload path. Berths not in
  // the EOI bundle aren't considered (they're not part of this deal's
  // signature scope and so shouldn't influence the public-map default).
  const inBundleBerths = useMemo(
    () => (berthsData?.data ?? []).filter((b) => b.isInEoiBundle),
    [berthsData],
  );
  const inheritsPublicFlag = useMemo(
    () => inBundleBerths.some((b) => b.isSpecificInterest),
    [inBundleBerths],
  );
  const [publicFlagOverride, setPublicFlagOverride] = useState<boolean | null>(null);
  const publicFlagChecked = publicFlagOverride ?? inheritsPublicFlag;

  // Detect a generated EOI in flight on this interest so the dialog can
  // offer "Replace the generated envelope" instead of leaving two parallel
  // EOIs on the deal. Only documents in non-terminal status count — already-
  // cancelled / completed / rejected EOIs need no reconciliation.
  const { data: docsData } = useQuery<{
    data: Array<{ id: string; status: string; title: string; createdAt: string }>;
  }>({
    queryKey: ['documents', { interestId, documentType: 'eoi' }],
    queryFn: () =>
      apiFetch<{
        data: Array<{ id: string; status: string; title: string; createdAt: string }>;
      }>(`/api/v1/documents?interestId=${interestId}&documentType=eoi`),
    enabled: open && isEoi,
    staleTime: 30_000,
  });
  const activeEoi = useMemo(
    () =>
      (docsData?.data ?? []).find(
        (d) => d.status === 'sent' || d.status === 'partially_signed' || d.status === 'draft',
      ) ?? null,
    [docsData],
  );

  const defaultTitle = useMemo(() => {
    const date = signedAt || new Date().toISOString().slice(0, 10);
    const moorings = (berthsData?.data ?? [])
      .map((b) => b.mooringNumber)
      .filter((m): m is string => !!m);
    const berthLabel = moorings.length > 0 ? formatBerthRange(moorings) : null;
    const clientName = interestData?.clientName ?? null;
    const parts = [`External ${DOC_LABEL}`];
    if (clientName) parts.push(clientName);
    if (berthLabel) parts.push(berthLabel);
    parts.push(date);
    return parts.join(' - ');
  }, [interestData, berthsData, signedAt, DOC_LABEL]);

  // The title input is controlled with `displayTitle` (derived from
  // either the rep's typed value or the auto-derived default). Reps
  // were treating the empty field as "this needs me to type something"
  // even though the placeholder showed a sensible default - now the
  // default is visible inside the input itself. Typing replaces the
  // default; clearing the field re-shows it.
  const displayTitle = title || defaultTitle;

  const mutation = useMutation<{ data?: { stageChanged?: boolean } }, Error, void>({
    mutationFn: async () => {
      if (!file) throw new Error('No file selected');
      const form = new FormData();
      form.append('file', file);
      const effectiveTitle = title.trim() || defaultTitle;
      if (effectiveTitle) form.append('title', effectiveTitle);
      if (signedAt) form.append('signedAt', signedAt);
      const cleanSignatories = signatories
        .map((s) => ({ name: s.name.trim(), email: s.email.trim(), role: s.role }))
        .filter((s) => s.name && s.email);
      if (cleanSignatories.length > 0) {
        form.append('signatories', JSON.stringify(cleanSignatories));
        // Back-compat for any consumer that still reads signerNames.
        form.append('signerNames', cleanSignatories.map((s) => s.name).join(', '));
      }
      if (notes) form.append('notes', notes);
      // When a generated EOI is active AND the rep didn't opt out via the
      // Advanced toggle, tell the server to cancel it as part of this
      // upload so the deal carries one canonical EOI.
      form.append('docType', docType);
      if (isEoi && activeEoi && replaceMode === 'replace') {
        form.append('cancelActiveDocumentId', activeEoi.id);
      }
      const res = await fetch(`/api/v1/interests/${interestId}/external-eoi`, {
        method: 'POST',
        body: form,
        credentials: 'include',
      });
      if (!res.ok) {
        const err = (await res.json().catch(() => ({ error: 'Upload failed' }))) as {
          error?: string;
        };
        throw new Error(err.error ?? 'Upload failed');
      }
      return (await res.json()) as { data?: { stageChanged?: boolean } };
    },
    onSuccess: async (response) => {
      const stageChanged = response?.data?.stageChanged === true;
      // Public-map flag reconciliation. After a successful upload, sync
      // each in-bundle berth's isSpecificInterest to the checkbox state.
      // Fires only the PATCHes that change state - berths already in
      // sync are skipped. Failures here don't undo the upload (the doc
      // is already filed) but surface as a non-blocking toast so the
      // rep knows the flag didn't propagate.
      if (isEoi && inBundleBerths.length > 0) {
        const targets = inBundleBerths.filter((b) => b.isSpecificInterest !== publicFlagChecked);
        if (targets.length > 0) {
          try {
            await Promise.all(
              targets.map((b) =>
                apiFetch(`/api/v1/interests/${interestId}/berths/${b.berthId}`, {
                  method: 'PATCH',
                  body: { isSpecificInterest: publicFlagChecked },
                }),
              ),
            );
            qc.invalidateQueries({ queryKey: ['berth', undefined, 'active-interests'] });
          } catch {
            toast.error('Upload succeeded, but the public-map flag could not be updated.');
          }
        }
      }
      toast.success(
        stageChanged
          ? `Signed ${DOC_LABEL} uploaded. Pipeline stage advanced.`
          : `Signed ${DOC_LABEL} uploaded. Filed against this deal (stage unchanged).`,
      );
      qc.invalidateQueries({ queryKey: ['interests', interestId] });
      qc.invalidateQueries({ queryKey: ['interests'] });
      qc.invalidateQueries({ queryKey: ['documents'] });
      setFile(null);
      setTitle('');
      setSignatoriesOverride(null);
      setNotes('');
      setPublicFlagOverride(null);
      onOpenChange(false);
      onSuccess?.();
    },
    onError: (err: unknown) => {
      toastError(err, 'Upload failed');
    },
  });

  return (
    <Dialog open={open} onOpenChange={onOpenChange}>
      <DialogContent className="sm:max-w-2xl lg:max-w-4xl">
        <DialogHeader>
          <DialogTitle>Upload externally-signed {DOC_LABEL}</DialogTitle>
          <DialogDescription>
            For a {DOC_LABEL} signed outside our signing service (paper, in person, alternate e-sign
            vendor). The uploaded PDF is filed against this interest and the pipeline stage is
            advanced accordingly.
          </DialogDescription>
        </DialogHeader>

        <div className="space-y-3 py-2">
          {isEoi && activeEoi ? (
            <div className="rounded-md border border-amber-300 bg-amber-50 p-3 text-sm dark:border-amber-900 dark:bg-amber-950/40">
              <p className="font-medium text-amber-900 dark:text-amber-100">
                A generated EOI is already in flight on this deal.
              </p>
              <p className="mt-1 text-amber-800 dark:text-amber-200">
                {activeEoi.title} ({activeEoi.status.replace(/_/g, ' ')}, created{' '}
                {new Date(activeEoi.createdAt).toLocaleDateString()}). Uploading a signed copy can
                cancel the generated envelope so this deal carries one canonical EOI.
              </p>
              <div
                role="radiogroup"
                aria-label="What to do with the generated EOI"
                className="mt-2 space-y-1"
              >
                <label className="flex cursor-pointer items-start gap-2">
                  <input
                    type="radio"
                    name="replace-mode"
                    value="replace"
                    checked={replaceMode === 'replace'}
                    onChange={() => setReplaceMode('replace')}
                    className="mt-1 h-4 w-4 cursor-pointer accent-amber-700"
                  />
                  <span className="text-amber-900 dark:text-amber-100">
                    Cancel the generated envelope and replace it with this upload (recommended).
                  </span>
                </label>
                <label className="flex cursor-pointer items-start gap-2">
                  <input
                    type="radio"
                    name="replace-mode"
                    value="keep"
                    checked={replaceMode === 'keep'}
                    onChange={() => setReplaceMode('keep')}
                    className="mt-1 h-4 w-4 cursor-pointer accent-amber-700"
                  />
                  <span className="text-amber-900 dark:text-amber-100">
                    Keep both records (advanced - leaves two EOIs on the deal).
                  </span>
                </label>
              </div>
            </div>
          ) : null}
          <div>
            <Label>PDF file *</Label>
            <div className="mt-1">
              <FileInputButton
                accept="application/pdf,.pdf"
                label={file ? file.name : 'Choose PDF'}
                onFilesPicked={(files) => setFile(files[0] ?? null)}
              />
            </div>
          </div>
          <div>
            <Label>Title (optional)</Label>
            <Input
              value={displayTitle}
              onChange={(e) => setTitle(e.target.value)}
              placeholder={defaultTitle}
              className="mt-1"
            />
            <p className="text-xs text-muted-foreground mt-1">
              Edit the auto-filled title or clear it to restore the default.
            </p>
          </div>
          <div>
            <Label>Date signed</Label>
            <div className="mt-1">
              <DatePicker value={signedAt} onChange={setSignedAt} toDate={new Date()} />
            </div>
          </div>
          <div className="space-y-2">
            <Label>Signatories</Label>
            <div className="space-y-2">
              {signatories.map((s, i) => (
                <div key={i} className="flex gap-2 items-center">
                  <Input
                    placeholder="Name"
                    value={s.name}
                    onChange={(e) =>
                      setSignatoriesOverride(
                        signatories.map((row, idx) =>
                          idx === i ? { ...row, name: e.target.value } : row,
                        ),
                      )
                    }
                    className="flex-1 min-w-0"
                  />
                  <Input
                    type="email"
                    placeholder="email@example.com"
                    value={s.email}
                    onChange={(e) =>
                      setSignatoriesOverride(
                        signatories.map((row, idx) =>
                          idx === i ? { ...row, email: e.target.value } : row,
                        ),
                      )
                    }
                    className="flex-[2] min-w-0"
                  />
                  <Select
                    value={s.role}
                    onValueChange={(v) =>
                      setSignatoriesOverride(
                        signatories.map((row, idx) =>
                          idx === i ? { ...row, role: v as SignatoryRole } : row,
                        ),
                      )
                    }
                  >
                    <SelectTrigger className="w-36 shrink-0">
                      <SelectValue />
                    </SelectTrigger>
                    <SelectContent>
                      {(Object.keys(ROLE_LABELS) as SignatoryRole[]).map((r) => (
                        <SelectItem key={r} value={r}>
                          {ROLE_LABELS[r]}
                        </SelectItem>
                      ))}
                    </SelectContent>
                  </Select>
                  <Button
                    type="button"
                    variant="ghost"
                    size="icon"
                    onClick={() =>
                      setSignatoriesOverride(signatories.filter((_, idx) => idx !== i))
                    }
                    aria-label="Remove signatory"
                    className="shrink-0"
                  >
                    <Trash2 className="size-4" aria-hidden />
                  </Button>
                </div>
              ))}
            </div>
            <Button
              type="button"
              variant="outline"
              size="sm"
              onClick={() =>
                setSignatoriesOverride([...signatories, { name: '', email: '', role: 'client' }])
              }
            >
              <Plus className="size-4 mr-1.5" aria-hidden />
              Add signatory
            </Button>
            <p className="text-xs text-muted-foreground">
              Recorded against the document for the audit trail and the signed-count badge. CC
              recipients aren&apos;t counted as signatories.
            </p>
          </div>
          <div>
            <Label>Notes (optional)</Label>
            <Textarea
              value={notes}
              onChange={(e) => setNotes(e.target.value)}
              placeholder="Where / how this EOI was signed"
              rows={2}
              className="mt-1"
            />
          </div>
          {isEoi && inBundleBerths.length > 0 ? (
            <div className="rounded-md border bg-muted/40 p-3 text-sm">
              <label className="flex cursor-pointer items-start gap-2">
                <input
                  type="checkbox"
                  checked={publicFlagChecked}
                  onChange={(e) => setPublicFlagOverride(e.target.checked)}
                  className="mt-1 h-4 w-4 cursor-pointer"
                />
                <span>
                  <span className="font-medium">
                    Mark berth{inBundleBerths.length > 1 ? 's' : ''} as Under Offer on the public
                    map
                  </span>
                  <span className="block text-xs text-muted-foreground">
                    {inheritsPublicFlag
                      ? 'Default ON because at least one in-bundle berth is already flagged on the public map.'
                      : 'Default OFF - turn on to make the berth(s) visible as Under Offer on the public site.'}
                  </span>
                </span>
              </label>
            </div>
          ) : null}
        </div>

        <DialogFooter>
          <Button
            variant="outline"
            onClick={() => onOpenChange(false)}
            disabled={mutation.isPending}
          >
            Cancel
          </Button>
          <Button onClick={() => mutation.mutate()} disabled={!file || mutation.isPending}>
            {mutation.isPending ? (
              <Loader2 className="h-3.5 w-3.5 animate-spin mr-1.5" aria-hidden />
            ) : (
              <Upload className="h-3.5 w-3.5 mr-1.5" aria-hidden />
            )}
            Upload
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
}
