"""Daemon process management: double-fork, PID files, signal handling.

IMPORTANT: Audio modules must NOT be imported at module level.
They are imported inside run_daemon() after daemonize().
"""

from __future__ import annotations

import asyncio
import atexit
import json
import logging
import os
import signal
import sys
import time
from typing import Optional

from meeting_helper.config import LOG_FILE, META_FILE, PID_FILE, AppConfig

from meeting_helper.ssl_fix import patch_ssl
patch_ssl()

logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# PID file helpers
# ---------------------------------------------------------------------------

def write_pid_file(pid: Optional[int] = None) -> None:
    pid = pid or os.getpid()
    PID_FILE.write_text(str(pid))


def remove_pid_file() -> None:
    try:
        PID_FILE.unlink(missing_ok=True)
    except Exception:
        pass


def read_pid() -> Optional[int]:
    try:
        return int(PID_FILE.read_text().strip())
    except Exception:
        return None


def is_process_running(pid: int) -> bool:
    try:
        os.kill(pid, 0)
        return True
    except (ProcessLookupError, PermissionError):
        return False


def kill_existing() -> bool:
    """Kill any running daemon. Returns True if a process was killed."""
    pid = read_pid()
    if pid is None:
        return False
    if not is_process_running(pid):
        remove_pid_file()
        return False
    try:
        os.kill(pid, signal.SIGTERM)
        for _ in range(50):
            time.sleep(0.1)
            if not is_process_running(pid):
                break
        else:
            os.kill(pid, signal.SIGKILL)
    except Exception:
        pass
    remove_pid_file()
    return True


# ---------------------------------------------------------------------------
# Metadata helpers
# ---------------------------------------------------------------------------

def write_meta(config: AppConfig, transcriber_name: str = "") -> None:
    meta = {
        "started_at": time.time(),
        "port": config.port,
        "pid": os.getpid(),
        "claude_model": config.claude_model,
        "transcriber": transcriber_name,
        "repos": config.repos,
        "knowledge_path": config.knowledge_path,
    }
    META_FILE.write_text(json.dumps(meta))


def read_meta() -> Optional[dict]:
    try:
        return json.loads(META_FILE.read_text())
    except Exception:
        return None


def remove_meta() -> None:
    try:
        META_FILE.unlink(missing_ok=True)
    except Exception:
        pass


# ---------------------------------------------------------------------------
# MicProbe: detect when meeting app releases the microphone
# ---------------------------------------------------------------------------

class MicProbe:
    """Probes whether another app is still using the microphone.

    Works by briefly pausing our mic stream and checking
    kAudioDevicePropertyDeviceIsRunningSomewhere.
    """

    def __init__(
        self,
        probe_interval: float = 20.0,
        settle_time: float = 0.3,
        required_off_count: int = 2,
    ):
        self.probe_interval = probe_interval
        self.settle_time = settle_time
        self.required_off_count = required_off_count
        self._last_probe = 0.0
        self._off_count = 0

    def should_probe(self, now: float) -> bool:
        return (now - self._last_probe) >= self.probe_interval

    def probe(self, capture) -> bool:
        """Pause mic, check if anyone else uses it, resume.

        Returns True if the meeting app has released the microphone.
        """
        self._last_probe = time.time()
        capture.pause_mic()
        time.sleep(self.settle_time)

        mic_in_use = self._check_mic_running()

        capture.resume_mic()

        if not mic_in_use:
            self._off_count += 1
            if self._off_count >= self.required_off_count:
                return True
        else:
            self._off_count = 0
        return False

    @staticmethod
    def _check_mic_running() -> bool:
        """Check if any app (besides us) is using the default input device."""
        from meeting_helper.meeting_detector import is_mic_in_use_by_another_app
        return is_mic_in_use_by_another_app()


# ---------------------------------------------------------------------------
# daemonize: double-fork to detach from terminal
# ---------------------------------------------------------------------------

def daemonize() -> None:
    """Double-fork to create a background daemon. Call BEFORE importing audio libs."""
    try:
        pid = os.fork()
        if pid > 0:
            sys.exit(0)
    except OSError as exc:
        sys.stderr.write(f"First fork failed: {exc}\n")
        sys.exit(1)

    os.setsid()

    try:
        pid = os.fork()
        if pid > 0:
            sys.exit(0)
    except OSError as exc:
        sys.stderr.write(f"Second fork failed: {exc}\n")
        sys.exit(1)

    os.umask(0)
    os.chdir("/")

    LOG_FILE.parent.mkdir(parents=True, exist_ok=True)
    sys.stdout.flush()
    sys.stderr.flush()

    with open(os.devnull, "r") as f:
        os.dup2(f.fileno(), sys.stdin.fileno())
    with open(LOG_FILE, "a") as f:
        os.dup2(f.fileno(), sys.stdout.fileno())
        os.dup2(f.fileno(), sys.stderr.fileno())


# ---------------------------------------------------------------------------
# Main daemon entrypoint
# ---------------------------------------------------------------------------

