import {useCallback, useEffect, useMemo, useRef, useState} from "react";
import {useQuery, useQueryClient} from "@tanstack/react-query";

import {api} from "@/client";
import {useAuth} from "@/context/auth-context";
import {useChannelSocket, type WsOutEvent} from "@/hooks/use-channel";
import {useMessageCache} from "@/hooks/use-message-cache";
import {useToast} from "@/hooks/use-toast";
import type {
  MessageEditedService,
  MessageNewService,
  MessageRevokedService,
  MessageStreamChunkService,
  MessageStreamDoneService,
  MessageStreamStartService,
  PinAddedService,
  PinRemovedService,
  ReactionAddedService,
  ReactionBatchUpdatedService,
  ReactionRemovedService,
  RoomDeletedService,
  RoomRenamedService,
  RoomTopicUpdatedService,
  ThreadArchivedService,
  ThreadCreatedService,
  ThreadResolvedService,
  ThreadUpdatedService,
} from "@/socket";
import type {Category, Room} from "./channel-sidebar";
import type {Thread} from "./thread-sidebar";

type TypingUser = {
  userId: string;
  displayName: string;
};

export type ChannelState = {
  messages: MessageNewService[];
  loadingMessages: boolean;
  hasMore: boolean;
  typingUsers: TypingUser[];
  typingText: string | null;
  threads: Thread[];
  streamingMessages: Map<string, { content: string; displayName?: string }>;
  rooms: Room[];
  categories: Category[];
  currentRoom: Room | undefined;
  loadingRooms: boolean;
  currentUserId?: string;
};

export type ChannelActions = {
  handleSend: (content: string, inReplyTo?: string) => Promise<void>;
  handleLoadMore: () => void;
  handleTyping: (typing: boolean) => void;
  handlePinToggle: (messageId: string, pinned: boolean) => void;
  handleDeleteMessage: (messageId: string) => void;
  handleEditMessage: (messageId: string, content: string) => void;
  handleStartThread: (messageId: string, seq: number) => void;
  handleReactionToggle: (messageId: string, emoji: string, add: boolean) => void;
};

