import { DragEvent, FormEvent, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import { api, ChatMessage, ChatSession, ChatStreamChunk } from "../api/client";
import MessageList, { MessageListHandle } from "../components/MessageList";
import PomodoroWidget from "../components/PomodoroWidget";
import { usePomodoroNotify, usePomodoroRefresh } from "../hooks/usePomodoroNotify";
import { useThrottledStreaming } from "../hooks/useThrottledStreaming";
import { dedupeMessages, mergeMessages, stripOptimisticMessages } from "../utils/mergeMessages";
import "./Chat.css";
import "./Chat.performance.css";

const INITIAL_MESSAGE_LIMIT = 30;
const LOAD_OLDER_LIMIT = 30;
const SYNC_TAIL_LIMIT = 15;
const GENERATION_POLL_MS = 2000;
const MAX_PENDING_IMAGES = 8;

type PendingImageItem = {
  file: File;
  previewUrl: string;
};

function sleep(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

function buildImageMarkdown(items: PendingImageItem[]): string {
  if (!items.length) return "";
  return items
    .map((item, index) => {
      const label = items.length > 1 ? `скриншот ${index + 1}/${items.length}` : "скриншот";
      return `![${label}](${item.previewUrl})`;
    })
    .join("\n");
}

function buildUserMessagePreview(items: PendingImageItem[], text: string): string {
  return [buildImageMarkdown(items), text.trim()].filter(Boolean).join("\n\n");
}

function shouldShowMessage(msg: ChatMessage): boolean {
  if (msg.role === "tool") return false;
  if (msg.role === "assistant" && msg.tool_calls_json) return false;
  if (msg.role === "assistant" && !msg.content.trim()) return false;
  return true;
}

export default function Chat() {
  const [sessions, setSessions] = useState<ChatSession[]>([]);
  const [activeId, setActiveId] = useState<number | null>(null);
  const [messages, setMessages] = useState<ChatMessage[]>([]);
  const [hasMoreOlder, setHasMoreOlder] = useState(false);
  const [input, setInput] = useState("");
  const [loading, setLoading] = useState(false);
  const { streaming, setStreaming, resetStreaming, flushStreaming } = useThrottledStreaming();
  const [pendingPhase, setPendingPhase] = useState<
    "thinking" | "preparing" | "generating" | "tools"
  >("thinking");
  const [chatError, setChatError] = useState<string | null>(null);
  const [pendingImages, setPendingImages] = useState<PendingImageItem[]>([]);
  const [inputDragOver, setInputDragOver] = useState(false);
  const tempMessageId = useRef(0);
  const fileInputRef = useRef<HTMLInputElement>(null);
  const messagesRef = useRef<HTMLDivElement>(null);
  const messageListRef = useRef<MessageListHandle>(null);
  const bottomAnchorRef = useRef<HTMLDivElement>(null);
  const inputRef = useRef<HTMLTextAreaElement>(null);
  const stickToBottomRef = useRef(true);
  const pendingScrollToBottomRef = useRef(false);
  const loadingOlderRef = useRef(false);
  const messagesStateRef = useRef<ChatMessage[]>([]);
  const refreshPomodoro = usePomodoroRefresh();
  const lastReminderNotifySeq = useRef(0);
  const remindersNotifyReady = useRef(false);
  const pendingHistoryReload = useRef(false);
  const resumeSessionRef = useRef<number | null>(null);

  messagesStateRef.current = messages;

  const loadSessions = async () => {
    const data = await api.listSessions();
    setSessions(data);
    if (!activeId && data.length > 0) {
      setActiveId(data[0].id);
    }
  };

  const scrollToBottom = useCallback((force = false) => {
    if (!force && !stickToBottomRef.current) return;
    messageListRef.current?.scrollToBottom();
    bottomAnchorRef.current?.scrollIntoView({ block: "end", behavior: "auto" });
  }, []);

  const applyMessages = useCallback(
    (updater: (prev: ChatMessage[]) => ChatMessage[], options?: { preserveScroll?: boolean; scrollToBottom?: boolean }) => {
      const container = messagesRef.current;
      const wasAtBottom = stickToBottomRef.current;
      const preserveScroll = options?.preserveScroll ?? true;
      const forceBottom = options?.scrollToBottom ?? false;
      const prevScrollHeight = container?.scrollHeight ?? 0;
      const prevScrollTop = container?.scrollTop ?? 0;

      setMessages((prev) => dedupeMessages(updater(prev)));

      if (forceBottom || wasAtBottom) {
        pendingScrollToBottomRef.current = true;
        return;
      }

      if (!preserveScroll || !container) return;

      requestAnimationFrame(() => {
        requestAnimationFrame(() => {
          container.scrollTop = prevScrollTop + (container.scrollHeight - prevScrollHeight);
        });
      });
    },
    [],
  );

  const syncRecentMessages = useCallback(async (sessionId: number) => {
    const maxId = messagesStateRef.current
      .filter((m) => m.id > 0)
      .reduce((max, m) => Math.max(max, m.id), 0);

    const data =
      maxId > 0
        ? await api.getSessionMessages(sessionId, { limit: SYNC_TAIL_LIMIT, after_id: maxId })
        : await api.getSessionMessages(sessionId, { limit: SYNC_TAIL_LIMIT });

    if (data.messages.length === 0) return;
    applyMessages((prev) => mergeMessages(stripOptimisticMessages(prev), data.messages));
  }, [applyMessages]);

  const nextTempId = () => {
    tempMessageId.current -= 1;
    return tempMessageId.current;
  };

  const appendNotice = useCallback((content: string) => {
    applyMessages((prev) => {
      const last = prev[prev.length - 1];
      if (last?.role === "notice" && last.content === content) {
        return prev;
      }
      return [
        ...prev,
        {
          id: nextTempId(),
          role: "notice",
          content,
          created_at: new Date().toISOString(),
        },
      ];
    });
  }, [applyMessages]);

  const processStreamChunk = useCallback(
    (chunk: ChatStreamChunk, assistantTextRef: { current: string }) => {
      if (chunk.event === "vision") {
        setPendingPhase("preparing");
      }
      if (chunk.event === "status") {
        const phase = chunk.data.phase;
        if (phase === "preparing") {
          setPendingPhase("preparing");
        }
        if (phase === "generating") {
          setPendingPhase("generating");
        }
        if (phase === "tools") {
          setPendingPhase("tools");
          assistantTextRef.current = "";
          resetStreaming();
        }
      }
      if (chunk.event === "token") {
        assistantTextRef.current += String(chunk.data.content ?? "");
        setPendingPhase("generating");
        setStreaming(assistantTextRef.current);
      }
      if (chunk.event === "notice") {
        appendNotice(String(chunk.data.content ?? ""));
        if (String(chunk.data.content).startsWith("⏱")) {
          refreshPomodoro();
        }
      }
      if (chunk.event === "pomodoro") {
        refreshPomodoro();
      }
      if (chunk.event === "error") {
        throw new Error(String(chunk.data.message ?? "Ошибка чата"));
      }
    },
    [appendNotice, refreshPomodoro, resetStreaming, setStreaming],
  );

  const waitForServerGeneration = useCallback(
    async (sessionId: number) => {
      while (true) {
        const status = await api.getGenerationStatus(sessionId);
        await syncRecentMessages(sessionId);
        if (!status.active) {
          break;
        }
        await sleep(GENERATION_POLL_MS);
      }
    },
    [syncRecentMessages],
  );

  const resumeOngoingGeneration = useCallback(
    async (sessionId: number) => {
      if (resumeSessionRef.current === sessionId) return;

      let status;
      try {
        status = await api.getGenerationStatus(sessionId);
      } catch {
        return;
      }
      if (!status.active) return;

      resumeSessionRef.current = sessionId;
      setLoading(true);
      setPendingPhase("generating");
      setChatError(null);
      stickToBottomRef.current = true;

      const pollId = window.setInterval(() => {
        syncRecentMessages(sessionId).catch(console.error);
      }, GENERATION_POLL_MS);

      const assistantTextRef = { current: "" };

      try {
        for await (const chunk of api.streamGeneration(sessionId)) {
          processStreamChunk(chunk, assistantTextRef);
          if (chunk.event === "done") {
            break;
          }
        }
      } catch (err) {
        console.error(err);
        try {
          await waitForServerGeneration(sessionId);
        } catch (pollErr) {
          console.error(pollErr);
        }
      } finally {
        clearInterval(pollId);
        flushStreaming();
        resetStreaming();
        await syncRecentMessages(sessionId);
        await loadSessions();
        setLoading(false);
        resumeSessionRef.current = null;
      }
    },
    [
      flushStreaming,
      processStreamChunk,
      resetStreaming,
      syncRecentMessages,
      waitForServerGeneration,
    ],
  );

  const loadInitialMessages = useCallback(async (sessionId: number) => {
    const data = await api.getSessionMessages(sessionId, { limit: INITIAL_MESSAGE_LIMIT });
    setHasMoreOlder(data.has_more);
    setMessages(dedupeMessages(data.messages));
    stickToBottomRef.current = true;
    pendingScrollToBottomRef.current = true;
    await resumeOngoingGeneration(sessionId);
  }, [resumeOngoingGeneration]);

  const loadOlderMessages = useCallback(async () => {
    if (!activeId || loadingOlderRef.current || !hasMoreOlder) return;

    const oldest = messagesStateRef.current.find((m) => m.id > 0);
    if (!oldest) return;

    loadingOlderRef.current = true;
    const container = messagesRef.current;
    const prevScrollHeight = container?.scrollHeight ?? 0;
    const prevScrollTop = container?.scrollTop ?? 0;

    try {
      const data = await api.getSessionMessages(activeId, {
        limit: LOAD_OLDER_LIMIT,
        before_id: oldest.id,
      });
      setHasMoreOlder(data.has_more);
      applyMessages((prev) => dedupeMessages([...data.messages, ...prev]), { preserveScroll: false });

      requestAnimationFrame(() => {
        if (container) {
          container.scrollTop = prevScrollTop + (container.scrollHeight - prevScrollHeight);
        }
      });
    } catch (err) {
      console.error(err);
    } finally {
      loadingOlderRef.current = false;
    }
  }, [activeId, applyMessages, hasMoreOlder]);

  useEffect(() => {
    loadSessions().catch(console.error);
  }, []);

  useEffect(() => {
    if (!activeId) {
      setMessages([]);
      setHasMoreOlder(false);
      return;
    }
    stickToBottomRef.current = true;
    loadInitialMessages(activeId).catch(console.error);
  }, [activeId, loadInitialMessages]);

  const updateStickiness = useCallback(() => {
    const container = messagesRef.current;
    if (!container) return;
    const distance = container.scrollHeight - container.scrollTop - container.clientHeight;
    stickToBottomRef.current = distance < 150;
  }, []);

  const handleMessagesScroll = useCallback(() => {
    updateStickiness();
    const container = messagesRef.current;
    if (!container || container.scrollTop > 120) return;
    loadOlderMessages().catch(console.error);
  }, [loadOlderMessages, updateStickiness]);

  useLayoutEffect(() => {
    if (!pendingScrollToBottomRef.current) return;
    pendingScrollToBottomRef.current = false;
    scrollToBottom(true);
  }, [messages, streaming, scrollToBottom]);

  useEffect(() => {
    const container = messagesRef.current;
    if (!container || !activeId) return;

    container.addEventListener("scroll", handleMessagesScroll, { passive: true });
    updateStickiness();

    let rafId: number | null = null;
    const scheduleScroll = () => {
      if (rafId !== null) return;
      rafId = requestAnimationFrame(() => {
        rafId = null;
        scrollToBottom();
      });
    };

    const observer = new ResizeObserver(scheduleScroll);
    observer.observe(container);

    return () => {
      container.removeEventListener("scroll", handleMessagesScroll);
      observer.disconnect();
      if (rafId !== null) {
        cancelAnimationFrame(rafId);
      }
    };
  }, [activeId, handleMessagesScroll, scrollToBottom, updateStickiness]);

  const dismissKeyboard = useCallback(() => {
    inputRef.current?.blur();
  }, []);

  const waitingForStream = loading && !streaming;

  const pendingLabel =
    pendingPhase === "tools"
      ? "Выполняю команды…"
      : pendingPhase === "preparing"
        ? "Собираю контекст…"
        : pendingPhase === "generating"
          ? "Генерирую ответ…"
          : "Думаю…";

  const handlePomodoroNotify = useCallback(() => {
    refreshPomodoro().catch(console.error);
    if (!activeId) return;
    if (loading) {
      pendingHistoryReload.current = true;
    } else {
      syncRecentMessages(activeId).catch(console.error);
    }
  }, [activeId, loading, refreshPomodoro, syncRecentMessages]);

  usePomodoroNotify(handlePomodoroNotify);

  useEffect(() => {
    return () => {
      setPendingImages((prev) => {
        for (const item of prev) {
          URL.revokeObjectURL(item.previewUrl);
        }
        return prev;
      });
    };
  }, []);

  useEffect(() => {
    let cancelled = false;

    const poll = async () => {
      try {
        const data = await api.getRemindersSnapshot();
        if (cancelled) return;
        if (!remindersNotifyReady.current) {
          remindersNotifyReady.current = true;
          lastReminderNotifySeq.current = data.notify_seq;
          return;
        }
        if (data.notify_seq > lastReminderNotifySeq.current) {
          lastReminderNotifySeq.current = data.notify_seq;
          if (activeId) {
            if (loading) {
              pendingHistoryReload.current = true;
            } else {
              syncRecentMessages(activeId).catch(console.error);
            }
          }
        }
      } catch {
        // ignore polling errors
      }
    };

    poll().catch(console.error);
    const id = setInterval(() => poll().catch(console.error), 60000);
    return () => {
      cancelled = true;
      clearInterval(id);
    };
  }, [activeId, loading, syncRecentMessages]);

  const handleNewChat = async () => {
    const session = await api.createSession();
    await loadSessions();
    setActiveId(session.id);
    setMessages([]);
    setHasMoreOlder(false);
  };

  const handleDelete = async (id: number) => {
    await api.deleteSession(id);
    const data = await api.listSessions();
    setSessions(data);
    if (activeId === id) {
      setActiveId(data[0]?.id ?? null);
      setMessages([]);
      setHasMoreOlder(false);
    }
  };

  const clearPendingImages = useCallback(() => {
    setPendingImages((prev) => {
      for (const item of prev) {
        URL.revokeObjectURL(item.previewUrl);
      }
      return [];
    });
    if (fileInputRef.current) {
      fileInputRef.current.value = "";
    }
  }, []);

  const removePendingImage = useCallback((index: number) => {
    setPendingImages((prev) => {
      const next = [...prev];
      const [removed] = next.splice(index, 1);
      if (removed) {
        URL.revokeObjectURL(removed.previewUrl);
      }
      return next;
    });
  }, []);

  const handleImagePick = (fileList: FileList | null) => {
    if (!fileList?.length) return;
    const picked = Array.from(fileList).filter((file) => file.type.startsWith("image/"));
    if (!picked.length) return;

    setPendingImages((prev) => {
      const room = MAX_PENDING_IMAGES - prev.length;
      if (room <= 0) return prev;
      const nextItems = picked.slice(0, room).map((file) => ({
        file,
        previewUrl: URL.createObjectURL(file),
      }));
      return [...prev, ...nextItems];
    });
    if (fileInputRef.current) {
      fileInputRef.current.value = "";
    }
  };

  const handleInputDragOver = (event: DragEvent<HTMLFormElement>) => {
    event.preventDefault();
    if (loading) return;
    setInputDragOver(true);
  };

  const handleInputDragLeave = (event: DragEvent<HTMLFormElement>) => {
    if (event.currentTarget.contains(event.relatedTarget as Node | null)) return;
    setInputDragOver(false);
  };

  const handleInputDrop = (event: DragEvent<HTMLFormElement>) => {
    event.preventDefault();
    setInputDragOver(false);
    if (loading) return;
    handleImagePick(event.dataTransfer.files);
  };

  const handleSubmit = async (e: FormEvent) => {
    e.preventDefault();
    if (!activeId || loading) return;
    const text = input.trim();
    const submittingImages = [...pendingImages];
    if (!text && submittingImages.length === 0) return;

    setInput("");
    dismissKeyboard();
    stickToBottomRef.current = true;
    setLoading(true);
    resetStreaming();
    setPendingPhase(submittingImages.length > 0 ? "preparing" : "thinking");
    setChatError(null);

    const displayContent = buildUserMessagePreview(submittingImages, text);

    const tempUser: ChatMessage = {
      id: nextTempId(),
      role: "user",
      content: displayContent,
      created_at: new Date().toISOString(),
    };
    applyMessages((prev) => [...prev, tempUser], { scrollToBottom: true });

    const imageFiles = submittingImages.map((item) => item.file);
    setPendingImages([]);
    if (fileInputRef.current) {
      fileInputRef.current.value = "";
    }

    try {
      const assistantTextRef = { current: "" };
      const stream = imageFiles.length
        ? api.sendMessageWithImages(activeId, text, imageFiles)
        : api.sendMessage(activeId, text);
      for await (const chunk of stream) {
        processStreamChunk(chunk, assistantTextRef);
        if (chunk.event === "done") {
          flushStreaming();
          resetStreaming();
          setChatError(null);
          await syncRecentMessages(activeId);
          await loadSessions();
        }
      }
    } catch (err) {
      console.error(err);
      if (activeId) {
        try {
          const status = await api.getGenerationStatus(activeId);
          if (status.active) {
            setChatError(null);
            setLoading(true);
            await waitForServerGeneration(activeId);
            await loadSessions();
            return;
          }
        } catch {
          // ignore status probe errors
        }
      }
      const message = err instanceof Error ? err.message : "Ошибка чата";
      setChatError(message);
      resetStreaming();
      if (activeId) {
        await syncRecentMessages(activeId);
      }
    } finally {
      for (const item of submittingImages) {
        URL.revokeObjectURL(item.previewUrl);
      }
      setLoading(false);
      if (pendingHistoryReload.current && activeId) {
        pendingHistoryReload.current = false;
        syncRecentMessages(activeId).catch(console.error);
      }
    }
  };

  const visibleMessages = useMemo(
    () => messages.filter(shouldShowMessage),
    [messages],
  );

  return (
    <div className="chat-layout">
      <aside className="chat-sidebar">
        <button className="primary-btn" onClick={handleNewChat}>
          + Новый чат
        </button>

        <PomodoroWidget />

        <ul className="session-list">
          {sessions.map((session) => (
            <li key={session.id} className={activeId === session.id ? "active" : ""}>
              <button onClick={() => setActiveId(session.id)}>{session.title}</button>
              <button className="delete-btn" onClick={() => handleDelete(session.id)}>
                ×
              </button>
            </li>
          ))}
        </ul>
      </aside>

      <section className="chat-main">
        {!activeId ? (
          <div className="chat-empty">Создайте новый чат, чтобы начать</div>
        ) : (
          <>
            <div className="chat-mobile-bar">
              <select
                className="chat-session-select"
                value={activeId}
                onChange={(e) => setActiveId(Number(e.target.value))}
                aria-label="Выбор чата"
              >
                {sessions.map((session) => (
                  <option key={session.id} value={session.id}>
                    {session.title}
                  </option>
                ))}
              </select>
              <button type="button" className="chat-mobile-new" onClick={handleNewChat}>
                + Новый
              </button>
            </div>

            <div
              className="messages"
              ref={messagesRef}
              onClick={dismissKeyboard}
            >
              {hasMoreOlder && (
                <div className="messages-load-older-hint">Прокрутите вверх, чтобы загрузить историю</div>
              )}

              <MessageList
                ref={messageListRef}
                messages={visibleMessages}
                containerRef={messagesRef}
              />

              {waitingForStream && (
                <div className="message message-assistant message-pending" aria-live="polite">
                  <div className="message-role">assistant</div>
                  <div className="message-content message-pending-content">
                    <span className="typing-indicator" aria-hidden="true">
                      <span />
                      <span />
                      <span />
                    </span>
                    <span className="typing-label">{pendingLabel}</span>
                  </div>
                </div>
              )}

              {streaming && (
                <div className="message message-assistant">
                  <div className="message-role">assistant</div>
                  <div className="message-content streaming-text">{streaming}</div>
                </div>
              )}

              {chatError && (
                <div className="message message-error" role="alert">
                  <div className="message-role">ошибка</div>
                  <div className="message-content">{chatError}</div>
                </div>
              )}
              <div
                className="messages-bottom-anchor"
                ref={bottomAnchorRef}
                aria-hidden="true"
              />
            </div>

            <form
              className={`chat-input${inputDragOver ? " chat-input-dragover" : ""}`}
              onSubmit={handleSubmit}
              onDragOver={handleInputDragOver}
              onDragLeave={handleInputDragLeave}
              onDrop={handleInputDrop}
            >
              {pendingImages.length ? (
                <div className="chat-image-previews">
                  {pendingImages.map((item, index) => (
                    <div key={`${item.file.name}-${index}`} className="chat-image-preview">
                      <img src={item.previewUrl} alt={`Превью ${index + 1}`} />
                      <button type="button" onClick={() => removePendingImage(index)} aria-label="Убрать">
                        ×
                      </button>
                    </div>
                  ))}
                  <button type="button" className="chat-image-clear-all" onClick={clearPendingImages}>
                    Очистить все
                  </button>
                </div>
              ) : null}
              <div className="chat-input-row">
                <input
                  ref={fileInputRef}
                  type="file"
                  accept="image/*"
                  multiple
                  className="chat-file-input"
                  onChange={(e) => handleImagePick(e.target.files)}
                />
                <button
                  type="button"
                  className="chat-attach-btn"
                  title="Прикрепить скриншоты"
                  onClick={() => fileInputRef.current?.click()}
                  disabled={loading || pendingImages.length >= MAX_PENDING_IMAGES}
                >
                  📎
                </button>
                <textarea
                  ref={inputRef}
                  value={input}
                  onChange={(e) => setInput(e.target.value)}
                  placeholder="Напишите сообщение или прикрепите скриншоты…"
                  rows={2}
                  enterKeyHint="send"
                  autoComplete="off"
                  onKeyDown={(e) => {
                    if (e.key === "Enter" && !e.shiftKey) {
                      e.preventDefault();
                      handleSubmit(e);
                    }
                  }}
                />
                <button type="submit" disabled={loading || (!input.trim() && pendingImages.length === 0)}>
                  {loading ? "..." : "Отправить"}
                </button>
              </div>
            </form>
          </>
        )}
      </section>
    </div>
  );
}
