"""Simplified media manager: go2rtc process + JSON stream config.

No database, no SQLAlchemy. Streams are stored in data/streams.json.
go2rtc binary is expected at media/go2rtc or in PATH.
ffmpeg is expected in system PATH.
"""
from __future__ import annotations

import asyncio
import json
import os
import shutil
import signal
import subprocess
import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional


class StreamConfig:
    """Lightweight stream config (no ORM)."""

    def __init__(
        self,
        id: str = "",
        name: str = "",
        source_url: str = "",
        protocol: str = "webrtc",
        codec: str = "copy",
        enabled: bool = True,
        status: str = "inactive",
        created_at: Optional[str] = None,
        updated_at: Optional[str] = None,
    ):
        self.id = id or uuid.uuid4().hex[:12]
        self.name = name
        self.source_url = source_url
        self.protocol = protocol
        self.codec = codec
        self.enabled = enabled
        self.status = status
        self.created_at = created_at or datetime.now(timezone.utc).isoformat()
        self.updated_at = updated_at or self.created_at

    def to_dict(self) -> dict:
        return {
            "id": self.id,
            "name": self.name,
            "source_url": self.source_url,
            "protocol": self.protocol,
            "codec": self.codec,
            "enabled": self.enabled,
            "status": self.status,
            "created_at": self.created_at,
            "updated_at": self.updated_at,
        }

    @classmethod
    def from_dict(cls, d: dict) -> "StreamConfig":
        return cls(**{k: v for k, v in d.items() if k in cls.__init__.__code__.co_varnames})


