import {
  useCallback,
  useMemo,
  type Dispatch,
  type MutableRefObject,
  type SetStateAction,
} from 'react';
import { api } from '../api';
import type { UseWebSocketOptions } from '../useWebSocket';
import { toast } from '../components/ui/sonner';
import { getStateKey } from '../utils/conversationState';
import { mergeContactIntoList } from '../utils/contactMerge';
import { getContactDisplayName } from '../utils/pubkey';
import { appendRawPacketUnique } from '../utils/rawPacketIdentity';
import type {
  Channel,
  Contact,
  Conversation,
  HealthStatus,
  Message,
  MessagePath,
  RawPacket,
} from '../types';

interface UseRealtimeAppStateArgs {
  prevHealthRef: MutableRefObject<HealthStatus | null>;
  setHealth: Dispatch<SetStateAction<HealthStatus | null>>;
  fetchConfig: () => void | Promise<void>;
  setRawPackets: Dispatch<SetStateAction<RawPacket[]>>;
  reconcileOnReconnect: () => void;
  refreshUnreads: () => Promise<void>;
  setChannels: Dispatch<SetStateAction<Channel[]>>;
  fetchAllContacts: () => Promise<Contact[]>;
  setContacts: Dispatch<SetStateAction<Contact[]>>;
  blockedKeysRef: MutableRefObject<string[]>;
  blockedNamesRef: MutableRefObject<string[]>;
  activeConversationRef: MutableRefObject<Conversation | null>;
  observeMessage: (msg: Message) => { added: boolean; activeConversation: boolean };
  recordMessageEvent: (args: {
    msg: Message;
    activeConversation: boolean;
    isNewMessage: boolean;
    hasMention?: boolean;
  }) => void;
  renameConversationState: (oldStateKey: string, newStateKey: string) => void;
  checkMention: (text: string) => boolean;
  pendingDeleteFallbackRef: MutableRefObject<boolean>;
  setActiveConversation: (conv: Conversation | null) => void;
  renameConversationMessages: (oldId: string, newId: string) => void;
  removeConversationMessages: (conversationId: string) => void;
  receiveMessageAck: (
    messageId: number,
    ackCount: number,
    paths?: MessagePath[],
    packetId?: number | null
  ) => void;
  notifyIncomingMessage?: (msg: Message) => void;
  recordRawPacketObservation?: (packet: RawPacket) => void;
  maxRawPackets?: number;
}

function isMessageBlocked(msg: Message, blockedKeys: string[], blockedNames: string[]): boolean {
  if (msg.outgoing) {
    return false;
  }

  if (blockedKeys.length > 0) {
    if (msg.type === 'PRIV' && blockedKeys.includes(msg.conversation_key.toLowerCase())) {
      return true;
    }
    if (
      msg.type === 'CHAN' &&
      msg.sender_key &&
      blockedKeys.includes(msg.sender_key.toLowerCase())
    ) {
      return true;
    }
  }

  return blockedNames.length > 0 && !!msg.sender_name && blockedNames.includes(msg.sender_name);
}

