import {
  ActionIcon,
  Avatar,
  Box,
  Combobox,
  FileButton,
  Group,
  Loader,
  Paper,
  ScrollArea,
  Stack,
  Text,
  Textarea,
  Tooltip,
  useCombobox,
} from "@mantine/core";
import { IconPaperclip, IconSend, IconTrash } from "@tabler/icons-react";
import { notifications } from "@mantine/notifications";
import {
  DragEvent,
  KeyboardEvent,
  useCallback,
  useEffect,
  useMemo,
  useRef,
  useState,
} from "react";
import * as api from "../api";
import type { CardMessage, User } from "../types";
import { tagColor } from "./colors";
import { formatDateTimeShort } from "./format";
import { MessageBody } from "./MessageBody";

interface Props {
  cardId: string;
  users: User[];
  currentUserId?: string;
  onMessagesChange?: (messages: CardMessage[]) => void;
  onFileUploaded?: () => void;
  // When set, the panel scrolls the matching message into view and flashes a
  // brief highlight (~2s). Used by notification click → open card.
  highlightMessageId?: string;
}

function refForFile(filename: string, url: string, mime: string): string {
  const safe = filename.replace(/]/g, "");
  return mime.startsWith("image/") ? `![${safe}](${url})` : `[${safe}](${url})`;
}

// Window for considering a peer "actively typing" after its last event.
const TYPING_LIFETIME_MS = 4000;
// Minimum gap between successive typing pings emitted while the user types.
const TYPING_THROTTLE_MS = 1500;

interface MentionMatch {
  start: number; // index of '@' in the textarea value
  query: string; // text after '@', lowercased
}

function detectMention(value: string, cursor: number): MentionMatch | null {
  // Look backwards from cursor for an '@' that starts a word.
  for (let i = cursor - 1; i >= 0 && cursor - i <= 64; i--) {
    const ch = value[i];
    if (ch === "@") {
      // Valid start: beginning of string or whitespace before.
      if (i === 0 || /\s/.test(value[i - 1])) {
        const q = value.slice(i + 1, cursor);
        if (/^[a-z0-9_.-]*$/i.test(q)) {
          return { start: i, query: q.toLowerCase() };
        }
      }
      return null;
    }
    if (/\s/.test(ch)) return null;
  }
  return null;
}

