from __future__ import annotations

import argparse
import logging
import os
import re
import subprocess
import time
from pathlib import Path
import asyncio

from app.config import ServerConfiguration, is_running_in_windows

# https://www.pexels.com/videos/

logger = logging.getLogger(__name__)

def get_default_mediamtx_config() -> Path:
    if is_running_in_windows():
        return Path(__file__).resolve().parent.parent / "mediamtx/windows/mediamtx.yml"
    else:
        return Path(__file__).resolve().parent.parent / "mediamtx/linux/mediamtx.yml"

def get_default_mediamtx_binary() -> str:
    if is_running_in_windows():
        return "D:/dev/mediamtx/mediamtx.exe"
    else:
        return "/opt/mediamtx/mediamtx"

class MediaMTXLaucher:
    def __init__(
        self,
        mediamtx_binary: str,
        mediamtx_config: str,
    ):
        self.mediamtx_binary = mediamtx_binary
        self.mediamtx_config = mediamtx_config
        self._mediamtx_process: subprocess.Popen[bytes] | None = None

    def _build_mediamtx_command(self) -> list[str]:
        command = [self.mediamtx_binary]
        if self.mediamtx_config:
            command.append(self.mediamtx_config)
        return command

    def start(self) -> None:
        if self._mediamtx_process is None:
            mediamtx_cmd = self._build_mediamtx_command()
            logger.info("Starting MediaMTX: %s", " ".join(mediamtx_cmd))
            self._mediamtx_process = subprocess.Popen(mediamtx_cmd)
            # Small delay so MediaMTX has time to bind protocol listeners before ffmpeg publishes.
            time.sleep(0.8)
            if self._mediamtx_process.poll() is not None:
                raise RuntimeError("MediaMTX exited immediately after startup. Check mediamtx path/config and running instances.")

    def stop(self) -> None:
        if self._mediamtx_process is not None:
            logger.info("Stopping MediaMTX")
            self._mediamtx_process.terminate()
            try:
                self._mediamtx_process.wait(timeout=5)
            except subprocess.TimeoutExpired:
                self._mediamtx_process.kill()
            self._mediamtx_process = None

