import {
  createContext,
  useCallback,
  useContext,
  useEffect,
  useMemo,
  useRef,
  useState,
  type ReactNode,
} from 'react';
import { useChannel } from '@/hooks/use-channel';
import { useAuthStore } from '@/stores/auth-store';

export type ComposingMode = 'recording' | 'typing' | 'screen';

export interface ComposingUser {
  humanId: string;
  mode: ComposingMode;
  lastSeen: number;
}

interface StreamPresenceContextValue {
  onlineHumanIds: Set<string>;
  composingUsers: ComposingUser[];
  startComposing: (mode: ComposingMode) => void;
  stopComposing: () => void;
}

const COMPOSING_TIMEOUT_MS = 10_000;
const COMPOSING_HEARTBEAT_MS = 5_000;
const COMPOSING_CLEANUP_INTERVAL_MS = 2_000;

const StreamPresenceContext = createContext<StreamPresenceContextValue | null>(
  null,
);

interface StreamPresenceProviderProps {
  networkId: string;
  streamId: string;
  children: ReactNode;
}

export function StreamPresenceProvider({
  networkId,
  streamId,
  children,
}: StreamPresenceProviderProps) {
  const channelId = `stream:${networkId}:${streamId}`;
  const { presence, messages, sendMessage } = useChannel(channelId);
  const currentUserId = useAuthStore((s) => s.user?.id);

  const onlineHumanIds = useMemo(() => new Set(presence), [presence]);

  // --- Composing state ---
  const [composingUsers, setComposingUsers] = useState<ComposingUser[]>([]);
  const composingMapRef = useRef(new Map<string, ComposingUser>());
  const processedCountRef = useRef(0);

  // Process new messages incrementally — slicing the messages array means
  // we don't re-scan the whole history every render.
  useEffect(() => {
    if (messages.length <= processedCountRef.current) return;

    const newMessages = messages.slice(processedCountRef.current);
    processedCountRef.current = messages.length;

    let changed = false;
    const map = composingMapRef.current;

    for (const msg of newMessages) {
      const payload = msg.payload as
        | { type: string; mode?: string }
        | undefined;
      if (!payload?.type) continue;
      if (msg.humanId === currentUserId) continue;

      if (payload.type === 'composing_start' && payload.mode) {
        map.set(msg.humanId, {
          humanId: msg.humanId,
          mode: payload.mode as ComposingMode,
          lastSeen: Date.now(),
        });
        changed = true;
      } else if (payload.type === 'composing_stop') {
        if (map.delete(msg.humanId)) changed = true;
      }
    }

    if (changed) {
      setComposingUsers(Array.from(map.values()));
    }
  }, [messages, currentUserId]);

  // Drop composing entries when a user leaves the channel — covers the
  // "they backgrounded the app without sending stop" case.
  useEffect(() => {
    const map = composingMapRef.current;
    const onlineSet = new Set(presence);
    let changed = false;

    for (const humanId of map.keys()) {
      if (!onlineSet.has(humanId)) {
        map.delete(humanId);
        changed = true;
      }
    }

    if (changed) {
      setComposingUsers(Array.from(map.values()));
    }
  }, [presence]);

  // Sweep stale composing entries (last heartbeat > 10s ago).
  useEffect(() => {
    const interval = setInterval(() => {
      const map = composingMapRef.current;
      const now = Date.now();
      let changed = false;

      for (const [humanId, entry] of map) {
        if (now - entry.lastSeen > COMPOSING_TIMEOUT_MS) {
          map.delete(humanId);
          changed = true;
        }
      }

      if (changed) {
        setComposingUsers(Array.from(map.values()));
      }
    }, COMPOSING_CLEANUP_INTERVAL_MS);

    return () => clearInterval(interval);
  }, []);

  // --- Composing broadcast ---
  const heartbeatRef = useRef<ReturnType<typeof setInterval> | undefined>(
    undefined,
  );

  const startComposing = useCallback(
    (mode: ComposingMode) => {
      sendMessage({ type: 'composing_start', mode });
      if (heartbeatRef.current) clearInterval(heartbeatRef.current);
      heartbeatRef.current = setInterval(() => {
        sendMessage({ type: 'composing_start', mode });
      }, COMPOSING_HEARTBEAT_MS);
    },
    [sendMessage],
  );

  const stopComposing = useCallback(() => {
    if (heartbeatRef.current) clearInterval(heartbeatRef.current);
    heartbeatRef.current = undefined;
    sendMessage({ type: 'composing_stop' });
  }, [sendMessage]);

  useEffect(() => {
    return () => {
      if (heartbeatRef.current) clearInterval(heartbeatRef.current);
    };
  }, []);

  const value = useMemo<StreamPresenceContextValue>(
    () => ({
      onlineHumanIds,
      composingUsers,
      startComposing,
      stopComposing,
    }),
    [onlineHumanIds, composingUsers, startComposing, stopComposing],
  );

  return (
    <StreamPresenceContext.Provider value={value}>
      {children}
    </StreamPresenceContext.Provider>
  );
}

function useStreamPresenceContext() {
  const ctx = useContext(StreamPresenceContext);
  if (!ctx) {
    throw new Error(
      'useStreamPresence must be used within a StreamPresenceProvider',
    );
  }
  return ctx;
}

export function useStreamPresence() {
  const { onlineHumanIds } = useStreamPresenceContext();
  return { onlineHumanIds };
}

export function useStreamComposing() {
  const { composingUsers } = useStreamPresenceContext();
  return { composingUsers };
}

export function useStreamComposingBroadcast() {
  const { startComposing, stopComposing } = useStreamPresenceContext();
  return { startComposing, stopComposing };
}

/**
 * Like {@link useStreamComposingBroadcast}, but returns null instead of throwing
 * when rendered outside a provider — for callers (e.g. the compose dock) that
 * can appear both inside and outside a stream.
 */
export function useStreamComposingBroadcastOptional() {
  const ctx = useContext(StreamPresenceContext);
  if (!ctx) return null;
  return {
    startComposing: ctx.startComposing,
    stopComposing: ctx.stopComposing,
  };
}
