"""
app/services/audio_listener.py
-------------------------------
서버의 Line-in(또는 기본 마이크)를 24/7 감청하다가,
VAD(음성 구간 감지)로 무전 발화를 포착 → 임시 .wav 파일 저장
→ WhisperSTT 파이프라인 → LLM 메타데이터 추출 → WebSocket broadcast
를 한 사이클로 처리하는 백그라운드 데몬 스레드 모듈.
"""
import asyncio
import logging
import math
import os
import tempfile
import threading
import time
import wave
from datetime import datetime

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

# ─── 기본 설정값 (환경에 따라 .env 또는 생성자 파라미터로 변경 가능) ───
SAMPLE_RATE    = 16000     # Whisper 권장 샘플링 레이트 (16kHz)
CHANNELS       = 1         # 모노
CHUNK          = 1024      # 한 번에 읽을 프레임 수
FORMAT         = None      # pyaudio.paInt16 — 아래 init 시 할당
THRESHOLD      = 600       # RMS 볼륨 임계치 (0~32767 범위에서 경험값)
                           # 조용한 환경 → 500~800 / 시끄러운 환경 → 1000~1500
SILENCE_LIMIT  = 1.8       # 이 초(sec) 이상 침묵이 지속되면 발화 종료 판정
MIN_SILENCE    = 0.3       # 첫 발화 후 최소 이 초는 녹음 유지
RECORD_TIMEOUT = 30.0      # 단일 무전 최대 녹음 시간 (무한 루프 방지)
PRE_ROLL_SECS  = 0.3       # 발화 시작 0.3초 전 버퍼 — 첫 음절 잘림 방지


