'use client';

import { Passenger, SUPPORT_ATTACHMENT_ACCEPT } from '@edr/types';
import { Headset, Paperclip, Send, X } from 'lucide-react';
import { useEffect, useLayoutEffect, useRef, useState } from 'react';

import { AttachmentDraftBar } from './AttachmentDraftBar';
import { ImagePreview } from './ImagePreview';
import { MessageAttachments } from './MessageAttachments';
import { useAttachmentDraft } from './useAttachmentDraft';
import { useLazyAttachmentObjectUrl } from './useAttachmentObjectUrl';
import { useMarkRead, useSendMessage, useThread } from './useSupport';

const GREEN = 'rgb(20 113 76)';
type MessageDto = Passenger.PassengerSupportMessageDto;
type AttachmentDto = Passenger.PassengerSupportAttachmentDto;

/** How close to an edge counts as "at" it, in px. */
const SCROLL_EDGE_SLOP = 40;

function formatTime(iso?: string | null): string {
  if (!iso) return '';
  const d = new Date(iso);
  const now = new Date();
  return d.toDateString() === now.toDateString()
    ? d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
    : d.toLocaleDateString([], { month: 'short', day: 'numeric' });
}

export function SupportPanel({ onClose }: { onClose: () => void }) {
  const { messages, pageCount, isLoading, hasNextPage, isFetchingNextPage, fetchNextPage } =
    useThread();
  const send = useSendMessage();
  const markRead = useMarkRead();
  const [draft, setDraft] = useState('');
  const [dragging, setDragging] = useState(false);
  const [error, setError] = useState<string | null>(null);
  // Holds the blob object URL the thumbnail already fetched, not the attachment
  // itself — the preview can't load from `a.url` on its own (see MessageAttachments).
  const [preview, setPreview] = useState<{ name: string; src: string } | null>(null);
  const viewport = useRef<HTMLDivElement>(null);
  const fileInput = useRef<HTMLInputElement>(null);
  const attach = useAttachmentDraft(setError);
  const loadAttachment = useLazyAttachmentObjectUrl();

  /**
   * Scroll height captured just before an older page was requested, tagged with
   * the page count at that moment.
   *
   * The page count is what makes this safe. Keyed on presence alone, a message
   * arriving over the socket while history was still in flight would consume the
   * snapshot on a one-bubble append, and the real 30-message prepend would then
   * land with nothing to correct against — throwing the visitor exactly as far as
   * this exists to prevent. Comparing counts means only an actual new page can
   * claim it.
   */
  const pendingRestore = useRef<{ height: number; atPageCount: number } | null>(null);
  /** Whether the visitor is parked at the bottom and wants to follow new messages. */
  const stick = useRef(true);

  const messageCount = messages.length;

  // Mark read on open + whenever new messages arrive.
  useEffect(() => {
    markRead.mutate();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [messageCount]);

  /**
   * Keep the viewport sensible as the list changes underneath it.
   *
   * Two different things change `messages`, and they want opposite behaviour: a
   * new message at the bottom should follow (if the visitor is already there),
   * while an older page prepended at the top must NOT move the content they're
   * reading. Layout effect, not effect — this has to run before paint or the
   * prepend visibly jumps.
   */
  useLayoutEffect(() => {
    const el = viewport.current;
    if (!el) return;
    const restore = pendingRestore.current;
    if (restore && pageCount > restore.atPageCount) {
      // An older page went in above: push the scroll down by exactly the height
      // that was added, so the same message stays under the cursor.
      el.scrollTop += el.scrollHeight - restore.height;
      pendingRestore.current = null;
      return;
    }
    if (stick.current) el.scrollTo({ top: el.scrollHeight });
  }, [messages, pageCount]);

  const onScroll = () => {
    const el = viewport.current;
    if (!el) return;
    const y = el.scrollTop;
    stick.current = el.scrollHeight - y - el.clientHeight < SCROLL_EDGE_SLOP;
    if (y < SCROLL_EDGE_SLOP && hasNextPage && !isFetchingNextPage) {
      // A failed fetch leaves this set, which is harmless: the list didn't
      // change, so the height is still accurate for the retry, and the count
      // tag stops it being mistaken for a landed page in the meantime.
      pendingRestore.current = {
        height: el.scrollHeight,
        atPageCount: pageCount,
      };
      fetchNextPage();
    }
  };

  // Images already hold their bytes as an object URL from rendering the
  // thumbnail, so reuse it rather than fetching the same file twice.
  const viewImage = (a: AttachmentDto, src: string) => setPreview({ name: a.name, src });

  /**
   * Documents aren't fetched until opened. There's no inline viewer in this
   * widget, so the bytes are handed to the browser as a download: a
   * `window.open` after the await has lost its user-gesture and gets caught by
   * popup blockers, whereas a synthetic anchor click does not.
   */
  const openFile = async (a: AttachmentDto) => {
    try {
      const src = await loadAttachment(a.url);
      const link = document.createElement('a');
      link.href = src;
      link.download = a.name;
      link.click();
    } catch {
      setError(`Couldn't open ${a.name}.`);
    }
  };

  const submit = async () => {
    const text = draft.trim();
    if (!text && attach.attachments.length === 0) return;
    const files = attach.files;
    // Clear optimistically so the composer feels instant; on failure the text is
    // restored below rather than silently lost.
    setDraft('');
    attach.clear();
    setError(null);
    stick.current = true;
    try {
      await send.mutateAsync({ text: text || undefined, attachments: files });
    } catch (e) {
      setDraft(text);
      setError(e instanceof Error ? e.message : "Couldn't send that message.");
    }
  };

  return (
    <div className="flex h-[560px] max-h-[calc(100vh-120px)] w-[min(384px,calc(100vw-32px))] flex-col overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-2xl dark:border-slate-700 dark:bg-slate-900">
      {/* Header */}
      <div
        className="flex items-center justify-between gap-2 px-4 py-3 text-white"
        style={{
          background: `linear-gradient(135deg, ${GREEN}, rgb(30 140 96))`,
        }}
      >
        <div className="flex items-center gap-2">
          <span className="flex h-8 w-8 items-center justify-center rounded-full bg-white/20">
            <Headset size={18} />
          </span>
          <div>
            <p className="text-sm font-semibold">Support</p>
            <p className="text-xs text-white/80">We usually reply in a few minutes</p>
          </div>
        </div>
        <button onClick={onClose} className="rounded-full p-1 hover:bg-white/20" aria-label="Close">
          <X size={20} />
        </button>
      </div>

      {/* Messages */}
      <div ref={viewport} onScroll={onScroll} className="flex-1 space-y-3 overflow-y-auto p-4">
        {isLoading ? (
          <div className="p-8 text-center text-sm text-gray-400">Loading…</div>
        ) : messages.length === 0 ? (
          <div className="flex flex-col items-center gap-2 px-6 py-10 text-center text-sm text-gray-500 dark:text-slate-400">
            <span
              className="flex h-12 w-12 items-center justify-center rounded-full"
              style={{ background: 'rgba(20,113,76,0.1)', color: GREEN }}
            >
              <Headset size={24} />
            </span>
            Hi! 👋 How can we help you today? Send us a message and our team will get back to you.
          </div>
        ) : (
          <>
            {isFetchingNextPage && (
              <p className="py-1 text-center text-xs text-gray-400">Loading earlier messages…</p>
            )}
            {!hasNextPage && (
              <p className="text-center text-[10px] text-gray-400">Start of conversation</p>
            )}
            {messages.map((m) => (
              <MessageBubble key={m.id} m={m} onViewImage={viewImage} onOpenFile={openFile} />
            ))}
          </>
        )}
      </div>

      {/* Composer */}
      <div
        className={`border-t p-3 ${
          dragging
            ? 'border-emerald-500 bg-emerald-50 dark:bg-emerald-950/30'
            : 'border-gray-100 dark:border-slate-700'
        }`}
        onDragOver={(e) => {
          e.preventDefault();
          setDragging(true);
        }}
        onDragLeave={() => setDragging(false)}
        onDrop={(e) => {
          e.preventDefault();
          setDragging(false);
          attach.add(Array.from(e.dataTransfer.files));
        }}
      >
        {error && (
          <p className="mb-2 text-xs text-red-600 dark:text-red-400" role="alert">
            {error}
          </p>
        )}
        <AttachmentDraftBar attachments={attach.attachments} onRemove={attach.remove} />
        <div className="flex items-end gap-2">
          <input
            ref={fileInput}
            type="file"
            multiple
            accept={SUPPORT_ATTACHMENT_ACCEPT}
            hidden
            onChange={(e) => {
              attach.add(Array.from(e.currentTarget.files ?? []));
              // Reset so picking the same file twice in a row still fires change.
              e.currentTarget.value = '';
            }}
          />
          <button
            onClick={() => fileInput.current?.click()}
            aria-label="Attach files"
            className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg text-gray-500 transition hover:bg-gray-100 dark:text-slate-400 dark:hover:bg-slate-800"
          >
            <Paperclip size={18} />
          </button>
          <textarea
            value={draft}
            onChange={(e) => setDraft(e.target.value)}
            // Screenshots land on the clipboard as files. Take them and suppress
            // the default, which would otherwise also paste the image's name (or
            // nothing) as text.
            onPaste={(e) => {
              if (attach.addFromPaste(e.clipboardData)) e.preventDefault();
            }}
            onKeyDown={(e) => {
              if (e.key === 'Enter' && !e.shiftKey) {
                e.preventDefault();
                submit();
              }
            }}
            rows={1}
            placeholder="Type a message, or paste an image…"
            className="max-h-24 flex-1 resize-none rounded-lg border border-gray-300 px-3 py-2 text-sm outline-none focus:border-emerald-500 dark:border-slate-600 dark:bg-slate-800 dark:text-white"
          />
          <button
            onClick={submit}
            disabled={(!draft.trim() && attach.attachments.length === 0) || send.isPending}
            className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg text-white transition hover:opacity-90 disabled:opacity-50"
            style={{ background: GREEN }}
            aria-label="Send"
          >
            <Send size={18} />
          </button>
        </div>
      </div>

      {preview && (
        <ImagePreview src={preview.src} alt={preview.name} onClose={() => setPreview(null)} />
      )}
    </div>
  );
}

function MessageBubble({
  m,
  onViewImage,
  onOpenFile,
}: {
  m: MessageDto;
  onViewImage: (a: AttachmentDto, src: string) => void;
  onOpenFile: (a: AttachmentDto) => void;
}) {
  const mine = m.sender === 'USER';
  return (
    <div className={`flex ${mine ? 'justify-end' : 'justify-start'}`}>
      <div className="max-w-[78%]">
        {!mine && (
          <p className="mb-0.5 ml-1 text-xs text-gray-400">{m.authorName || 'Support agent'}</p>
        )}
        <div
          className={`whitespace-pre-wrap break-words rounded-2xl px-3 py-2 text-sm ${
            mine
              ? 'rounded-br-sm text-white'
              : 'rounded-bl-sm bg-gray-100 text-gray-800 dark:bg-slate-800 dark:text-slate-100'
          }`}
          style={mine ? { background: GREEN } : undefined}
        >
          {/* Attachment-only messages carry text: "" — rendering it anyway would
              leave an empty line above the thumbnail. */}
          {m.text && <p>{m.text}</p>}
          <MessageAttachments
            attachments={m.attachments}
            mine={mine}
            onViewImage={onViewImage}
            onOpenFile={onOpenFile}
          />
        </div>
        <p className={`mt-0.5 text-[10px] text-gray-400 ${mine ? 'text-right' : 'text-left'}`}>
          {formatTime(m.createdAt)}
        </p>
      </div>
    </div>
  );
}

export default SupportPanel;
