"""Support for Frigate cameras."""
from __future__ import annotations

import datetime
import logging
from typing import Any, cast

import aiohttp
import async_timeout
from jinja2 import Template
import voluptuous as vol
from yarl import URL

from custom_components.frigate.api import FrigateApiClient
from homeassistant.components.camera import Camera, CameraEntityFeature, StreamType
from homeassistant.components.mqtt import async_publish
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_URL
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_platform
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity

from . import (
    FrigateDataUpdateCoordinator,
    FrigateEntity,
    FrigateMQTTEntity,
    ReceiveMessage,
    decode_if_necessary,
    get_friendly_name,
    get_frigate_device_identifier,
    get_frigate_entity_unique_id,
)
from .const import (
    ATTR_CLIENT,
    ATTR_CONFIG,
    ATTR_COORDINATOR,
    ATTR_END_TIME,
    ATTR_EVENT_ID,
    ATTR_FAVORITE,
    ATTR_PLAYBACK_FACTOR,
    ATTR_PTZ_ACTION,
    ATTR_PTZ_ARGUMENT,
    ATTR_START_TIME,
    CONF_ENABLE_WEBRTC,
    CONF_RTMP_URL_TEMPLATE,
    CONF_RTSP_URL_TEMPLATE,
    DEVICE_CLASS_CAMERA,
    DOMAIN,
    NAME,
    SERVICE_EXPORT_RECORDING,
    SERVICE_FAVORITE_EVENT,
    SERVICE_PTZ,
)
from .views import get_frigate_instance_id_for_config_entry

_LOGGER: logging.Logger = logging.getLogger(__name__)


