import { useEffect } from 'react';
import { WsFrameSchema } from '@boocode/contracts/ws-frames';
import { sessionEvents } from './sessionEvents';
import { createWsReconnectToast } from './wsReconnectToast';

const RECONNECT_INITIAL_MS = 1000;
const RECONNECT_MAX_MS = 30000;

export function useUserEvents(): void {
  useEffect(() => {
    let ws: WebSocket | null = null;
    let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
    let reconnectDelay = RECONNECT_INITIAL_MS;
    let unmounted = false;

    // v1.8.1: silent on the first disconnect; gray "reconnecting…" after 3
    // fails / 15 s; red "connection lost" with a Retry Now action after 60 s.
    const reconnectToast = createWsReconnectToast({
      label: 'Live updates',
      onRetryNow: () => {
        if (reconnectTimer) {
          clearTimeout(reconnectTimer);
          reconnectTimer = null;
          reconnectDelay = RECONNECT_INITIAL_MS;
          connect();
        }
      },
    });

    const connect = () => {
      if (unmounted) return;
      const url = new URL('/api/ws/user', window.location.href);
      url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
      ws = new WebSocket(url.toString());

      ws.onopen = () => {
        reconnectDelay = RECONNECT_INITIAL_MS;
        reconnectToast.onConnected();
      };

      ws.onmessage = (ev) => {
        // v1.13.11-a: Zod-validate every inbound frame. Fail-closed — invalid
        // frames are logged and dropped instead of dispatched onto the
        // sessionEvents bus where a stale or wrong shape would silently
        // corrupt sidebar / chat state.
        let raw: unknown;
        try {
          raw = JSON.parse(ev.data);
        } catch (err) {
          console.warn('useUserEvents: failed to parse frame', err);
          return;
        }
        const validated = WsFrameSchema.safeParse(raw);
        if (!validated.success) {
          console.error('ws-frame-validation-failed (user channel)', {
            frame_type: (raw as { type?: unknown })?.type,
            errors: validated.error.flatten(),
          });
          return;
        }
        // Bridge cast: Zod's union is broader than SessionEvent (it includes
        // per-session-channel frames too, which never arrive on the user
        // channel). sessionEvents.emit only dispatches frames whose type
        // appears in SessionEvent; the narrowing happens via the existing
        // useSidebar.ts applyEvent switch.
        sessionEvents.emit(
          validated.data as unknown as import('./sessionEvents').SessionEvent,
        );
      };

      ws.onclose = () => {
        if (unmounted) return;
        reconnectToast.onFailure();
        const delay = reconnectDelay;
        reconnectDelay = Math.min(reconnectDelay * 2, RECONNECT_MAX_MS);
        reconnectTimer = setTimeout(connect, delay);
      };

      ws.onerror = () => {
        // close handler will trigger reconnect; best-effort, ignore failure
        // because the socket may already be closing
        try { ws?.close(); } catch {}
      };
    };

    connect();

    return () => {
      unmounted = true;
      reconnectToast.dispose();
      if (reconnectTimer) clearTimeout(reconnectTimer);
      if (ws) try { ws.close(); } catch {}
    };
  }, []);
}
