"use client";

// v2 live stream: one IIOS MessageSocket for the whole Messenger panel, using the SDK
// (@insignia/iios-kernel-client) — not raw socket.io. The delegated realtime token comes from
// the be-crm data door (crm.messenger.realtime). Threads subscribe through a context; the socket
// re-opens every joined thread on reconnect (handled inside the SDK). In mock mode this is a no-op
// passthrough and the thread hook falls back to the REST poll.
//
// Beyond plain messages, the kernel exposes typing, read receipts, and reactions (generic
// annotations). This provider fans each server event out to per-thread listeners so the UI can
// render typing indicators, "seen" ticks, and emoji reactions live.

import { createContext, useCallback, useContext, useEffect, useRef, useState, type ReactNode } from "react";
import { MessageSocket, type Message, type AnnotationEvent } from "@insignia/iios-kernel-client";
import { useAuth, useQuery } from "@abe-kap/appshell-sdk/react";
import { isShellConfigured } from "./appshell";
import { toReactions, type UiMessage } from "./messenger-api";

interface RealtimeDTO { url: string; audience: string; token?: string }

export interface ReceiptHit { interactionId: string; actorId: string }

export interface MessengerSocket {
  ready: boolean;
  myUserId?: string;
  openThread: (threadId: string) => Promise<UiMessage[]>;
  send: (threadId: string, content: string, opts?: { parentInteractionId?: string; attachment?: { contentRef: string; mimeType: string; sizeBytes: number } }) => Promise<void>;
  subscribe: (threadId: string, cb: (m: UiMessage) => void) => () => void;
  /** Fires for EVERY inbound message regardless of thread — drives live sidebar previews. */
  onAnyMessage: (cb: (threadId: string, m: UiMessage) => void) => () => void;
  sendTyping: (threadId: string) => void;
  onTyping: (threadId: string, cb: (userId: string) => void) => () => void;
  markRead: (threadId: string, interactionId: string) => void;
  /** The kernel's receipt event carries no threadId, so this is a global stream; the thread hook
   *  filters to receipts for its own (mine) messages. */
  onReceipt: (cb: (e: ReceiptHit) => void) => () => void;
  react: (threadId: string, interactionId: string, emoji: string) => void;
  onAnnotation: (threadId: string, cb: (e: AnnotationEvent) => void) => () => void;
}

const Ctx = createContext<MessengerSocket | null>(null);
export function useMessengerSocket(): MessengerSocket | null { return useContext(Ctx); }

const SHELL = isShellConfigured();

const toUi = (m: Message, myUserId?: string): UiMessage => ({
  id: m.id, actorId: m.senderActorId ?? null, senderId: m.senderId ?? null, text: m.content ?? "", at: m.createdAt,
  mine: !!myUserId && m.senderId === myUserId,
  ...(m.parentInteractionId ? { parentInteractionId: m.parentInteractionId } : {}),
  ...(m.attachment ? { attachment: { contentRef: m.attachment.contentRef, mimeType: m.attachment.mimeType, sizeBytes: m.attachment.sizeBytes } } : {}),
  reactions: toReactions(m.annotations, myUserId),
});

export function MessengerSocketProvider({ children }: { children: ReactNode }) {
  // SHELL is a build-time constant, so the branch is stable across renders (Rules-of-Hooks safe).
  if (!SHELL) return <>{children}</>;
  return <LiveSocketProvider>{children}</LiveSocketProvider>;
}

// A tiny per-thread listener registry, reused for messages / typing / receipts / annotations.
function makeRegistry<T>() {
  const map = new Map<string, Set<(v: T) => void>>();
  const add = (key: string, cb: (v: T) => void) => {
    if (!map.has(key)) map.set(key, new Set());
    map.get(key)!.add(cb);
    return () => { map.get(key)?.delete(cb); };
  };
  const emit = (key: string, v: T) => map.get(key)?.forEach((cb) => cb(v));
  return { add, emit };
}

