"""Support for Google Nest SDM Cameras."""

from __future__ import annotations

from abc import ABC
import asyncio
from collections.abc import Awaitable, Callable
import datetime
import functools
import logging
from pathlib import Path

from google_nest_sdm.camera_traits import (
    CameraLiveStreamTrait,
    RtspStream,
    StreamingProtocol,
    WebRtcStream,
)
from google_nest_sdm.device import Device
from google_nest_sdm.exceptions import ApiException
from webrtc_models import RTCIceCandidateInit

from homeassistant.components.camera import (
    Camera,
    CameraEntityFeature,
    WebRTCAnswer,
    WebRTCClientConfiguration,
    WebRTCSendMessage,
)
from homeassistant.components.stream import CONF_EXTRA_PART_WAIT_TIME
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.util.dt import utcnow

from .device_info import NestDeviceInfo
from .types import NestConfigEntry

_LOGGER = logging.getLogger(__name__)

PLACEHOLDER = Path(__file__).parent / "placeholder.png"

# Used to schedule an alarm to refresh the stream before expiration
STREAM_EXPIRATION_BUFFER = datetime.timedelta(seconds=30)

# Refresh streams with a bounded interval and backoff on failure
MIN_REFRESH_BACKOFF_INTERVAL = datetime.timedelta(minutes=1)
MAX_REFRESH_BACKOFF_INTERVAL = datetime.timedelta(minutes=10)
BACKOFF_MULTIPLIER = 1.5


