"""Video file/URL live stream — decodes video frames via OpenCV.

Supports local files, HTTP URLs, RTSP streams, and YouTube URLs (via yt-dlp).
Optional sync clock integration for frame-accurate seeking.
"""

import re
import threading
import time
from typing import Optional

import numpy as np

try:
    import cv2
    _has_cv2 = True
except ImportError:
    _has_cv2 = False

from wled_controller.core.capture_engines.base import ScreenCapture
from wled_controller.core.processing.live_stream import LiveStream
from wled_controller.utils import get_logger

logger = get_logger(__name__)

# YouTube URL patterns
_YT_PATTERNS = [
    re.compile(r"(?:https?://)?(?:www\.)?youtube\.com/watch\?v=([a-zA-Z0-9_-]{11})"),
    re.compile(r"(?:https?://)?youtu\.be/([a-zA-Z0-9_-]{11})"),
    re.compile(r"(?:https?://)?(?:www\.)?youtube\.com/shorts/([a-zA-Z0-9_-]{11})"),
    re.compile(r"youtube://([a-zA-Z0-9_-]{11})"),
]


def is_youtube_url(url: str) -> bool:
    return any(p.search(url) for p in _YT_PATTERNS)


def resolve_youtube_url(url: str, resolution_limit: Optional[int] = None) -> str:
    """Resolve a YouTube URL to a direct stream URL using yt-dlp."""
    try:
        import yt_dlp
    except ImportError:
        raise RuntimeError("yt-dlp is required for YouTube support: pip install yt-dlp")

    max_h = resolution_limit or 720
    format_spec = f"bestvideo[height<={max_h}][ext=mp4]/best[height<={max_h}][ext=mp4]/best[height<={max_h}]/best"

    ydl_opts = {
        "format": format_spec,
        "quiet": True,
        "no_warnings": True,
        "extract_flat": False,
    }

    with yt_dlp.YoutubeDL(ydl_opts) as ydl:
        info = ydl.extract_info(url, download=False)
        stream_url = info.get("url")
        if not stream_url:
            formats = info.get("requested_formats") or []
            for f in formats:
                if f.get("vcodec") != "none":
                    stream_url = f["url"]
                    break
        if not stream_url:
            raise RuntimeError(f"Could not extract video stream URL from: {url}")

        logger.info(
            f"Resolved YouTube URL: {info.get('title', '?')} "
            f"({info.get('width', '?')}x{info.get('height', '?')})"
        )
        return stream_url


def _require_cv2():
    """Raise a clear error if OpenCV is not installed."""
    if not _has_cv2:
        raise ImportError(
            "OpenCV is required for camera and video support. "
            "Install it with: pip install opencv-python-headless"
        )


def extract_thumbnail(url: str, resolution_limit: Optional[int] = None) -> Optional[np.ndarray]:
    """Extract the first frame of a video as a thumbnail (RGB numpy array).

    For YouTube URLs, resolves via yt-dlp first.
    Returns None on failure.
    """
    _require_cv2()
    try:
        actual_url = url
        if is_youtube_url(url):
            actual_url = resolve_youtube_url(url, resolution_limit)

        cap = cv2.VideoCapture(actual_url)
        if not cap.isOpened():
            return None

        ret, frame = cap.read()
        cap.release()

        if not ret or frame is None:
            return None

        # Convert BGR → RGB
        frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)

        # Downscale if needed
        if resolution_limit and frame.shape[1] > resolution_limit:
            scale = resolution_limit / frame.shape[1]
            new_w = resolution_limit
            new_h = int(frame.shape[0] * scale)
            frame = cv2.resize(frame, (new_w, new_h), interpolation=cv2.INTER_AREA)

        return frame
    except Exception as e:
        logger.warning(f"Failed to extract thumbnail from {url}: {e}")
        return None