function LiveSocketProvider({ children }: { children: ReactNode }) {
  const { user } = useAuth();
  const rt = useQuery<RealtimeDTO>("crm.messenger.realtime", {});
  const [ready, setReady] = useState(false);
  const socketRef = useRef<MessageSocket | null>(null);
  const myRef = useRef<string | undefined>(user?.id);
  myRef.current = user?.id;

  // One registry per event kind, keyed by threadId (plus a global message fan-out).
  const msgReg = useRef(makeRegistry<UiMessage>()).current;
  const anyMsg = useRef(new Set<(threadId: string, m: UiMessage) => void>()).current;
  const typingReg = useRef(makeRegistry<string>()).current;
  const receiptSet = useRef(new Set<(e: ReceiptHit) => void>()).current;
  const annReg = useRef(makeRegistry<AnnotationEvent>()).current;

  const url = rt.data?.url;
  const token = rt.data?.token;

  useEffect(() => {
    if (!url || !token) return;
    const socket = new MessageSocket({ serviceUrl: url, token, autoConnect: false });
    socketRef.current = socket;
    const offConnected = socket.onConnected(() => setReady(true));
    const offMessage = socket.on("message", (m) => {
      const ui = toUi(m, myRef.current);
      msgReg.emit(m.threadId, ui);
      anyMsg.forEach((cb) => cb(m.threadId, ui));
    });
    const offTyping = socket.on("typing", (e) => { if (e.userId !== myRef.current) typingReg.emit(e.threadId, e.userId); });
    const offReceipt = socket.on("receipt", (e) => receiptSet.forEach((cb) => cb({ interactionId: e.interactionId, actorId: e.actorId })));
    const offAnn = socket.on("annotation", (e) => annReg.emit(e.threadId, e));
    socket.connect();
    return () => {
      offConnected(); offMessage(); offTyping(); offReceipt(); offAnn();
      socket.disconnect(); socketRef.current = null; setReady(false);
    };
  }, [url, token, msgReg, anyMsg, typingReg, receiptSet, annReg]);

  const openThread = useCallback(async (threadId: string): Promise<UiMessage[]> => {
    const s = socketRef.current;
    if (!s) return [];
    const res = await s.openThread(threadId);
    return res.history.map((m) => toUi(m, myRef.current));
  }, []);

  const send = useCallback(async (threadId: string, content: string, opts?: { parentInteractionId?: string; attachment?: { contentRef: string; mimeType: string; sizeBytes: number } }) => {
    const s = socketRef.current;
    if (!s) throw new Error("Not connected");
    const sendOpts = {
      ...(opts?.parentInteractionId ? { parentInteractionId: opts.parentInteractionId } : {}),
      ...(opts?.attachment ? { attachment: opts.attachment } : {}),
    };
    await s.sendMessage(threadId, content, Object.keys(sendOpts).length ? sendOpts : undefined);
  }, []);

  const subscribe = useCallback((threadId: string, cb: (m: UiMessage) => void) => msgReg.add(threadId, cb), [msgReg]);
  const onAnyMessage = useCallback((cb: (threadId: string, m: UiMessage) => void) => {
    anyMsg.add(cb); return () => { anyMsg.delete(cb); };
  }, [anyMsg]);
  const onTyping = useCallback((threadId: string, cb: (userId: string) => void) => typingReg.add(threadId, cb), [typingReg]);
  const onReceipt = useCallback((cb: (e: ReceiptHit) => void) => {
    receiptSet.add(cb); return () => { receiptSet.delete(cb); };
  }, [receiptSet]);
  const onAnnotation = useCallback((threadId: string, cb: (e: AnnotationEvent) => void) => annReg.add(threadId, cb), [annReg]);

  const sendTyping = useCallback((threadId: string) => socketRef.current?.typing(threadId), []);
  const markRead = useCallback((threadId: string, interactionId: string) => { void socketRef.current?.markRead(threadId, interactionId); }, []);
  const react = useCallback((threadId: string, interactionId: string, emoji: string) => { void socketRef.current?.react(threadId, interactionId, emoji); }, []);

  return (
    <Ctx.Provider value={{
      ready, myUserId: user?.id, openThread, send, subscribe, onAnyMessage,
      sendTyping, onTyping, markRead, onReceipt, react, onAnnotation,
    }}>
      {children}
    </Ctx.Provider>
  );
}
