import {
  SUPPORT_ATTACHMENT_ACCEPT,
  SupportAuthorRole,
  type SupportAttachmentDto,
  type SupportMessageDto,
} from "@edr/types";
import { useFileViewer } from "@edr/ui-common";
import {
  ActionIcon,
  Avatar,
  Box,
  Group,
  Loader,
  Paper,
  ScrollArea,
  Stack,
  Text,
  Textarea,
  ThemeIcon,
} from "@mantine/core";
import { Headset, Paperclip, Send, X } from "lucide-react";
import { useEffect, useLayoutEffect, useRef, useState } from "react";
import toast from "react-hot-toast";

import { AttachmentDraftBar } from "./AttachmentDraftBar";
import { MessageAttachments } from "./MessageAttachments";
import { useAttachmentDraft } from "./useAttachmentDraft";
import { useLazyAttachmentObjectUrl } from "./useAttachmentObjectUrl";
import {
  useConversation,
  useMarkConversationRead,
  useMessages,
  useSendMessage,
} from "./useSupport";

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

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

export interface SupportPanelProps {
  onClose: () => void;
}

/**
 * The whole support surface: one ongoing thread with the EDR team. There is
 * nothing to pick and nothing to open — the company has a single conversation,
 * created server-side the moment the first message is sent.
 */