class MediaManager:
    """Manages go2rtc process and stream configs."""

    GO2RTC_API_PORT = 1984
    GO2RTC_WEBRTC_PORT = 8555

    def __init__(self, data_dir: str | Path):
        self.data_dir = Path(data_dir)
        self.data_dir.mkdir(parents=True, exist_ok=True)
        self.config_path = self.data_dir / "streams.json"
        self.go2rtc_config_path = self.data_dir / "go2rtc.yaml"
        self.go2rtc_proc: Optional[subprocess.Popen] = None
        self._lock = asyncio.Lock()

    def _find_go2rtc(self) -> Optional[str]:
        # Prefer bundled binary
        bundled = Path(__file__).parent / "media" / "go2rtc"
        if bundled.exists():
            return str(bundled)
        # Fallback to PATH
        path = shutil.which("go2rtc")
        if path:
            return path
        return None

    def _find_ffmpeg(self) -> str:
        path = shutil.which("ffmpeg")
        if path:
            return path
        # Fallback common locations
        for p in ("/usr/bin/ffmpeg", "/usr/local/bin/ffmpeg"):
            if os.path.exists(p):
                return p
        raise RuntimeError("ffmpeg not found in PATH")

    def _load_streams(self) -> List[StreamConfig]:
        if not self.config_path.exists():
            self._ensure_default_stream()
        try:
            data = json.loads(self.config_path.read_text(encoding="utf-8"))
            streams = [StreamConfig.from_dict(s) for s in data.get("streams", [])]
            if not streams:
                self._ensure_default_stream()
                data = json.loads(self.config_path.read_text(encoding="utf-8"))
                streams = [StreamConfig.from_dict(s) for s in data.get("streams", [])]
            return streams
        except Exception:
            return []

    def _ensure_default_stream(self):
        """如果没有流配置，自动插入默认相机流。"""
        default = StreamConfig(
            name="default_camera",
            source_url="rtsp://192.168.144.25:8554/main.264",
            protocol="webrtc",
            codec="h264",
            enabled=False,
        )
        self._save_streams([default])

    def _save_streams(self, streams: List[StreamConfig]):
        try:
            self.config_path.write_text(
                json.dumps(
                    {"streams": [s.to_dict() for s in streams]},
                    ensure_ascii=False,
                    indent=2,
                ),
                encoding="utf-8",
            )
        except Exception:
            pass

    def _build_go2rtc_config(self, streams: List[StreamConfig]) -> str:
        lines = [
            "api:",
            f'  listen: ":{self.GO2RTC_API_PORT}"',
            '  origin: "*"',
            "",
            "streams:",
        ]
        for s in streams:
            if not s.enabled or not s.source_url:
                continue
            src = s.source_url
            codec = s.codec or "copy"
            if codec in ("h264", "h265"):
                ffmpeg_path = self._find_ffmpeg()
                vcodec = "libx264" if codec == "h264" else "libx265"
                src = (
                    f"exec:{ffmpeg_path} -hide_banner -v error "
                    f"-rtsp_transport tcp -fflags nobuffer -flags low_delay "
                    f"-i {src} -c:v {vcodec} -preset ultrafast -tune zerolatency "
                    f"-pix_fmt yuv420p -g 30 -an -f mpegts -"
                )
            lines.append(f'  {s.id}: "{src}"')
        if not any(s.enabled and s.source_url for s in streams):
            lines.append("  {}  # no active streams")
        lines.extend([
            "",
            "webrtc:",
            f'  listen: ":{self.GO2RTC_WEBRTC_PORT}"',
        ])
        return "\n".join(lines) + "\n"

    async def restart_go2rtc(self):
        """Rebuild config and restart go2rtc process."""
        async with self._lock:
            streams = self._load_streams()
            active_streams = [s for s in streams if s.enabled and s.source_url]
            config = self._build_go2rtc_config(streams)
            self.go2rtc_config_path.write_text(config, encoding="utf-8")

            # Stop existing
            if self.go2rtc_proc and self.go2rtc_proc.poll() is None:
                try:
                    self.go2rtc_proc.terminate()
                    await asyncio.wait_for(
                        asyncio.get_event_loop().run_in_executor(None, self.go2rtc_proc.wait),
                        timeout=3,
                    )
                except (asyncio.TimeoutError, ProcessLookupError):
                    try:
                        self.go2rtc_proc.kill()
                        await asyncio.get_event_loop().run_in_executor(None, self.go2rtc_proc.wait)
                    except ProcessLookupError:
                        pass
                self.go2rtc_proc = None

            if not active_streams:
                return

            go2rtc_bin = self._find_go2rtc()
            if not go2rtc_bin:
                raise RuntimeError("go2rtc binary not found")

            self.go2rtc_proc = subprocess.Popen(
                [go2rtc_bin, "-c", str(self.go2rtc_config_path)],
                stdout=subprocess.DEVNULL,
                stderr=subprocess.DEVNULL,
                start_new_session=True,
            )

    async def stop_go2rtc(self):
        async with self._lock:
            if self.go2rtc_proc and self.go2rtc_proc.poll() is None:
                try:
                    self.go2rtc_proc.terminate()
                    await asyncio.wait_for(
                        asyncio.get_event_loop().run_in_executor(None, self.go2rtc_proc.wait),
                        timeout=3,
                    )
                except (asyncio.TimeoutError, ProcessLookupError):
                    try:
                        self.go2rtc_proc.kill()
                        await asyncio.get_event_loop().run_in_executor(None, self.go2rtc_proc.wait)
                    except ProcessLookupError:
                        pass
                self.go2rtc_proc = None

    # ---- Public CRUD ----

    def list_streams(self) -> List[dict]:
        return [s.to_dict() for s in self._load_streams()]

    def get_stream(self, stream_id: str) -> Optional[dict]:
        for s in self._load_streams():
            if s.id == stream_id:
                return s.to_dict()
        return None

    def create_stream(self, data: dict) -> dict:
        streams = self._load_streams()
        s = StreamConfig(
            name=data.get("name", ""),
            source_url=data.get("source_url", ""),
            protocol=data.get("protocol", "webrtc"),
            codec=data.get("codec", "h264"),
            enabled=data.get("enabled", False),
        )
        streams.append(s)
        self._save_streams(streams)
        return s.to_dict()

    def update_stream(self, stream_id: str, data: dict) -> Optional[dict]:
        streams = self._load_streams()
        for s in streams:
            if s.id == stream_id:
                if "name" in data:
                    s.name = data["name"]
                if "source_url" in data:
                    s.source_url = data["source_url"]
                if "protocol" in data:
                    s.protocol = data["protocol"]
                if "codec" in data:
                    s.codec = data["codec"]
                s.updated_at = datetime.now(timezone.utc).isoformat()
                self._save_streams(streams)
                return s.to_dict()
        return None

    def delete_stream(self, stream_id: str) -> bool:
        streams = self._load_streams()
        new_streams = [s for s in streams if s.id != stream_id]
        if len(new_streams) != len(streams):
            self._save_streams(new_streams)
            return True
        return False

    async def start_stream(self, stream_id: str) -> dict:
        streams = self._load_streams()
        for s in streams:
            if s.id == stream_id:
                s.enabled = True
                s.status = "active"
                s.updated_at = datetime.now(timezone.utc).isoformat()
                break
        else:
            raise ValueError(f"Stream {stream_id} not found")
        self._save_streams(streams)
        await self.restart_go2rtc()
        return {"status": "started", "stream_id": stream_id}

    async def stop_stream(self, stream_id: str) -> dict:
        streams = self._load_streams()
        for s in streams:
            if s.id == stream_id:
                s.enabled = False
                s.status = "inactive"
                s.updated_at = datetime.now(timezone.utc).isoformat()
                break
        else:
            raise ValueError(f"Stream {stream_id} not found")
        self._save_streams(streams)
        await self.restart_go2rtc()
        return {"status": "stopped", "stream_id": stream_id}

    async def stop_all(self):
        """Shutdown hook: stop go2rtc."""
        await self.stop_go2rtc()

    def reset_all_streams_on_startup(self):
        """程序重启后，将所有流重置为停止状态。"""
        streams = self._load_streams()
        changed = False
        for s in streams:
            if s.enabled:
                s.enabled = False
                s.status = "inactive"
                s.updated_at = datetime.now(timezone.utc).isoformat()
                changed = True
        if changed:
            self._save_streams(streams)

    def is_go2rtc_running(self) -> bool:
        return self.go2rtc_proc is not None and self.go2rtc_proc.poll() is None

    # ---- WHEP proxy helper ----

    async def webrtc_offer(self, stream_id: str, offer_sdp: str) -> str:
        """Proxy SDP offer to go2rtc WHEP endpoint, return answer SDP."""
        import urllib.request
        url = f"http://127.0.0.1:{self.GO2RTC_API_PORT}/api/webrtc?src={stream_id}"
        req = urllib.request.Request(
            url,
            data=offer_sdp.encode("utf-8"),
            headers={"Content-Type": "application/sdp"},
            method="POST",
        )
        loop = asyncio.get_event_loop()
        try:
            response = await loop.run_in_executor(None, urllib.request.urlopen, req)
            return response.read().decode("utf-8")
        except Exception as e:
            raise RuntimeError(f"go2rtc WHEP error: {e}") from e