export function useRealtimeAppState({
  prevHealthRef,
  setHealth,
  fetchConfig,
  setRawPackets,
  reconcileOnReconnect,
  refreshUnreads,
  setChannels,
  fetchAllContacts,
  setContacts,
  blockedKeysRef,
  blockedNamesRef,
  activeConversationRef,
  observeMessage,
  recordMessageEvent,
  renameConversationState,
  checkMention,
  pendingDeleteFallbackRef,
  setActiveConversation,
  renameConversationMessages,
  removeConversationMessages,
  receiveMessageAck,
  notifyIncomingMessage,
  recordRawPacketObservation,
  maxRawPackets = 500,
}: UseRealtimeAppStateArgs): UseWebSocketOptions {
  const mergeChannelIntoList = useCallback(
    (updated: Channel) => {
      setChannels((prev) => {
        const existingIndex = prev.findIndex((channel) => channel.key === updated.key);
        if (existingIndex === -1) {
          return [...prev, updated].sort((a, b) => a.name.localeCompare(b.name));
        }
        const next = [...prev];
        next[existingIndex] = updated;
        return next;
      });
    },
    [setChannels]
  );

  return useMemo(
    () => ({
      onHealth: (data: HealthStatus) => {
        const prev = prevHealthRef.current;
        prevHealthRef.current = data;
        setHealth(data);
        const nextRadioState =
          data.radio_state ??
          (data.radio_initializing
            ? 'initializing'
            : data.radio_connected
              ? 'connected'
              : 'disconnected');
        const initializationCompleted =
          prev !== null &&
          prev.radio_connected &&
          prev.radio_initializing &&
          data.radio_connected &&
          !data.radio_initializing;

        if (prev !== null && prev.radio_connected !== data.radio_connected) {
          if (data.radio_connected) {
            toast.success('Radio connected', {
              description: data.connection_info
                ? `Connected via ${data.connection_info}`
                : undefined,
            });
            fetchConfig();
          } else {
            if (nextRadioState === 'paused') {
              toast.success('Radio connection paused');
            } else {
              toast.error('Radio disconnected', {
                description: 'Check radio connection and power',
              });
            }
          }
        }

        if (initializationCompleted) {
          fetchConfig();
        }
      },
      onError: (error: { message: string; details?: string }) => {
        toast.error(error.message, {
          description: error.details,
        });
      },
      onSuccess: (success: { message: string; details?: string }) => {
        toast.success(success.message, {
          description: success.details,
        });
      },
      onReconnect: () => {
        setRawPackets([]);
        reconcileOnReconnect();
        refreshUnreads();
        api.getChannels().then(setChannels).catch(console.error);
        fetchAllContacts()
          .then((data) => setContacts(data))
          .catch(console.error);
      },
      onMessage: (msg: Message) => {
        if (isMessageBlocked(msg, blockedKeysRef.current, blockedNamesRef.current)) {
          return;
        }

        const { added: isNewMessage, activeConversation: isForActiveConversation } =
          observeMessage(msg);
        recordMessageEvent({
          msg,
          activeConversation: isForActiveConversation,
          isNewMessage,
          hasMention: checkMention(msg.text),
        });

        if (!msg.outgoing && isNewMessage) {
          notifyIncomingMessage?.(msg);
        }
      },
      onContact: (contact: Contact) => {
        setContacts((prev) => mergeContactIntoList(prev, contact));
      },
      onContactResolved: (previousPublicKey: string, contact: Contact) => {
        setContacts((prev) =>
          mergeContactIntoList(
            prev.filter((candidate) => candidate.public_key !== previousPublicKey),
            contact
          )
        );
        renameConversationMessages(previousPublicKey, contact.public_key);
        renameConversationState(
          getStateKey('contact', previousPublicKey),
          getStateKey('contact', contact.public_key)
        );

        const active = activeConversationRef.current;
        if (active?.type === 'contact' && active.id === previousPublicKey) {
          setActiveConversation({
            type: 'contact',
            id: contact.public_key,
            name: getContactDisplayName(contact.name, contact.public_key, contact.last_advert),
          });
        }
      },
      onChannel: (channel: Channel) => {
        mergeChannelIntoList(channel);
      },
      onContactDeleted: (publicKey: string) => {
        setContacts((prev) => prev.filter((c) => c.public_key !== publicKey));
        removeConversationMessages(publicKey);
        const active = activeConversationRef.current;
        if (active?.type === 'contact' && active.id === publicKey) {
          pendingDeleteFallbackRef.current = true;
          setActiveConversation(null);
        }
      },
      onChannelDeleted: (key: string) => {
        setChannels((prev) => prev.filter((c) => c.key !== key));
        removeConversationMessages(key);
        const active = activeConversationRef.current;
        if (active?.type === 'channel' && active.id === key) {
          pendingDeleteFallbackRef.current = true;
          setActiveConversation(null);
        }
      },
      onRawPacket: (packet: RawPacket) => {
        recordRawPacketObservation?.(packet);
        setRawPackets((prev) => appendRawPacketUnique(prev, packet, maxRawPackets));
      },
      onMessageAcked: (
        messageId: number,
        ackCount: number,
        paths?: MessagePath[],
        packetId?: number | null
      ) => {
        receiveMessageAck(messageId, ackCount, paths, packetId);
      },
    }),
    [
      activeConversationRef,
      blockedKeysRef,
      blockedNamesRef,
      checkMention,
      fetchAllContacts,
      fetchConfig,
      renameConversationState,
      renameConversationMessages,
      maxRawPackets,
      mergeChannelIntoList,
      pendingDeleteFallbackRef,
      prevHealthRef,
      recordMessageEvent,
      recordRawPacketObservation,
      receiveMessageAck,
      observeMessage,
      refreshUnreads,
      reconcileOnReconnect,
      removeConversationMessages,
      setActiveConversation,
      setChannels,
      setContacts,
      setHealth,
      setRawPackets,
      notifyIncomingMessage,
    ]
  );
}