class VideoStreamer:
    def __init__(
        self,
        video_file: str | None = None,
        source_type: str = "file",
        duplicate_stream: bool = False,
        webcam_device: str = "video=Integrated Camera",
        webcam_framerate: int = 30,
        webcam_resolution: str = "1280x720",
        stream_name: str = "camera1",
        ffmpeg_binary: str = "ffmpeg",
        publish_protocol: str = "rtmp",
        rtsp_host: str = "127.0.0.1",
        rtsp_port: int = 8554,
        rtmp_host: str = "127.0.0.1",
        rtmp_port: int = 1935,
        webrtc_host: str = "127.0.0.1",
        webrtc_port: int = 8889,
    ):
        self.video_file = video_file
        self.source_type = source_type.lower()
        self.duplicate_stream = duplicate_stream
        self.webcam_device = webcam_device
        self.webcam_framerate = webcam_framerate
        self.webcam_resolution = webcam_resolution
        self.stream_name = stream_name
        self.ffmpeg_binary = ffmpeg_binary
        self.publish_protocol = publish_protocol.lower()
        self.rtsp_host = rtsp_host
        self.rtsp_port = rtsp_port
        self.rtmp_host = rtmp_host
        self.rtmp_port = rtmp_port
        self.webrtc_host = webrtc_host
        self.webrtc_port = webrtc_port

        self._ffmpeg_process: subprocess.Popen[bytes] | None = None
        self._mediamtx_process: subprocess.Popen[bytes] | None = None

    def _list_windows_webcam_devices(self) -> list[str]:
        command = [
            self.ffmpeg_binary,
            "-hide_banner",
            "-list_devices",
            "true",
            "-f",
            "dshow",
            "-i",
            "dummy",
        ]
        try:
            result = subprocess.run(
                command,
                check=False,
                capture_output=True,
                text=True,
            )
        except FileNotFoundError as exc:
            raise FileNotFoundError(
                f"ffmpeg binary not found: {self.ffmpeg_binary}"
            ) from exc

        output = f"{result.stdout}\n{result.stderr}"
        devices: list[str] = []
        in_video_section = False

        for line in output.splitlines():
            if "DirectShow video devices" in line:
                in_video_section = True
                continue
            if in_video_section and "DirectShow audio devices" in line:
                break

            if in_video_section:
                match = re.search(r'"([^"]+)"', line)
                if match:
                    device_name = match.group(1).strip()
                    if device_name and device_name not in devices:
                        devices.append(device_name)

        return devices

    def _resolve_webcam_device(self) -> str:
        if not self.is_windows:
            return self.webcam_device

        requested = (self.webcam_device or "").strip()
        if requested and requested.lower() != "auto":
            return requested

        devices = self._list_windows_webcam_devices()
        if not devices:
            raise RuntimeError(
                "No Windows webcam detected by ffmpeg (dshow). "
                "Use '--webcam-device ""video=...""' with a valid device name."
            )

        selected = f"video={devices[0]}"
        logger.info("Auto-detected Windows webcam: %s", selected)
        return selected

    @property
    def is_windows(self) -> bool:
        return os.name == "nt"

    @property
    def is_linux(self) -> bool:
        return os.name == "posix"

    @property
    def rtsp_publish_url(self) -> str:
        return f"rtsp://{self.rtsp_host}:{self.rtsp_port}/{self.stream_name}"

    @property
    def rtmp_publish_url(self) -> str:
        return f"rtmp://{self.rtmp_host}:{self.rtmp_port}/{self.stream_name}"

    @property
    def publish_url(self) -> str:
        if self.publish_protocol == "rtsp":
            return self.rtsp_publish_url
        if self.publish_protocol == "rtmp":
            return self.rtmp_publish_url
        raise ValueError(f"Unsupported publish protocol: {self.publish_protocol}")

    @property
    def webrtc_play_url(self) -> str:
        return f"http://{self.webrtc_host}:{self.webrtc_port}/{self.stream_name}"

    def _build_ffmpeg_command(self) -> list[str]:
        base = [
            self.ffmpeg_binary,
            "-hide_banner",
            "-nostats",
            "-loglevel",
            "error",
        ]

        if self.source_type == "file":
            if self.duplicate_stream:
                # -fflags +genpts fixes non-monotonic timestamps across stream_loop iterations.
                # -avoid_negative_ts make_zero prevents DTS < 0 at loop restart.
                base.extend(["-fflags", "+genpts", "-avoid_negative_ts", "make_zero"])
            base.extend([
                "-re",
                "-stream_loop",
                "-1",
                "-i",
                str(self.video_file),
            ])

            if self.duplicate_stream:
                base.extend([
                    "-c",
                    "copy",
                ])
            else:
                base.extend([
                    "-an",
                    "-c:v",
                    "libx264",
                    "-preset",
                    "veryfast",
                    "-tune",
                    "zerolatency",
                    "-pix_fmt",
                    "yuv420p",
                    "-g",
                    "30",
                    "-profile:v",
                    "baseline",
                ])
        elif self.source_type == "webcam":
            if self.duplicate_stream:
                raise ValueError("duplicate_stream is only supported when source_type is 'file'")

            if self.is_windows:
                # Windows webcam capture via DirectShow, device format: video=Device Name
                base.extend([
                    "-f",
                    "dshow",
                    "-framerate",
                    str(self.webcam_framerate),
                    "-video_size",
                    self.webcam_resolution,
                    "-i",
                    self.webcam_device,
                ])
            elif self.is_linux:
                # Linux webcam capture via Video4Linux2, device format: /dev/video0
                base.extend([
                    "-f",
                    "v4l2",
                    "-framerate",
                    str(self.webcam_framerate),
                    "-video_size",
                    self.webcam_resolution,
                    "-i",
                    self.webcam_device,
                ])
            else:
                raise ValueError("Unsupported platform for webcam capture")

            # Webcam always requires encoding (no stream copy possible)
            base.extend([
                "-an",
                "-c:v",
                "libx264",
                "-preset",
                "veryfast",
                "-tune",
                "zerolatency",
                "-pix_fmt",
                "yuv420p",
                "-g",
                "30",
                "-profile:v",
                "baseline",
            ])
        else:
            raise ValueError("source_type must be 'file' or 'webcam'")

        if self.publish_protocol == "rtsp":
            return [
                *base,
                "-f",
                "rtsp",
                "-rtsp_transport",
                "tcp",
                self.rtsp_publish_url,
            ]

        if self.publish_protocol == "rtmp":
            cmd = [*base]
            if self.duplicate_stream:
                # MP4 stores H.264 as AVCC; RTMP/FLV requires Annex B.
                # Without this filter the decoder misses SPS/PPS and shows a black/frozen frame.
                cmd.extend(["-bsf:v", "h264_mp4toannexb"])
            cmd.extend(["-f", "flv", self.rtmp_publish_url])
            return cmd

        raise ValueError(f"Unsupported publish protocol: {self.publish_protocol}")

    def _validate_configuration(self) -> None:
        if self.source_type not in {"file", "webcam"}:
            raise ValueError("source_type must be 'file' or 'webcam'")

        if self.source_type == "file" and self.video_file is None:
            raise ValueError("video_file is required when source_type is 'file'")

        if self.source_type != "file" and self.duplicate_stream:
            raise ValueError("duplicate_stream is only supported when source_type is 'file'")

        if self.source_type == "webcam" and not self.is_windows and not self.webcam_device:
            raise ValueError("webcam_device is required when source_type is 'webcam'")

        if self.publish_protocol not in {"rtsp", "rtmp"}:
            raise ValueError("publish_protocol must be 'rtsp' or 'rtmp'")

    async def start(self) -> None:
        if self.source_type == "webcam":
            self.webcam_device = self._resolve_webcam_device()

        self._validate_configuration()

        if self.source_type == "file" and self.video_file and not Path(self.video_file).is_file():
            raise FileNotFoundError(f"Video file not found: {self.video_file}")

        if self._ffmpeg_process is not None:
            logger.info("ffmpeg is already running")
            return

        ffmpeg_cmd = self._build_ffmpeg_command()
        logger.info("Starting ffmpeg: %s", " ".join(ffmpeg_cmd))
        self._ffmpeg_process = subprocess.Popen(ffmpeg_cmd)

        logger.info("Publish protocol: %s", self.publish_protocol)
        logger.info("Publish URL: %s", self.publish_url)
        logger.info("WebRTC play URL: %s", self.webrtc_play_url)

        await asyncio.sleep(1)  # Give ffmpeg a moment to start and bind to the port before MediaMTX tries to consume the stream.

    async def stop(self) -> None:
        if self._ffmpeg_process is not None:
            logger.info("Stopping ffmpeg")
            self._ffmpeg_process.terminate()
            try:
                self._ffmpeg_process.wait(timeout=5)
            except subprocess.TimeoutExpired:
                self._ffmpeg_process.kill()
            self._ffmpeg_process = None


