"""MeetingSession: orchestrates the detect → record → transcribe → stop lifecycle."""

from __future__ import annotations

import logging
import threading
import time
from pathlib import Path
from typing import TYPE_CHECKING, Callable, Optional

if TYPE_CHECKING:
    from meeting_helper.buffer import SentenceBuffer
    from meeting_helper.config import AppConfig
    from meeting_helper.meeting_detector import MeetingInfo

logger = logging.getLogger(__name__)


class ProactiveMonitor:
    """Watches the transcript buffer and fires Claude queries automatically.

    Trigger rule: new sentence ends with '?' + 1 second of silence after it.

    Constraints:
    - 60s minimum cooldown between proactive fires
    - 30s cooldown after any manual query
    - Never interrupts an in-progress response
    - Buffer is never cleared — manual queries still have full context
    """

    SILENCE_SECS = 2.0  # silence after question before firing

    def __init__(self, config, buffer) -> None:
        self._config = config
        self._buffer = buffer
        self._pending_sentence: str | None = None
        self._timer: threading.Timer | None = None
        self._fire_id: int = 0
        self._lock = threading.Lock()
        self._enabled: bool = True

    def set_enabled(self, enabled: bool) -> None:
        self._enabled = enabled
        if not enabled:
            self._cancel_pending()
        logger.info("Proactive monitor %s", "enabled" if enabled else "disabled")

    def is_enabled(self) -> bool:
        return self._enabled

    def notify_manual_query(self) -> None:
        """Cancel any pending proactive trigger when user manually queries."""
        self._cancel_pending()

    def on_sentence(self, sentence: str) -> None:
        """Called by SentenceBuffer on every new final sentence."""
        if not self._enabled:
            return
        if not sentence.rstrip().endswith("?"):
            self._cancel_pending()
            return
        with self._lock:
            if self._timer:
                self._timer.cancel()
                self._timer = None
            self._pending_sentence = sentence
            self._fire_id += 1
            current_id = self._fire_id
        # Start the 1s silence timer, passing fire_id to detect stale firings
        t = threading.Timer(self.SILENCE_SECS, self._maybe_fire, args=(current_id,))
        t.daemon = True
        with self._lock:
            self._timer = t
        t.start()

    def _cancel_pending(self, cancel_sentence: bool = True) -> None:
        with self._lock:
            if self._timer:
                self._timer.cancel()
                self._timer = None
            if cancel_sentence:
                self._pending_sentence = None
                self._fire_id += 1  # invalidate any running timer

    def _maybe_fire(self, fire_id: int) -> None:
        with self._lock:
            if fire_id != self._fire_id:
                return  # Stale timer — a newer question arrived, skip
            sentence = self._pending_sentence
            self._pending_sentence = None
            self._timer = None

        if not sentence:
            return

        from meeting_helper.state import state
        if state.status == "processing":
            logger.debug("Proactive: skipped (response in progress)")
            return

        logger.info("Proactive: firing for question: %s", sentence[:80])

        loop = state.asyncio_loop
        if loop is not None:
            import asyncio
            asyncio.run_coroutine_threadsafe(
                self._fire(sentence), loop
            )

    async def _fire(self, question: str) -> None:
        from meeting_helper.ai.client import handle_query
        from meeting_helper.state import state
        await handle_query(state, self._config)

    def stop(self) -> None:
        self._cancel_pending()


