// Coder user-channel WS — mirrors useUserEvents but connects to the BooCoder
// host service's /api/coder/ws/user endpoint. Forwards flow_run_started and
// flow_run_step_updated frames onto the sessionEvents bus so OrchestratorPane
// can subscribe to run-level lifecycle updates without a per-session WS.
//
// Event-dedup discipline: do NOT additionally emit these frames locally after
// a POST /api/coder/runs call — this hook forwards the authoritative WS frame.
import { useEffect } from 'react';
import { WsFrameSchema } from '@boocode/contracts/ws-frames';
import { sessionEvents } from './sessionEvents';
import type {
  AgentMessageEvent,
  BattleStartedEvent,
  BattleUpdatedEvent,
  CollisionWarningEvent,
  ContestantUpdatedEvent,
  FlowRunStartedEvent,
  FlowRunStepUpdatedEvent,
} from './sessionEvents';

const RECONNECT_INITIAL_MS = 1000;
const RECONNECT_MAX_MS = 30_000;

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

    const connect = () => {
      if (unmounted) return;
      const proto = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
      ws = new WebSocket(`${proto}//${window.location.host}/api/coder/ws/user`);

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

      ws.onmessage = (ev) => {
        let raw: unknown;
        try {
          raw = JSON.parse(ev.data as string);
        } catch {
          return;
        }
        const validated = WsFrameSchema.safeParse(raw);
        if (!validated.success) {
          console.error('ws-frame-validation-failed (coder user channel)', {
            frame_type: (raw as { type?: unknown })?.type,
            errors: validated.error.flatten(),
          });
          return;
        }
        const frame = validated.data;
        if (frame.type === 'flow_run_started') {
          sessionEvents.emit(frame as unknown as FlowRunStartedEvent);
        } else if (frame.type === 'flow_run_step_updated') {
          sessionEvents.emit(frame as unknown as FlowRunStepUpdatedEvent);
        } else if (frame.type === 'battle_started') {
          sessionEvents.emit(frame as unknown as BattleStartedEvent);
        } else if (frame.type === 'contestant_updated') {
          sessionEvents.emit(frame as unknown as ContestantUpdatedEvent);
        } else if (frame.type === 'battle_updated') {
          sessionEvents.emit(frame as unknown as BattleUpdatedEvent);
        } else if (frame.type === 'agent_message') {
          sessionEvents.emit({
            type: 'agent_message',
            from_agent: frame.sender_step_id,
            to_agent: frame.channel ?? '',
            content: frame.content,
          } as AgentMessageEvent);
        } else if (frame.type === 'collision_warning') {
          sessionEvents.emit({
            type: 'collision_warning',
            file_path: frame.file_path,
            agents: frame.agents,
          } as CollisionWarningEvent);
        }
      };

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

      ws.onerror = () => {
        try { ws?.close(); } catch {}
      };
    };

    connect();

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