class VideoStreamerApp:
    mtx_launcher: MediaMTXLaucher
    video_streamers: list[VideoStreamer] = []

    def __init__(self):
        mediamtx_binary = os.getenv("SVL_MEDIAMTX_BINARY", get_default_mediamtx_binary())
        mediamtx_config = os.getenv("SVL_MEDIAMTX_CONFIG", str(get_default_mediamtx_config()))
        self.mtx_launcher = MediaMTXLaucher(
            mediamtx_binary=mediamtx_binary,
            mediamtx_config=mediamtx_config,
        )

    async def init(self, config: ServerConfiguration):
        for source in config.video.sources:
            if source.disabled:
                logger.info("Source '%s' is disabled, skipping", source.name)
                continue
            if source.type == "webcam":
                webcam_device = source.path
                stream_name = source.name
                webcam_resolution = config.webcam.resolution
                webcam_framerate = config.webcam.framerate
                streamer = VideoStreamer(
                    source_type="webcam",
                    webcam_device=webcam_device,
                    webcam_resolution=webcam_resolution,
                    webcam_framerate=webcam_framerate,
                    stream_name=stream_name,
                )
                self.video_streamers.append(streamer)
            elif source.type == "file":
                streamer = VideoStreamer(
                    video_file=source.path,
                    stream_name=source.name,
                    duplicate_stream=source.duplicate_stream,
                )
                self.video_streamers.append(streamer)
            else:
                raise ValueError(f"Unsupported source type: {source.type}")
    
    async def start(self):
        self.mtx_launcher.start()
        for streamer in self.video_streamers:
            await streamer.start()
    
    async def stop(self):
        for streamer in self.video_streamers:
            await streamer.stop()
        self.mtx_launcher.stop()