class AutoContextSelector:
    """Periodically selects relevant repos/knowledge based on transcript + history.

    Runs every `interval_secs` seconds during an active session. Uses Claude Haiku
    to pick from the low-level index, then accumulates selections (never removes).
    """

    def __init__(self, config, buffer, interval_secs: int = 60) -> None:
        self._config = config
        self._buffer = buffer
        self._interval = interval_secs
        self._stop_event = threading.Event()
        self._thread: Optional[threading.Thread] = None
        self._enabled = True

    def set_enabled(self, enabled: bool) -> None:
        self._enabled = enabled

    def is_enabled(self) -> bool:
        return self._enabled

    def start(self) -> None:
        self._stop_event.clear()
        self._thread = threading.Thread(target=self._run, daemon=True)
        self._thread.start()
        logger.info("AutoContextSelector started (interval=%ds)", self._interval)

    def stop(self) -> None:
        self._stop_event.set()

    def _run(self) -> None:
        # Wait the full interval before first check
        self._stop_event.wait(timeout=self._interval)
        while not self._stop_event.is_set():
            if not self._enabled:
                self._stop_event.wait(timeout=self._interval)
                continue
            try:
                import asyncio
                asyncio.run(self._select_and_apply())
            except Exception as exc:
                logger.warning("AutoContextSelector error: %s", exc)
            self._stop_event.wait(timeout=self._interval)

    async def _select_and_apply(self) -> None:
        from meeting_helper.ai.client import _select_context
        from meeting_helper.context_loader import build_selective_context, get_repos_from_workspaces
        from meeting_helper.ai.prompts import build_system_prompt
        from meeting_helper.state import state

        transcript = self._buffer.get_last_n_as_text(self._config.sentence_window * 3)
        if not transcript.strip():
            return

        index = state.auto_context_index
        if not index:
            return

        history = state.conversation_history[-4:]
        selection = await _select_context(index, transcript, history, self._config)

        # Accumulate: union with existing selection (never shrink)
        prev = state.auto_context_selection
        new_repos = sorted(set(prev.get("repos", [])) | set(selection.get("repos", [])))
        new_knowledge = sorted(set(prev.get("knowledge", [])) | set(selection.get("knowledge", [])))
        accumulated = {"repos": new_repos, "knowledge": new_knowledge}

        if accumulated == prev:
            logger.debug("AutoContextSelector: no new context to add")
            return

        state.auto_context_selection = accumulated

        # Map repo names → full paths
        all_repos = {r["name"]: r["path"] for r in get_repos_from_workspaces(
            self._config.repos_workspaces or ([self._config.repos_workspace] if self._config.repos_workspace else []),
            ignored=self._config.ignored_repos,
        )}
        selected_paths = [all_repos[n] for n in new_repos if n in all_repos]
        kp = (self._config.knowledge_paths or [self._config.knowledge_path] if self._config.knowledge_path else [])[0] if (self._config.knowledge_paths or self._config.knowledge_path) else ""

        context_block = build_selective_context(selected_paths, kp, new_knowledge)
        state.system_prompt = build_system_prompt(context_block)

        added_repos = sorted(set(new_repos) - set(prev.get("repos", [])))
        added_knowledge = sorted(set(new_knowledge) - set(prev.get("knowledge", [])))
        logger.info("Auto-context updated: +repos=%s +knowledge=%s", added_repos, added_knowledge)

        # Broadcast to overlay/GUI
        loop = state.asyncio_loop
        if loop and not loop.is_closed():
            import asyncio
            asyncio.run_coroutine_threadsafe(
                _broadcast_auto_context(accumulated, added_repos, added_knowledge), loop
            )


async def _broadcast_auto_context(accumulated: dict, added_repos: list, added_knowledge: list) -> None:
    try:
        from meeting_helper.server.websocket import broadcast
        await broadcast({
            "type": "context_updated",
            "repos": accumulated["repos"],
            "knowledge": accumulated["knowledge"],
            "added_repos": added_repos,
            "added_knowledge": added_knowledge,
        })
    except Exception:
        pass


