'use client';

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

import { AttachmentDraftBar } from '@/features/support/AttachmentDraftBar';
import { MessageAttachments } from '@/features/support/MessageAttachments';
import { useAttachmentDraft } from '@/features/support/useAttachmentDraft';
import { useLazyAttachmentObjectUrl } from '@/features/support/useAttachmentObjectUrl';
import { useFilePreview } from '@/features/support/useFilePreview';
import {
  useConversations,
  useMarkRead,
  useMessages,
  useSendMessage,
  useSetStatus,
} from '@/features/support/useSupport';
import { useSupportSocket } from '@/features/support/useSupportSocket';

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

/** Distance from an edge (px) that counts as "at" it. */
const SCROLL_EDGE_SLOP = 120;

const STATUS_CLASS: Record<string, string> = {
  OPEN: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300',
  RESOLVED: 'bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300',
  CLOSED: 'bg-gray-200 text-gray-600 dark:bg-slate-700 dark:text-slate-300',
};

const FILTERS = ['ALL', 'OPEN', 'RESOLVED', 'CLOSED'] as const;

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 default function SupportPage() {
  const [status, setStatus] = useState<(typeof FILTERS)[number]>('ALL');
  const [search, setSearch] = useState('');
  const [selectedId, setSelectedId] = useState<string | null>(null);

  const { items, isLoading, hasNextPage, isFetchingNextPage, fetchNextPage } = useConversations(
    status === 'ALL' ? { search } : { status, search },
  );

  useSupportSocket(true);

  const selected = useMemo(
    () => items.find((c) => c.id === selectedId) ?? null,
    [items, selectedId],
  );

  // Pull the next page in as the agent nears the end of the list. The list
  // previously rendered a single fetch, so any thread past the server's default
  // page was simply unreachable.
  const onInboxScroll = (e: React.UIEvent<HTMLDivElement>) => {
    if (!hasNextPage || isFetchingNextPage) return;
    const el = e.currentTarget;
    if (el.scrollHeight - el.scrollTop - el.clientHeight < SCROLL_EDGE_SLOP) {
      fetchNextPage();
    }
  };

  return (
    <div className="space-y-4">
      <div className="flex items-center gap-3">
        <span
          className="flex h-10 w-10 items-center justify-center rounded-lg"
          style={{ background: 'rgba(20,113,76,0.1)', color: GREEN }}
        >
          <Headset size={20} />
        </span>
        <div>
          <h1 className="text-2xl font-bold text-foreground">Support Center</h1>
          <p className="text-sm text-muted-foreground">
            Shared inbox — respond to passenger requests in real time
          </p>
        </div>
      </div>

      <div className="flex h-[calc(100vh-220px)] overflow-hidden rounded-xl border border-border bg-card">
        {/* Conversation list */}
        <div className="flex w-[340px] shrink-0 flex-col border-r border-border">
          <div className="space-y-2 border-b border-border p-3">
            <div className="relative">
              <Search size={16} className="absolute left-3 top-2.5 text-muted-foreground" />
              <input
                value={search}
                onChange={(e) => setSearch(e.target.value)}
                placeholder="Search subject or passenger"
                className="w-full rounded-lg border border-border bg-background py-2 pl-9 pr-3 text-sm outline-none focus:border-emerald-500"
              />
            </div>
            <div className="flex gap-1">
              {FILTERS.map((f) => (
                <button
                  key={f}
                  onClick={() => setStatus(f)}
                  className={`flex-1 rounded-md px-2 py-1 text-xs font-medium capitalize transition ${
                    status === f ? 'text-white' : 'bg-muted text-muted-foreground hover:bg-muted/70'
                  }`}
                  style={status === f ? { background: GREEN } : undefined}
                >
                  {f.toLowerCase()}
                </button>
              ))}
            </div>
          </div>
          <div className="flex-1 overflow-y-auto" onScroll={onInboxScroll}>
            {isLoading ? (
              <div className="p-6 text-center text-sm text-muted-foreground">Loading…</div>
            ) : items.length === 0 ? (
              <div className="p-6 text-center text-sm text-muted-foreground">No conversations.</div>
            ) : (
              <>
                {items.map((c) => (
                  <InboxRow
                    key={c.id}
                    c={c}
                    active={c.id === selectedId}
                    onClick={() => setSelectedId(c.id)}
                  />
                ))}
                {isFetchingNextPage && (
                  <div className="p-3 text-center text-xs text-muted-foreground">Loading more…</div>
                )}
              </>
            )}
          </div>
        </div>

        {/* Thread */}
        <div className="min-w-0 flex-1">
          {selected ? (
            <ConversationThread conversation={selected} />
          ) : (
            <div className="flex h-full flex-col items-center justify-center gap-2 text-muted-foreground">
              <span
                className="flex h-14 w-14 items-center justify-center rounded-full"
                style={{ background: 'rgba(20,113,76,0.1)', color: GREEN }}
              >
                <Headset size={28} />
              </span>
              <p className="text-sm">Select a conversation to start replying.</p>
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

function InboxRow({
  c,
  active,
  onClick,
}: {
  c: ConversationDto;
  active: boolean;
  onClick: () => void;
}) {
  const unread = c.unreadCount > 0;
  return (
    <button
      onClick={onClick}
      className={`block w-full border-b border-border px-4 py-3 text-left transition hover:bg-muted/50 ${
        active ? 'bg-muted/70' : ''
      }`}
      style={
        active ? { borderLeft: `3px solid ${GREEN}` } : { borderLeft: '3px solid transparent' }
      }
    >
      <div className="flex items-center justify-between gap-2">
        <span
          className={`truncate text-sm ${
            unread ? 'font-bold text-foreground' : 'font-semibold text-foreground/90'
          }`}
        >
          {c.subject || 'Support request'}
        </span>
        <span className="shrink-0 text-xs text-muted-foreground">
          {formatTime(c.lastMessageAt)}
        </span>
      </div>
      <p className="mt-0.5 truncate text-xs text-muted-foreground">
        {c.passengerName || 'Passenger'}
      </p>
      <div className="mt-1 flex items-center justify-between gap-2">
        <span className="truncate text-xs text-muted-foreground">
          {c.lastMessageSender === 'AGENT' ? 'You: ' : ''}
          {c.lastMessagePreview ?? '—'}
        </span>
        {unread ? (
          <span
            className="flex h-5 min-w-5 items-center justify-center rounded-full px-1.5 text-xs font-semibold text-white"
            style={{ background: GREEN }}
          >
            {c.unreadCount}
          </span>
        ) : (
          <span
            className={`rounded-full px-2 py-0.5 text-[10px] font-medium capitalize ${
              STATUS_CLASS[c.status] ?? ''
            }`}
          >
            {c.status.toLowerCase()}
          </span>
        )}
      </div>
    </button>
  );
}

function ConversationThread({ conversation }: { conversation: ConversationDto }) {
  const { messages, pageCount, isLoading, hasNextPage, isFetchingNextPage, fetchNextPage } =
    useMessages(conversation.id);
  const send = useSendMessage(conversation.id);
  const setStatus = useSetStatus();
  const markRead = useMarkRead();
  const [draft, setDraft] = useState('');
  const [dragging, setDragging] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const viewport = useRef<HTMLDivElement>(null);
  const fileInput = useRef<HTMLInputElement>(null);
  const { view, viewer } = useFilePreview();
  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 reader 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 agent is parked at the bottom and wants to follow new messages. */
  const stick = useRef(true);
  /** Which thread the refs above describe; a switch resets them. */
  const anchoredThread = useRef(conversation.id);

  const messageCount = messages.length;

  useEffect(() => {
    markRead.mutate(conversation.id);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [conversation.id, messageCount]);

  // A send failure belongs to the thread it was typed in, not the next one.
  useEffect(() => {
    setError(null);
  }, [conversation.id]);

  /**
   * 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 agent is already there),
   * while an older page prepended at the top must NOT move the content the
   * agent is reading. Layout effect, not effect — this has to run before paint
   * or the prepend visibly jumps.
   */
  useLayoutEffect(() => {
    const el = viewport.current;
    if (!el) return;

    // Thread switch: start a fresh read at the bottom and drop the previous
    // thread's anchoring state.
    if (anchoredThread.current !== conversation.id) {
      anchoredThread.current = conversation.id;
      pendingRestore.current = null;
      stick.current = true;
      el.scrollTo({ top: el.scrollHeight });
      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, conversation.id]);

  const onScroll = (e: React.UIEvent<HTMLDivElement>) => {
    const el = e.currentTarget;
    stick.current = el.scrollHeight - el.scrollTop - el.clientHeight < SCROLL_EDGE_SLOP;
    if (el.scrollTop < 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();
    }
  };

  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 (err) {
      setDraft(text);
      setError(err instanceof Error ? err.message : "Couldn't send that message.");
    }
  };

  const changeStatus = (status: string) => setStatus.mutate({ id: conversation.id, status });

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

  // Documents aren't fetched until opened.
  const openFile = async (a: AttachmentDto) => {
    try {
      view({ name: a.name, url: await loadAttachment(a.url), mimeType: a.mimeType });
    } catch {
      setError(`Couldn't open ${a.name}.`);
    }
  };

  return (
    <div className="flex h-full flex-col">
      <div className="flex items-center justify-between gap-2 border-b border-border p-4">
        <div className="min-w-0">
          <div className="flex items-center gap-2">
            <span className="truncate font-bold text-foreground">
              {conversation.subject || 'Conversation'}
            </span>
            <span
              className={`rounded-full px-2 py-0.5 text-[10px] font-medium capitalize ${
                STATUS_CLASS[conversation.status] ?? ''
              }`}
            >
              {conversation.status.toLowerCase()}
            </span>
          </div>
          <p className="truncate text-xs text-muted-foreground">
            {conversation.passengerName || 'Passenger'}
            {conversation.guestId ? ' · Guest' : ''}
            {conversation.guestEmail ? ` · ${conversation.guestEmail}` : ''}
            {conversation.guestPhone ? ` · ${conversation.guestPhone}` : ''}
          </p>
        </div>
        <div className="flex shrink-0 gap-2">
          {conversation.status !== 'OPEN' && (
            <button
              onClick={() => changeStatus('OPEN')}
              className="rounded-md px-3 py-1.5 text-xs font-medium text-white"
              style={{ background: GREEN }}
            >
              Reopen
            </button>
          )}
          {conversation.status === 'OPEN' && (
            <button
              onClick={() => changeStatus('RESOLVED')}
              className="rounded-md bg-blue-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-blue-700"
            >
              Resolve
            </button>
          )}
          {conversation.status !== 'CLOSED' && (
            <button
              onClick={() => changeStatus('CLOSED')}
              className="rounded-md bg-muted px-3 py-1.5 text-xs font-medium text-muted-foreground hover:bg-muted/70"
            >
              Close
            </button>
          )}
        </div>
      </div>

      <div ref={viewport} onScroll={onScroll} className="flex-1 space-y-3 overflow-y-auto p-4">
        {isLoading ? (
          <div className="p-6 text-center text-sm text-muted-foreground">Loading…</div>
        ) : messages.length === 0 ? (
          <div className="p-6 text-center text-sm text-muted-foreground">No messages yet.</div>
        ) : (
          <>
            {isFetchingNextPage && (
              <div className="py-2 text-center text-xs text-muted-foreground">
                Loading earlier messages…
              </div>
            )}
            {!hasNextPage && (
              <div className="text-center text-[10px] text-muted-foreground">
                Start of conversation
              </div>
            )}
            {messages.map((m) => (
              <AgentBubble key={m.id} m={m} onView={openAttachment} onOpenFile={openFile} />
            ))}
          </>
        )}
      </div>

      <div
        className={`border-t border-border p-3 ${
          dragging
            ? 'bg-emerald-50 outline-dashed outline-2 -outline-offset-4 outline-emerald-600 dark:bg-emerald-900/20'
            : ''
        }`}
        onDragOver={(e) => {
          e.preventDefault();
          setDragging(true);
        }}
        onDragLeave={() => setDragging(false)}
        onDrop={(e) => {
          e.preventDefault();
          setDragging(false);
          attach.add(Array.from(e.dataTransfer.files));
        }}
      >
        {error && (
          <div className="mb-2 flex items-center justify-between gap-2 rounded-lg bg-red-100 px-3 py-1.5 text-xs text-red-700 dark:bg-red-900/40 dark:text-red-300">
            <span>{error}</span>
            <button onClick={() => setError(null)} aria-label="Dismiss">
              <X size={12} />
            </button>
          </div>
        )}
        <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-muted-foreground transition hover:bg-muted"
          >
            <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 your reply, or paste an image…  (Enter to send, Shift+Enter for newline)"
            className="max-h-28 flex-1 resize-none rounded-lg border border-border bg-background px-3 py-2 text-sm outline-none focus:border-emerald-500"
          />
          <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>
      {viewer}
    </div>
  );
}

function AgentBubble({
  m,
  onView,
  onOpenFile,
}: {
  m: MessageDto;
  onView: (a: AttachmentDto, src: string) => void;
  onOpenFile: (a: AttachmentDto) => void;
}) {
  const mine = m.sender === 'AGENT';
  return (
    <div className={`flex ${mine ? 'justify-end' : 'justify-start'} items-end gap-2`}>
      {!mine && (
        <span className="flex h-7 w-7 items-center justify-center rounded-full bg-muted text-muted-foreground">
          <User size={14} />
        </span>
      )}
      <div className="max-w-[70%]">
        <p className={`mb-0.5 text-xs text-muted-foreground ${mine ? 'text-right' : 'text-left'}`}>
          {mine ? m.authorName || 'You' : m.authorName || 'Passenger'}
        </p>
        <div
          className={`rounded-2xl px-3 py-2 text-sm ${
            mine ? 'rounded-br-sm text-white' : 'rounded-bl-sm bg-muted text-foreground'
          }`}
          style={mine ? { background: GREEN } : undefined}
        >
          {m.text && <p className="whitespace-pre-wrap break-words">{m.text}</p>}
          <MessageAttachments
            attachments={m.attachments}
            mine={mine}
            onView={onView}
            onOpenFile={onOpenFile}
          />
        </div>
        <p
          className={`mt-0.5 text-[10px] text-muted-foreground ${
            mine ? 'text-right' : 'text-left'
          }`}
        >
          {formatTime(m.createdAt)}
        </p>
      </div>
    </div>
  );
}