export function useChannelState(roomId: string | undefined) {
  const { subscribe, unsubscribe, send, onEvent } = useChannelSocket();
  const { user } = useAuth();
  const currentUserId = user?.id;
  const queryClient = useQueryClient();
  const { toast } = useToast();
  const messageCache = useMessageCache(roomId ?? undefined);

  const [messages, setMessages] = useState<MessageNewService[]>([]);
  const [loadingMessages, setLoadingMessages] = useState(false);
  const [hasMore, setHasMore] = useState(true);
  const [typingUsers, setTypingUsers] = useState<TypingUser[]>([]);
  const [threads, setThreads] = useState<Thread[]>([]);
  const [streamingMessages, setStreamingMessages] = useState<
    Map<string, { content: string; displayName?: string }>
  >(new Map());

  const lastSeq = useRef(0);
  const prevRoomId = useRef<string | undefined>(undefined);

  // Load threads for current room
  const { data: threadsResponse } = useQuery({
    queryKey: ["threads", roomId],
    queryFn: async () => {
      const response = await api.get(
        `/api/v1/ws/rooms/${roomId}/threads`,
      );
      // REST returns WsOutEvent::ThreadList { type, data: { threads } }
      const payload = response.data as Record<string, unknown> | undefined;
      const inner = payload?.data as Record<string, unknown> | undefined;
      const list = (inner?.threads ?? []) as Array<Record<string, unknown>>;
      // Normalize: room comes back as RoomInfo { id, name } object, need string UUID
      return list.map((t) => ({
        ...t,
        room:
          typeof t.room === "object" && t.room !== null
            ? ((t.room as Record<string, unknown>).id as string)
            : t.room,
      })) as Thread[];
    },
    enabled: Boolean(roomId),
    retry: false,
    staleTime: 5000,
  });

  // Merge fetched threads with websocket events
  useEffect(() => {
    if (threadsResponse && threadsResponse.length > 0) {
      setThreads((prev) => {
        const existingIds = new Set(prev.map((t) => t.id));
        const newThreads = threadsResponse.filter(
          (t: Thread) => !existingIds.has(t.id),
        );
        return newThreads.length > 0 ? [...prev, ...newThreads] : prev;
      });
    }
  }, [threadsResponse]);

  const { data: channelData, isLoading: loadingRooms } = useQuery({
    queryKey: ["channel", "rooms"],
    queryFn: async () => {
      const response = await api.get<{ rooms: Room[]; categories: Category[] }>(
        "/api/v1/ws/rooms",
      );
      return response.data;
    },
    retry: false,
  });
  const rooms = channelData?.rooms ?? [];
  const categories = channelData?.categories ?? [];

  const currentRoom = useMemo(
    () => rooms.find((r) => r.id === roomId),
    [rooms, roomId],
  );

  const loadMessages = useCallback(
    async (targetRoomId: string, beforeSeq?: number) => {
      if (!targetRoomId) return;
      setLoadingMessages(true);
      try {
        const params: Record<string, string | number> = { limit: 50 };
        if (beforeSeq) params.before_seq = beforeSeq;

        const response = await api.get<WsOutEvent>(
          `/api/v1/ws/rooms/${targetRoomId}/messages`,
          { params },
        );

        const result = (response.data as Record<string, unknown>)?.data as { messages?: MessageNewService[] } | undefined;
        if (result?.messages) {
          const allMsgs = result.messages as MessageNewService[];
          // Persist all messages (including thread) to cache
          messageCache.persistBatch(allMsgs).catch(() => {});
          // Only show non-thread messages in main channel view
          const channelMsgs = allMsgs.filter((m) => !m.thread);
          if (beforeSeq) {
            setMessages((prev) => [...channelMsgs, ...prev]);
            setHasMore(allMsgs.length >= 50);
          } else {
            setMessages(channelMsgs);
            setHasMore(allMsgs.length >= 50);
            if (allMsgs.length > 0) {
              lastSeq.current = allMsgs[allMsgs.length - 1].seq;
            }
          }
        }
      } catch (err) {
        console.error("Failed to load messages:", err);
      } finally {
        setLoadingMessages(false);
      }
    },
    [messageCache],
  );

  useEffect(() => {
    if (!roomId) return;

    if (prevRoomId.current && prevRoomId.current !== roomId) {
      unsubscribe(prevRoomId.current);
      setMessages([]);
      setHasMore(true);
      setTypingUsers([]);
      setStreamingMessages(new Map());
      setThreads([]);
      lastSeq.current = 0;
    }

    prevRoomId.current = roomId;
    subscribe(roomId);
    loadMessages(roomId);

    return () => {
      unsubscribe(roomId);
    };
  }, [roomId, subscribe, unsubscribe, loadMessages]);

  useEffect(() => {
    return onEvent((event: WsOutEvent) => {
      if (!roomId) return;

      switch (event.type) {
        case "message_new": {
          if (event.room.id !== roomId) break;
          const msg = event.data as MessageNewService;
          messageCache.persistMessage(msg).catch(() => {
          });
          // Only add to main channel if message is NOT in a thread
          if (!msg.thread) {
            setMessages((prev) => {
              if (prev.some((m) => m.id === msg.id)) return prev;
              return [...prev, msg];
            });
          }
          if (msg.seq > lastSeq.current) lastSeq.current = msg.seq;
          break;
        }
        case "message_edited": {
          if (event.room.id !== roomId) break;
          const edited = event.data as MessageEditedService;
          setMessages((prev) =>
              prev.map((m) =>
                  m.id === edited.id ? {...m, content: edited.content} : m,
              ),
          );
          break;
        }
        case "message_revoked": {
          if (event.room.id !== roomId) break;
          const revoked = event.data as MessageRevokedService;
          setMessages((prev) => prev.filter((m) => m.id !== revoked.id));
          break;
        }

        case "message_stream_start": {
          if (event.room.id !== roomId) break;
          const data = event.data as MessageStreamStartService;
          setStreamingMessages((prev) =>
              new Map(prev).set(data.message_id, {
                content: "",
                displayName: data.display_name ?? undefined,
              }),
          );
          break;
        }
        case "message_stream_chunk": {
          if (event.room.id !== roomId) break;
          const data = event.data as MessageStreamChunkService;
          setStreamingMessages((prev) => {
            const next = new Map(prev);
            const existing = next.get(data.message_id);
            next.set(data.message_id, {
              content: (existing?.content ?? "") + data.content,
              displayName:
                  existing?.displayName ?? data.display_name ?? undefined,
            });
            return next;
          });
          break;
        }
        case "message_stream_done": {
          if (event.room.id !== roomId) break;
          const data = event.data as MessageStreamDoneService;
          setStreamingMessages((prev) => {
            const next = new Map(prev);
            next.delete(data.message_id);
            return next;
          });
          if (data.error) {
            toast({
              title: "AI Error",
              description: data.error,
              variant: "destructive",
            });
          }
          break;
        }

        case "reaction_batch_updated": {
          if (event.room.id !== roomId) break;
          const batch = event.data as ReactionBatchUpdatedService;
          setMessages((prev) =>
              prev.map((m) =>
                  m.id === batch.message
                      ? {...m, reactions: batch.reactions}
                      : m,
              ),
          );
          break;
        }
        case "reaction_added": {
          if (event.room.id !== roomId) break;
          const r = event.data as ReactionAddedService;
          const reactedByMe = r.user.id === currentUserId;
          setMessages((prev) =>
              prev.map((m) => {
                if (m.id !== r.message) return m;
                const existing = m.reactions?.find((g) => g.emoji === r.emoji);
                if (existing) {
                  if (reactedByMe && existing.users.some((u) => u.id === currentUserId)) {
                    return m;
                  }
                  return {
                    ...m,
                    reactions: m.reactions.map((g) =>
                        g.emoji === r.emoji
                            ? {
                              ...g,
                              count: g.count + 1,
                              reacted_by_me: g.reacted_by_me || reactedByMe,
                              users: g.users.some((u) => u.id === r.user.id)
                                  ? g.users
                                  : [...g.users, r.user],
                            }
                            : g,
                    ),
                  };
                }
                return {
                  ...m,
                  reactions: [
                    ...(m.reactions ?? []),
                    {
                      emoji: r.emoji,
                      count: 1,
                      reacted_by_me: reactedByMe,
                      users: [r.user],
                    },
                  ],
                };
              }),
          );
          break;
        }
        case "reaction_removed": {
          if (event.room.id !== roomId) break;
          const r = event.data as ReactionRemovedService;
          const removedByMe = r.user.id === currentUserId;
          setMessages((prev) =>
              prev.map((m) => {
                if (m.id !== r.message) return m;
                if (removedByMe) {
                  const group = m.reactions?.find((g) => g.emoji === r.emoji);
                  if (group && !group.users.some((u) => u.id === currentUserId)) {
                    return m;
                  }
                }
                return {
                  ...m,
                  reactions: (m.reactions ?? []).flatMap((g) => {
                    if (g.emoji !== r.emoji) return [g];
                    const count = Math.max(0, g.count - 1);
                    return count > 0
                      ? [{ ...g, count, reacted_by_me: removedByMe ? false : g.reacted_by_me, users: g.users.filter((u) => u.id !== r.user.id) }]
                      : [];
                  }),
                };
              }),
          );
          break;
        }

        case "typing_start": {
          if (event.room.id !== roomId) break;
          const t = event.data as { user: { id: string; display_name?: string; username?: string } };
          const uid = t.user.id;
          if (!uid) break;
          setTypingUsers((prev) => {
            if (prev.some((u) => u.userId === uid)) return prev;
            return [
              ...prev,
              {
                userId: uid,
                displayName:
                    t.user.display_name || t.user.username || "Someone",
              },
            ];
          });
          break;
        }
        case "typing_stop": {
          if (event.room.id !== roomId) break;
          const sd = event.data as { user: { id: string } };
          const uid = sd.user.id;
          if (!uid) break;
          setTypingUsers((prev) => prev.filter((x) => x.userId !== uid));
          break;
        }

        case "pin_added": {
          if (event.room.id !== roomId) break;
          const pin = event.data as PinAddedService;
          setMessages((prev) =>
              prev.map((m) => (m.id === pin.message ? {...m, pinned: true} : m)),
          );
          break;
        }
        case "pin_removed": {
          if (event.room.id !== roomId) break;
          const pinR = event.data as PinRemovedService;
          setMessages((prev) =>
              prev.map((m) => (m.id === pinR.message ? {...m, pinned: false} : m)),
          );
          break;
        }

        case "thread_created": {
          if (event.room.id !== roomId) break;
          const tc = event.data as ThreadCreatedService;
          setThreads((prev) => {
            if (prev.some((t) => t.id === tc.id)) return prev;
            return [
              ...prev,
              {
                id: tc.id,
                room: tc.room.id,
                seq: 0,
                parent_seq: tc.parent,
                title: "",
                created_by: tc.created_by,
                archived: false,
                locked: false,
                last_message_at: null,
                created_at: tc.created_at,
              },
            ];
          });
          toast({
            title: "Thread created",
            description: `${tc.created_by.display_name || "Someone"} started a thread`,
          });
          break;
        }
        case "thread_updated": {
          if (event.room.id !== roomId) break;
          const tu = event.data as ThreadUpdatedService;
          setThreads((prev) =>
              prev.map((t) =>
                  t.id === tu.id
                      ? {
                        ...t,
                        last_message_at: tu.last_message_at,
                        last_message_preview: tu.last_message_preview,
                      }
                      : t,
              ),
          );
          break;
        }
        case "thread_resolved": {
          if (event.room.id !== roomId) break;
          const tr = event.data as ThreadResolvedService;
          setThreads((prev) =>
              prev.map((t) => (t.id === tr.id ? {...t, locked: true} : t)),
          );
          break;
        }
        case "thread_archived": {
          if (event.room.id !== roomId) break;
          const ta = event.data as ThreadArchivedService;
          setThreads((prev) =>
              prev.map((t) => (t.id === ta.id ? {...t, archived: true} : t)),
          );
          break;
        }

        case "room_deleted": {
          const d = event.data as RoomDeletedService;
          if (d.room.id === roomId) {
            toast({title: "Room deleted", description: `#${d.room.name} was deleted`});
          }
          queryClient.invalidateQueries({queryKey: ["channel", "rooms"]});
          break;
        }
        case "room_renamed": {
          const d = event.data as RoomRenamedService;
          toast({title: "Room renamed", description: `#${d.old_name} → #${d.new_name}`});
          queryClient.invalidateQueries({queryKey: ["channel", "rooms"]});
          break;
        }
        case "room_topic_updated": {
          const d = event.data as RoomTopicUpdatedService;
          queryClient.setQueryData<{ rooms: Room[]; categories: Category[] }>(
              ["channel", "rooms"],
              (old) => {
                if (!old) return old;
                return {
                  ...old,
                  rooms: old.rooms.map((r) =>
                      r.id === d.room.id ? {...r, topic: d.new_topic} : r,
                  ),
                };
              },
          );
          break;
        }

        case "room_created":
        case "room_moved":
        case "category_created":
        case "category_updated":
        case "category_deleted":
        case "workspace_room_created":
        case "workspace_room_deleted":
          queryClient.invalidateQueries({queryKey: ["channel", "rooms"]});
          break;

        case "room_settings_updated":
          toast({title: "Room settings updated"});
          queryClient.invalidateQueries({queryKey: ["channel", "rooms"]});
          break;

        case "user_banned": {
          const b = event.data as { user: { username?: string } };
          toast({title: "User banned", description: `${b.user.username || "User"} was banned`});
          queryClient.invalidateQueries({queryKey: ["channel", "rooms"]});
          break;
        }
        case "user_unbanned": {
          const ub = event.data as { user: { username?: string } };
          toast({title: "User unbanned", description: `${ub.user.username || "User"} was unbanned`});
          break;
        }

        case "invite_created": {
          const ic = event.data as { code: string };
          toast({title: "Invite created", description: `Invite code: ${ic.code}`});
          break;
        }
        case "invite_accepted": {
          const ia = event.data as { user: { username?: string } };
          toast({title: "Invite accepted", description: `${ia.user.username || "Someone"} joined`});
          queryClient.invalidateQueries({queryKey: ["channel", "rooms"]});
          break;
        }

        case "notify_created": {
          const nc = event.data as { title: string; content?: string | null };
          toast({title: nc.title, description: nc.content ?? undefined});
          break;
        }

        case "voice_channel_joined": {
          const vj = event.data as { user: { display_name?: string } };
          toast({title: "Voice", description: `${vj.user.display_name || "Someone"} joined voice`});
          break;
        }

        case "attachment_uploaded": {
          const au = event.data as { filename: string };
          toast({title: "File uploaded", description: au.filename});
          break;
        }
      }
    });
  }, [roomId, currentUserId, onEvent, queryClient, toast, messageCache]);

  const handleSend = useCallback(
    async (content: string, inReplyTo?: string) => {
      if (!roomId) return;
      try {
        const body: Record<string, unknown> = { content, content_type: "text" };
        if (inReplyTo) body.in_reply_to = inReplyTo;
        const res = await api.post(
          `/api/v1/ws/rooms/${roomId}/messages`,
          body,
        );
        // res.data = WsOutEvent::MessageNew { type, room, data: MessageNewService }
        const msg = (res.data as Record<string, unknown>)?.data as
          | MessageNewService
          | undefined;
        if (msg) {
          messageCache.persistMessage(msg).catch(() => {});
          setMessages((prev) => {
            if (prev.some((m) => m.id === msg.id)) return prev;
            return [...prev, msg];
          });
          if (msg.seq > lastSeq.current) lastSeq.current = msg.seq;
        }
      } catch (err) {
        console.error("Failed to send message:", err);
        toast({
          title: "Error",
          description: "Failed to send message",
          variant: "destructive",
        });
      }
    },
    [roomId, messageCache, toast],
  );

  const handleLoadMore = useCallback(() => {
    if (!roomId || messages.length === 0) return;
    loadMessages(roomId, messages[0].seq);
  }, [roomId, messages, loadMessages]);

  const handleTyping = useCallback(
    (typing: boolean) => {
      if (!roomId) return;
      send({
        type: typing ? "typing_start" : "typing_stop",
        room: roomId,
      }).catch(() => {});
    },
    [roomId, send],
  );

  const handlePinToggle = useCallback(
    async (messageId: string, pinned: boolean) => {
      if (!roomId) return;
      if (pinned) {
        await api.post(`/api/v1/ws/rooms/${roomId}/pins`, { message: messageId });
      } else {
        await api.delete(`/api/v1/ws/rooms/${roomId}/pins`, {
          data: { message: messageId },
        });
      }
    },
    [roomId],
  );

  const handleDeleteMessage = useCallback(
    async (messageId: string) => {
      try {
        await api.delete(`/api/v1/ws/messages/${messageId}`);
      } catch (err) {
        console.error("Failed to delete message:", err);
        toast({
          title: "Error",
          description: "Failed to delete message",
          variant: "destructive",
        });
      }
    },
    [toast],
  );

  const handleEditMessage = useCallback(
    async (messageId: string, content: string) => {
      try {
        await api.patch(`/api/v1/ws/messages/${messageId}`, { content });
      } catch (err) {
        console.error("Failed to edit message:", err);
        toast({
          title: "Error",
          description: "Failed to edit message",
          variant: "destructive",
        });
      }
    },
    [toast],
  );

  const handleStartThread = useCallback(
    async (_messageId: string, seq: number) => {
      if (!roomId) return;
      await api.post(`/api/v1/ws/rooms/${roomId}/threads`, { parent: seq });
    },
    [roomId],
  );

  const handleReactionToggle = useCallback(
    (messageId: string, emoji: string, add: boolean) => {
      setMessages((prev) =>
        prev.map((m) => {
          if (m.id !== messageId) return m;

          if (add) {
            const existing = m.reactions?.find((g) => g.emoji === emoji);
            if (existing && existing.users.some((u) => u.id === currentUserId)) {
              return m;
            }
            const me = {
              id: currentUserId ?? "",
              username: user?.username ?? "",
              display_name: user?.display_name ?? "",
              avatar_url: user?.avatar_url ?? "",
            };
            if (existing) {
              return {
                ...m,
                reactions: m.reactions!.map((g) =>
                  g.emoji === emoji
                    ? {
                        ...g,
                        count: g.count + 1,
                        reacted_by_me: true,
                        users: [...g.users, me],
                      }
                    : g,
                ),
              };
            }
            return {
              ...m,
              reactions: [
                ...(m.reactions ?? []),
                { emoji, count: 1, reacted_by_me: true, users: [me] },
              ],
            };
          } else {
            return {
              ...m,
              reactions: (m.reactions ?? []).flatMap((g) => {
                if (g.emoji !== emoji) return [g];
                const count = Math.max(0, g.count - 1);
                return count > 0
                  ? [{ ...g, count, reacted_by_me: false, users: g.users.filter((u) => u.id !== currentUserId) }]
                  : [];
              }),
            };
          }
        }),
      );
    },
    [currentUserId, user],
  );

  const typingText = useMemo(() => {
    if (typingUsers.length === 0) return null;
    if (typingUsers.length === 1)
      return `${typingUsers[0].displayName} is typing...`;
    if (typingUsers.length === 2)
      return `${typingUsers[0].displayName} and ${typingUsers[1].displayName} are typing...`;
    return `${typingUsers[0].displayName} and ${typingUsers.length - 1} others are typing...`;
  }, [typingUsers]);

  return {
    state: {
      messages,
      loadingMessages,
      hasMore,
      typingUsers,
      typingText,
      threads,
      streamingMessages,
      rooms,
      categories,
      currentRoom,
      loadingRooms,
      currentUserId,
    } satisfies ChannelState,
    actions: {
      handleSend,
      handleLoadMore,
      handleTyping,
      handlePinToggle,
      handleDeleteMessage,
      handleEditMessage,
      handleStartThread,
      handleReactionToggle,
    } satisfies ChannelActions,
  };
}
