'use client';

import { apiFetch } from '@/lib/api/client';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useEffect, useState } from 'react';
import { toast } from 'sonner';
import { Check, Clock, X, Mail, Eye, Bell, Send, Link2 } from 'lucide-react';

import { toastError } from '@/lib/api/toast-error';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';

interface Signer {
  id: string;
  signerName: string;
  signerEmail: string;
  signerRole: string;
  signingOrder: number;
  status: string;
  signedAt?: string | null;
  invitedAt?: string | null;
  openedAt?: string | null;
  lastReminderSentAt?: string | null;
  /** Documenso-issued URL the recipient hits to sign. Available as soon
   *  as the doc has been created+sent - independent of whether the
   *  invitation email has actually been dispatched, so reps can copy it
   *  for manual delivery / QA before triggering the auto-send. */
  signingUrl?: string | null;
}

interface SigningProgressProps {
  documentId: string;
  signers: Signer[];
}

const ROLE_LABELS: Record<string, string> = {
  client: 'Client',
  signer: 'Signer',
  developer: 'Developer',
  approver: 'Approver',
  sales: 'Sales / Approver',
  cc: 'CC',
  viewer: 'Viewer',
  other: 'Other',
};

type Tone = 'pending' | 'opened' | 'signed' | 'declined';

const STATUS_META: Record<string, { label: string; tone: Tone; icon: typeof Check }> = {
  pending: { label: 'Pending', tone: 'pending', icon: Clock },
  signed: { label: 'Signed', tone: 'signed', icon: Check },
  declined: { label: 'Declined', tone: 'declined', icon: X },
};

// Card styling per status - colour-tinted background + left accent stripe.
// `opened` is a runtime-derived tone (pending status + openedAt set) so a
// signer who's actually looked at the doc reads visually distinct from one
// who hasn't yet - the rep can tell at a glance who's stalling vs who
// hasn't engaged at all.
const TONE_STYLES: Record<
  Tone,
  {
    card: string;
    accentBar: string;
    circle: string;
    statusChipBg: string;
    statusChipText: string;
    iconBubble: string;
  }
> = {
  pending: {
    card: 'bg-card hover:shadow-sm',
    accentBar: 'before:bg-amber-300/70',
    circle: 'bg-muted text-foreground/70 border-border',
    statusChipBg: 'bg-amber-50 border-amber-200',
    statusChipText: 'text-amber-800',
    iconBubble: 'bg-amber-100 text-amber-700 border-card',
  },
  opened: {
    card: 'bg-sky-50/40 hover:bg-sky-50/60',
    accentBar: 'before:bg-sky-400',
    circle: 'bg-sky-100 text-sky-800 border-sky-200',
    statusChipBg: 'bg-sky-50 border-sky-200',
    statusChipText: 'text-sky-800',
    iconBubble: 'bg-sky-500 text-white border-card',
  },
  signed: {
    card: 'bg-emerald-50/50 hover:bg-emerald-50/70',
    accentBar: 'before:bg-emerald-500',
    circle: 'bg-emerald-500 text-white border-emerald-500',
    statusChipBg: 'bg-emerald-100 border-emerald-300',
    statusChipText: 'text-emerald-800',
    iconBubble: 'bg-emerald-500 text-white border-card',
  },
  declined: {
    card: 'bg-rose-50/40 hover:bg-rose-50/60',
    accentBar: 'before:bg-rose-500',
    circle: 'bg-rose-500 text-white border-rose-500',
    statusChipBg: 'bg-rose-100 border-rose-300',
    statusChipText: 'text-rose-800',
    iconBubble: 'bg-rose-500 text-white border-card',
  },
};

/**
 * "X minutes/hours/days ago" using Intl.RelativeTimeFormat. Returns null
 * when the input is null/invalid so callers can skip rendering.
 */
function humanRelative(isoOrNull: string | null | undefined): string | null {
  if (!isoOrNull) return null;
  const t = new Date(isoOrNull).getTime();
  if (Number.isNaN(t)) return null;
  const diffMs = Date.now() - t;
  const seconds = Math.round(diffMs / 1000);
  const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });
  if (Math.abs(seconds) < 60) return rtf.format(-Math.round(seconds), 'second');
  const minutes = Math.round(seconds / 60);
  if (Math.abs(minutes) < 60) return rtf.format(-minutes, 'minute');
  const hours = Math.round(minutes / 60);
  if (Math.abs(hours) < 24) return rtf.format(-hours, 'hour');
  const days = Math.round(hours / 24);
  return rtf.format(-days, 'day');
}

