"""Meeting mode — Jarvis records, transcribes live, and takes notes silently.

Ahmed triggers it ("record this meeting" / "record this online meeting"). While
active, Jarvis DOES NOT reply to what's said — he just transcribes every
utterance into a growing transcript and stays quiet, catching up on context.
He only speaks/answers when Ahmed ADDRESSES him ("Jarvis, …") or types a
question — and answering never pauses the recording (capture continues on the
mic while he thinks). When Ahmed ends it, the full transcript is saved and
summarised into the long-term memory brain, linked to the meeting.

  face-to-face : microphone (the room)
  online       : mic (Ahmed, via AEC) + the laptop's own audio (the remote
                 participants), captured natively with ScreenCaptureKit on a
                 SEPARATE channel — see SystemAudioStream in voice/audio_io.py
                 and _capture_remote in main.py (env flag MEETING_SYSAUDIO).
                 Ahmed's mic is labelled "Ahmed"; the call audio is "them".

Transcription (BOTH modes) goes through multilingual auto-detect Whisper
(MeetingSTT below) so real Saudi/Gulf code-switching — Arabic + English in the
SAME sentence — is decoded natively, instead of the English-only Parakeet the
main loop uses. It runs on a background worker (live where it keeps up, backlog-
tolerant when it can't) and the RAW audio is always written to WAV alongside the
transcript, so a bad/slow transcript can be re-run and nothing is ever lost.

This module holds the session + transcript + the STT/WAV helpers; main.py drives
capture and decides silence-vs-answer. Recordings are written to recordings/.
"""
from __future__ import annotations

import os
import threading
import time
import wave
from pathlib import Path

import numpy as np

_REC = Path(__file__).resolve().parent.parent / "recordings"

# Multilingual meeting STT. Whisper large-v3 auto-detects language and decodes
# Arabic<->English code-switching inline. Overridable; language "" = auto.
MEETING_STT_MODEL = os.environ.get(
    "MEETING_STT_MODEL", "mlx-community/whisper-large-v3-mlx")
MEETING_STT_LANG = os.environ.get("MEETING_STT_LANG") or None  # None = auto-detect
# If the preferred model won't load, degrade (still multilingual): large-v3 ->
# large-v3-turbo (faster, slightly less accurate) -> small (tiny, always fits).
_STT_FALLBACKS = [
    MEETING_STT_MODEL,
    "mlx-community/whisper-large-v3-turbo",
    "mlx-community/whisper-small-mlx",
]


class MeetingSession:
    def __init__(self, mode: str = "in_person", title: str = "") -> None:
        self.mode = mode                    # "in_person" | "online"
        self.title = title or "meeting"
        self.started = time.time()
        # (t, speaker, text) — t is the CAPTURE time (not add time), so lines
        # transcribed late by the background worker still sort into order.
        self.lines: list[tuple[float, str, str]] = []
        safe = "".join(c if c.isalnum() or c in " -_" else ""
                       for c in self.title)[:40].strip().replace(" ", "_")
        # One stable stem for the session → transcript + WAVs share a timestamp.
        self.stem = (f"meeting_{time.strftime('%Y%m%d_%H%M', time.localtime(self.started))}"
                     f"_{safe or 'notes'}")

    def add(self, speaker: str, text: str, ts: float | None = None) -> None:
        text = (text or "").strip()
        if text:
            self.lines.append((ts if ts is not None else time.time(), speaker, text))

    def _ordered(self) -> list[tuple[float, str, str]]:
        return sorted(self.lines, key=lambda x: x[0])

    @property
    def minutes(self) -> float:
        return (time.time() - self.started) / 60.0

    def recent(self, n: int = 40) -> str:
        """The tail of the transcript, for answering a mid-meeting question
        with full context of what was just said."""
        return "\n".join(f"{sp}: {tx}" for _t, sp, tx in self._ordered()[-n:])

    def full_text(self) -> str:
        out = [f"# {self.title}  ({self.mode}, "
               f"{time.strftime('%Y-%m-%d %H:%M')})", ""]
        for _t, sp, tx in self._ordered():
            out.append(f"{sp}: {tx}")
        return "\n".join(out)

    def room_wav_path(self) -> Path:
        """Mic/room channel (Ahmed + the room)."""
        return _REC / f"{self.stem}_room.wav"

    def them_wav_path(self) -> Path:
        """System-audio channel (the remote participants) — online only."""
        return _REC / f"{self.stem}_them.wav"

    def save(self) -> str:
        """Write the transcript to recordings/ and return the file path."""
        _REC.mkdir(exist_ok=True)
        path = _REC / f"{self.stem}.md"
        path.write_text(self.full_text(), encoding="utf-8")
        return str(path)