class RadioListener:
    """
    백그라운드 스레드에서 Line-in을 24/7 감청하는 VAD 감청기.
    
    사용법:
        listener = RadioListener(on_segment=mycallback, loop=asyncio_loop)
        listener.start()
        ...
        listener.stop()
    """

    def __init__(self, on_segment, loop: asyncio.AbstractEventLoop,
                 threshold: int = THRESHOLD,
                 silence_limit: float = SILENCE_LIMIT,
                 device_index: int | None = None):
        """
        :param on_segment:    녹음 완료된 .wav 파일 경로(str)를 받는 async 콜백
        :param loop:          FastAPI 메인 이벤트 루프
        :param threshold:     RMS 볼륨 임계치
        :param silence_limit: 침묵 지속 시간(초) 임계치
        :param device_index:  PyAudio 입력 장치 인덱스 (None = 시스템 기본값)
        """
        self.on_segment   = on_segment
        self.loop         = loop
        self.threshold    = threshold
        self.silence_limit = silence_limit
        self.device_index = device_index

        self._stop_event  = threading.Event()
        self._thread: threading.Thread | None = None
        self._pa          = None     # PyAudio 인스턴스

    # ── 공개 인터페이스 ──────────────────────────────────────────────────────
    def start(self):
        """백그라운드 데몬 스레드로 감청 루프 시작."""
        self._stop_event.clear()
        self._thread = threading.Thread(target=self._listen_loop, daemon=True)
        self._thread.name = "RadioListenerThread"
        self._thread.start()
        logger.info("🎙️  RadioListener 백그라운드 스레드 시작")

    def stop(self):
        """감청 루프를 안전하게 종료하고 자원을 해제."""
        self._stop_event.set()
        if self._thread and self._thread.is_alive():
            self._thread.join(timeout=5.0)
        if self._pa:
            try:
                self._pa.terminate()
            except Exception:
                pass
        logger.info("🔇 RadioListener 종료 완료")

    # ── 내부 로직 ──────────────────────────────────────────────────────────
    @staticmethod
    def _rms(chunk_bytes: bytes) -> float:
        """16-bit PCM 바이트 배열로부터 RMS 볼륨을 계산."""
        import struct
        shorts = struct.unpack(f"{len(chunk_bytes) // 2}h", chunk_bytes)
        mean_sq = sum(s * s for s in shorts) / len(shorts)
        return math.sqrt(mean_sq)

    def _listen_loop(self):
        """메인 감청 스레드 루프. 오디오 장치 부재 시 warning 후 종료."""
        try:
            import pyaudio
            global FORMAT
            FORMAT = pyaudio.paInt16

            self._pa = pyaudio.PyAudio()

            # 입력 장치 검증
            device_count = self._pa.get_device_count()
            if device_count == 0:
                raise RuntimeError("오디오 입력 장치를 찾을 수 없습니다.")

            # 스트림 오픈
            stream = self._pa.open(
                format=FORMAT,
                channels=CHANNELS,
                rate=SAMPLE_RATE,
                input=True,
                input_device_index=self.device_index,
                frames_per_buffer=CHUNK,
            )
            logger.info(f"🔊 오디오 스트림 오픈 완료 (SR={SAMPLE_RATE}Hz, CH={CHANNELS}, CHUNK={CHUNK})")

            # pre-roll 버퍼: 발화 시작 직전 청크를 미리 채워 두어 첫 음절 잘림 방지
            pre_roll_size = int((PRE_ROLL_SECS * SAMPLE_RATE) / CHUNK)
            pre_roll: list[bytes] = []

            # VAD 상태
            is_recording   = False
            frames: list[bytes] = []
            silence_chunks = 0
            record_chunks  = 0
            silence_limit_chunks = int((self.silence_limit * SAMPLE_RATE) / CHUNK)

            logger.info(f"⏳ 무전 감청 대기 중... (임계치 RMS={self.threshold}, 침묵판정={self.silence_limit}s)")

            while not self._stop_event.is_set():
                try:
                    chunk = stream.read(CHUNK, exception_on_overflow=False)
                except OSError:
                    continue

                rms = self._rms(chunk)

                if not is_recording:
                    # ─ 대기 상태: 임계치 초과 시 녹음 시작 ─
                    pre_roll.append(chunk)
                    if len(pre_roll) > pre_roll_size:
                        pre_roll.pop(0)

                    if rms > self.threshold:
                        logger.info(f"🎤 발화 감지 (RMS={rms:.0f}) — 녹음 시작")
                        is_recording   = True
                        silence_chunks = 0
                        record_chunks  = 0
                        frames         = list(pre_roll)   # pre-roll 삽입
                        frames.append(chunk)
                else:
                    # ─ 녹음 상태 ─
                    frames.append(chunk)
                    record_chunks += 1

                    if rms < self.threshold:
                        silence_chunks += 1
                    else:
                        silence_chunks = 0  # 다시 소리가 나면 침묵 카운터 리셋

                    # 침묵 초과 → 발화 종료
                    if silence_chunks >= silence_limit_chunks:
                        logger.info(f"🔚 발화 종료 (총 {record_chunks * CHUNK / SAMPLE_RATE:.1f}s)")
                        is_recording = False
                        self._dispatch(frames)
                        frames = []

                    # 최대 녹음 시간 초과 → 강제 종료
                    elif record_chunks * CHUNK / SAMPLE_RATE > RECORD_TIMEOUT:
                        logger.warning(f"⚠️ 최대 녹음 시간({RECORD_TIMEOUT}s) 초과 — 강제 분할")
                        is_recording = False
                        self._dispatch(frames)
                        frames = []

            stream.stop_stream()
            stream.close()

        except ImportError:
            logger.warning("⚠️ pyaudio 미설치 — 감청 모듈 비활성화됨. `uv pip install pyaudio`로 설치할 것.")
        except RuntimeError as e:
            logger.warning(f"⚠️ 오디오 장치 오류 — 감청 모듈 비활성화 ({e})")
        except Exception as e:
            logger.warning(f"⚠️ 감청 루프 예외 발생 — 모듈 비활성화 ({e})")

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

        # 임시 wav 파일 저장
        try:
            tmp = tempfile.NamedTemporaryFile(
                delete=False, suffix="_live.wav", dir=tempfile.gettempdir()
            )
            tmp_path = tmp.name
            tmp.close()

            import pyaudio
            with wave.open(tmp_path, "wb") as wf:
                wf.setnchannels(CHANNELS)
                wf.setsampwidth(self._pa.get_sample_size(pyaudio.paInt16))
                wf.setframerate(SAMPLE_RATE)
                wf.writeframes(b"".join(frames))

            logger.info(f"💾 임시 wav 저장 완료 → {tmp_path}")

            # asyncio 메인 루프로 코루틴 스케줄링 (thread-safe)
            asyncio.run_coroutine_threadsafe(
                self.on_segment(tmp_path),
                self.loop
            )

        except Exception as e:
            logger.error(f"❌ wav 저장 또는 디스패치 실패: {e}")