/** Compact absolute timestamp for inline display next to relative time.
 *  Always renders date + time so a signer who signed weeks ago still
 *  reads as a real moment in the timeline (not just "Signed 12 days
 *  ago"). Year is omitted for the current calendar year to keep the
 *  string short; long-running EOIs that span year boundaries see the
 *  year so "Dec 3, 23:14" doesn't ambiguously mean last year or this. */
function compactAbsolute(isoOrNull: string | null | undefined): string | null {
  if (!isoOrNull) return null;
  const d = new Date(isoOrNull);
  if (Number.isNaN(d.getTime())) return null;
  const sameYear = d.getFullYear() === new Date().getFullYear();
  const dateOpts: Intl.DateTimeFormatOptions = sameYear
    ? { month: 'short', day: 'numeric' }
    : { year: 'numeric', month: 'short', day: 'numeric' };
  const date = d.toLocaleDateString(undefined, dateOpts);
  const time = d.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' });
  return `${date}, ${time}`;
}

/** Tick state every minute so relative-time strings ("Signed 3 min ago")
 *  re-render without a manual refresh. Returns a number that increments
 *  every 60s - components read it to invalidate memoization. */
function useMinuteTick(): number {
  const [tick, setTick] = useState(0);
  useEffect(() => {
    const id = setInterval(() => setTick((t) => t + 1), 60_000);
    return () => clearInterval(id);
  }, []);
  return tick;
}

/**
 * Initials shown in the avatar circle.
 *
 * Cleans the signer name before deriving initials:
 *   - Strips the `(was: <orig-email>)` suffix that `applyRecipientRedirect`
 *     bakes into Documenso recipients when EMAIL_REDIRECT_TO is on.
 *   - Strips Documenso template placeholder markers like `(placeholder)`.
 *
 * Then derives the bubble label:
 *   - Real CRM-source-of-truth name (e.g. "David Mizrahi") → "DM".
 *   - Single-word role placeholder ("Developer" / "Approver" / "Client")
 *     → first letter only ("D" / "A" / "C"). Reads as a typed role
 *     marker rather than a truncated name.
 *   - Empty string → "?".
 */
function getInitials(name: string): string {
  const clean = name
    .replace(/\s*\(was:[^)]*\)/i, '')
    .replace(/\s*\(placeholder\)/i, '')
    .replace(/\s*\(placeholder\b[^)]*\)/i, '')
    .trim();
  const parts = clean.split(/\s+/).filter(Boolean);
  if (parts.length === 0) return '?';
  if (parts.length === 1) {
    const word = parts[0]!;
    return word.slice(0, 1).toUpperCase();
  }
  return (parts[0]![0]! + parts[parts.length - 1]![0]!).toUpperCase();
}

/**
 * Cleaned signer display name (matches the initials derivation above).
 * The raw `signerName` may carry redirect/placeholder suffixes; this is
 * what the card surfaces as the headline. Exported so the document
 * detail page can apply the same scrub (#67).
 */
export function cleanSignerName(name: string): string {
  return name
    .replace(/\s*\(was:[^)]*\)/i, '')
    .replace(/\s*\(placeholder\)/i, '')
    .replace(/\s*\(placeholder\b[^)]*\)/i, '')
    .trim();
}

