import {
  SUPPORT_ATTACHMENT_ACCEPT,
  SupportAuthorRole,
  type SupportAttachmentDto,
  type SupportConversationDto,
  type SupportMessageDto,
} from "@edr/types";
import { useFileViewer } from "@edr/ui-common";
import {
  ActionIcon,
  Avatar,
  Badge,
  Box,
  Button,
  Group,
  Loader,
  Modal,
  Paper,
  ScrollArea,
  SegmentedControl,
  Select,
  Stack,
  Text,
  Textarea,
  TextInput,
  ThemeIcon,
} from "@mantine/core";
import { useQuery } from "@tanstack/react-query";
import {
  Building2,
  Headset,
  Paperclip,
  Plus,
  Search,
  Send,
  User,
} from "lucide-react";
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import toast from "react-hot-toast";

import { useLazyAttachmentObjectUrl } from "@/features/support/useAttachmentObjectUrl";
import { AttachmentDraftBar } from "@/features/support/AttachmentDraftBar";
import { MessageAttachments } from "@/features/support/MessageAttachments";
import { useAttachmentDraft } from "@/features/support/useAttachmentDraft";
import {
  useConversations,
  useMarkConversationRead,
  useMessages,
  useSendMessage,
  useStartConversation,
} from "@/features/support/useSupport";
import { useSupportSocket } from "@/features/support/useSupportSocket";
import { customersService } from "@/services/customers.service";

type ReadFilter = "ALL" | "UNREAD";

/** 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 default function SupportInboxPage() {
  const [readFilter, setReadFilter] = useState<ReadFilter>("ALL");
  const [search, setSearch] = useState("");
  const [selectedId, setSelectedId] = useState<string | null>(null);
  const [pickerOpen, setPickerOpen] = useState(false);
  const inboxViewport = useRef<HTMLDivElement>(null);
  /**
   * The thread just opened from the company picker.
   *
   * Selection resolves against the *loaded* pages, and a company picked from the
   * modal may well have a thread that sits far enough down the list to not be
   * loaded yet — in which case the lookup below would find nothing and the pane
   * would sit blank. Hold onto the conversation the server handed back so the
   * pane can open immediately, regardless of where it falls in the inbox.
   */
  const [startedConversation, setStartedConversation] =
    useState<SupportConversationDto | null>(null);

  const { items, isLoading, hasNextPage, isFetchingNextPage, fetchNextPage } =
    useConversations({
      search,
      unreadOnly: readFilter === "UNREAD",
    });

  useSupportSocket(true, (event) => {
    if (event.message.authorRole === SupportAuthorRole.CUSTOMER) {
      toast(
        `New message from ${event.conversation.companyName ?? "a customer"}`,
        { icon: "💬" },
      );
    }
  });

  // Prefer the live row from the list (its unread count and last message stay
  // current); fall back to the picker's copy while its page is still unloaded.
  const selected = useMemo(() => {
    const fromList = items.find((c) => c.id === selectedId);
    if (fromList) return fromList;
    return startedConversation?.id === selectedId ? startedConversation : null;
  }, [items, selectedId, startedConversation]);

  return (
    <Box p="md">
      <Group mb="md" gap="sm">
        <ThemeIcon size="lg" radius="md" color="edr-green" variant="light">
          <Headset size={20} />
        </ThemeIcon>
        <Box>
          <Text fw={700} size="lg">
            Customer Support
          </Text>
          <Text size="xs" c="dimmed">
            Shared inbox — chat with customers in real time
          </Text>
        </Box>
      </Group>

      <Paper
        withBorder
        radius="lg"
        style={{
          display: "flex",
          height: "calc(100vh - 190px)",
          overflow: "hidden",
        }}
      >
        {/* ── Conversation list ── */}
        <Stack
          gap={0}
          style={{
            width: 340,
            flexShrink: 0,
            borderRight: "1px solid var(--mantine-color-gray-2)",
          }}
        >
          <Box
            p="sm"
            style={{ borderBottom: "1px solid var(--mantine-color-gray-2)" }}
          >
            <Button
              fullWidth
              color="edr-green"
              radius="md"
              size="xs"
              leftSection={<Plus size={16} />}
              onClick={() => setPickerOpen(true)}
              mb="sm"
            >
              New chat
            </Button>
            <TextInput
              placeholder="Search company"
              leftSection={<Search size={16} />}
              value={search}
              onChange={(e) => setSearch(e.currentTarget.value)}
              radius="md"
              mb="sm"
            />
            <SegmentedControl
              fullWidth
              size="xs"
              value={readFilter}
              onChange={(v) => setReadFilter(v as ReadFilter)}
              data={[
                { label: "All", value: "ALL" },
                { label: "Unread", value: "UNREAD" },
              ]}
            />
          </Box>
          <ScrollArea
            style={{ flex: 1 }}
            type="hover"
            // Pull the next page in as the agent nears the end of the list.
            // Previously the hook asked for 100 rows and stopped there, so any
            // company past the hundredth was simply unreachable.
            onScrollPositionChange={({ y }) => {
              const el = inboxViewport.current;
              if (!el || !hasNextPage || isFetchingNextPage) return;
              if (el.scrollHeight - y - el.clientHeight < SCROLL_EDGE_SLOP) {
                fetchNextPage();
              }
            }}
            viewportRef={inboxViewport}
          >
            {isLoading ? (
              <Group justify="center" p="xl">
                <Loader size="sm" color="edr-green" />
              </Group>
            ) : items.length === 0 ? (
              <Text c="dimmed" size="sm" ta="center" p="xl">
                {readFilter === "UNREAD"
                  ? "Nothing unread."
                  : "No conversations."}
              </Text>
            ) : (
              <>
                {items.map((c) => (
                  <InboxRow
                    key={c.id}
                    c={c}
                    active={c.id === selectedId}
                    onClick={() => setSelectedId(c.id)}
                  />
                ))}
                {isFetchingNextPage && (
                  <Group justify="center" p="sm">
                    <Loader size="xs" color="edr-green" />
                  </Group>
                )}
              </>
            )}
          </ScrollArea>
        </Stack>

        {/* ── Thread ── */}
        <Box style={{ flex: 1, minWidth: 0 }}>
          {selected ? (
            <ConversationThread conversation={selected} />
          ) : (
            <Stack align="center" justify="center" h="100%" c="dimmed" gap="xs">
              <ThemeIcon
                variant="light"
                color="edr-green"
                radius="xl"
                size={56}
              >
                <Headset size={28} />
              </ThemeIcon>
              <Text size="sm">Select a conversation, or start a new chat.</Text>
            </Stack>
          )}
        </Box>
      </Paper>

      <CompanyPicker
        opened={pickerOpen}
        onClose={() => setPickerOpen(false)}
        onStarted={(conversation) => {
          setStartedConversation(conversation);
          setSelectedId(conversation.id);
          setPickerOpen(false);
        }}
      />
    </Box>
  );
}