export function SupportPanel({ onClose }: SupportPanelProps) {
  const { data: conversation } = useConversation();
  const {
    messages,
    pageCount,
    isLoading,
    hasNextPage,
    isFetchingNextPage,
    fetchNextPage,
  } = useMessages();
  const send = useSendMessage();
  const markRead = useMarkConversationRead();
  const [draft, setDraft] = useState("");
  const [dragging, setDragging] = useState(false);
  const viewport = useRef<HTMLDivElement>(null);
  const fileInput = useRef<HTMLInputElement>(null);
  const { view, viewer } = useFileViewer();
  const attach = useAttachmentDraft((reason) => toast.error(reason));

  /**
   * 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 customer 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);

  // Mark read on open + whenever new messages arrive. No-op before the thread
  // exists, so opening the panel never creates one.
  useEffect(() => {
    if (conversation) markRead.mutate();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [conversation?.id, messages.length]);

  /**
   * 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 customer 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;

    // A different thread underneath us (the first send creates one): 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 = ({ y }: { y: number }) => {
    const el = viewport.current;
    if (!el) return;
    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();
    }
  };

  const submit = async () => {
    const body = draft.trim();
    if (!body && 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();
    stick.current = true;
    try {
      await send.mutateAsync({ body: body || undefined, attachments: files });
    } catch (error) {
      setDraft(body);
      toast.error(
        error instanceof Error ? error.message : "Couldn't send that message.",
      );
    }
  };

  const loadAttachment = useLazyAttachmentObjectUrl();

  // 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: SupportAttachmentDto, src: string) =>
    view({ name: a.name, url: src, mimeType: a.mimeType });

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

  const isEmpty = !isLoading && messages.length === 0;

  return (
    <Paper
      shadow="xl"
      radius="lg"
      withBorder
      style={{
        display: "flex",
        flexDirection: "column",
        width: 384,
        height: 560,
        maxHeight: "calc(100vh - 120px)",
        overflow: "hidden",
      }}
    >
      <Group
        justify="space-between"
        wrap="nowrap"
        px="md"
        py="sm"
        style={{
          background:
            "linear-gradient(135deg, var(--mantine-color-edr-green-7), var(--mantine-color-edr-green-5))",
          color: "white",
        }}
      >
        <Group gap="sm" wrap="nowrap" style={{ minWidth: 0 }}>
          <ThemeIcon variant="white" radius="xl" size="lg" color="edr-green">
            <Headset size={18} />
          </ThemeIcon>
          <Box style={{ minWidth: 0 }}>
            <Text fw={600} size="sm" truncate>
              Support
            </Text>
            <Text size="xs" opacity={0.85} truncate>
              We usually reply within a few minutes
            </Text>
          </Box>
        </Group>
        <ActionIcon variant="transparent" color="white" onClick={onClose}>
          <X size={20} />
        </ActionIcon>
      </Group>

      <ScrollArea
        style={{ flex: 1 }}
        viewportRef={viewport}
        type="hover"
        onScrollPositionChange={onScroll}
      >
        {isLoading ? (
          <Group justify="center" p="xl">
            <Loader size="sm" color="edr-green" />
          </Group>
        ) : isEmpty ? (
          <Stack align="center" gap="xs" px="lg" py={48} c="dimmed">
            <ThemeIcon variant="light" color="edr-green" radius="xl" size={48}>
              <Headset size={24} />
            </ThemeIcon>
            <Text size="sm" ta="center">
              Send us a message and our team will help you out.
            </Text>
          </Stack>
        ) : (
          <Stack gap="sm" p="md">
            {isFetchingNextPage && (
              <Group justify="center" py="xs">
                <Loader size="xs" color="edr-green" />
              </Group>
            )}
            {!hasNextPage && (
              <Text size="10px" c="dimmed" ta="center">
                Start of conversation
              </Text>
            )}
            {messages.map((m) => (
              <MessageBubble
                key={m.id}
                m={m}
                onView={openAttachment}
                onOpenFile={openFile}
              />
            ))}
          </Stack>
        )}
      </ScrollArea>

      <Box
        p="sm"
        style={{
          borderTop: "1px solid var(--mantine-color-gray-2)",
          background: dragging ? "var(--mantine-color-edr-green-0)" : undefined,
          outline: dragging
            ? "2px dashed var(--mantine-color-edr-green-6)"
            : undefined,
          outlineOffset: -4,
        }}
        onDragOver={(e) => {
          e.preventDefault();
          setDragging(true);
        }}
        onDragLeave={() => setDragging(false)}
        onDrop={(e) => {
          e.preventDefault();
          setDragging(false);
          attach.add(Array.from(e.dataTransfer.files));
        }}
      >
        <AttachmentDraftBar
          attachments={attach.attachments}
          onRemove={attach.remove}
        />
        <Group gap="xs" align="flex-end" wrap="nowrap">
          <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 = "";
            }}
          />
          <ActionIcon
            size={38}
            radius="md"
            variant="subtle"
            color="gray"
            aria-label="Attach files"
            onClick={() => fileInput.current?.click()}
          >
            <Paperclip size={18} />
          </ActionIcon>
          <Textarea
            value={draft}
            onChange={(e) => setDraft(e.currentTarget.value)}
            placeholder="Type a message, or paste an image…"
            autosize
            minRows={1}
            maxRows={4}
            radius="md"
            style={{ flex: 1 }}
            // 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();
              }
            }}
          />
          <ActionIcon
            size={38}
            radius="md"
            color="edr-green"
            variant="filled"
            loading={send.isPending}
            disabled={!draft.trim() && attach.attachments.length === 0}
            onClick={submit}
          >
            <Send size={18} />
          </ActionIcon>
        </Group>
      </Box>
      {viewer}
    </Paper>
  );
}

function MessageBubble({
  m,
  onView,
  onOpenFile,
}: {
  m: SupportMessageDto;
  onView: (a: SupportAttachmentDto, src: string) => void;
  onOpenFile: (a: SupportAttachmentDto) => void;
}) {
  const mine = m.authorRole === SupportAuthorRole.CUSTOMER;
  return (
    <Group
      justify={mine ? "flex-end" : "flex-start"}
      wrap="nowrap"
      align="flex-end"
      gap="xs"
    >
      {!mine && (
        <Avatar size="sm" radius="xl" color="edr-green" variant="filled">
          <Headset size={14} />
        </Avatar>
      )}
      <Box style={{ maxWidth: "78%" }}>
        {!mine && (
          <Text size="xs" c="dimmed" mb={2} ml={4}>
            {m.authorName || "Support agent"}
          </Text>
        )}
        <Paper
          px="sm"
          py={8}
          radius="lg"
          style={{
            background: mine
              ? "var(--mantine-color-edr-green-6)"
              : "var(--mantine-color-gray-1)",
            color: mine ? "white" : "var(--mantine-color-dark-7)",
            borderBottomRightRadius: mine ? 4 : undefined,
            borderBottomLeftRadius: mine ? undefined : 4,
          }}
        >
          {m.body && (
            <Text
              size="sm"
              style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}
            >
              {m.body}
            </Text>
          )}
          <MessageAttachments
            attachments={m.attachments}
            mine={mine}
            onView={onView}
            onOpenFile={onOpenFile}
          />
        </Paper>
        <Text size="10px" c="dimmed" mt={2} ta={mine ? "right" : "left"}>
          {formatTime(m.createdAt)}
        </Text>
      </Box>
    </Group>
  );
}

export default SupportPanel;