export function SigningProgress({ documentId, signers }: SigningProgressProps) {
  const queryClient = useQueryClient();
  // Force a re-render every 60s so the "X minutes ago" labels update
  // even when the user leaves the tab open without a webhook arriving.
  // Reading `tick` below is enough to wire the dependency.
  const tick = useMinuteTick();
  void tick;

  const sorted = [...signers].sort((a, b) => a.signingOrder - b.signingOrder);

  // Reminder = follow-up nudge to someone who's already been invited.
  // Documenso enforces per-signer rate-limiting (default once / 7 days)
  // so this only fires when the cooldown has elapsed.
  const remindMutation = useMutation({
    mutationFn: (signerId: string) =>
      apiFetch<{ data: { sent: boolean; reason?: string } }>(
        `/api/v1/documents/${documentId}/remind`,
        { method: 'POST', body: { signerId } },
      ),
    onSuccess: (res) => {
      if (res.data.sent) {
        toast.success('Reminder sent.');
      } else {
        toast.info(res.data.reason ?? 'Reminder skipped.');
      }
      queryClient.invalidateQueries({ queryKey: ['documents', documentId, 'signers'] });
    },
    onError: (err) => toastError(err, 'Failed to send reminder'),
  });

  // Initial invitation = the first branded email containing the signing
  // link. In `manual` send mode (per-port admin setting) the EOI is
  // generated without auto-sending, so this is the rep's first chance
  // to dispatch. In `auto` mode the initial email goes out at generate
  // time and the button is hidden because invitedAt is already stamped.
  const inviteMutation = useMutation({
    mutationFn: (signerId: string) =>
      apiFetch<{ data: { recipientId: string; sent: boolean } }>(
        `/api/v1/documents/${documentId}/send-invitation`,
        { method: 'POST', body: { recipientId: signerId } },
      ),
    onSuccess: () => {
      toast.success('Invitation sent.');
      queryClient.invalidateQueries({ queryKey: ['documents', documentId, 'signers'] });
    },
    onError: (err) => toastError(err, 'Failed to send invitation'),
  });

  return (
    <div className="space-y-2.5">
      {sorted.map((signer) => {
        const baseStatus = STATUS_META[signer.status] ?? STATUS_META.pending!;
        // Promote `pending + has been opened` to the `opened` tone so the
        // card reads visually distinct from "invited but never clicked".
        const tone: Tone =
          baseStatus.tone === 'pending' && signer.openedAt ? 'opened' : baseStatus.tone;
        const styles = TONE_STYLES[tone];
        const StatusIcon =
          tone === 'opened' ? Eye : tone === 'signed' ? Check : tone === 'declined' ? X : Clock;
        const statusLabel =
          tone === 'opened'
            ? 'Opened'
            : tone === 'signed'
              ? 'Signed'
              : tone === 'declined'
                ? 'Declined'
                : 'Pending';
        const invitedAgo = humanRelative(signer.invitedAt);
        const openedAgo = humanRelative(signer.openedAt);
        const remindedAgo = humanRelative(signer.lastReminderSentAt);

        return (
          <div key={signer.id} className="relative">
            <div
              className={cn(
                // Left accent stripe via a `::before` so the colour reads
                // immediately at the line of the card without competing
                // with the avatar circle.
                'relative flex items-start gap-3 rounded-lg border p-3 pl-4 transition-colors',
                'before:absolute before:left-0 before:top-2 before:bottom-2 before:w-1 before:rounded-r',
                styles.card,
                styles.accentBar,
              )}
            >
              {/* Avatar circle (initials) with status icon overlay so the
                  state reads from the avatar itself even before the
                  status pill is parsed. */}
              <div className="relative shrink-0">
                <div
                  className={cn(
                    'flex h-11 w-11 items-center justify-center rounded-full border-2 text-sm font-bold shadow-sm',
                    styles.circle,
                  )}
                >
                  {getInitials(signer.signerName)}
                </div>
                <div
                  className={cn(
                    'absolute -bottom-1 -right-1 flex h-5 w-5 items-center justify-center rounded-full border-2 shadow-sm',
                    styles.iconBubble,
                  )}
                >
                  <StatusIcon className="size-3" aria-hidden />
                </div>
              </div>

              {/* Name + role + email + status pill + activity */}
              <div className="min-w-0 flex-1 space-y-1">
                <div className="flex flex-wrap items-center gap-x-2 gap-y-1">
                  <span className="text-sm font-medium text-foreground">
                    {cleanSignerName(signer.signerName) || signer.signerEmail}
                  </span>
                  <span
                    className={cn(
                      'inline-flex items-center gap-1 rounded-full border px-1.5 py-0.5 text-xs font-semibold uppercase tracking-wide',
                      styles.statusChipBg,
                      styles.statusChipText,
                    )}
                  >
                    <StatusIcon className="size-2.5" aria-hidden />
                    {statusLabel}
                  </span>
                  <span className="text-xs text-muted-foreground">
                    · {ROLE_LABELS[signer.signerRole] ?? signer.signerRole}
                    {' · '}
                    <span className="font-medium">#{signer.signingOrder}</span>
                  </span>
                </div>
                <p className="truncate text-xs text-muted-foreground">{signer.signerEmail}</p>

                {/* Activity timeline - explicit "Not yet invited" state so
                    reps in manual-send mode know an action is required.
                    Once invited, each event surfaces with a precise
                    timestamp tooltip (the relative-time is the headline). */}
                <div className="flex flex-wrap items-center gap-x-3 gap-y-0.5 pt-0.5 text-xs text-muted-foreground">
                  {!signer.invitedAt && signer.status === 'pending' ? (
                    <span className="inline-flex items-center gap-1 italic text-amber-700">
                      <Mail className="size-3" aria-hidden />
                      Not yet invited
                    </span>
                  ) : null}
                  {invitedAgo && (
                    <span
                      className="inline-flex items-center gap-1"
                      title={signer.invitedAt ? new Date(signer.invitedAt).toLocaleString() : ''}
                    >
                      <Mail className="size-3" aria-hidden />
                      Invited {invitedAgo}
                    </span>
                  )}
                  {openedAgo && (
                    <span
                      className="inline-flex items-center gap-1"
                      title={signer.openedAt ? new Date(signer.openedAt).toLocaleString() : ''}
                    >
                      <Eye className="size-3" aria-hidden />
                      Opened {openedAgo}
                    </span>
                  )}
                  {remindedAgo && (
                    <span
                      className="inline-flex items-center gap-1"
                      title={
                        signer.lastReminderSentAt
                          ? new Date(signer.lastReminderSentAt).toLocaleString()
                          : ''
                      }
                    >
                      <Bell className="size-3" aria-hidden />
                      Reminded {remindedAgo}
                    </span>
                  )}
                  {signer.signedAt && (
                    <span
                      className="inline-flex items-center gap-1 font-medium text-emerald-700"
                      title={new Date(signer.signedAt).toLocaleString()}
                    >
                      <Check className="size-3" aria-hidden />
                      Signed {humanRelative(signer.signedAt)}
                      <span className="font-normal text-emerald-700/70">
                        · {compactAbsolute(signer.signedAt)}
                      </span>
                    </span>
                  )}
                </div>
              </div>

              {/* Per-signer actions. Order: Copy link (when available)
                  then the primary action button.
                  • Copy: surfaces the Documenso signing URL for QA /
                    manual delivery. Available the moment Documenso has
                    issued the URL - independent of whether the
                    invitation email has gone out - so reps can preview
                    the page before triggering auto-send.
                  • `invitedAt === null` → "Send invitation" (rep
                    dispatches the first email; fires branded invite +
                    stamps invitedAt).
                  • `invitedAt !== null` → "Send reminder"
                    (Documenso-side nudge, rate-limited per cooldown).
                  • Signed/declined → no action buttons. */}
              {signer.status === 'pending' ? (
                <Button
                  variant="outline"
                  size="sm"
                  className="h-7 shrink-0 gap-1.5 px-2.5 text-xs [&_svg]:size-3"
                  disabled={!signer.signingUrl}
                  onClick={async () => {
                    if (!signer.signingUrl) return;
                    try {
                      await navigator.clipboard.writeText(signer.signingUrl);
                      toast.success(`Signing link for ${signer.signerName} copied`);
                    } catch {
                      toast.error('Could not copy to clipboard');
                    }
                  }}
                  title={
                    signer.signingUrl
                      ? "Copy this signer's signing link to your clipboard so you can share it directly (Slack, WhatsApp, in person) without going through email."
                      : 'Signing URL is not available yet — Documenso issues it once the document has been sent.'
                  }
                >
                  <Link2 />
                  Copy link
                </Button>
              ) : null}
              {signer.status === 'pending' &&
                (signer.invitedAt ? (
                  <Button
                    variant="outline"
                    size="sm"
                    className="h-7 shrink-0 gap-1.5 px-2.5 text-xs [&_svg]:size-3"
                    disabled={remindMutation.isPending}
                    onClick={() => remindMutation.mutate(signer.id)}
                    title="Send a follow-up reminder. Rate-limited by Documenso."
                  >
                    <Bell />
                    {remindMutation.isPending && remindMutation.variables === signer.id
                      ? 'Sending…'
                      : 'Send reminder'}
                  </Button>
                ) : (
                  <Button
                    variant="default"
                    size="sm"
                    className="h-7 shrink-0 gap-1.5 px-2.5 text-xs [&_svg]:size-3"
                    disabled={inviteMutation.isPending}
                    onClick={() => inviteMutation.mutate(signer.id)}
                    title="Send the initial signing invitation to this recipient."
                  >
                    <Send />
                    {inviteMutation.isPending && inviteMutation.variables === signer.id
                      ? 'Sending…'
                      : 'Send invitation'}
                  </Button>
                ))}
            </div>
          </div>
        );
      })}
    </div>
  );
}