class MeetingSession:
    """Manages the full lifecycle of meeting recording and transcription.

    States: idle → active → stopping → idle
    Runs in its own background thread.
    """

    def __init__(
        self,
        config: AppConfig,
        sentence_buffer: SentenceBuffer,
        on_meeting_start: Callable[[MeetingInfo], None],
        on_meeting_end: Callable[[Optional[Path]], None],
    ) -> None:
        self._config = config
        self._buffer = sentence_buffer
        self._on_meeting_start = on_meeting_start
        self._on_meeting_end = on_meeting_end
        self._stop_event = threading.Event()
        self._thread: Optional[threading.Thread] = None

    def start(self) -> None:
        self._stop_event.clear()
        self._thread = threading.Thread(target=self._run, daemon=True)
        self._thread.start()

    def stop(self) -> None:
        self._stop_event.set()

    def _restart_daemon(self) -> None:
        """Spawn a fresh daemon process and exit, giving the next meeting a clean PortAudio state."""
        import subprocess, sys, os
        try:
            executable = sys.executable
            subprocess.Popen(
                [executable, "-m", "meeting_helper"],
                stdin=subprocess.DEVNULL,
                stdout=subprocess.DEVNULL,
                stderr=subprocess.DEVNULL,
                start_new_session=True,
            )
            logger.info("MeetingSession: spawned fresh daemon — exiting current process")
            os._exit(0)
        except Exception as exc:
            logger.warning("MeetingSession: could not restart daemon: %s", exc)

    def _run(self) -> None:
        """Main loop: detect meeting → start capture/transcription → wait for end."""
        # ALL audio imports happen HERE (after daemon fork)
        from meeting_helper.meeting_detector import MeetingMonitor

        monitor = MeetingMonitor()
        logger.info("MeetingSession: watching for meetings (mic-based detection)...")

        while not self._stop_event.is_set():
            started, ended, meeting_info = monitor.check()

            if started and meeting_info:
                logger.info("Meeting detected: %s (%s)", meeting_info.app_name, meeting_info.meeting_name or "unnamed")
                _portaudio_retries = 0
                while not self._stop_event.is_set():
                    try:
                        self._run_active_session(meeting_info)
                        break  # session ended normally
                    except Exception as exc:
                        msg = str(exc)
                        if "PaErrorCode -9986" in msg or "PortAudio" in msg or "InputStream" in msg:
                            _portaudio_retries += 1
                            if _portaudio_retries <= 5:
                                logger.warning("PortAudio mic error (attempt %d/5) — retrying in 3s: %s", _portaudio_retries, exc)
                                self._stop_event.wait(timeout=3.0)
                                continue
                        logger.error("MeetingSession: session error: %s", exc, exc_info=True)
                        break
                monitor.set_meeting_ended()
                # Cooldown: wait for mic to fully release before re-monitoring
                logger.info("MeetingSession: cooldown before re-watching...")
                self._stop_event.wait(timeout=1.0)
                # Restart daemon to get a fresh PortAudio context for the next meeting
                self._restart_daemon()
                logger.info("MeetingSession: back to watching for meetings...")
            else:
                self._stop_event.wait(timeout=2.0)  # poll every 2s (matching meeting-noter)

    def _run_active_session(self, meeting_info: MeetingInfo) -> None:
        """Capture + transcribe + record until meeting app releases the mic.

        No silence detection — recording continues as long as the meeting app
        holds the microphone. Only MicProbe ends the session.
        """
        from meeting_helper.audio.capture import CombinedAudioCapture
        from meeting_helper.audio.transcriber import create_transcriber
        from meeting_helper.audio.encoder import RecordingSession
        from meeting_helper.daemon import MicProbe

        from meeting_helper.state import state as app_state
        self._on_meeting_start(meeting_info)

        capture = CombinedAudioCapture(sample_rate=16000)
        transcriber = None
        recording = None
        saved_path = None
        live_transcript_path = None

        # Proactive monitor — only active when enabled in config
        proactive = None
        if self._config.proactive_answers:
            proactive = ProactiveMonitor(self._config, self._buffer)
            self._buffer.add_sentence_listener(proactive.on_sentence)
            app_state.proactive_monitor = proactive
            logger.info("Proactive answers enabled")

        # Auto-context selector — periodically picks relevant repos/knowledge
        auto_ctx = None
        if self._config.auto_context_enabled:
            auto_ctx = AutoContextSelector(
                self._config, self._buffer,
                interval_secs=self._config.auto_context_interval,
            )
            auto_ctx.start()
            app_state.auto_context_selector = auto_ctx

        try:
            capture.start()
            app_state.audio_active = True
            logger.info("Audio capture started (system_audio=%s)", capture.has_system_audio)

            # Start transcription
            transcriber = create_transcriber(
                audio_capture=capture,
                transcript_buffer=self._buffer,
                deepgram_api_key=self._config.deepgram_api_key,
                transcription_language=self._config.transcription_language,
            )

            # Update daemon metadata with transcriber name
            from meeting_helper.daemon import write_meta
            t_name = "Deepgram" if self._config.deepgram_api_key and transcriber else "Whisper"
            if transcriber is None:
                t_name = "none"
            write_meta(self._config, transcriber_name=t_name)

            # Always record to MP3
            recordings_dir = self._config.recordings_dir
            recordings_dir.mkdir(parents=True, exist_ok=True)
            recording = RecordingSession(
                output_dir=recordings_dir,
                sample_rate=capture.sample_rate,
                channels=capture.channels,
                meeting_name=meeting_info.meeting_name or meeting_info.app_name,
            )
            mp3_path = recording.start()
            live_transcript_path = mp3_path.with_suffix(".live.txt")
            logger.info("Recording to: %s", mp3_path.name)

            mic_probe = MicProbe(probe_interval=2.0, settle_time=0.3, required_off_count=1)

            while not self._stop_event.is_set():
                audio = capture.get_audio(timeout=0.5)
                if audio is None:
                    continue

                audio_flat = audio[:, 0] if audio.ndim > 1 else audio

                # Write to MP3
                if recording.is_active:
                    recording.write(audio)

                # Write live transcript to file periodically
                if live_transcript_path and len(self._buffer) > 0:
                    try:
                        live_transcript_path.write_text(self._buffer.get_full_transcript())
                    except Exception:
                        pass

                # MicProbe: only way to end session — meeting app released the mic
                if mic_probe.should_probe(time.time()):
                    if mic_probe.probe(capture):
                        logger.info("MicProbe: meeting app released mic — ending session")
                        break

        except Exception as exc:
            logger.error("Session error: %s", exc)
        finally:
            if proactive is not None:
                proactive.stop()
                self._buffer.remove_sentence_listener(proactive.on_sentence)
                app_state.proactive_monitor = None
            if auto_ctx is not None:
                auto_ctx.stop()
            app_state.auto_context_selector = None
            if transcriber is not None:
                transcriber.stop()

            capture.stop()
            app_state.audio_active = False

            # Save final live transcript
            if live_transcript_path:
                try:
                    live_transcript_path.write_text(self._buffer.get_full_transcript())
                except Exception:
                    pass

            # Finalize MP3
            if recording is not None and recording.is_active:
                saved_path, duration = recording.stop()
                if saved_path:
                    logger.info("Recording saved: %s (%.1fs)", saved_path.name, duration)

            self._buffer.clear()
            self._on_meeting_end(saved_path)
