import { useState, useEffect, useRef } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { X, Loader2, Send, Paperclip } from 'lucide-react';
import { toast } from 'sonner';
import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog';
import { useClient } from 'hooks/useClient';
import { useGlobal } from 'hooks/useGlobal';

export type ComposeDraft = { to?: string; cc?: string; subject?: string; body?: string; inReplyTo?: string };

// Shared open/close handle. Set a draft to open the composer; null closes it.
export const useComposer = () => useGlobal<ComposeDraft | null>('EMAIL_COMPOSE', null);

type Contact = { address: string; name: string };

// A recipient field with contact autocomplete on the last comma-separated segment.
const RecipientInput = ({
  value,
  onChange,
  placeholder,
  autoFocus,
}: {
  value: string;
  onChange: (v: string) => void;
  placeholder: string;
  autoFocus?: boolean;
}) => {
  const client = useClient();
  const [suggestions, setSuggestions] = useState<Contact[]>([]);
  const [open, setOpen] = useState(false);
  const seg = value.split(',').pop()?.trim() ?? '';

  useEffect(() => {
    if (seg.length < 1) {
      setSuggestions([]);
      return;
    }
    const t = setTimeout(() => {
      client
        .get<Contact[]>(`/email/contacts?q=${encodeURIComponent(seg)}`)
        .then(setSuggestions)
        .catch(() => {});
    }, 180);
    return () => clearTimeout(t);
  }, [seg]);

  const pick = (address: string) => {
    const segs = value.split(',');
    segs[segs.length - 1] = ` ${address}`;
    onChange(segs.join(',').replace(/^\s+/, '') + ', ');
    setOpen(false);
  };

  return (
    <div className="relative">
      <input
        autoFocus={autoFocus}
        value={value}
        onChange={(ev) => {
          onChange(ev.target.value);
          setOpen(true);
        }}
        onFocus={() => setOpen(true)}
        onBlur={() => setTimeout(() => setOpen(false), 150)}
        placeholder={placeholder}
        className="w-full bg-transparent px-3 py-2 text-sm outline-none placeholder:opacity-40"
      />
      {open && suggestions.length > 0 && (
        <div className="absolute left-0 right-0 top-full z-10 mt-0.5 max-h-56 overflow-y-auto rounded-md border bg-background shadow-lg">
          {suggestions.map((s) => (
            <button
              key={s.address}
              onMouseDown={(ev) => {
                ev.preventDefault();
                pick(s.address);
              }}
              className="flex w-full flex-col items-start px-3 py-1.5 text-left text-sm hover:bg-accent cursor-pointer"
            >
              {s.name && <span className="font-medium">{s.name}</span>}
              <span className={s.name ? 'text-xs opacity-60' : ''}>{s.address}</span>
            </button>
          ))}
        </div>
      )}
    </div>
  );
};

const escapeHtml = (s: string) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');