async def async_setup_entry(
    hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
    """Camera entry setup."""

    frigate_config = hass.data[DOMAIN][entry.entry_id][ATTR_CONFIG]
    frigate_client = hass.data[DOMAIN][entry.entry_id][ATTR_CLIENT]
    client_id = get_frigate_instance_id_for_config_entry(hass, entry)
    coordinator = hass.data[DOMAIN][entry.entry_id][ATTR_COORDINATOR]

    async_add_entities(
        [
            FrigateCamera(
                entry,
                cam_name,
                frigate_client,
                client_id,
                coordinator,
                frigate_config,
                camera_config,
            )
            for cam_name, camera_config in frigate_config["cameras"].items()
        ]
        + (
            [BirdseyeCamera(entry, frigate_client)]
            if frigate_config.get("birdseye", {}).get("restream", False)
            else []
        )
    )

    # setup services
    platform = entity_platform.async_get_current_platform()
    platform.async_register_entity_service(
        SERVICE_EXPORT_RECORDING,
        {
            vol.Required(ATTR_PLAYBACK_FACTOR, default="realtime"): str,
            vol.Required(ATTR_START_TIME): str,
            vol.Required(ATTR_END_TIME): str,
        },
        SERVICE_EXPORT_RECORDING,
    )
    platform.async_register_entity_service(
        SERVICE_FAVORITE_EVENT,
        {
            vol.Required(ATTR_EVENT_ID): str,
            vol.Optional(ATTR_FAVORITE, default=True): bool,
        },
        SERVICE_FAVORITE_EVENT,
    )
    platform.async_register_entity_service(
        SERVICE_PTZ,
        {
            vol.Required(ATTR_PTZ_ACTION): str,
            vol.Optional(ATTR_PTZ_ARGUMENT, default=""): str,
        },
        SERVICE_PTZ,
    )


class FrigateCamera(FrigateMQTTEntity, CoordinatorEntity, Camera):  # type: ignore[misc]
    """Representation of a Frigate camera."""

    # sets the entity name to same as device name ex: camera.front_doorbell
    _attr_name = None

    def __init__(
        self,
        config_entry: ConfigEntry,
        cam_name: str,
        frigate_client: FrigateApiClient,
        frigate_client_id: Any | None,
        coordinator: FrigateDataUpdateCoordinator,
        frigate_config: dict[str, Any],
        camera_config: dict[str, Any],
    ) -> None:
        """Initialize a Frigate camera."""
        self._client = frigate_client
        self._client_id = frigate_client_id
        self._frigate_config = frigate_config
        self._camera_config = camera_config
        self._cam_name = cam_name
        super().__init__(
            config_entry,
            frigate_config,
            {
                "state_topic": {
                    "msg_callback": self._state_message_received,
                    "qos": 0,
                    "topic": (
                        f"{self._frigate_config['mqtt']['topic_prefix']}"
                        f"/{self._cam_name}/recordings/state"
                    ),
                    "encoding": None,
                },
                "motion_topic": {
                    "msg_callback": self._motion_message_received,
                    "qos": 0,
                    "topic": (
                        f"{self._frigate_config['mqtt']['topic_prefix']}"
                        f"/{self._cam_name}/motion/state"
                    ),
                    "encoding": None,
                },
            },
        )
        FrigateEntity.__init__(self, config_entry)
        CoordinatorEntity.__init__(self, coordinator)
        Camera.__init__(self)
        self._url = config_entry.data[CONF_URL]
        self._attr_is_on = True
        # The device_class is used to filter out regular camera entities
        # from motion camera entities on selectors
        self._attr_device_class = DEVICE_CLASS_CAMERA
        self._stream_source = None
        self._attr_is_streaming = (
            self._camera_config.get("rtmp", {}).get("enabled")
            or self._cam_name
            in self._frigate_config.get("go2rtc", {}).get("streams", {}).keys()
        )
        self._attr_is_recording = self._camera_config.get("record", {}).get("enabled")
        self._attr_motion_detection_enabled = self._camera_config.get("motion", {}).get(
            "enabled"
        )
        self._ptz_topic = (
            f"{frigate_config['mqtt']['topic_prefix']}" f"/{self._cam_name}/ptz"
        )
        self._set_motion_topic = (
            f"{frigate_config['mqtt']['topic_prefix']}" f"/{self._cam_name}/motion/set"
        )

        if (
            self._cam_name
            in self._frigate_config.get("go2rtc", {}).get("streams", {}).keys()
        ):
            if config_entry.options.get(CONF_ENABLE_WEBRTC, False):
                self._restream_type = "webrtc"
                self._attr_frontend_stream_type = StreamType.WEB_RTC
            else:
                self._restream_type = "rtsp"
                self._attr_frontend_stream_type = StreamType.HLS

            streaming_template = config_entry.options.get(
                CONF_RTSP_URL_TEMPLATE, ""
            ).strip()

            if streaming_template:
                # Can't use homeassistant.helpers.template as it requires hass which
                # is not available in the constructor, so use direct jinja2
                # template instead. This means templates cannot access HomeAssistant
                # state, but rather only the camera config.
                self._stream_source = Template(streaming_template).render(
                    **self._camera_config
                )
            else:
                self._stream_source = (
                    f"rtsp://{URL(self._url).host}:8554/{self._cam_name}"
                )
        elif self._camera_config.get("rtmp", {}).get("enabled"):
            self._restream_type = "rtmp"
            streaming_template = config_entry.options.get(
                CONF_RTMP_URL_TEMPLATE, ""
            ).strip()

            if streaming_template:
                # Can't use homeassistant.helpers.template as it requires hass which
                # is not available in the constructor, so use direct jinja2
                # template instead. This means templates cannot access HomeAssistant
                # state, but rather only the camera config.
                self._stream_source = Template(streaming_template).render(
                    **self._camera_config
                )
            else:
                self._stream_source = (
                    f"rtmp://{URL(self._url).host}/live/{self._cam_name}"
                )
        else:
            self._restream_type = "none"

    @callback  # type: ignore[misc]
    def _state_message_received(self, msg: ReceiveMessage) -> None:
        """Handle a new received MQTT state message."""
        self._attr_is_recording = decode_if_necessary(msg.payload) == "ON"
        self.async_write_ha_state()

    @callback  # type: ignore[misc]
    def _motion_message_received(self, msg: ReceiveMessage) -> None:
        """Handle a new received MQTT extra message."""
        self._attr_motion_detection_enabled = decode_if_necessary(msg.payload) == "ON"
        self.async_write_ha_state()

    @property
    def available(self) -> bool:
        """Signal when frigate loses connection to camera."""
        if self.coordinator.data:
            if (
                self.coordinator.data.get("cameras", {})
                .get(self._cam_name, {})
                .get("camera_fps", 0)
                == 0
            ):
                return False
        return super().available

    @property
    def unique_id(self) -> str:
        """Return a unique ID to use for this entity."""
        return get_frigate_entity_unique_id(
            self._config_entry.entry_id,
            "camera",
            self._cam_name,
        )

    @property
    def device_info(self) -> dict[str, Any]:
        """Return the device information."""
        return {
            "identifiers": {
                get_frigate_device_identifier(self._config_entry, self._cam_name)
            },
            "via_device": get_frigate_device_identifier(self._config_entry),
            "name": get_friendly_name(self._cam_name),
            "model": self._get_model(),
            "configuration_url": f"{self._url}/cameras/{self._cam_name}",
            "manufacturer": NAME,
        }

    @property
    def extra_state_attributes(self) -> dict[str, str]:
        """Return entity specific state attributes."""
        return {
            "client_id": str(self._client_id),
            "camera_name": self._cam_name,
            "restream_type": self._restream_type,
        }

    @property
    def supported_features(self) -> CameraEntityFeature:
        """Return supported features of this camera."""
        if not self._attr_is_streaming:
            return CameraEntityFeature(0)

        return CameraEntityFeature.STREAM

    async def async_camera_image(
        self, width: int | None = None, height: int | None = None
    ) -> bytes | None:
        """Return bytes of camera image."""
        websession = cast(aiohttp.ClientSession, async_get_clientsession(self.hass))

        image_url = str(
            URL(self._url)
            / f"api/{self._cam_name}/latest.jpg"
            % ({"h": height} if height is not None and height > 0 else {})
        )

        async with async_timeout.timeout(10):
            response = await websession.get(image_url)
            return await response.read()

    async def stream_source(self) -> str | None:
        """Return the source of the stream."""
        if not self._attr_is_streaming:
            return None
        return self._stream_source

    async def async_handle_web_rtc_offer(self, offer_sdp: str) -> str | None:
        """Handle the WebRTC offer and return an answer."""
        websession = cast(aiohttp.ClientSession, async_get_clientsession(self.hass))
        url = f"{self._url}/api/go2rtc/webrtc?src={self._cam_name}"
        payload = {"type": "offer", "sdp": offer_sdp}
        async with websession.post(url, json=payload) as resp:
            answer = await resp.json()
            return cast(str, answer["sdp"])

    async def async_enable_motion_detection(self) -> None:
        """Enable motion detection for this camera."""
        await async_publish(
            self.hass,
            self._set_motion_topic,
            "ON",
            0,
            False,
        )

    async def async_disable_motion_detection(self) -> None:
        """Disable motion detection for this camera."""
        await async_publish(
            self.hass,
            self._set_motion_topic,
            "OFF",
            0,
            False,
        )

    async def export_recording(
        self, playback_factor: str, start_time: str, end_time: str
    ) -> None:
        """Export recording."""
        await self._client.async_export_recording(
            self._cam_name,
            playback_factor,
            datetime.datetime.strptime(start_time, "%Y-%m-%d %H:%M:%S").timestamp(),
            datetime.datetime.strptime(end_time, "%Y-%m-%d %H:%M:%S").timestamp(),
        )

    async def favorite_event(self, event_id: str, favorite: bool) -> None:
        """Favorite an event."""
        await self._client.async_retain(event_id, favorite)

    async def ptz(self, action: str, argument: str) -> None:
        """Run PTZ command."""
        await async_publish(
            self.hass,
            self._ptz_topic,
            f"{action}{f'_{argument}' if argument else ''}",
            0,
            False,
        )


class BirdseyeCamera(FrigateEntity, Camera):  # type: ignore[misc]
    """Representation of the Frigate birdseye camera."""

    # sets the entity name to same as device name ex: camera.front_doorbell
    _attr_name = None

    def __init__(
        self,
        config_entry: ConfigEntry,
        frigate_client: FrigateApiClient,
    ) -> None:
        """Initialize the birdseye camera."""
        self._client = frigate_client
        FrigateEntity.__init__(self, config_entry)
        Camera.__init__(self)
        self._url = config_entry.data[CONF_URL]
        self._attr_is_on = True
        # The device_class is used to filter out regular camera entities
        # from motion camera entities on selectors
        self._attr_device_class = DEVICE_CLASS_CAMERA
        self._attr_is_streaming = True
        self._attr_is_recording = False

        streaming_template = config_entry.options.get(
            CONF_RTSP_URL_TEMPLATE, ""
        ).strip()

        if streaming_template:
            # Can't use homeassistant.helpers.template as it requires hass which
            # is not available in the constructor, so use direct jinja2
            # template instead. This means templates cannot access HomeAssistant
            # state, but rather only the camera config.
            self._stream_source = Template(streaming_template).render(
                {"name": "birdseye"}
            )
        else:
            self._stream_source = f"rtsp://{URL(self._url).host}:8554/birdseye"

    @property
    def unique_id(self) -> str:
        """Return a unique ID to use for this entity."""
        return get_frigate_entity_unique_id(
            self._config_entry.entry_id,
            "camera",
            "birdseye",
        )

    @property
    def device_info(self) -> dict[str, Any]:
        """Return the device information."""
        return {
            "identifiers": {
                get_frigate_device_identifier(self._config_entry, "birdseye")
            },
            "via_device": get_frigate_device_identifier(self._config_entry),
            "name": "Birdseye",
            "model": self._get_model(),
            "configuration_url": f"{self._url}/cameras/birdseye",
            "manufacturer": NAME,
        }

    @property
    def supported_features(self) -> CameraEntityFeature:
        """Return supported features of this camera."""
        return CameraEntityFeature.STREAM

    async def async_camera_image(
        self, width: int | None = None, height: int | None = None
    ) -> bytes | None:
        """Return bytes of camera image."""
        websession = cast(aiohttp.ClientSession, async_get_clientsession(self.hass))

        image_url = str(
            URL(self._url)
            / "api/birdseye/latest.jpg"
            % ({"h": height} if height is not None and height > 0 else {})
        )

        async with async_timeout.timeout(10):
            response = await websession.get(image_url)
            return await response.read()

    async def stream_source(self) -> str | None:
        """Return the source of the stream."""
        return self._stream_source
