"""Recording Watchdog for Browser Use Sessions."""

import asyncio
from pathlib import Path
from typing import Any, ClassVar

from bubus import BaseEvent
from cdp_use.cdp.page.events import ScreencastFrameEvent
from pydantic import PrivateAttr
from uuid_extensions import uuid7str

from noesium.subagents.bu.browser.events import AgentFocusChangedEvent, BrowserConnectedEvent, BrowserStopEvent
from noesium.subagents.bu.browser.profile import ViewportSize
from noesium.subagents.bu.browser.video_recorder import VideoRecorderService
from noesium.subagents.bu.browser.watchdog_base import BaseWatchdog
from noesium.subagents.bu.utils import create_task_with_error_handling


class RecordingWatchdog(BaseWatchdog):
    """
    Manages video recording of a browser session using CDP screencasting.
    """

    LISTENS_TO: ClassVar[list[type[BaseEvent]]] = [BrowserConnectedEvent, BrowserStopEvent, AgentFocusChangedEvent]
    EMITS: ClassVar[list[type[BaseEvent]]] = []

    _recorder: VideoRecorderService | None = PrivateAttr(default=None)
    _current_session_id: str | None = PrivateAttr(default=None)
    _screencast_params: dict[str, Any] | None = PrivateAttr(default=None)

    async def on_BrowserConnectedEvent(self, event: BrowserConnectedEvent) -> None:
        """
        Starts video recording if it is configured in the browser profile.
        """
        profile = self.browser_session.browser_profile
        if not profile.record_video_dir:
            return

        # Dynamically determine video size
        size = profile.record_video_size
        if not size:
            self.logger.debug("record_video_size not specified, detecting viewport size...")
            size = await self._get_current_viewport_size()

        if not size:
            self.logger.warning("Cannot start video recording: viewport size could not be determined.")
            return

        video_format = getattr(profile, "record_video_format", "mp4").strip(".")
        output_path = Path(profile.record_video_dir) / f"{uuid7str()}.{video_format}"

        self.logger.debug(f"Initializing video recorder for format: {video_format}")
        self._recorder = VideoRecorderService(
            output_path=output_path, size=size, framerate=profile.record_video_framerate
        )
        self._recorder.start()

        if not self._recorder._is_active:
            self._recorder = None
            return

        self.browser_session.cdp_client.register.Page.screencastFrame(self.on_screencastFrame)

        self._screencast_params = {
            "format": "png",
            "quality": 90,
            "maxWidth": size["width"],
            "maxHeight": size["height"],
            "everyNthFrame": 1,
        }

        await self._start_screencast()

    async def on_AgentFocusChangedEvent(self, event: AgentFocusChangedEvent) -> None:
        """
        Switches video recording to the new tab.
        """
        if self._recorder:
            self.logger.debug(f"Agent focus changed to {event.target_id}, switching screencast...")
            await self._start_screencast()

    async def _start_screencast(self) -> None:
        """Starts screencast on the currently focused tab."""
        if not self._recorder or not self._screencast_params:
            return

        try:
            # Get the current session (for the focused target)
            cdp_session = await self.browser_session.get_or_create_cdp_session()

            # If we are already recording this session, do nothing
            if self._current_session_id == cdp_session.session_id:
                return

            # Stop recording on the previous session
            if self._current_session_id:
                try:
                    # Use the root client to stop screencast on the specific session
                    await self.browser_session.cdp_client.send.Page.stopScreencast(session_id=self._current_session_id)
                except Exception as e:
                    # It's possible the session is already closed
                    self.logger.debug(f"Failed to stop screencast on old session {self._current_session_id}: {e}")

            self._current_session_id = cdp_session.session_id

            # Start recording on the new session
            await cdp_session.cdp_client.send.Page.startScreencast(
                params=self._screencast_params,  # type: ignore
                session_id=cdp_session.session_id,
            )
            self.logger.info(f"📹 Started/Switched video recording to target {cdp_session.target_id}")
        except Exception as e:
            self.logger.error(f"Failed to switch screencast via CDP: {e}")
            # If we fail to start on the new tab, we reset current session id
            self._current_session_id = None

    async def _get_current_viewport_size(self) -> ViewportSize | None:
        """Gets the current viewport size directly from the browser via CDP."""
        try:
            cdp_session = await self.browser_session.get_or_create_cdp_session()
            metrics = await cdp_session.cdp_client.send.Page.getLayoutMetrics(session_id=cdp_session.session_id)

            # Use cssVisualViewport for the most accurate representation of the visible area
            viewport = metrics.get("cssVisualViewport", {})
            width = viewport.get("clientWidth")
            height = viewport.get("clientHeight")

            if width and height:
                self.logger.debug(f"Detected viewport size: {width}x{height}")
                return ViewportSize(width=int(width), height=int(height))
        except Exception as e:
            self.logger.warning(f"Failed to get viewport size from browser: {e}")

        return None

    def on_screencastFrame(self, event: ScreencastFrameEvent, session_id: str | None) -> None:
        """
        Synchronous handler for incoming screencast frames.
        """
        # Only process frames from the current session we intend to record
        # This handles race conditions where old session might still send frames before stop completes
        if self._current_session_id and session_id != self._current_session_id:
            return

        if not self._recorder:
            return
        self._recorder.add_frame(event["data"])
        create_task_with_error_handling(
            self._ack_screencast_frame(event, session_id),
            name="ack_screencast_frame",
            logger_instance=self.logger,
            suppress_exceptions=True,
        )

    async def _ack_screencast_frame(self, event: ScreencastFrameEvent, session_id: str | None) -> None:
        """
        Asynchronously acknowledges a screencast frame.
        """
        try:
            await self.browser_session.cdp_client.send.Page.screencastFrameAck(
                params={"sessionId": event["sessionId"]}, session_id=session_id
            )
        except Exception as e:
            self.logger.debug(f"Failed to acknowledge screencast frame: {e}")

    async def on_BrowserStopEvent(self, event: BrowserStopEvent) -> None:
        """
        Stops the video recording and finalizes the video file.
        """
        if self._recorder:
            recorder = self._recorder
            self._recorder = None
            self._current_session_id = None
            self._screencast_params = None

            self.logger.debug("Stopping video recording and saving file...")
            loop = asyncio.get_event_loop()
            await loop.run_in_executor(None, recorder.stop_and_save)