export function CardChatPanel({
  cardId,
  users,
  currentUserId,
  onMessagesChange,
  onFileUploaded,
  highlightMessageId,
}: Props) {
  const [messages, setMessages] = useState<CardMessage[]>([]);
  const [loading, setLoading] = useState(true);
  const [body, setBody] = useState("");
  const [sending, setSending] = useState(false);
  const [uploading, setUploading] = useState(false);
  const [dragOver, setDragOver] = useState(false);
  const [typingUsers, setTypingUsers] = useState<Record<string, number>>({});
  const [mention, setMention] = useState<MentionMatch | null>(null);
  const viewportRef = useRef<HTMLDivElement | null>(null);
  const wsRef = useRef<WebSocket | null>(null);
  const textareaRef = useRef<HTMLTextAreaElement | null>(null);
  const lastTypingEmitRef = useRef(0);

  const usersById = useMemo(() => new Map(users.map((u) => [u.id, u])), [users]);

  const reload = useCallback(async () => {
    try {
      const ms = await api.listCardMessages(cardId);
      setMessages(ms);
      onMessagesChange?.(ms);
    } catch (e) {
      notifications.show({ color: "red", message: (e as Error).message });
    } finally {
      setLoading(false);
    }
  }, [cardId, onMessagesChange]);

  useEffect(() => {
    reload();
  }, [reload]);

  // Open one WebSocket per cardId for realtime chat + typing.
  useEffect(() => {
    const ws = new WebSocket(api.cardChatWSURL(cardId));
    wsRef.current = ws;
    ws.onmessage = (ev) => {
      try {
        const data = JSON.parse(ev.data) as
          | { type: "message.created"; message: CardMessage }
          | { type: "typing"; user_id: string }
          | { type: "error"; error: string };
        if (data.type === "message.created" && data.message) {
          setMessages((prev) => {
            if (prev.some((m) => m.id === data.message!.id)) return prev;
            const next = [...prev, data.message!];
            onMessagesChange?.(next);
            return next;
          });
        } else if (data.type === "typing" && data.user_id) {
          setTypingUsers((prev) => ({ ...prev, [data.user_id]: Date.now() }));
        } else if (data.type === "error") {
          notifications.show({ color: "red", message: data.error });
        }
      } catch {
        // ignore malformed
      }
    };
    ws.onerror = () => {
      // browser will report; we keep the panel functional via REST fallback
    };
    return () => {
      ws.close();
      wsRef.current = null;
    };
  }, [cardId, onMessagesChange]);

  // Sweep stale typing entries.
  useEffect(() => {
    const t = setInterval(() => {
      const now = Date.now();
      setTypingUsers((prev) => {
        const next: Record<string, number> = {};
        for (const [k, v] of Object.entries(prev)) {
          if (now - v < TYPING_LIFETIME_MS) next[k] = v;
        }
        return next;
      });
    }, 1000);
    return () => clearInterval(t);
  }, []);

  useEffect(() => {
    if (viewportRef.current) {
      viewportRef.current.scrollTo({ top: viewportRef.current.scrollHeight, behavior: "smooth" });
    }
  }, [messages.length]);

  // Scroll to + briefly pulse the message that triggered an incoming
  // notification. Runs whenever the highlight id changes AND the message
  // is present in the list (it may arrive asynchronously after WS sync).
  const [pulse, setPulse] = useState<string | null>(null);
  useEffect(() => {
    if (!highlightMessageId) return;
    if (!messages.some((m) => m.id === highlightMessageId)) return;
    const el = document.querySelector(`[data-msg-id="${highlightMessageId}"]`);
    if (el && el instanceof HTMLElement) {
      el.scrollIntoView({ behavior: "smooth", block: "center" });
    }
    setPulse(highlightMessageId);
    const t = setTimeout(() => setPulse(null), 2200);
    return () => clearTimeout(t);
  }, [highlightMessageId, messages]);

  const sendTypingPing = () => {
    const ws = wsRef.current;
    if (!ws || ws.readyState !== WebSocket.OPEN) return;
    const now = Date.now();
    if (now - lastTypingEmitRef.current < TYPING_THROTTLE_MS) return;
    lastTypingEmitRef.current = now;
    ws.send(JSON.stringify({ type: "typing" }));
  };

  const combobox = useCombobox({
    onDropdownClose: () => setMention(null),
  });

  const mentionCandidates = useMemo(() => {
    if (!mention) return [] as User[];
    return users
      .filter((u) => u.username.toLowerCase().startsWith(mention.query))
      .slice(0, 8);
  }, [users, mention]);

  useEffect(() => {
    if (mention && mentionCandidates.length > 0) {
      combobox.openDropdown();
      combobox.selectFirstOption();
    } else {
      combobox.closeDropdown();
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [mention?.query, mentionCandidates.length]);

  const insertMention = (username: string) => {
    if (!mention) return;
    const before = body.slice(0, mention.start);
    const after = body.slice(mention.start + 1 + mention.query.length);
    const inserted = `@${username} `;
    const next = before + inserted + after;
    setBody(next);
    setMention(null);
    // Restore caret right after the inserted mention.
    requestAnimationFrame(() => {
      const el = textareaRef.current;
      if (!el) return;
      const pos = (before + inserted).length;
      el.focus();
      el.setSelectionRange(pos, pos);
    });
  };

  const send = async () => {
    const text = body.trim();
    if (!text || sending) return;
    setSending(true);
    const ws = wsRef.current;
    try {
      if (ws && ws.readyState === WebSocket.OPEN) {
        ws.send(JSON.stringify({ type: "send", body: text }));
        // Optimistic clear; server will broadcast the persisted message.
        setBody("");
      } else {
        const m = await api.createCardMessage(cardId, text);
        setMessages((prev) => [...prev, m]);
        onMessagesChange?.([...messages, m]);
        setBody("");
      }
    } catch (e) {
      notifications.show({ color: "red", message: (e as Error).message });
    } finally {
      setSending(false);
    }
  };

  const remove = async (mid: string) => {
    try {
      await api.deleteCardMessage(cardId, mid);
      const next = messages.filter((m) => m.id !== mid);
      setMessages(next);
      onMessagesChange?.(next);
    } catch (e) {
      notifications.show({ color: "red", message: (e as Error).message });
    }
  };

  const onChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
    setBody(e.currentTarget.value);
    sendTypingPing();
    const cursor = e.currentTarget.selectionStart ?? e.currentTarget.value.length;
    setMention(detectMention(e.currentTarget.value, cursor));
  };

  const onKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
    if (mention && mentionCandidates.length > 0 && (e.key === "Enter" || e.key === "Tab")) {
      e.preventDefault();
      const sel = combobox.getSelectedOptionIndex();
      const pick = mentionCandidates[Math.max(0, sel)];
      if (pick) insertMention(pick.username);
      return;
    }
    if (mention && e.key === "Escape") {
      setMention(null);
      return;
    }
    if (e.key === "Enter" && !e.shiftKey) {
      e.preventDefault();
      send();
    }
  };

  const handleFiles = async (files: FileList | File[]) => {
    setUploading(true);
    try {
      for (const file of Array.from(files)) {
        try {
          const cf = await api.uploadCardFile(cardId, file, "chat");
          const ref = refForFile(cf.filename, cf.url, cf.mime);
          const m = await api.createCardMessage(cardId, ref);
          setMessages((prev) => {
            const next = [...prev, m];
            onMessagesChange?.(next);
            return next;
          });
          onFileUploaded?.();
        } catch (e) {
          notifications.show({ color: "red", message: `${file.name}: ${(e as Error).message}` });
        }
      }
    } finally {
      setUploading(false);
    }
  };

  const onDrop = (e: DragEvent<HTMLDivElement>) => {
    e.preventDefault();
    setDragOver(false);
    if (!e.dataTransfer.files || e.dataTransfer.files.length === 0) return;
    handleFiles(e.dataTransfer.files);
  };

  const onDragOver = (e: DragEvent<HTMLDivElement>) => {
    if (!e.dataTransfer.types.includes("Files")) return;
    e.preventDefault();
    setDragOver(true);
  };

  const onDragLeave = (e: DragEvent<HTMLDivElement>) => {
    e.preventDefault();
    setDragOver(false);
  };

  const typingNames = Object.keys(typingUsers)
    .filter((uid) => uid !== currentUserId)
    .map((uid) => {
      const u = usersById.get(uid);
      return u?.display_name || u?.username || "alguien";
    });

  return (
    <Stack
      gap="xs"
      style={{
        height: "100%",
        minHeight: 0,
        position: "relative",
        outline: dragOver ? "2px dashed var(--mantine-color-blue-5)" : undefined,
        outlineOffset: dragOver ? -2 : undefined,
        borderRadius: 4,
      }}
      onDrop={onDrop}
      onDragOver={onDragOver}
      onDragLeave={onDragLeave}
    >
      <ScrollArea
        viewportRef={viewportRef}
        style={{ flex: 1, minHeight: 200 }}
        type="auto"
        offsetScrollbars
      >
        {loading ? (
          <Group justify="center" p="md"><Loader size="sm" /></Group>
        ) : messages.length === 0 ? (
          <Text size="sm" c="dimmed" ta="center" p="md">
            Sin mensajes aun. Escribe el primero o arrastra un archivo.
          </Text>
        ) : (
          <Stack gap={6} p={4}>
            {messages.map((m) => {
              const author = m.author_id ? usersById.get(m.author_id) : null;
              const isMe = m.author_id && m.author_id === currentUserId;
              const label = author ? author.display_name || author.username : "Anonimo";
              const highlighted = pulse === m.id;
              return (
                <Paper
                  key={m.id}
                  withBorder
                  p="xs"
                  radius="sm"
                  data-msg-id={m.id}
                  bg={
                    highlighted
                      ? "var(--mantine-color-yellow-light)"
                      : isMe
                      ? "var(--mantine-color-blue-light)"
                      : undefined
                  }
                  style={{
                    transition: "background-color 600ms ease",
                    boxShadow: highlighted ? "0 0 0 2px var(--mantine-color-yellow-5)" : undefined,
                  }}
                >
                  <Group gap={6} wrap="nowrap" align="flex-start">
                    <Avatar size={22} radius="xl" color={author?.color || tagColor(label)}>
                      {label.slice(0, 2).toUpperCase()}
                    </Avatar>
                    <Box style={{ flex: 1, minWidth: 0 }}>
                      <Group gap={6} wrap="nowrap" justify="space-between">
                        <Group gap={6} wrap="nowrap">
                          <Text size="xs" fw={600}>{label}</Text>
                          <Text size="xs" c="dimmed">{formatDateTimeShort(m.created_at)}</Text>
                        </Group>
                        {isMe && (
                          <Tooltip label="Borrar" withArrow>
                            <ActionIcon size="xs" variant="subtle" color="red" onClick={() => remove(m.id)}>
                              <IconTrash size={12} />
                            </ActionIcon>
                          </Tooltip>
                        )}
                      </Group>
                      <Stack gap={4}>
                        <MessageBody text={m.body} />
                      </Stack>
                    </Box>
                  </Group>
                </Paper>
              );
            })}
          </Stack>
        )}
      </ScrollArea>
      {typingNames.length > 0 && (
        <Text size="xs" c="dimmed" px={6}>
          {typingNames.length === 1
            ? `${typingNames[0]} esta escribiendo...`
            : `${typingNames.slice(0, 2).join(", ")}${typingNames.length > 2 ? "..." : ""} estan escribiendo...`}
        </Text>
      )}
      <Combobox
        store={combobox}
        onOptionSubmit={(value) => insertMention(value)}
        position="top-start"
        withinPortal={false}
      >
        <Combobox.DropdownTarget>
          <Group gap="xs" align="flex-end">
            <Textarea
              ref={textareaRef}
              value={body}
              onChange={onChange}
              onKeyDown={onKeyDown}
              placeholder="Escribe un mensaje (Enter = enviar, @ para mencionar). Arrastra archivos o usa el clip."
              autosize
              minRows={1}
              maxRows={6}
              style={{ flex: 1 }}
              disabled={sending}
            />
            <FileButton onChange={(file) => file && handleFiles([file])} disabled={uploading}>
              {(props) => (
                <Tooltip label="Adjuntar archivo" withArrow>
                  <ActionIcon
                    size="lg"
                    variant="subtle"
                    color="gray"
                    aria-label="Adjuntar"
                    loading={uploading}
                    {...props}
                  >
                    <IconPaperclip size={16} />
                  </ActionIcon>
                </Tooltip>
              )}
            </FileButton>
            <Tooltip label="Enviar" withArrow>
              <ActionIcon
                size="lg"
                variant="filled"
                color="blue"
                onClick={send}
                disabled={!body.trim() || sending}
                aria-label="Enviar"
              >
                <IconSend size={16} />
              </ActionIcon>
            </Tooltip>
          </Group>
        </Combobox.DropdownTarget>
        <Combobox.Dropdown hidden={!mention || mentionCandidates.length === 0}>
          <Combobox.Options>
            {mentionCandidates.map((u) => (
              <Combobox.Option key={u.id} value={u.username}>
                <Group gap={6} wrap="nowrap">
                  <Avatar size={18} radius="xl" color={u.color || tagColor(u.username)}>
                    {(u.display_name || u.username).slice(0, 2).toUpperCase()}
                  </Avatar>
                  <Text size="sm" fw={600}>@{u.username}</Text>
                  {u.display_name && u.display_name !== u.username && (
                    <Text size="xs" c="dimmed">{u.display_name}</Text>
                  )}
                </Group>
              </Combobox.Option>
            ))}
          </Combobox.Options>
        </Combobox.Dropdown>
      </Combobox>
      {(dragOver || uploading) && (
        <Box
          style={{
            position: "absolute",
            inset: 0,
            background: "rgba(34,139,230,0.08)",
            display: "flex",
            alignItems: "center",
            justifyContent: "center",
            pointerEvents: "none",
            borderRadius: 4,
          }}
        >
          <Text size="sm" fw={500} c="blue">
            {uploading ? "Subiendo..." : "Suelta para adjuntar"}
          </Text>
        </Box>
      )}
    </Stack>
  );
}
