/**
 * High-level React hooks for imks Socket.IO events.
 */

import { useCallback, useEffect, useRef, useState } from 'react';

import { useSocket } from './context';
import type {
  ArticleDeletePayload,
  ArticlePayload,
  ArticleUpdatePayload,
  ComponentInteractPayload,
  ComponentUpdatePayload,
  DraftPayload,
  MessageNewPayload,
  SendMessagePayload,
  ThreadCreatePayload,
  TypingPayloadServer,
} from './types';
import { ClientEvent, ServerEvent } from './types';

export interface UseChannelReturn {
  /** Join the channel room. */
  join: () => void;
  /** Leave the channel room. */
  leave: () => void;
  /** Send a text message. */
  sendMessage: (body: string, extra?: Partial<SendMessagePayload>) => void;
  /** Edit a message. */
  editMessage: (messageId: string, body: string) => void;
  /** Delete a message. */
  deleteMessage: (messageId: string) => void;
  /** Latest messages received from realtime events. */
  latestMessages: MessageNewPayload[];
  /** Clear the local message buffer. */
  clearMessages: () => void;
}

/** Join a channel and receive realtime message changes. */
export function useChannel(channelId: string): UseChannelReturn {
  const { state, emit, on } = useSocket();
  const [latestMessages, setLatestMessages] = useState<MessageNewPayload[]>([]);
  const joinedRef = useRef(false);

  useEffect(() => {
    if (state !== 'connected') return;

    emit(ClientEvent.ChannelJoin, { channel_id: channelId });
    joinedRef.current = true;

    return () => {
      emit(ClientEvent.ChannelLeave, { channel_id: channelId });
      joinedRef.current = false;
    };
  }, [state, channelId, emit]);

  useEffect(() => {
    const unsubNew = on(ServerEvent.MessageNew, (msg) => {
      if (msg.channel_id === channelId) {
        setLatestMessages((prev) => [...prev, msg]);
      }
    });

    const unsubUpdated = on(ServerEvent.MessageUpdated, (msg) => {
      if (msg.channel_id === channelId) {
        setLatestMessages((prev) => prev.map((m) => (m.id === msg.id ? { ...m, ...msg } : m)));
      }
    });

    const unsubDeleted = on(ServerEvent.MessageDeleted, (payload) => {
      if (payload.channel_id === channelId) {
        setLatestMessages((prev) => prev.filter((m) => m.id !== payload.id));
      }
    });

    return () => {
      unsubNew();
      unsubUpdated();
      unsubDeleted();
    };
  }, [on, channelId]);

  const join = useCallback(() => {
    emit(ClientEvent.ChannelJoin, { channel_id: channelId });
    joinedRef.current = true;
  }, [emit, channelId]);

  const leave = useCallback(() => {
    emit(ClientEvent.ChannelLeave, { channel_id: channelId });
    joinedRef.current = false;
  }, [emit, channelId]);

  const sendMessage = useCallback(
    (body: string, extra?: Partial<SendMessagePayload>) => {
      emit(ClientEvent.MessageSend, {
        channel_id: channelId,
        body,
        thread_id: extra?.thread_id ?? null,
        reply_to_message_id: extra?.reply_to_message_id ?? null,
        nonce: extra?.nonce,
        mentioned_user_ids: extra?.mentioned_user_ids,
        attachments: extra?.attachments,
        embeds: extra?.embeds,
        sticker: extra?.sticker,
        forward: extra?.forward,
      });
    },
    [emit, channelId],
  );

  const editMessage = useCallback(
    (messageId: string, body: string) => {
      emit(ClientEvent.MessageEdit, { message_id: messageId, body });
    },
    [emit],
  );

  const deleteMessage = useCallback(
    (messageId: string) => {
      emit(ClientEvent.MessageDelete, { message_id: messageId });
    },
    [emit],
  );

  const clearMessages = useCallback(() => {
    setLatestMessages([]);
  }, []);

  return {
    join,
    leave,
    sendMessage,
    editMessage,
    deleteMessage,
    latestMessages,
    clearMessages,
  };
}

