"""
app/services/mock_audio_listener.py
-------------------------------------
물리적 마이크(Line-in)가 없는 서버 환경에서 실시간 감청 파이프라인을
완벽하게 테스트하기 위한 시뮬레이터 모듈.

실제 .wav 파일을 CHUNK 단위로 읽어 실제 재생 속도만큼 딜레이를 주며,
RadioListener와 동일한 VAD(음성 감지) 로직으로 on_segment 콜백을 트리거한다.
"""
import asyncio
import logging
import math
import os
import struct
import tempfile
import threading
import time
import wave

logger = logging.getLogger("uvicorn.error")

# ─── 기본 설정값 (RadioListener와 동일한 상수 체계) ───────────────────────────
SAMPLE_RATE   = 16000   # 처리 샘플링 레이트
CHANNELS      = 1       # 모노
CHUNK         = 1024    # 한 번에 읽을 프레임 수
THRESHOLD     = 600     # RMS 볼륨 임계치
SILENCE_LIMIT = 1.8     # 침묵 지속 시간(초) 임계치
PRE_ROLL_SECS = 0.3     # 발화 시작 직전 버퍼 (첫 음절 잘림 방지)


class MockAudioListener:
    """
    로컬 .wav 파일을 실시간 감청처럼 스트리밍하는 시뮬레이터.

    RadioListener와 동일한 인터페이스(start / stop, on_segment 콜백)를 가지므로
    main.py에서 완전히 대체(drop-in replacement)하여 사용 가능하다.

    Parameters
    ----------
    on_segment : callable (async)
        완성된 wav 임시 파일 경로(str)를 인자로 받는 비동기 콜백.
    loop : asyncio.AbstractEventLoop
        FastAPI 메인 이벤트 루프.
    wav_path : str
        시뮬레이션 소스로 사용할 .wav 파일의 경로.
    threshold : int, optional
        RMS 음량 임계치 (기본값 600).
    silence_limit : float, optional
        침묵 종료 판정까지의 시간(초) (기본값 1.8).
    loop_playback : bool, optional
        파일 재생이 끝났을 때 반복 재생 여부 (기본값 True).
    """

    def __init__(
        self,
        on_segment,
        loop: asyncio.AbstractEventLoop,
        wav_path: str,
        threshold: int = THRESHOLD,
        silence_limit: float = SILENCE_LIMIT,
        loop_playback: bool = True,
    ):
        if not os.path.exists(wav_path):
            raise FileNotFoundError(f"Mock 오디오 파일을 찾을 수 없습니다: {wav_path}")

        self.on_segment    = on_segment
        self.loop          = loop
        self.wav_path      = wav_path
        self.threshold     = threshold
        self.silence_limit = silence_limit
        self.loop_playback = loop_playback

        self._stop_event = threading.Event()
        self._thread: threading.Thread | None = None

    # ── 공개 인터페이스 ──────────────────────────────────────────────────────
    def start(self):
        """백그라운드 데몬 스레드로 시뮬레이션 루프 시작."""
        self._stop_event.clear()
        self._thread = threading.Thread(target=self._mock_loop, daemon=True)
        self._thread.name = "MockAudioListenerThread"
        self._thread.start()
        logger.info(f"🧪 MockAudioListener 시작 → {self.wav_path}")

    def stop(self):
        """시뮬레이션 루프를 안전하게 종료."""
        self._stop_event.set()
        if self._thread and self._thread.is_alive():
            self._thread.join(timeout=5.0)
        logger.info("🧪 MockAudioListener 종료")

    # ── 내부 로직 ──────────────────────────────────────────────────────────
    @staticmethod
    def _rms(chunk_bytes: bytes) -> float:
        """16-bit PCM 바이트 배열로부터 RMS 볼륨을 계산.

        Parameters
        ----------
        chunk_bytes : bytes
            PCM 16-bit little-endian 바이트 배열.

        Returns
        -------
        float
            RMS 볼륨 값 (0 ~ 32767 범위).
        """
        shorts = struct.unpack(f"{len(chunk_bytes) // 2}h", chunk_bytes)
        mean_sq = sum(s * s for s in shorts) / max(len(shorts), 1)
        return math.sqrt(mean_sq)

    def _mock_loop(self):
        """시뮬레이션 메인 루프: wav → 청크 단위 스트리밍 + VAD."""
        chunk_duration = CHUNK / SAMPLE_RATE  # 청크 한 개의 실제 재생 시간(초)

        # wav가 아닌 파일 포맷 최초 1회 자동 변환 (m4a, mp3 등 → 16kHz 모노 wav)
        target_path   = self.wav_path
        tmp_converted = None
        if not self.wav_path.lower().endswith(".wav"):
            try:
                import subprocess
                import imageio_ffmpeg as _iff
                ffmpeg_exe = _iff.get_ffmpeg_exe()
                tmp_conv = tempfile.NamedTemporaryFile(delete=False, suffix="_mock_conv.wav")
                tmp_conv.close()
                # ffmpeg 로 직접 16kHz 모노 PCM wav 변환
                subprocess.run(
                    [ffmpeg_exe, "-y", "-i", self.wav_path,
                     "-ar", str(SAMPLE_RATE), "-ac", "1", "-acodec", "pcm_s16le",
                     tmp_conv.name],
                    check=True, capture_output=True
                )
                target_path   = tmp_conv.name
                tmp_converted = tmp_conv.name
                logger.info(f"🔄 [Mock] 포맷 변환 완료 (ffmpeg): {self.wav_path} → {target_path}")
            except Exception as conv_err:
                logger.error(f"❌ [Mock] 파일 변환 실패: {conv_err}")
                return  # 변환 실패 시 루프 진입 없이 종료

        try:
            while not self._stop_event.is_set():
                try:
                    with wave.open(target_path, "rb") as wf:
                        actual_sr   = wf.getframerate()
                        actual_ch   = wf.getnchannels()
                        actual_sw   = wf.getsampwidth()

                        logger.info(
                            f"▶ 시뮬레이션 재생 시작 | "
                            f"SR={actual_sr}Hz CH={actual_ch} SW={actual_sw}byte | "
                            f"파일={os.path.basename(self.wav_path)}"
                        )

                        # VAD 상태 초기화
                        pre_roll_size         = int((PRE_ROLL_SECS * actual_sr) / CHUNK)
                        pre_roll: list[bytes] = []
                        is_recording          = False
                        frames: list[bytes]   = []
                        silence_chunks        = 0
                        silence_limit_chunks  = int((self.silence_limit * actual_sr) / CHUNK)

                        while not self._stop_event.is_set():
                            raw = wf.readframes(CHUNK)
                            if not raw:
                                break  # 파일 끝

                            # ─ 실제 재생 속도 흉내 ─
                            time.sleep(chunk_duration)

                            rms = self._rms(raw)

                            if not is_recording:
                                pre_roll.append(raw)
                                if len(pre_roll) > pre_roll_size:
                                    pre_roll.pop(0)

                                if rms > self.threshold:
                                    logger.info(f"[Mock] VAD: speech start (RMS={rms:.0f})")
                                    is_recording   = True
                                    silence_chunks = 0
                                    frames         = list(pre_roll)
                                    frames.append(raw)
                            else:
                                frames.append(raw)
                                if rms < self.threshold:
                                    silence_chunks += 1
                                else:
                                    silence_chunks = 0

                                if silence_chunks >= silence_limit_chunks:
                                    logger.info(f"[Mock] VAD: speech end ({len(frames) * CHUNK / actual_sr:.1f}s)")
                                    is_recording = False
                                    self._dispatch(frames, actual_sr, actual_ch, actual_sw)
                                    frames = []

                        if is_recording and frames:
                            logger.info("[Mock] VAD: EOF reached, dispatching last segment")
                            self._dispatch(frames, actual_sr, actual_ch, actual_sw)

                except Exception as e:
                    logger.error(f"❌ [Mock] 루프 오류: {e}")
                    break

                if self.loop_playback and not self._stop_event.is_set():
                    logger.info("[Mock] playback done - waiting 10s before repeat")
                    time.sleep(10.0)
                else:
                    logger.info("✅ [Mock] 단일 재생 완료 — 시뮬레이터 종료")
                    break
        finally:
            # 변환된 임시 파일 정리
            if tmp_converted and os.path.exists(tmp_converted):
                try:
                    os.remove(tmp_converted)
                except OSError:
                    pass

    def _dispatch(self, frames: list[bytes], sr: int, ch: int, sw: int):
        """녹음된 프레임을 임시 .wav로 저장하고 비동기 STT 파이프라인으로 전달.

        Parameters
        ----------
        frames : list[bytes]
            PCM 프레임 버퍼 목록.
        sr : int
            샘플링 레이트.
        ch : int
            채널 수.
        sw : int
            샘플 폭(byte).
        """
        if not frames:
            return
        try:
            tmp = tempfile.NamedTemporaryFile(
                delete=False, suffix="_mock.wav", dir=tempfile.gettempdir()
            )
            tmp_path = tmp.name
            tmp.close()

            with wave.open(tmp_path, "wb") as wf:
                wf.setnchannels(ch)
                wf.setsampwidth(sw)
                wf.setframerate(sr)
                wf.writeframes(b"".join(frames))

            logger.info(f"💾 [Mock] 임시 wav 저장 → {tmp_path}")

            asyncio.run_coroutine_threadsafe(
                self.on_segment(tmp_path),
                self.loop
            )
        except Exception as e:
            logger.error(f"❌ [Mock] dispatch 실패: {e}")