/**
 * Pick a company to chat with. Starting is idempotent server-side, so choosing a
 * company that already has a thread simply selects it rather than erroring.
 */
function CompanyPicker({
  opened,
  onClose,
  onStarted,
}: {
  opened: boolean;
  onClose: () => void;
  onStarted: (conversation: SupportConversationDto) => void;
}) {
  const [companyId, setCompanyId] = useState<string | null>(null);
  const start = useStartConversation();

  const { data: companies, isLoading } = useQuery({
    queryKey: ["companies", "list"],
    queryFn: () => customersService.list({ page: 1, pageSize: 1000 }),
    enabled: opened,
  });

  const options = useMemo(
    () =>
      (companies?.items ?? []).map((c) => ({
        value: c.id,
        label: c.name || c.email || c.tin || c.id,
      })),
    [companies],
  );

  const submit = async () => {
    if (!companyId) return;
    const conversation = await start.mutateAsync(companyId);
    setCompanyId(null);
    onStarted(conversation);
  };

  return (
    <Modal
      opened={opened}
      onClose={onClose}
      title="Start a chat"
      radius="md"
      centered
    >
      <Stack gap="md">
        <Select
          label="Customer"
          placeholder={
            isLoading ? "Loading companies…" : "Search for a company"
          }
          data={options}
          value={companyId}
          onChange={setCompanyId}
          searchable
          nothingFoundMessage="No companies match."
          disabled={isLoading}
          radius="md"
        />
        <Button
          color="edr-green"
          radius="md"
          loading={start.isPending}
          disabled={!companyId}
          onClick={submit}
        >
          Start chatting
        </Button>
      </Stack>
    </Modal>
  );
}