export const ComposeModal = () => {
  const client = useClient();
  const queryClient = useQueryClient();
  const [draft, setDraft] = useComposer();
  const [to, setTo] = useState('');
  const [cc, setCc] = useState('');
  const [showCc, setShowCc] = useState(false);
  const [bcc, setBcc] = useState('');
  const [showBcc, setShowBcc] = useState(false);
  const [subject, setSubject] = useState('');
  const [files, setFiles] = useState<File[]>([]);
  const [dragging, setDragging] = useState(false);
  const [bodyEmpty, setBodyEmpty] = useState(true);
  const [sending, setSending] = useState(false);
  const seeded = useRef<ComposeDraft | null>(null);
  const fileInput = useRef<HTMLInputElement | null>(null);
  const editorRef = useRef<HTMLDivElement | null>(null);
  const inlineMap = useRef(new Map<string, File>()); // data-inline-id → File, for the images embedded in the body
  const nextImgId = useRef(0);

  const refreshEmpty = () => {
    const el = editorRef.current;
    setBodyEmpty(!el || (!el.textContent?.trim() && !el.querySelector('img')));
  };

  // Free object URLs for any images still in the editor, and clear the id→File map.
  const revokeInline = () => {
    editorRef.current?.querySelectorAll('img[data-inline-id]').forEach((img) => {
      const src = (img as HTMLImageElement).src;
      if (src.startsWith('blob:')) URL.revokeObjectURL(src);
    });
    inlineMap.current.clear();
  };

  useEffect(() => {
    if (draft && draft !== seeded.current) {
      seeded.current = draft;
      setTo(draft.to ?? '');
      setCc(draft.cc ?? '');
      setShowCc(!!draft.cc);
      setBcc('');
      setShowBcc(false);
      setSubject(draft.subject ?? '');
      setFiles([]);
      inlineMap.current.clear();
      nextImgId.current = 0;
      // Seed the contenteditable body directly (uncontrolled — React never re-renders its content).
      if (editorRef.current)
        editorRef.current.innerHTML = draft.body ? escapeHtml(draft.body).replace(/\n/g, '<br>') : '';
      refreshEmpty();
    }
    if (!draft) seeded.current = null;
  }, [draft]);

  // Clipboard images often come nameless — give them a sensible filename.
  const named = (f: File) =>
    f.name ? f : new File([f], `pasted-${Date.now()}.${f.type.split('/')[1] || 'png'}`, { type: f.type });

  // Attach button (and non-image paste/drop): everything goes as a regular attachment.
  const addAttachments = (incoming: FileList | File[]) => {
    const list = Array.from(incoming).map(named);
    if (list.length) setFiles((prev) => [...prev, ...list]);
  };

  // Insert an image into the body at the given range (or the current caret, or the end).
  const insertImage = (file: File, at?: Range | null) => {
    const el = editorRef.current;
    if (!el) return;
    const id = `img-${nextImgId.current++}`;
    inlineMap.current.set(id, file);
    const img = document.createElement('img');
    img.src = URL.createObjectURL(file);
    img.setAttribute('data-inline-id', id);
    img.style.maxWidth = '100%';
    img.style.height = 'auto';

    const sel = window.getSelection();
    const caret = at ?? (sel && sel.rangeCount && el.contains(sel.anchorNode) ? sel.getRangeAt(0) : null);
    if (caret && el.contains(caret.startContainer)) {
      caret.deleteContents();
      caret.insertNode(img);
      caret.setStartAfter(img);
      caret.collapse(true);
      sel?.removeAllRanges();
      sel?.addRange(caret);
    } else {
      el.appendChild(img);
    }
    refreshEmpty();
  };

  // Paste inside the body: images insert at the caret, other files attach, text pastes as plain text.
  const onEditorPaste = (ev: React.ClipboardEvent) => {
    const fileItems = Array.from(ev.clipboardData?.items ?? []).filter((it) => it.kind === 'file');
    if (fileItems.length) {
      ev.preventDefault();
      fileItems
        .map((it) => it.getAsFile())
        .filter((f): f is File => !!f)
        .map(named)
        .forEach((f) => (f.type.startsWith('image/') ? insertImage(f) : addAttachments([f])));
      return;
    }
    const text = ev.clipboardData?.getData('text/plain');
    if (text) {
      ev.preventDefault();
      document.execCommand('insertText', false, text);
    }
  };

  const onDrop = (ev: React.DragEvent) => {
    ev.preventDefault();
    setDragging(false);
    const dropped = Array.from(ev.dataTransfer?.files ?? []).map(named);
    if (!dropped.length) return;
    // Place images at the drop point if it lands inside the body, otherwise append at the end.
    const el = editorRef.current;
    const point = document.caretRangeFromPoint?.(ev.clientX, ev.clientY) ?? null;
    const range = point && el?.contains(point.startContainer) ? point : null;
    dropped.forEach((f) => (f.type.startsWith('image/') ? insertImage(f, range) : addAttachments([f])));
  };

  const close = () => {
    revokeInline();
    setDraft(null);
  };

  const send = async () => {
    if (!to.trim()) {
      toast.error('Add at least one recipient');
      return;
    }
    setSending(true);
    try {
      const fd = new FormData();
      fd.append('to', to);
      if (cc.trim()) fd.append('cc', cc);
      if (bcc.trim()) fd.append('bcc', bcc);
      fd.append('subject', subject);
      if (draft?.inReplyTo) fd.append('inReplyTo', draft.inReplyTo);
      files.forEach((f) => fd.append('files', f));

      // Serialize the body. Clone it, rewrite each inline <img> to a `cid:inline-N` ref (backend
      // matches by order), and collect those files. Plain text goes as the fallback part.
      const el = editorRef.current;
      const inlineFiles: File[] = [];
      if (el) {
        const clone = el.cloneNode(true) as HTMLElement;
        clone.querySelectorAll('img[data-inline-id]').forEach((node) => {
          const img = node as HTMLImageElement;
          const file = inlineMap.current.get(img.getAttribute('data-inline-id') ?? '');
          if (!file) return img.remove();
          img.setAttribute('src', `cid:inline-${inlineFiles.length}`);
          img.removeAttribute('data-inline-id');
          inlineFiles.push(file);
        });
        fd.append('body', el.innerText);
        if (inlineFiles.length) {
          fd.append('html', clone.innerHTML);
          inlineFiles.forEach((f) => fd.append('inline', f));
        }
      }

      await client.post('/email/send', fd);
      toast.success('Email sent');
      // Gmail files the sent copy in Sent, so refresh the list to surface it.
      queryClient.invalidateQueries({ queryKey: ['email-messages'] });
      close();
    } catch (err) {
      toast.error(err instanceof Error ? err.message : 'Failed to send');
    } finally {
      setSending(false);
    }
  };

  const fmtSize = (n: number) =>
    n < 1024 ? `${n} B` : n < 1024 * 1024 ? `${(n / 1024).toFixed(0)} KB` : `${(n / 1024 / 1024).toFixed(1)} MB`;

  if (!draft) return null;

  return (
    <Dialog open onOpenChange={(o) => !o && close()}>
      <DialogContent
        className="flex h-[70vh] max-w-2xl flex-col gap-0 overflow-hidden p-0"
        onDrop={onDrop}
        onDragOver={(ev) => {
          ev.preventDefault();
          setDragging(true);
        }}
        onDragLeave={(ev) => {
          if (ev.currentTarget === ev.target) setDragging(false);
        }}
      >
        {dragging && (
          <div className="pointer-events-none absolute inset-0 z-20 flex items-center justify-center rounded-md border-2 border-dashed border-duck-teal bg-background/80 text-sm font-medium">
            Drop images to inline · other files attach
          </div>
        )}
        <div className="flex items-center border-b px-4 py-2.5 pr-10">
          <DialogTitle className="text-sm font-semibold">{/^re:/i.test(subject) ? 'Reply' : 'New message'}</DialogTitle>
        </div>

        <div className="flex items-center border-b">
          <span className="pl-4 text-xs opacity-50">To</span>
          <div className="flex-1">
            <RecipientInput value={to} onChange={setTo} placeholder="Recipients" autoFocus />
          </div>
          <div className="flex items-center gap-2 px-3">
            {!showCc && (
              <button onClick={() => setShowCc(true)} className="text-xs opacity-50 hover:opacity-100 cursor-pointer">
                Cc
              </button>
            )}
            {!showBcc && (
              <button onClick={() => setShowBcc(true)} className="text-xs opacity-50 hover:opacity-100 cursor-pointer">
                Bcc
              </button>
            )}
          </div>
        </div>
        {showCc && (
          <div className="flex items-center border-b">
            <span className="pl-4 text-xs opacity-50">Cc</span>
            <div className="flex-1">
              <RecipientInput value={cc} onChange={setCc} placeholder="Cc recipients" />
            </div>
          </div>
        )}
        {showBcc && (
          <div className="flex items-center border-b">
            <span className="pl-4 text-xs opacity-50">Bcc</span>
            <div className="flex-1">
              <RecipientInput value={bcc} onChange={setBcc} placeholder="Bcc recipients" />
            </div>
          </div>
        )}
        <input
          value={subject}
          onChange={(ev) => setSubject(ev.target.value)}
          placeholder="Subject"
          className="border-b bg-transparent px-4 py-2 text-sm outline-none placeholder:opacity-40"
        />
        <div className="relative flex-1 overflow-hidden">
          {bodyEmpty && (
            <div className="pointer-events-none absolute left-4 top-3 text-sm opacity-40">Write your message…</div>
          )}
          <div
            ref={editorRef}
            contentEditable
            onInput={refreshEmpty}
            onPaste={onEditorPaste}
            className="h-full overflow-y-auto px-4 py-3 text-sm outline-none [&_img]:my-1 [&_img]:rounded"
          />
        </div>

        {files.length > 0 && (
          <div className="flex flex-wrap gap-1.5 border-t px-4 py-2">
            {files.map((f, i) => (
              <span key={`${f.name}-${i}`} className="flex items-center gap-1.5 rounded bg-accent px-2 py-1 text-xs">
                <Paperclip className="h-3 w-3 opacity-60" />
                <span className="max-w-[16rem] truncate">{f.name}</span>
                <span className="opacity-50">{fmtSize(f.size)}</span>
                <button
                  onClick={() => setFiles((prev) => prev.filter((_, idx) => idx !== i))}
                  className="opacity-50 hover:opacity-100 cursor-pointer"
                  title="Remove"
                >
                  <X className="h-3 w-3" />
                </button>
              </span>
            ))}
          </div>
        )}

        <div className="flex items-center justify-end gap-2 border-t px-4 py-2.5">
          <input
            ref={fileInput}
            type="file"
            multiple
            className="hidden"
            onChange={(ev) => {
              if (ev.target.files?.length) addAttachments(ev.target.files);
              ev.target.value = '';
            }}
          />
          <button
            onClick={() => fileInput.current?.click()}
            className="mr-auto flex items-center gap-1.5 rounded-md px-2 py-1.5 text-sm opacity-60 hover:opacity-100 cursor-pointer"
            title="Attach files"
          >
            <Paperclip className="h-4 w-4" />
          </button>
          <button
            onClick={close}
            className="rounded-md px-4 py-1.5 text-sm opacity-60 hover:opacity-100 cursor-pointer"
          >
            Cancel
          </button>
          <button
            onClick={send}
            disabled={sending}
            className="flex items-center gap-1.5 rounded-md bg-duck-teal px-4 py-1.5 text-sm font-medium text-white hover:bg-duck-teal/90 disabled:opacity-50 cursor-pointer"
          >
            {sending ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Send className="h-3.5 w-3.5" />}
            Send
          </button>
        </div>
      </DialogContent>
    </Dialog>
  );
};

// Build a reply draft from a viewed message. (No In-Reply-To yet — the real RFC Message-ID isn't
// stored; `m.id` is a local hash. Gmail still threads by Re: subject + participants. Proper threading
// is a follow-up: store the Message-Id header on ingest.)
export const replyDraft = (m: {
  from: string;
  subject: string;
  date: string;
  text?: string;
  snippet?: string;
}): ComposeDraft => {
  const addr = m.from.match(/<([^>]+)>/)?.[1] ?? m.from.trim();
  const subject = /^re:/i.test(m.subject) ? m.subject : `Re: ${m.subject}`;
  const original = (m.text || m.snippet || '').trim();
  const quoted = original
    ? `\n\nOn ${new Date(m.date).toLocaleString()}, ${m.from} wrote:\n${original
        .split('\n')
        .map((l) => `> ${l}`)
        .join('\n')}`
    : '';
  return { to: addr, subject, body: quoted };
};