async def async_setup_entry(
    hass: HomeAssistant,
    entry: NestConfigEntry,
    async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
    """Set up the cameras."""

    def devices_added(devices: list[Device]) -> None:
        entities: list[NestCameraBaseEntity] = []
        for device in devices:
            if (live_stream := device.traits.get(CameraLiveStreamTrait.NAME)) is None:
                continue
            if StreamingProtocol.WEB_RTC in live_stream.supported_protocols:
                entities.append(NestWebRTCEntity(device))
            elif StreamingProtocol.RTSP in live_stream.supported_protocols:
                entities.append(NestRTSPEntity(device))

        async_add_entities(entities)

    entry.runtime_data.register_devices_listener(devices_added)


class StreamRefresh:
    """Class that will refresh an expiring stream.

    This class will schedule an alarm for the next expiration time of a stream.
    When the alarm fires, it runs the provided `refresh_cb` to extend the
    lifetime of the stream and return a new expiration time.

    A simple backoff will be applied when the refresh callback fails.
    """

    def __init__(
        self,
        hass: HomeAssistant,
        expires_at: datetime.datetime,
        refresh_cb: Callable[[], Awaitable[datetime.datetime | None]],
    ) -> None:
        """Initialize StreamRefresh."""
        self._hass = hass
        self._unsub: Callable[[], None] | None = None
        self._min_refresh_interval = MIN_REFRESH_BACKOFF_INTERVAL
        self._refresh_cb = refresh_cb
        self._schedule_stream_refresh(expires_at - STREAM_EXPIRATION_BUFFER)

    def unsub(self) -> None:
        """Invalidates the stream."""
        if self._unsub:
            self._unsub()

    async def _handle_refresh(self, _: datetime.datetime) -> None:
        """Alarm that fires to check if the stream should be refreshed."""
        self._unsub = None
        try:
            expires_at = await self._refresh_cb()
        except ApiException as err:
            _LOGGER.debug("Failed to refresh stream: %s", err)
            # Increase backoff until the max backoff interval is reached
            self._min_refresh_interval = min(
                self._min_refresh_interval * BACKOFF_MULTIPLIER,
                MAX_REFRESH_BACKOFF_INTERVAL,
            )
            refresh_time = utcnow() + self._min_refresh_interval
        else:
            if expires_at is None:
                return
            self._min_refresh_interval = MIN_REFRESH_BACKOFF_INTERVAL  # Reset backoff
            # Defend against invalid stream expiration time in the past
            refresh_time = max(
                expires_at - STREAM_EXPIRATION_BUFFER,
                utcnow() + self._min_refresh_interval,
            )
        self._schedule_stream_refresh(refresh_time)

    def _schedule_stream_refresh(self, refresh_time: datetime.datetime) -> None:
        """Schedules an alarm to refresh any streams before expiration."""
        _LOGGER.debug("Scheduling stream refresh for %s", refresh_time)
        self._unsub = async_track_point_in_utc_time(
            self._hass,
            self._handle_refresh,
            refresh_time,
        )


class NestCameraBaseEntity(Camera, ABC):
    """Devices that support cameras."""

    _attr_has_entity_name = True
    _attr_name = None
    _attr_is_streaming = True
    _attr_supported_features = CameraEntityFeature.STREAM

    def __init__(self, device: Device) -> None:
        """Initialize the camera."""
        super().__init__()
        self._device = device
        nest_device_info = NestDeviceInfo(device)
        self._attr_device_info = nest_device_info.device_info
        self._attr_brand = nest_device_info.device_brand
        self._attr_model = nest_device_info.device_model
        self.stream_options[CONF_EXTRA_PART_WAIT_TIME] = 3
        # The API "name" field is a unique device identifier.
        self._attr_unique_id = f"{self._device.name}-camera"

    async def async_added_to_hass(self) -> None:
        """Run when entity is added to register update signal handler."""
        self.async_on_remove(
            self._device.add_update_listener(self.async_write_ha_state)
        )


class NestRTSPEntity(NestCameraBaseEntity):
    """Nest cameras that use RTSP."""

    _rtsp_stream: RtspStream | None = None
    _rtsp_live_stream_trait: CameraLiveStreamTrait

    def __init__(self, device: Device) -> None:
        """Initialize the camera."""
        super().__init__(device)
        self._create_stream_url_lock = asyncio.Lock()
        self._rtsp_live_stream_trait = device.traits[CameraLiveStreamTrait.NAME]
        self._refresh_unsub: Callable[[], None] | None = None

    @property
    def use_stream_for_stills(self) -> bool:
        """Always use the RTSP stream to generate snapshots."""
        return True

    @property
    def available(self) -> bool:
        """Return True if entity is available."""
        # Cameras are marked unavailable on stream errors in #54659 however nest
        # streams have a high error rate (#60353). Given nest streams are so flaky,
        # marking the stream unavailable has other side effects like not showing
        # the camera image which sometimes are still able to work. Until the
        # streams are fixed, just leave the streams as available.
        return True

    async def stream_source(self) -> str | None:
        """Return the source of the stream."""
        async with self._create_stream_url_lock:
            if not self._rtsp_stream:
                _LOGGER.debug("Fetching stream url")
                try:
                    self._rtsp_stream = (
                        await self._rtsp_live_stream_trait.generate_rtsp_stream()
                    )
                except ApiException as err:
                    raise HomeAssistantError(f"Nest API error: {err}") from err
                refresh = StreamRefresh(
                    self.hass,
                    self._rtsp_stream.expires_at,
                    self._async_refresh_stream,
                )
                self._refresh_unsub = refresh.unsub
        assert self._rtsp_stream
        if self._rtsp_stream.expires_at < utcnow():
            _LOGGER.warning("Stream already expired")
        return self._rtsp_stream.rtsp_stream_url

    async def _async_refresh_stream(self) -> datetime.datetime | None:
        """Refresh stream to extend expiration time."""
        if not self._rtsp_stream:
            return None
        _LOGGER.debug("Extending RTSP stream")
        try:
            self._rtsp_stream = await self._rtsp_stream.extend_rtsp_stream()
        except ApiException as err:
            _LOGGER.debug("Failed to extend stream: %s", err)
            # Next attempt to catch a url will get a new one
            self._rtsp_stream = None
            if self.stream:
                await self.stream.stop()
                self.stream = None
            return None
        # Update the stream worker with the latest valid url
        if self.stream:
            self.stream.update_source(self._rtsp_stream.rtsp_stream_url)
        return self._rtsp_stream.expires_at

    async def async_will_remove_from_hass(self) -> None:
        """Invalidates the RTSP token when unloaded."""
        await super().async_will_remove_from_hass()
        if self._refresh_unsub is not None:
            self._refresh_unsub()
        if self._rtsp_stream:
            try:
                await self._rtsp_stream.stop_stream()
            except ApiException as err:
                _LOGGER.debug("Error stopping stream: %s", err)
            self._rtsp_stream = None


class NestWebRTCEntity(NestCameraBaseEntity):
    """Nest cameras that use WebRTC."""

    def __init__(self, device: Device) -> None:
        """Initialize the camera."""
        super().__init__(device)
        self._webrtc_sessions: dict[str, WebRtcStream] = {}
        self._refresh_unsub: dict[str, Callable[[], None]] = {}

    async def _async_refresh_stream(self, session_id: str) -> datetime.datetime | None:
        """Refresh stream to extend expiration time."""
        if not (webrtc_stream := self._webrtc_sessions.get(session_id)):
            return None
        _LOGGER.debug("Extending WebRTC stream %s", webrtc_stream.media_session_id)
        webrtc_stream = await webrtc_stream.extend_stream()
        if session_id in self._webrtc_sessions:
            self._webrtc_sessions[session_id] = webrtc_stream
            return webrtc_stream.expires_at
        return None

    async def async_camera_image(
        self, width: int | None = None, height: int | None = None
    ) -> bytes | None:
        """Return a placeholder image for WebRTC cameras that don't support snapshots."""
        return await self.hass.async_add_executor_job(self.placeholder_image)

    @classmethod
    @functools.cache
    def placeholder_image(cls) -> bytes:
        """Return placeholder image to use when no stream is available."""
        return PLACEHOLDER.read_bytes()

    async def async_handle_async_webrtc_offer(
        self, offer_sdp: str, session_id: str, send_message: WebRTCSendMessage
    ) -> None:
        """Return the source of the stream."""
        trait: CameraLiveStreamTrait = self._device.traits[CameraLiveStreamTrait.NAME]
        try:
            stream = await trait.generate_web_rtc_stream(offer_sdp)
        except ApiException as err:
            raise HomeAssistantError(f"Nest API error: {err}") from err
        _LOGGER.debug(
            "Started WebRTC session %s, %s", session_id, stream.media_session_id
        )
        self._webrtc_sessions[session_id] = stream
        send_message(WebRTCAnswer(stream.answer_sdp))
        refresh = StreamRefresh(
            self.hass,
            stream.expires_at,
            functools.partial(self._async_refresh_stream, session_id),
        )
        self._refresh_unsub[session_id] = refresh.unsub

    async def async_on_webrtc_candidate(
        self, session_id: str, candidate: RTCIceCandidateInit
    ) -> None:
        """Ignore WebRTC candidates for Nest cloud based cameras."""
        return

    @callback
    def close_webrtc_session(self, session_id: str) -> None:
        """Close a WebRTC session."""
        if (stream := self._webrtc_sessions.pop(session_id, None)) is not None:
            _LOGGER.debug(
                "Closing WebRTC session %s, %s", session_id, stream.media_session_id
            )
            unsub = self._refresh_unsub.pop(session_id)
            unsub()

            async def stop_stream() -> None:
                try:
                    await stream.stop_stream()
                except ApiException as err:
                    _LOGGER.debug("Error stopping stream: %s", err)

            self.hass.async_create_task(stop_stream())
        super().close_webrtc_session(session_id)

    @callback
    def _async_get_webrtc_client_configuration(self) -> WebRTCClientConfiguration:
        """Return the WebRTC client configuration adjustable per integration."""
        return WebRTCClientConfiguration(data_channel="dataSendChannel")

    async def async_will_remove_from_hass(self) -> None:
        """Invalidates the RTSP token when unloaded."""
        await super().async_will_remove_from_hass()
        for session_id in list(self._webrtc_sessions.keys()):
            self.close_webrtc_session(session_id)