def _launch_overlay(config: AppConfig) -> None:
    """Auto-launch the overlay process when daemon starts.

    Stores the process reference in the hotkey module so 2×ESC
    can toggle it instead of spawning duplicates.
    """
    import subprocess
    from pathlib import Path
    import meeting_helper.hotkeys as hk

    # Kill stale overlays first
    hk._kill_stale_overlays()

    ui_log = Path.home() / ".meeting-helper-ui.log"
    with open(ui_log, "a") as log_fh:
        proc = subprocess.Popen(
            [sys.executable, "-m", "meeting_helper.overlay", str(config.port)],
            stdout=log_fh, stderr=log_fh, stdin=subprocess.DEVNULL,
        )
    # Share the process ref with hotkey module so 2×ESC toggles, not relaunches
    hk._overlay_proc = proc
    hk._overlay_visible = True
    logger.info("Overlay auto-launched on port %d (pid=%d)", config.port, proc.pid)


def run_daemon(config: AppConfig) -> None:
    """Main daemon process. Called after daemonize()."""
    daemonize()

    logging.basicConfig(
        level=logging.INFO,
        format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
        handlers=[logging.StreamHandler()],
    )
    logger.info("Daemon started (pid=%d)", os.getpid())

    write_pid_file()
    atexit.register(remove_pid_file)
    atexit.register(remove_meta)

    # Hide from macOS Dock
    try:
        from AppKit import NSApplication, NSApplicationActivationPolicyProhibited
        NSApplication.sharedApplication().setActivationPolicy_(
            NSApplicationActivationPolicyProhibited
        )
    except Exception:
        pass

    shutdown_flag = {"stop": False}

    def _term_handler(signum, frame):
        logger.info("SIGTERM received, shutting down")
        shutdown_flag["stop"] = True

    signal.signal(signal.SIGTERM, _term_handler)

    # -----------------------------------------------------------------------
    # NOW safe to import audio and network libraries
    # -----------------------------------------------------------------------
    from meeting_helper.buffer import SentenceBuffer
    from meeting_helper.state import state
    from meeting_helper.session import MeetingSession
    from meeting_helper.server.app import create_app
    from meeting_helper.hotkeys import start_hotkey_listener

    state.started_at = time.time()
    state.sentence_buffer = SentenceBuffer(max_sentences=50)
    state.system_prompt = config.system_prompt

    def on_meeting_start(info):
        # Build low-level index (READMEs only) — set as initial system prompt
        try:
            from meeting_helper.context_loader import build_low_level_index
            from meeting_helper.ai.prompts import build_system_prompt
            index = build_low_level_index(
                config.repos_workspaces or config.repos_workspace,
                config.knowledge_paths or config.knowledge_path,
                ignored_repos=config.ignored_repos,
                ignored_knowledge=config.ignored_knowledge,
            )
            state.auto_context_index = index
            state.system_prompt = build_system_prompt(index)
            logger.info("Low-level index loaded (%d chars) — auto-context ready", len(index))
        except Exception as exc:
            logger.warning("Failed to build low-level index: %s", exc)

        state.status = "listening"
        state.meeting_info = info
        state.session_active = True
        loop = state.asyncio_loop
        if loop:
            asyncio.run_coroutine_threadsafe(
                _broadcast_meeting_status(True, info.app_name, info.meeting_name), loop
            )

    def on_meeting_end(saved_path):
        state.status = "idle"
        state.meeting_info = None
        state.session_active = False
        state.conversation_history.clear()
        state.last_response = ""
        state.auto_context_index = ""
        state.auto_context_selection = {"repos": [], "knowledge": []}
        loop = state.asyncio_loop
        if loop:
            asyncio.run_coroutine_threadsafe(
                _broadcast_meeting_ended(saved_path), loop
            )

    session = MeetingSession(
        config=config,
        sentence_buffer=state.sentence_buffer,
        on_meeting_start=on_meeting_start,
        on_meeting_end=on_meeting_end,
    )
    session.start()

    write_meta(config, transcriber_name="pending")
    start_hotkey_listener(config)

    async def _main():
        import aiohttp.web as web

        loop = asyncio.get_running_loop()
        state.asyncio_loop = loop
        state.shutdown_event = asyncio.Event()

        app = create_app(config)
        runner = web.AppRunner(app)
        await runner.setup()
        site = web.TCPSite(runner, "0.0.0.0", config.port)
        await site.start()
        logger.info("Server on 0.0.0.0:%d", config.port)

        while not shutdown_flag["stop"]:
            await asyncio.sleep(0.5)

        await runner.cleanup()

    try:
        asyncio.run(_main())
    except Exception as exc:
        logger.error("Event loop error: %s", exc)
    finally:
        session.stop()
        logger.info("Daemon exited cleanly")


async def _broadcast_meeting_status(active: bool, app_name: str, meeting_name: Optional[str]) -> None:
    from meeting_helper.server.websocket import broadcast
    await broadcast({
        "type": "meeting_status",
        "active": active,
        "app": app_name,
        "meeting_name": meeting_name or "",
    })
    await broadcast({"type": "status", "value": "listening" if active else "idle"})


async def _broadcast_meeting_ended(saved_path) -> None:
    from meeting_helper.server.websocket import broadcast
    await broadcast({"type": "meeting_status", "active": False, "app": "", "meeting_name": ""})
    await broadcast({"type": "status", "value": "idle"})
    await broadcast({"type": "meeting_ended", "recording": str(saved_path) if saved_path else ""})