export interface UseTypingReturn {
  /** Send a typing:start event. */
  startTyping: () => void;
  /** Send a typing:stop event. */
  stopTyping: () => void;
  /** Users currently typing in the channel. */
  typingUsers: TypingPayloadServer[];
}

/** Manage typing indicator events for a channel. */
export function useTypingIndicator(channelId: string): UseTypingReturn {
  const { emit, on } = useSocket();
  const [typingUsers, setTypingUsers] = useState<(TypingPayloadServer & { receivedAt: number })[]>(
    [],
  );

  useEffect(() => {
    const unsub = on(ServerEvent.Typing, (payload) => {
      if (payload.channel_id !== channelId) return;

      setTypingUsers((prev) => {
        const filtered = prev.filter((u) => u.user_id !== payload.user_id);
        if (!payload.typing) {
          return filtered;
        }
        return [...filtered, { ...payload, receivedAt: Date.now() }];
      });
    });

    return unsub;
  }, [on, channelId]);

  useEffect(() => {
    if (typingUsers.length === 0) return;

    const timer = setInterval(() => {
      const threshold = Date.now() - 5000;
      setTypingUsers((prev) => prev.filter((user) => user.receivedAt >= threshold));
    }, 1000);

    return () => clearInterval(timer);
  }, [typingUsers.length]);

  const startTyping = useCallback(() => {
    emit(ClientEvent.TypingStart, { channel_id: channelId });
  }, [emit, channelId]);

  const stopTyping = useCallback(() => {
    emit(ClientEvent.TypingStop, { channel_id: channelId });
  }, [emit, channelId]);

  return { startTyping, stopTyping, typingUsers };
}

/** Reaction event helpers. imks uses `content` for the reaction value. */
export function useReaction() {
  const { emit } = useSocket();

  const addReaction = useCallback(
    (messageId: string, content: string) => {
      emit(ClientEvent.ReactionAdd, { message_id: messageId, content });
    },
    [emit],
  );

  return { addReaction };
}

/** Pin event helpers. */
export function usePin() {
  const { emit } = useSocket();

  const pinMessage = useCallback(
    (channelId: string, messageId: string) => {
      emit(ClientEvent.PinAdd, { channel_id: channelId, message_id: messageId });
    },
    [emit],
  );

  const unpinMessage = useCallback(
    (channelId: string, messageId: string) => {
      emit(ClientEvent.PinRemove, { channel_id: channelId, message_id: messageId });
    },
    [emit],
  );

  return { pinMessage, unpinMessage };
}

/** Poll vote event helpers. */
export function usePollVote() {
  const { emit } = useSocket();

  const vote = useCallback(
    (pollId: string, optionId: string) => {
      emit(ClientEvent.PollVote, { poll_id: pollId, option_id: optionId });
    },
    [emit],
  );

  const removeVote = useCallback(
    (pollId: string, optionId: string) => {
      emit(ClientEvent.PollVoteRemove, { poll_id: pollId, option_id: optionId });
    },
    [emit],
  );

  return { vote, removeVote };
}

/** Draft event helpers. */
export function useDraft() {
  const { emit } = useSocket();

  const saveDraft = useCallback(
    (payload: DraftPayload & { body: string }) => {
      emit(ClientEvent.DraftSave, payload);
    },
    [emit],
  );

  const getDraft = useCallback(
    (channelId: string, threadId?: string | null) => {
      emit(ClientEvent.DraftGet, { channel_id: channelId, thread_id: threadId ?? null });
    },
    [emit],
  );

  const deleteDraft = useCallback(
    (channelId: string, threadId?: string | null) => {
      emit(ClientEvent.DraftDelete, { channel_id: channelId, thread_id: threadId ?? null });
    },
    [emit],
  );

  return { saveDraft, getDraft, deleteDraft };
}