function InboxRow({
  c,
  active,
  onClick,
}: {
  c: SupportConversationDto;
  active: boolean;
  onClick: () => void;
}) {
  const unread = c.unreadCount > 0;
  return (
    <Box
      component="button"
      onClick={onClick}
      px="md"
      py="sm"
      style={{
        display: "block",
        width: "100%",
        textAlign: "left",
        cursor: "pointer",
        border: "none",
        borderLeft: active
          ? "3px solid var(--mantine-color-edr-green-6)"
          : "3px solid transparent",
        background: active
          ? "var(--mantine-color-edr-green-0)"
          : unread
            ? "var(--mantine-color-gray-0)"
            : "transparent",
        borderBottom: "1px solid var(--mantine-color-gray-1)",
      }}
    >
      <Group justify="space-between" wrap="nowrap" gap="xs">
        <Group gap={6} wrap="nowrap" style={{ flex: 1, minWidth: 0 }}>
          <Building2 size={13} color="var(--mantine-color-gray-6)" />
          <Text fw={unread ? 700 : 600} size="sm" truncate style={{ flex: 1 }}>
            {c.companyName ?? "Unknown company"}
          </Text>
        </Group>
        <Text size="xs" c="dimmed" style={{ flexShrink: 0 }}>
          {formatTime(c.lastMessageAt)}
        </Text>
      </Group>
      <Group justify="space-between" wrap="nowrap" gap="xs" mt={4}>
        <Text size="xs" c="dimmed" truncate style={{ flex: 1 }}>
          {c.lastMessageAuthorRole === SupportAuthorRole.AGENT ? "You: " : ""}
          {c.lastMessagePreview ?? "No messages yet"}
        </Text>
        {unread && (
          <Badge size="sm" circle color="edr-green">
            {c.unreadCount}
          </Badge>
        )}
      </Group>
    </Box>
  );
}

function ConversationThread({
  conversation,
}: {
  conversation: SupportConversationDto;
}) {
  const {
    messages,
    pageCount,
    isLoading,
    hasNextPage,
    isFetchingNextPage,
    fetchNextPage,
  } = useMessages(conversation.id);
  const send = useSendMessage(conversation.id);
  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 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]);

  /**
   * 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 what they're reading.
   * Layout effect, not effect — this must 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 = ({ 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}.`);
    }
  };

  return (
    <Stack gap={0} h="100%">
      {/* Header */}
      <Group
        justify="space-between"
        wrap="nowrap"
        p="md"
        style={{ borderBottom: "1px solid var(--mantine-color-gray-2)" }}
      >
        <Group gap="xs" wrap="nowrap" style={{ minWidth: 0 }}>
          <ThemeIcon variant="light" color="edr-green" radius="xl" size="md">
            <Building2 size={14} />
          </ThemeIcon>
          <Text fw={700} truncate>
            {conversation.companyName ?? "Unknown company"}
          </Text>
        </Group>
      </Group>

      {/* Messages */}
      <ScrollArea
        style={{ flex: 1 }}
        viewportRef={viewport}
        type="hover"
        onScrollPositionChange={onScroll}
      >
        {isLoading ? (
          <Group justify="center" p="xl">
            <Loader size="sm" color="edr-green" />
          </Group>
        ) : messages.length === 0 ? (
          <Text c="dimmed" size="sm" ta="center" p="xl">
            No messages yet — say hello.
          </Text>
        ) : (
          <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) => (
              <AgentBubble
                key={m.id}
                m={m}
                onView={openAttachment}
                onOpenFile={openFile}
              />
            ))}
          </Stack>
        )}
      </ScrollArea>

      {/* Composer */}
      <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…  (Enter to send, Shift+Enter for newline)"
            autosize
            minRows={1}
            maxRows={5}
            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}
    </Stack>
  );
}

function AgentBubble({
  m,
  onView,
  onOpenFile,
}: {
  m: SupportMessageDto;
  onView: (a: SupportAttachmentDto, src: string) => void;
  onOpenFile: (a: SupportAttachmentDto) => void;
}) {
  const mine = m.authorRole === SupportAuthorRole.AGENT;
  return (
    <Group
      justify={mine ? "flex-end" : "flex-start"}
      wrap="nowrap"
      align="flex-end"
      gap="xs"
    >
      {!mine && (
        <Avatar size="sm" radius="xl" color="gray" variant="filled">
          <User size={14} />
        </Avatar>
      )}
      <Box style={{ maxWidth: "70%" }}>
        <Text
          size="xs"
          c="dimmed"
          mb={2}
          ml={mine ? 0 : 4}
          ta={mine ? "right" : "left"}
        >
          {mine ? m.authorName || "You" : m.authorName || "Customer"}
        </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>
  );
}