class VideoCaptureLiveStream(LiveStream):
    """Live stream that decodes video frames from a file, URL, or YouTube link.

    A background thread decodes frames at the video's native FPS (or target FPS).
    Supports loop, trim (start_time/end_time), playback speed, resolution limit,
    and optional sync clock for frame-accurate seeking.

    When a sync clock is attached:
    - clock.get_time() determines the current playback position
    - clock.speed overrides playback_speed
    - clock pause/resume pauses/resumes playback
    """

    def __init__(
        self,
        url: str,
        loop: bool = True,
        playback_speed: float = 1.0,
        start_time: Optional[float] = None,
        end_time: Optional[float] = None,
        resolution_limit: Optional[int] = None,
        target_fps: int = 30,
    ):
        _require_cv2()
        self._original_url = url
        self._resolved_url: Optional[str] = None
        self._loop = loop
        self._playback_speed = playback_speed
        self._start_time = start_time or 0.0
        self._end_time = end_time
        self._resolution_limit = resolution_limit
        self._target_fps = target_fps

        self._cap = None  # Optional[cv2.VideoCapture]
        self._video_fps: float = 30.0
        self._total_frames: int = 0
        self._video_duration: float = 0.0
        self._video_width: int = 0
        self._video_height: int = 0

        self._latest_frame: Optional[ScreenCapture] = None
        self._frame_lock = threading.Lock()
        self._running = False
        self._thread: Optional[threading.Thread] = None

        # Sync clock (set externally)
        self._clock = None

    @property
    def target_fps(self) -> int:
        return self._target_fps

    @property
    def display_index(self) -> Optional[int]:
        return None  # Not a screen capture

    def set_clock(self, clock) -> None:
        """Attach a SyncClockRuntime for frame-accurate seek."""
        self._clock = clock

    def start(self) -> None:
        if self._running:
            return

        # Resolve YouTube URL if needed
        actual_url = self._original_url
        if is_youtube_url(actual_url):
            actual_url = resolve_youtube_url(actual_url, self._resolution_limit)
        self._resolved_url = actual_url

        # Open capture
        self._cap = cv2.VideoCapture(actual_url)
        if not self._cap.isOpened():
            raise RuntimeError(f"Failed to open video: {self._original_url}")

        self._video_fps = self._cap.get(cv2.CAP_PROP_FPS) or 30.0
        self._total_frames = int(self._cap.get(cv2.CAP_PROP_FRAME_COUNT) or 0)
        self._video_width = int(self._cap.get(cv2.CAP_PROP_FRAME_WIDTH))
        self._video_height = int(self._cap.get(cv2.CAP_PROP_FRAME_HEIGHT))

        if self._total_frames > 0 and self._video_fps > 0:
            self._video_duration = self._total_frames / self._video_fps
        else:
            self._video_duration = 0.0  # Live stream or unknown

        # Use video FPS as target if not overridden, capped at 60
        if self._target_fps <= 0:
            self._target_fps = min(int(self._video_fps), 60)

        # Seek to start_time if set
        if self._start_time > 0:
            self._seek_to(self._start_time)

        self._running = True
        self._thread = threading.Thread(
            target=self._decode_loop,
            name="video-capture",
            daemon=True,
        )
        self._thread.start()

        logger.info(
            f"VideoCaptureLiveStream started: {self._original_url} "
            f"({self._video_width}x{self._video_height} @ {self._video_fps:.1f}fps, "
            f"duration={self._video_duration:.1f}s)"
        )

    def stop(self) -> None:
        if not self._running:
            return

        self._running = False
        if self._thread:
            self._thread.join(timeout=5.0)
            self._thread = None

        if self._cap:
            self._cap.release()
            self._cap = None

        self._latest_frame = None
        logger.info(f"VideoCaptureLiveStream stopped: {self._original_url}")

    def get_latest_frame(self) -> Optional[ScreenCapture]:
        with self._frame_lock:
            return self._latest_frame

    def _seek_to(self, time_sec: float) -> None:
        """Seek to a specific time in seconds."""
        if self._cap and self._total_frames > 0:
            self._cap.set(cv2.CAP_PROP_POS_MSEC, time_sec * 1000.0)

    def _get_effective_end_time(self) -> float:
        """Get the effective end time (end_time or video duration)."""
        if self._end_time is not None:
            return self._end_time
        if self._video_duration > 0:
            return self._video_duration
        return float("inf")

    def _decode_loop(self) -> None:
        """Background thread: decode frames, apply speed/clock, handle loop."""
        frame_time = 1.0 / self._target_fps if self._target_fps > 0 else 1.0 / 30
        playback_start = time.perf_counter()
        last_seek_time = -1.0
        buf: Optional[np.ndarray] = None
        consecutive_errors = 0

        try:
            while self._running:
                loop_start = time.perf_counter()

                # Determine current playback position
                if self._clock is not None:
                    clock_time = self._clock.get_time()
                    current_time = self._start_time + clock_time
                else:
                    wall_elapsed = time.perf_counter() - playback_start
                    current_time = self._start_time + wall_elapsed * self._playback_speed

                end_time = self._get_effective_end_time()

                # Handle end of range
                if current_time >= end_time:
                    if self._loop:
                        # Reset
                        if self._clock is not None:
                            # Can't control clock, just wrap
                            current_time = self._start_time + (
                                (current_time - self._start_time)
                                % max(end_time - self._start_time, 0.001)
                            )
                        else:
                            playback_start = time.perf_counter()
                            current_time = self._start_time

                        self._seek_to(current_time)
                        last_seek_time = -1.0
                    else:
                        # End — hold last frame
                        time.sleep(frame_time)
                        continue

                # Clock-based seeking: seek when clock position changes significantly
                if self._clock is not None and self._total_frames > 0:
                    # Only seek if position jumped more than 2 frames
                    threshold = 2.0 / self._video_fps
                    if abs(current_time - last_seek_time) > threshold or last_seek_time < 0:
                        self._seek_to(current_time)
                        last_seek_time = current_time

                # Decode next frame
                try:
                    ret, frame = self._cap.read()
                    if not ret or frame is None:
                        if self._loop and self._total_frames > 0:
                            self._seek_to(self._start_time)
                            if self._clock is None:
                                playback_start = time.perf_counter()
                            last_seek_time = -1.0
                            continue
                        else:
                            time.sleep(frame_time)
                            continue

                    consecutive_errors = 0

                    # BGR → RGB
                    cv2.cvtColor(frame, cv2.COLOR_BGR2RGB, dst=frame)

                    # Downscale if resolution limit set
                    if self._resolution_limit and frame.shape[1] > self._resolution_limit:
                        scale = self._resolution_limit / frame.shape[1]
                        new_w = self._resolution_limit
                        new_h = int(frame.shape[0] * scale)
                        frame = cv2.resize(
                            frame, (new_w, new_h), interpolation=cv2.INTER_AREA
                        )

                    h, w = frame.shape[:2]

                    # Reuse buffer if shape matches
                    if buf is None or buf.shape != frame.shape:
                        buf = frame
                    else:
                        np.copyto(buf, frame)

                    sc = ScreenCapture(
                        image=buf, width=w, height=h, display_index=-1
                    )
                    with self._frame_lock:
                        self._latest_frame = sc

                except Exception as e:
                    consecutive_errors += 1
                    logger.error(f"Video decode error: {e}")
                    if consecutive_errors > 10:
                        backoff = min(1.0, 0.1 * (consecutive_errors - 10))
                        time.sleep(backoff)
                        continue

                # Throttle to target FPS
                elapsed = time.perf_counter() - loop_start
                remaining = frame_time - elapsed
                if remaining > 0:
                    time.sleep(remaining)

        except Exception as e:
            logger.error(f"Fatal video decode loop error: {e}", exc_info=True)
        finally:
            self._running = False
