'use client';

import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react';

import {
  EMPTY_TRANSCRIPT,
  INITIAL_STATE,
  buildTranscript,
  getSpeechLogger,
  reducer,
} from '../core';

const log = getSpeechLogger();
import { createWebSpeechEngine } from '../core/engine/webspeech';
import { useSpeechPrefs } from '../store/prefsStore';
import type {
  RecognitionEngine,
  Segment,
  UseSpeechRecognitionConfig,
  UseSpeechRecognitionReturn,
} from '../types';
import { useMicLevel } from './useMicLevel';
import { useResolvedLanguage } from './useResolvedLanguage';

/**
 * Main entry point. With no config it uses the browser Web Speech API
 * and the persisted language from `useSpeechPrefs`. Pass a custom
 * `engine` to route through Deepgram / Whisper / custom WebSocket.
 */
export function useSpeechRecognition(
  config: UseSpeechRecognitionConfig = {},
): UseSpeechRecognitionReturn {
  const prefs = useSpeechPrefs();
  const language = useResolvedLanguage(config.language);
  const engine = useMemo<RecognitionEngine>(
    () => config.engine ?? createWebSpeechEngine(),
    [config.engine],
  );

  const [state, dispatch] = useReducer(reducer, INITIAL_STATE);
  const [stream, setStream] = useState<MediaStream | null>(null);
  const level = useMicLevel(stream);

  // Latest-callback refs so engine subscriptions never tear down on
  // every render — same trick the Chat reducer uses.
  const cbRef = useRef(config);
  cbRef.current = config;

  // Engine subscription lifecycle.
  useEffect(() => {
    log.engine.debug('subscribe', { engineId: engine.id });
    const offs = [
      engine.on('partial', (text, segmentId) => {
        log.engine.debug('partial', { len: text.length, segmentId });
        dispatch({ type: 'PARTIAL', text, segmentId });
        const seg: Segment = {
          id: segmentId,
          text,
          isFinal: false,
          startedAt: Date.now(),
        };
        cbRef.current.onPartial?.(text, seg);
      }),
      engine.on('final', (text, segmentId, confidence) => {
        log.engine.info('final', {
          len: text.length,
          segmentId,
          confidence,
        });
        dispatch({ type: 'FINAL', text, segmentId, confidence });
        const seg: Segment = {
          id: segmentId,
          text,
          isFinal: true,
          confidence,
          startedAt: Date.now(),
          endedAt: Date.now(),
        };
        cbRef.current.onFinal?.(text, seg);
      }),
      engine.on('error', (err) => {
        log.error.error('engine error', err);
        dispatch({ type: 'ERROR', error: err });
        cbRef.current.onError?.(err);
      }),
      engine.on('state', (s) => {
        log.engine.debug('state', s);
        if (s === 'listening') {
          dispatch({ type: 'STARTED' });
          cbRef.current.onStart?.();
          setStream(engine.getStream?.() ?? null);
        } else if (s === 'closed') {
          dispatch({ type: 'STOPPED' });
          cbRef.current.onStop?.();
          setStream(null);
        }
      }),
    ];
    return () => {
      offs.forEach((off) => off());
      // Release the mic / socket if the consumer unmounts (or swaps
      // the engine) mid-session. Without this the MediaRecorder and
      // getUserMedia stream keep running headless after unmount.
      engine.abort();
    };
  }, [engine]);

  // AutoStop driven by silence + maxMs caps. `level` updates ~60fps,
  // so it must NOT live in the effect deps — otherwise the silence
  // interval is torn down and recreated every animation frame, which
  // both wastes CPU and resets the silence timer before it can fire.
  // The interval reads the freshest level through a ref instead.
  const silenceTimer = useRef<number | null>(null);
  const maxTimer = useRef<number | null>(null);
  const levelRef = useRef(level);
  levelRef.current = level;
  useEffect(() => {
    if (state.status !== 'listening') return undefined;
    const { silenceMs, maxMs, silenceThreshold = 0.02 } = config.autoStop ?? {};
    if (maxMs) {
      maxTimer.current = window.setTimeout(() => {
        log.engine.debug('autoStop max duration hit');
        void engine.stop();
      }, maxMs);
    }
    let checkInterval: number | null = null;
    if (silenceMs) {
      checkInterval = window.setInterval(() => {
        if (levelRef.current < silenceThreshold) {
          if (silenceTimer.current == null) {
            silenceTimer.current = window.setTimeout(() => {
              log.engine.debug('autoStop silence detected');
              void engine.stop();
            }, silenceMs);
          }
        } else if (silenceTimer.current != null) {
          clearTimeout(silenceTimer.current);
          silenceTimer.current = null;
        }
      }, 200);
    }
    return () => {
      if (checkInterval != null) clearInterval(checkInterval);
      if (silenceTimer.current != null) clearTimeout(silenceTimer.current);
      silenceTimer.current = null;
      if (maxTimer.current != null) clearTimeout(maxTimer.current);
      maxTimer.current = null;
    };
  }, [state.status, config.autoStop, engine]);

  const start = useCallback(async () => {
    if (state.status === 'listening' || state.status === 'starting') return;
    dispatch({ type: 'START' });
    try {
      await engine.start({
        language,
        interim: config.interim ?? true,
        deviceId: config.deviceId ?? prefs.deviceId ?? undefined,
      });
    } catch (cause) {
      // engine already emitted 'error'; reducer caught it via subscription
      log.engine.warn('start threw', cause);
    }
  }, [engine, language, config.interim, config.deviceId, prefs.deviceId, state.status]);

  const stop = useCallback(async () => {
    if (state.status === 'idle' || state.status === 'stopping') return;
    dispatch({ type: 'STOP' });
    await engine.stop();
  }, [engine, state.status]);

  const abort = useCallback(() => {
    engine.abort();
    dispatch({ type: 'ABORT' });
  }, [engine]);

  const toggle = useCallback(async () => {
    if (state.status === 'listening' || state.status === 'starting') {
      await stop();
    } else {
      await start();
    }
  }, [state.status, start, stop]);

  const reset = useCallback(() => {
    dispatch({ type: 'RESET' });
  }, []);

  const transcript = useMemo(
    () => (state.segments.length === 0 ? EMPTY_TRANSCRIPT : buildTranscript(state.segments)),
    [state.segments],
  );

  return {
    status: state.status,
    isSupported: engine.isSupported,
    transcript,
    error: state.error,
    level,
    start,
    stop,
    abort,
    toggle,
    reset,
  };
}