/** Read state event helpers. */
export function useReadState() {
  const { emit } = useSocket();

  const markRead = useCallback(
    (channelId: string, messageId: string) => {
      emit(ClientEvent.ReadStateMark, { channel_id: channelId, message_id: messageId });
    },
    [emit],
  );

  const getReadState = useCallback(
    (channelId: string) => {
      emit(ClientEvent.ReadStateGet, { channel_id: channelId });
    },
    [emit],
  );

  return { markRead, getReadState };
}

/** Bookmark event helpers. */
export function useBookmark() {
  const { emit } = useSocket();

  const addBookmark = useCallback(
    (channelId: string, messageId: string, note?: string) => {
      emit(ClientEvent.BookmarkAdd, { channel_id: channelId, message_id: messageId, note });
    },
    [emit],
  );

  const removeBookmark = useCallback(
    (messageId: string) => {
      emit(ClientEvent.BookmarkRemove, { message_id: messageId });
    },
    [emit],
  );

  const listBookmarks = useCallback(
    (params?: { before?: string; limit?: number }) => {
      emit(ClientEvent.BookmarkList, params);
    },
    [emit],
  );

  return { addBookmark, removeBookmark, listBookmarks };
}

/** Thread event helpers. */
export function useThread() {
  const { emit } = useSocket();

  const createThread = useCallback(
    (payload: ThreadCreatePayload) => {
      emit(ClientEvent.ThreadCreate, payload);
    },
    [emit],
  );

  const resolveThread = useCallback(
    (threadId: string, resolved: boolean) => {
      emit(ClientEvent.ThreadResolve, { thread_id: threadId, resolved });
    },
    [emit],
  );

  const joinThread = useCallback(
    (threadId: string) => {
      emit(ClientEvent.ThreadJoin, { thread_id: threadId });
    },
    [emit],
  );

  const leaveThread = useCallback(
    (threadId: string) => {
      emit(ClientEvent.ThreadLeave, { thread_id: threadId });
    },
    [emit],
  );

  const listThreads = useCallback(
    (channelId: string) => {
      emit(ClientEvent.ThreadList, { channel_id: channelId });
    },
    [emit],
  );

  return { createThread, resolveThread, joinThread, leaveThread, listThreads };
}

/** Article event helpers. */
export function useArticle() {
  const { emit } = useSocket();

  const createArticle = useCallback(
    (payload: ArticlePayload) => {
      emit(ClientEvent.ArticleCreate, payload);
    },
    [emit],
  );

  const updateArticle = useCallback(
    (payload: ArticleUpdatePayload) => {
      emit(ClientEvent.ArticleUpdate, payload);
    },
    [emit],
  );

  const listArticles = useCallback(
    (channelId: string, limit?: number) => {
      emit(ClientEvent.ArticleList, { channel_id: channelId, limit });
    },
    [emit],
  );

  const deleteArticle = useCallback(
    (payload: ArticleDeletePayload) => {
      emit(ClientEvent.ArticleDelete, payload);
    },
    [emit],
  );

  return { createArticle, updateArticle, listArticles, deleteArticle };
}

/** Interactive component event helpers. */
export function useComponent() {
  const { emit } = useSocket();

  const interact = useCallback(
    (payload: ComponentInteractPayload) => {
      emit(ClientEvent.ComponentInteract, payload);
    },
    [emit],
  );

  const updateComponent = useCallback(
    (payload: ComponentUpdatePayload) => {
      emit(ClientEvent.ComponentUpdate, payload);
    },
    [emit],
  );

  return { interact, updateComponent };
}

/** Presence event helpers. */
export function usePresence() {
  const { emit } = useSocket();

  const updatePresence = useCallback(
    (channelId: string, online: boolean) => {
      emit(ClientEvent.PresenceUpdate, { channel_id: channelId, online });
    },
    [emit],
  );

  return { updatePresence };
}
