"use client";
import { useCallback, useEffect, useRef, useState } from 'react';

// Lifecycle hook for the OpenAI Realtime WebRTC session.
//
// State machine: idle → connecting → live → ending → idle. The hook
// owns the RTCPeerConnection, mic MediaStream, data channel, remote audio
// element, and the audio-level meter that drives the orb visualizer.
//
// Tool calls are received over the data channel as `response.function_call`
// events (per the OpenAI Realtime contract). The hook relays them to
// /api/voice/realtime/tool-result on the server (which routes through
// executeVoiceTool — same approval/rate-limit/allowlist gates as typed chat)
// and then writes the JSON result back to the model with a
// `response.create` + `conversation.item.create` pair.

const STATE_IDLE = 'idle';
const STATE_CONNECTING = 'connecting';
const STATE_LIVE = 'live';
const STATE_ENDING = 'ending';
const STATE_ERROR = 'error';

function safeJson(text) {
  try { return JSON.parse(text); } catch { return null; }
}

export function useRealtimeVoice({ projectId = null, agentId = 'main', activeSpace = null, allSpaces = null, onTranscript } = {}) {
  const [state, setState] = useState(STATE_IDLE);
  const [error, setError] = useState(null);
  const [transcript, setTranscript] = useState([]);
  const [audioLevel, setAudioLevel] = useState(0);
  const [pendingToolCalls, setPendingToolCalls] = useState([]);
  // Last ~6 finished tool calls so the panel can show a live activity strip
  // ("web_search · web_fetch · create_artifact"). Items: { call_id, name,
  // status: 'ok'|'error'|'approval', duration_ms, ended_at }.
  const [recentToolCalls, setRecentToolCalls] = useState([]);
  const [sessionInfo, setSessionInfo] = useState(null);
  const [muted, setMuted] = useState(false);

  const pcRef = useRef(null);
  const dcRef = useRef(null);
  const micStreamRef = useRef(null);
  const audioElRef = useRef(null);
  const analyserRef = useRef(null);
  const rafRef = useRef(null);
  // Tracks the in-flight response id so we don't dispatch a second
  // `response.create` while one is already streaming — the server rejects
  // that with "Conversation already has an active response in progress".
  const activeResponseIdRef = useRef(null);
  const pendingResponseCreateRef = useRef(false);

  const cleanup = useCallback(() => {
    try { dcRef.current?.close(); } catch {}
    try { pcRef.current?.close(); } catch {}
    try { micStreamRef.current?.getTracks().forEach(t => t.stop()); } catch {}
    if (rafRef.current) cancelAnimationFrame(rafRef.current);
    try {
      const el = audioElRef.current;
      if (el) {
        try { el.pause(); } catch {}
        try { el.srcObject = null; } catch {}
      }
    } catch {}
    pcRef.current = null;
    dcRef.current = null;
    micStreamRef.current = null;
    analyserRef.current = null;
    rafRef.current = null;
    activeResponseIdRef.current = null;
    pendingResponseCreateRef.current = false;
    setAudioLevel(0);
    setMuted(false);
  }, []);

  // Bound transcript length so a long Realtime session doesn't grow the
  // React state forever. The visible window is the last 8 lines anyway.
  const appendTranscript = useCallback((line) => {
    setTranscript(list => {
      const next = list.concat([line]);
      return next.length > 80 ? next.slice(next.length - 80) : next;
    });
  }, []);

  const stop = useCallback(() => {
    setState(STATE_ENDING);
    cleanup();
    setState(STATE_IDLE);
  }, [cleanup]);

  useEffect(() => {
    return () => cleanup();
  }, [cleanup]);

  const sendEvent = useCallback((event) => {
    const dc = dcRef.current;
    if (!dc || dc.readyState !== 'open') return false;
    try { dc.send(JSON.stringify(event)); return true; } catch { return false; }
  }, []);

  const handleToolCall = useCallback(async (event) => {
    // OpenAI Realtime emits function_call events shaped like:
    //   { type: 'response.function_call_arguments.done', call_id, name, arguments }
    const callId = event.call_id || event.id;
    const name = event.name;
    if (!name || !callId) return;
    let args = {};
    if (typeof event.arguments === 'string') {
      args = safeJson(event.arguments) || {};
    } else if (event.arguments && typeof event.arguments === 'object') {
      args = event.arguments;
    }
    const startedAt = Date.now();
    setPendingToolCalls(list => [...list, { call_id: callId, name, started_at: startedAt }]);
    let result = null;
    try {
      const res = await fetch('/api/voice/realtime/tool-result', {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({
          session_id: sessionInfo?.id || null,
          call_id: callId,
          name,
          args,
          project_id: projectId,
          agent_id: agentId,
        }),
      });
      result = await res.json();
    } catch (err) {
      result = { ok: false, error: String(err?.message || err) };
    }
    setPendingToolCalls(list => list.filter(c => c.call_id !== callId));
    const status = result?.result?.status === 'approval_required'
      ? 'approval'
      : (result?.ok === false ? 'error' : 'ok');
    setRecentToolCalls(list => {
      const next = list.concat([{
        call_id: callId,
        name,
        status,
        duration_ms: Date.now() - startedAt,
        ended_at: Date.now(),
      }]);
      return next.length > 6 ? next.slice(next.length - 6) : next;
    });

    // Hand the result back to the model.
    sendEvent({
      type: 'conversation.item.create',
      item: {
        type: 'function_call_output',
        call_id: callId,
        output: JSON.stringify(result?.result ?? result ?? { ok: false }).slice(0, 16000),
      },
    });
    // Only kick off a new response when none is in flight; otherwise queue
    // it so `response.done` can drain. Sending `response.create` while a
    // response is active returns code: conversation_already_has_active_response.
    if (activeResponseIdRef.current) {
      pendingResponseCreateRef.current = true;
    } else {
      sendEvent({ type: 'response.create' });
    }
  }, [agentId, projectId, sendEvent, sessionInfo?.id]);

  const handleMessage = useCallback((raw) => {
    const event = safeJson(raw);
    if (!event || typeof event !== 'object') return;
    switch (event.type) {
      case 'session.created':
      case 'session.updated':
        // server-side ack; no UI change beyond keeping state live.
        break;
      case 'response.audio_transcript.delta':
        if (event.delta) {
          appendTranscript({ role: 'agent', delta: event.delta, at: Date.now() });
          onTranscript?.({ role: 'agent', delta: event.delta });
        }
        break;
      case 'response.audio_transcript.done':
        // final transcript for the assistant's spoken reply
        if (event.transcript) {
          appendTranscript({ role: 'agent', text: event.transcript, at: Date.now() });
        }
        break;
      case 'conversation.item.input_audio_transcription.completed':
        if (event.transcript) {
          appendTranscript({ role: 'user', text: event.transcript, at: Date.now() });
        }
        break;
      case 'response.created':
        activeResponseIdRef.current = event.response?.id || 'unknown';
        break;
      case 'response.done':
      case 'response.cancelled':
        activeResponseIdRef.current = null;
        // Drain a queued response.create if a tool result arrived while the
        // previous response was still streaming.
        if (pendingResponseCreateRef.current) {
          pendingResponseCreateRef.current = false;
          sendEvent({ type: 'response.create' });
        }
        break;
      case 'response.function_call_arguments.done':
        handleToolCall(event);
        break;
      case 'error': {
        const code = event.error?.code || '';
        // This specific race is recoverable: it just means the server is
        // already producing a response, and our queued `response.create`
        // (above) will fire when `response.done` arrives. Don't blow up
        // the session over it.
        if (code === 'conversation_already_has_active_response' || code === 'response_already_active') {
          pendingResponseCreateRef.current = true;
          break;
        }
        setError(event.error?.message || 'realtime error');
        break;
      }
      default:
        break;
    }
  }, [handleToolCall, onTranscript, sendEvent]);

  const startAudioLevelMeter = useCallback((stream) => {
    try {
      const AC = window.AudioContext || window.webkitAudioContext;
      if (!AC) return;
      const ctx = new AC();
      const src = ctx.createMediaStreamSource(stream);
      const analyser = ctx.createAnalyser();
      analyser.fftSize = 512;
      src.connect(analyser);
      analyserRef.current = analyser;
      const buffer = new Uint8Array(analyser.frequencyBinCount);
      const tick = () => {
        if (!analyserRef.current) return;
        analyserRef.current.getByteFrequencyData(buffer);
        let sum = 0;
        for (let i = 0; i < buffer.length; i++) sum += buffer[i];
        const avg = sum / buffer.length;
        setAudioLevel(Math.min(1, avg / 140));
        rafRef.current = requestAnimationFrame(tick);
      };
      rafRef.current = requestAnimationFrame(tick);
    } catch {
      // best-effort meter; orb falls back to a quiet pulse
    }
  }, []);

  const start = useCallback(async () => {
    if (state === STATE_CONNECTING || state === STATE_LIVE) return;
    setError(null);
    setTranscript([]);
    setState(STATE_CONNECTING);

    try {
      // 1) Mint an ephemeral session via the server. This is where the long-lived
      // OPENAI_API_KEY stays — it never crosses to the browser. The server also
      // hydrates the instruction block from project + memory + state.
      // Pull user-selected realtime model + voice from the config panel
      // (persisted to localStorage). Server will fall back to its env defaults
      // when these aren't sent.
      let realtimeModel, realtimeVoice, speakingStyle;
      try {
        realtimeModel = window.localStorage.getItem('madarx:realtime-model') || undefined;
        realtimeVoice = window.localStorage.getItem('madarx:realtime-voice') || undefined;
        speakingStyle = window.localStorage.getItem('madarx:speaking-style') || undefined;
      } catch {}
      const mintRes = await fetch('/api/voice/realtime/session', {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({
          project_id: projectId,
          agent_id: agentId,
          model: realtimeModel,
          voice: realtimeVoice,
          speaking_style: speakingStyle,
          active_space: activeSpace,
          all_spaces: allSpaces,
        }),
      });
      if (!mintRes.ok) {
        const text = await mintRes.text().catch(() => '');
        throw new Error(`mint failed: ${mintRes.status} ${text.slice(0, 240)}`);
      }
      const session = await mintRes.json();
      if (!session.client_secret) throw new Error('mint returned no client_secret');
      setSessionInfo({ id: session.id, model: session.model, voice: session.voice });

      // 2) Open WebRTC peer connection to OpenAI Realtime.
      const pc = new RTCPeerConnection();
      pcRef.current = pc;

      const audioEl = audioElRef.current || (audioElRef.current = new Audio());
      audioEl.autoplay = true;
      pc.ontrack = (event) => {
        if (event.streams && event.streams[0]) {
          audioEl.srcObject = event.streams[0];
        }
      };

      // 3) Capture mic and add the track.
      let mic;
      try {
        mic = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true } });
      } catch (err) {
        cleanup();
        setState(STATE_ERROR);
        setError(err?.name === 'NotAllowedError' ? 'Microphone permission denied' : 'Microphone unavailable');
        return;
      }
      micStreamRef.current = mic;
      mic.getTracks().forEach(track => pc.addTrack(track, mic));
      startAudioLevelMeter(mic);

      // 4) Data channel for events.
      const dc = pc.createDataChannel('oai-events');
      dcRef.current = dc;
      dc.onmessage = (event) => handleMessage(event.data);
      dc.onopen = () => setState(STATE_LIVE);
      dc.onclose = () => {
        cleanup();
        setState(STATE_IDLE);
      };

      // 5) SDP offer/answer with OpenAI GA endpoint. Model is bound to the
      // ephemeral client_secret from the mint step, so no ?model= query needed.
      // Beta endpoint /v1/realtime?model=... was removed and now returns 400
      // "beta_api_shape_disabled".
      const offer = await pc.createOffer();
      await pc.setLocalDescription(offer);
      const sdpRes = await fetch('https://api.openai.com/v1/realtime/calls', {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${session.client_secret}`,
          'Content-Type': 'application/sdp',
        },
        body: offer.sdp,
      });
      if (!sdpRes.ok) {
        const detail = await sdpRes.text().catch(() => '');
        throw new Error(`sdp exchange failed: ${sdpRes.status} ${detail.slice(0, 240)}`);
      }
      const answerSdp = await sdpRes.text();
      await pc.setRemoteDescription({ type: 'answer', sdp: answerSdp });
    } catch (err) {
      cleanup();
      setState(STATE_ERROR);
      setError(String(err?.message || err));
    }
  }, [activeSpace, agentId, allSpaces, cleanup, handleMessage, projectId, startAudioLevelMeter, state]);

  // Toggle the local mic track without dropping the WebRTC connection. The
  // model still streams audio out (so the user can listen), but their voice
  // doesn't reach the model until they unmute. Mirrors the mute button on
  // every other call client (Zoom, Meet, ChatGPT voice, etc.).
  const toggleMute = useCallback(() => {
    const mic = micStreamRef.current;
    if (!mic) return;
    const next = !muted;
    mic.getAudioTracks().forEach(t => { t.enabled = !next; });
    setMuted(next);
  }, [muted]);

  return {
    state,
    error,
    transcript,
    audioLevel,
    pendingToolCalls,
    recentToolCalls,
    sessionInfo,
    muted,
    toggleMute,
    start,
    stop,
  };
}