videoStreamerApp = VideoStreamerApp()

def _parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="Stream MP4 in loop to MediaMTX and consume via WebRTC")
    parser.add_argument("--source", choices=["file", "webcam"], default="file", help="Source type for ffmpeg input")
    parser.add_argument(
        "--video",
        type=str,
        default=str(Path(__file__).resolve().parent.parent / "video" / "Video-generale_3.mp4"),
        help="Path to source MP4 file",
    )
    
    mediamtx_config = os.getenv("SVL_MEDIAMTX_CONFIG", str(get_default_mediamtx_config()))

    parser.add_argument("--webcam-device", type=str, default="auto", help="Webcam device (Windows dshow: video=Device Name or auto, Linux v4l2: /dev/video0)")
    parser.add_argument("--webcam-fps", type=int, default=30, help="Webcam capture framerate")
    parser.add_argument("--webcam-resolution", type=str, default="1280x720", help="Webcam resolution as WxH")
    parser.add_argument("--stream-name", type=str, default="camera1", help="MediaMTX stream path")
    parser.add_argument("--ffmpeg", type=str, default="ffmpeg", help="Path or command for ffmpeg")
    parser.add_argument("--mediamtx", type=str, default="D:/dev/mediamtx/mediamtx.exe", help="Path or command for mediamtx")
    parser.add_argument("--mediamtx-config", type=str, default=mediamtx_config, help="MediaMTX config file")
    parser.add_argument("--no-mediamtx", action="store_true", help="Do not launch MediaMTX process")
    parser.add_argument("--publish-protocol", choices=["rtmp", "rtsp"], default="rtmp", help="Protocol used by ffmpeg to publish to MediaMTX")
    parser.add_argument("--rtsp-host", type=str, default="127.0.0.1", help="RTSP host")
    parser.add_argument("--rtsp-port", type=int, default=8554, help="RTSP port")
    parser.add_argument("--rtmp-host", type=str, default="127.0.0.1", help="RTMP host")
    parser.add_argument("--rtmp-port", type=int, default=1935, help="RTMP port")
    parser.add_argument("--webrtc-host", type=str, default="127.0.0.1", help="WebRTC host for URL")
    parser.add_argument("--webrtc-port", type=int, default=8889, help="WebRTC port for URL")
    return parser.parse_args()


async def main() -> None:
    logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
    args = _parse_args()

    if not args.mediamtx:
        logger.warning("No MediaMTX binary provided. Assuming 'mediamtx' is in PATH, but this may not be correct.")

    if not args.mediamtx_config:
        logger.warning("No MediaMTX config file provided. Using default configuration which may not be suitable for all use cases.")
    
    mtx_launcher = MediaMTXLaucher(
        mediamtx_binary=args.mediamtx,
        mediamtx_config=args.mediamtx_config,
    )

    streamer = VideoStreamer(
        source_type=args.source,
        video_file=args.video,
        webcam_device=args.webcam_device,
        webcam_framerate=args.webcam_fps,
        webcam_resolution=args.webcam_resolution,
        stream_name=args.stream_name,
        ffmpeg_binary=args.ffmpeg,
        publish_protocol=args.publish_protocol,
        rtsp_host=args.rtsp_host,
        rtsp_port=args.rtsp_port,
        rtmp_host=args.rtmp_host,
        rtmp_port=args.rtmp_port,
        webrtc_host=args.webrtc_host,
        webrtc_port=args.webrtc_port,
    )

    mtx_launcher.start()
    await streamer.start()
    logger.info("Streaming started. Press Ctrl+C to stop.")

    try:
        while True:
            await asyncio.sleep(1)
    except KeyboardInterrupt:
        logger.info("Stopping streaming...")
        await streamer.stop()
        mtx_launcher.stop()

if __name__ == "__main__":
    asyncio.run(main())