class WavRecorder:
    """Incremental 16 kHz mono int16 WAV writer (stdlib `wave` → a proper RIFF
    header). Writes to disk as audio arrives (low RAM; a crash leaves only a
    slightly-wrong length field, data is still recoverable). This is the raw-
    audio safety net: every meeting is re-transcribable even if live STT lags."""

    def __init__(self, path) -> None:
        self.path = str(path)
        _REC.mkdir(exist_ok=True)
        self._wf: wave.Wave_write | None = wave.open(self.path, "wb")
        self._wf.setnchannels(1)
        self._wf.setsampwidth(2)      # int16
        self._wf.setframerate(16000)
        self.samples = 0
        self._lock = threading.Lock()

    def write(self, audio: np.ndarray) -> None:
        if audio is None or len(audio) == 0:
            return
        pcm16 = (np.clip(audio, -1.0, 1.0) * 32767.0).astype("<i2")
        with self._lock:
            if self._wf is None:
                return
            self._wf.writeframes(pcm16.tobytes())
            self.samples += len(pcm16)

    def close(self) -> None:
        with self._lock:
            if self._wf is not None:
                try:
                    self._wf.close()
                except Exception:  # noqa: BLE001
                    pass
                self._wf = None

    @property
    def seconds(self) -> float:
        return self.samples / 16000.0


class MeetingSTT:
    """Multilingual auto-detect Whisper (mlx-whisper) for meeting transcription.

    language=None → Whisper detects per-utterance and keeps Arabic+English
    code-switching inline (proven on Ahmed's live audio). All MLX inference is
    pinned to voice.stt._MLX — the single Metal-affine thread — so it serialises
    with (and never corrupts) the main loop's Parakeet stream.
    """

    def __init__(self, repo: str) -> None:
        self.repo = repo
        from voice.stt import _MLX
        self._MLX = _MLX

        def _load() -> None:
            import mlx_whisper
            self._whisper = mlx_whisper
            # warm/compile Metal kernels AND prove the repo is fetchable/loadable
            # (a bad/missing repo raises here → the loader falls back)
            self._whisper.transcribe(
                np.zeros(16000, dtype=np.float32),
                path_or_hf_repo=repo, language=MEETING_STT_LANG)
        _MLX.submit(_load).result()

    def transcribe(self, audio: np.ndarray) -> str:
        """audio: 1-D float32 mono @ 16 kHz -> code-switched transcript."""
        def _do() -> str:
            out = self._whisper.transcribe(
                np.ascontiguousarray(audio, dtype=np.float32),
                path_or_hf_repo=self.repo, language=MEETING_STT_LANG)
            return (out.get("text") or "").strip()
        return self._MLX.submit(_do).result()


def load_meeting_stt() -> tuple[MeetingSTT, str]:
    """Load the multilingual meeting STT, degrading through the fallback chain.
    Returns (stt, model_repo). Raises only if EVERY option fails (then main.py
    falls back to the main-loop STT). Blocking — call via asyncio.to_thread."""
    last: Exception | None = None
    tried: set[str] = set()
    for repo in _STT_FALLBACKS:
        if not repo or repo in tried:
            continue
        tried.add(repo)
        try:
            stt = MeetingSTT(repo)
            return stt, repo
        except Exception as e:  # noqa: BLE001
            last = e
            print(f"  [meeting-stt] {repo} unavailable ({e}); trying next")
    raise RuntimeError(f"no meeting STT could load: {last}")
