#!/usr/bin/python3

"""Platform for switch integration."""

from collections.abc import Callable
from datetime import timedelta
import logging
from typing import Any

from aiohttp.client_exceptions import ClientConnectionError
from wyzeapy import BulbService, CameraService, SwitchService, Wyzeapy
from wyzeapy.exceptions import AccessTokenError, ParameterError, UnknownApiError
from wyzeapy.services.bulb_service import Bulb
from wyzeapy.services.camera_service import Camera
from wyzeapy.services.switch_service import Switch
from wyzeapy.types import Device, DeviceTypes, Event

from homeassistant.components.automation import (
    automations_with_device,
    automations_with_entity,
)
from homeassistant.components.script import scripts_with_device, scripts_with_entity
from homeassistant.components.switch import SwitchEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import (
    device_registry as dr,
    entity_registry as er,
    issue_registry as ir,
)
from homeassistant.helpers.dispatcher import (
    async_dispatcher_connect,
    async_dispatcher_send,
)
from homeassistant.helpers.issue_registry import IssueSeverity

from .const import (
    CAMERA_UPDATED,
    CONF_CLIENT,
    DOMAIN,
    LIGHT_UPDATED,
    WYZE_CAMERA_EVENT,
    WYZE_NOTIFICATION_TOGGLE,
)
from .token_manager import token_exception_handler

_LOGGER = logging.getLogger(__name__)
ATTRIBUTION = "Data provided by Wyze"
SCAN_INTERVAL = timedelta(seconds=30)
OUTDOOR_PLUGS = "WLPPO"
OUTDOOR_PLUG_INDIVUAL_OUTLETS = "WLPPO-SUB"
MOTION_SWITCH_UNSUPPORTED = [
    "GW_BE1",
    "GW_GC1",
    "GW_GC2",
]  # Video doorbell pro, OG, OG 3x Telephoto
POWER_SWITCH_UNSUPPORTED = ["GW_BE1"]  # Video doorbell pro (device has no off function)
NOTIFICATION_SWITCH_UNSUPPORTED = {
    "GW_GC1",
    "GW_GC2",
}  # OG and OG 3x Telephoto models currently unsupported due to InvalidSignature2 error


# noinspection DuplicatedCode
@token_exception_handler
async def async_setup_entry(
    hass: HomeAssistant,
    config_entry: ConfigEntry,
    async_add_entities: Callable[[list[Any], bool], None],
) -> None:
    """This function sets up the config entry.

    :param hass: The Home Assistant Instance
    :param config_entry: The current config entry
    :param async_add_entities: This function adds entities to the config entry
    :return:
    """

    _LOGGER.debug("""Creating new WyzeApi light component""")
    client: Wyzeapy = hass.data[DOMAIN][config_entry.entry_id][CONF_CLIENT]
    switch_service = await client.switch_service
    wall_switch_service = await client.wall_switch_service
    camera_service = await client.camera_service
    bulb_service = await client.bulb_service

    switches: list[SwitchEntity] = []
    has_outdoor_plug: bool = False
    devices_to_migrate: list[str] = []
    devices = []
    device_registry = dr.async_get(hass)

    base_switches = await switch_service.get_switches()
    # The outdoor plug has a dummy switch that doesn't control anything
    # on the device. So we add non-outdoor plug switches and then
    # the switches for each individual outlet on the outdoor plug.
    switches.extend(
        WyzeSwitch(switch_service, switch)
        for switch in base_switches
        if switch.product_model not in [OUTDOOR_PLUGS, OUTDOOR_PLUG_INDIVUAL_OUTLETS]
    )

    for switch in base_switches:
        if switch.product_model in [OUTDOOR_PLUG_INDIVUAL_OUTLETS]:
            has_outdoor_plug = True
            switches.append(WyzeSwitch(switch_service, switch))

    switches.extend(
        WyzeSwitch(wall_switch_service, switch)
        for switch in await wall_switch_service.get_switches()
    )

    camera_switches = await camera_service.get_cameras()
    for switch in camera_switches:
        # Notification toggle switch
        if switch.product_model not in NOTIFICATION_SWITCH_UNSUPPORTED:
            switches.append(WyzeCameraNotificationSwitch(camera_service, switch))

        # IoT Power switch
        if switch.product_model not in POWER_SWITCH_UNSUPPORTED:
            switches.append(WyzeSwitch(camera_service, switch))

        # Motion toggle switch
        if switch.product_model not in MOTION_SWITCH_UNSUPPORTED:
            switches.append(WyzeCameraMotionSwitch(camera_service, switch))

    switches.append(WyzeNotifications(client))

    bulb_switches = await bulb_service.get_bulbs()
    switches.extend(
        WzyeLightstripSwitch(bulb_service, bulb)
        for bulb in bulb_switches
        if bulb.type is DeviceTypes.LIGHTSTRIP
    )

    # Catch old outdoor plug devices and entities and remove.
    # This can be removed at a later date.
    if has_outdoor_plug:
        devices = dr.async_entries_for_config_entry(
            device_registry, config_entry.entry_id
        )
        for device in devices:
            for identifier in device.identifiers:
                mac = identifier[1]
            if (
                "-" in mac and device.model == OUTDOOR_PLUG_INDIVUAL_OUTLETS
            ):  # The old devices have a '-' in the mac
                devices_to_migrate.append(device.id)

    # Also catch the old dummy switch to remove below.
    # Should only happen once while the old devices are still around.
    if devices_to_migrate:
        devices_to_migrate.extend(
            device.id for device in devices if device.model == OUTDOOR_PLUGS
        )
        await async_migrate_switch_data(
            hass, config_entry, devices_to_migrate, device_registry
        )

    async_add_entities(switches, False)


async def async_migrate_switch_data(
    hass: HomeAssistant,
    config_entry: ConfigEntry,
    device_list,
    device_registry,
):
    """Remove redundant switch devices and entities and flag for repair.

    This can be removed in the future.
    """

    entity_registry = er.async_get(hass)
    entities = er.async_entries_for_config_entry(entity_registry, config_entry.entry_id)

    for entity in entities:
        if entity.device_id in device_list and entity.domain == "switch":
            entity_automations = automations_with_entity(hass, entity.entity_id)
            entity_automations.extend(scripts_with_entity(hass, entity.entity_id))
            if entity_automations:
                for issue in entity_automations:
                    ir.async_create_issue(
                        hass,
                        DOMAIN,
                        issue_id=f"{issue}-entity-issue",
                        is_fixable=False,
                        is_persistent=True,
                        severity=IssueSeverity.ERROR,
                        learn_more_url="https://github.com/SecKatie/ha-wyzeapi/issues/789",
                        translation_key="entity_changed",
                        translation_placeholders={
                            "entity": entity.entity_id,
                            "automation": issue,
                        },
                    )
            entity_registry.async_remove(entity.id)

    for device in device_list:
        device_automations = automations_with_device(hass, device)
        device_automations.extend(scripts_with_device(hass, device))
        if device_automations:
            for issue in device_automations:
                ir.async_create_issue(
                    hass,
                    DOMAIN,
                    issue_id=f"{issue}-device-issue",
                    is_fixable=False,
                    is_persistent=True,
                    severity=IssueSeverity.ERROR,
                    learn_more_url="https://github.com/SecKatie/ha-wyzeapi/issues/789",
                    translation_key="device_changed",
                    translation_placeholders={
                        "automation": issue,
                    },
                )
        device_registry.async_remove_device(device)


class WyzeNotifications(SwitchEntity):
    """Class for notification switch."""

    _attr_should_poll = False
    _attr_name = "Wyze Notifications"

    def __init__(self, client: Wyzeapy) -> None:
        """Initialize the switch."""
        self._client = client
        self._is_on = False
        self._uid = WYZE_NOTIFICATION_TOGGLE
        self._just_updated = False

    @property
    def is_on(self) -> bool:
        """Return if the switch is on."""
        return self._is_on

    @property
    def device_info(self):
        """Return the device info."""
        return {
            "identifiers": {(DOMAIN, self._uid)},
            "name": "Wyze Notifications",
            "manufacturer": "WyzeLabs",
            "model": "WyzeNotificationToggle",
        }

    async def async_turn_on(self, **kwargs: Any) -> None:
        """Turn on the switch."""
        try:
            await self._client.enable_notifications()
        except (AccessTokenError, ParameterError, UnknownApiError) as err:
            raise HomeAssistantError(f"Wyze returned an error: {err.args}") from err
        except ClientConnectionError as err:
            raise HomeAssistantError(err) from err
        else:
            self._is_on = True
            self._just_updated = True
            self.async_schedule_update_ha_state()

    async def async_turn_off(self, **kwargs: Any) -> None:
        """Turn off the switch."""
        try:
            await self._client.disable_notifications()
        except (AccessTokenError, ParameterError, UnknownApiError) as err:
            raise HomeAssistantError(f"Wyze returned an error: {err.args}") from err
        except ClientConnectionError as err:
            raise HomeAssistantError(err) from err
        else:
            self._is_on = False
            self._just_updated = True
            self.async_schedule_update_ha_state()

    @property
    def available(self):
        """Return the connection status of this switch."""
        return True

    @property
    def unique_id(self):
        """Return the unique ID."""
        return self._uid

    async def async_update(self):
        """Update the switch."""
        if not self._just_updated:
            self._is_on = await self._client.notifications_are_on
        else:
            self._just_updated = False


class WyzeSwitch(SwitchEntity):
    """Representation of a Wyze Switch."""

    _on: bool
    _available: bool
    _just_updated = False
    _old_event_ts: int = 0  # preload with 0 so that we know when it's been updated
    _attr_should_poll = False

    def __init__(self, service: CameraService | SwitchService, device: Device) -> None:
        """Initialize a Wyze Bulb."""
        self._device = device
        self._service = service

        if type(self._device) is Camera:
            self._device = Camera(self._device.raw_dict)
        elif type(self._device) is Switch:
            self._device = Switch(self._device.raw_dict)

    @property
    def device_info(self):
        """Return the device info.

        Outdoor plug needs its own setup based on how the MAC's are
        displayed and to keep the plugs organized by device.
        """
        if self._device.product_model == OUTDOOR_PLUG_INDIVUAL_OUTLETS:
            mac = self._device.mac.split("-")[0]
            return {
                "identifiers": {(DOMAIN, mac)},
                "connections": {
                    (
                        dr.CONNECTION_NETWORK_MAC,
                        mac,
                    )
                },
                "name": f"Outdoor Plug {mac}",
                "manufacturer": "WyzeLabs",
                "model": self._device.product_model,
            }
        return {
            "identifiers": {(DOMAIN, self._device.mac)},
            "connections": {
                (
                    dr.CONNECTION_NETWORK_MAC,
                    self._device.mac,
                )
            },
            "name": self._device.nickname,
            "manufacturer": "WyzeLabs",
            "model": self._device.product_model,
        }

    @token_exception_handler
    async def async_turn_on(self, **kwargs: Any) -> None:
        """Turn on the switch."""
        try:
            await self._service.turn_on(self._device)
        except (AccessTokenError, ParameterError, UnknownApiError) as err:
            raise HomeAssistantError(f"Wyze returned an error: {err.args}") from err
        except ClientConnectionError as err:
            raise HomeAssistantError(err) from err
        else:
            self._device.on = True
            self._just_updated = True
            self.async_schedule_update_ha_state()

    @token_exception_handler
    async def async_turn_off(self, **kwargs: Any) -> None:
        """Turn off the switch."""
        try:
            await self._service.turn_off(self._device)
        except (AccessTokenError, ParameterError, UnknownApiError) as err:
            raise HomeAssistantError(f"Wyze returned an error: {err.args}") from err
        except ClientConnectionError as err:
            raise HomeAssistantError(err) from err
        else:
            self._device.on = False
            self._just_updated = True
            self.async_schedule_update_ha_state()

    @property
    def name(self):
        """Return the display name of this switch."""
        if type(self._device) is Camera:
            return f"{self._device.nickname} Power"
        return self._device.nickname

    @property
    def available(self):
        """Return the connection status of this switch."""
        return self._device.available

    @property
    def is_on(self):
        """Return true if switch is on."""
        return self._device.on

    @property
    def unique_id(self):
        """Return the unique ID."""
        return f"{self._device.mac}-switch"

    @property
    def extra_state_attributes(self):
        """Return device attributes of the entity."""
        dev_info = {}

        if self._device.device_params.get("electricity"):
            dev_info["Battery"] = str(
                self._device.device_params.get("electricity") + "%"
            )
        # noinspection DuplicatedCode
        if self._device.device_params.get("ip"):
            dev_info["IP"] = str(self._device.device_params.get("ip"))
        if self._device.device_params.get("rssi"):
            dev_info["RSSI"] = str(self._device.device_params.get("rssi"))
        if self._device.device_params.get("ssid"):
            dev_info["SSID"] = str(self._device.device_params.get("ssid"))

        return dev_info

    @token_exception_handler
    async def async_update(self):
        """Update the entity."""
        if not self._just_updated:
            self._device = await self._service.update(self._device)
        else:
            self._just_updated = False

    @callback
    def async_update_callback(self, switch: Switch):
        """Update the switch's state."""
        self._device = switch
        async_dispatcher_send(
            self.hass,
            f"{CAMERA_UPDATED}-{switch.mac}",
            switch,
        )
        self.async_schedule_update_ha_state()
        # if the switch is from a camera, lets check for new events
        if isinstance(switch, Camera):
            if (
                self._old_event_ts > 0
                and self._old_event_ts != switch.last_event_ts
                and switch.last_event is not None
            ):
                event: Event = switch.last_event
                # The screenshot/video urls are not always in the same positions in the lists, so we have to loop
                # through them
                _screenshot_url = None
                _video_url = None
                _ai_tag_list = []
                for resource in event.file_list:
                    _ai_tag_list = _ai_tag_list + resource["ai_tag_list"]
                    if resource["type"] == 1:
                        _screenshot_url = resource["url"]
                    elif resource["type"] == 2:
                        _video_url = resource["url"]
                _LOGGER.debug("Camera: %s has a new event", switch.nickname)
                self.hass.bus.fire(
                    WYZE_CAMERA_EVENT,
                    {
                        "device_name": switch.nickname,
                        "device_mac": switch.mac,
                        "ai_tag_list": _ai_tag_list,
                        "tag_list": event.tag_list,
                        "event_screenshot": _screenshot_url,
                        "event_video": _video_url,
                    },
                )
            self._old_event_ts = switch.last_event_ts

    async def async_added_to_hass(self) -> None:
        """Subscribe to update events."""
        self._device.callback_function = self.async_update_callback
        self._service.register_updater(self._device, 30)
        await self._service.start_update_manager()
        return await super().async_added_to_hass()

    async def async_will_remove_from_hass(self) -> None:
        """Unregister updated on removal."""
        self._service.unregister_updater(self._device)


class WyzeCameraNotificationSwitch(SwitchEntity):
    """Representation of a Wyze Camera Notification Switch."""

    _available: bool

    def __init__(self, service: CameraService, device: Camera) -> None:
        """Initialize a Wyze Notification Switch."""
        self._service = service
        self._device = device

    @property
    def device_info(self):
        """Return the device info."""
        return {
            "identifiers": {(DOMAIN, self._device.mac)},
            "name": self._device.nickname,
            "manufacturer": "WyzeLabs",
            "model": self._device.product_model,
        }

    @property
    def should_poll(self) -> bool:
        """No polling needed."""
        return False

    async def async_turn_on(self, **kwargs: Any) -> None:
        """Turn on the switch."""
        try:
            await self._service.turn_on_notifications(self._device)
        except (AccessTokenError, ParameterError, UnknownApiError) as err:
            raise HomeAssistantError(f"Wyze returned an error: {err.args}") from err
        except ClientConnectionError as err:
            raise HomeAssistantError(err) from err
        else:
            self._device.notify = True
            self.async_write_ha_state()

    async def async_turn_off(self, **kwargs: Any) -> None:
        """Turn off the switch."""
        try:
            await self._service.turn_off_notifications(self._device)
        except (AccessTokenError, ParameterError, UnknownApiError) as err:
            raise HomeAssistantError(f"Wyze returned an error: {err.args}") from err
        except ClientConnectionError as err:
            raise HomeAssistantError(err) from err
        else:
            self._device.notify = False
            self.async_write_ha_state()

    @property
    def name(self):
        """Return the display name of this switch."""
        return f"{self._device.nickname} Notifications"

    @property
    def available(self):
        """Return the connection status of this switch."""
        return self._device.available

    @property
    def is_on(self):
        """Return true if switch is on."""
        return self._device.notify

    @property
    def unique_id(self):
        """Add a unique ID to the switch."""
        return f"{self._device.mac}-notification_switch"

    @callback
    def handle_camera_update(self, camera: Camera) -> None:
        """Update the switch whenever there is an update."""
        self._device = camera
        self.async_write_ha_state()

    async def async_added_to_hass(self) -> None:
        """Listen for camera updates."""
        self.async_on_remove(
            async_dispatcher_connect(
                self.hass,
                f"{CAMERA_UPDATED}-{self._device.mac}",
                self.handle_camera_update,
            )
        )


class WyzeCameraMotionSwitch(SwitchEntity):
    """Representation of a Wyze Camera Motion Detection Switch."""

    _available: bool

    def __init__(self, service: CameraService, device: Camera) -> None:
        """Initialize a Wyze Notification Switch."""
        self._service = service
        self._device = device

    @property
    def device_info(self):
        """Return the device info."""
        return {
            "identifiers": {(DOMAIN, self._device.mac)},
            "name": self._device.nickname,
            "manufacturer": "WyzeLabs",
            "model": self._device.product_model,
        }

    @property
    def should_poll(self) -> bool:
        """No polling needed."""
        return False

    async def async_turn_on(self, **kwargs: Any) -> None:
        """Turn on the switch."""
        try:
            await self._service.turn_on_motion_detection(self._device)
        except (AccessTokenError, ParameterError, UnknownApiError) as err:
            raise HomeAssistantError(f"Wyze returned an error: {err.args}") from err
        except ClientConnectionError as err:
            raise HomeAssistantError(err) from err
        else:
            self._device.motion = True
            self.async_write_ha_state()

    async def async_turn_off(self, **kwargs: Any) -> None:
        """Turn off the switch."""
        try:
            await self._service.turn_off_motion_detection(self._device)
        except (AccessTokenError, ParameterError, UnknownApiError) as err:
            raise HomeAssistantError(f"Wyze returned an error: {err.args}") from err
        except ClientConnectionError as err:
            raise HomeAssistantError(err) from err
        else:
            self._device.motion = False
            self.async_write_ha_state()

    @property
    def name(self):
        """Return the display name of this switch."""
        return f"{self._device.nickname} Motion Detection"

    @property
    def available(self):
        """Return the connection status of this switch."""
        return self._device.available

    @property
    def is_on(self):
        """Return true if switch is on."""
        return self._device.motion

    @property
    def unique_id(self):
        """Add a unique ID to the switch."""
        return f"{self._device.mac}-motion_switch"

    @callback
    def handle_camera_update(self, camera: Camera) -> None:
        """Update the switch whenever there is an update."""
        self._device = camera
        self.async_write_ha_state()

    async def async_added_to_hass(self) -> None:
        """Listen for camera updates."""
        self.async_on_remove(
            async_dispatcher_connect(
                self.hass,
                f"{CAMERA_UPDATED}-{self._device.mac}",
                self.handle_camera_update,
            )
        )


class WzyeLightstripSwitch(SwitchEntity):
    """Music Mode Switch for Wyze Light Strip."""

    def __init__(self, service: BulbService, device: Device) -> None:
        """Initialize a Wyze Music Mode Switch."""
        self._service = service
        self._device = Bulb(device.raw_dict)

    @property
    def device_info(self):
        """Return the device info."""
        return {
            "identifiers": {(DOMAIN, self._device.mac)},
            "name": self._device.nickname,
            "manufacturer": "WyzeLabs",
            "model": self._device.product_model,
        }

    @property
    def should_poll(self) -> bool:
        """No polling needed."""
        return False

    async def async_turn_on(self, **kwargs: Any) -> None:
        """Turn on the switch."""
        try:
            await self._service.music_mode_on(self._device)
        except (AccessTokenError, ParameterError, UnknownApiError) as err:
            raise HomeAssistantError(f"Wyze returned an error: {err.args}") from err
        except ClientConnectionError as err:
            raise HomeAssistantError(err) from err
        else:
            self._device.music_mode = True
            self.async_schedule_update_ha_state()

    async def async_turn_off(self, **kwargs: Any) -> None:
        """Turn off the switch."""
        try:
            await self._service.music_mode_off(self._device)
        except (AccessTokenError, ParameterError, UnknownApiError) as err:
            raise HomeAssistantError(f"Wyze returned an error: {err.args}") from err
        except ClientConnectionError as err:
            raise HomeAssistantError(err) from err
        else:
            self._device.music_mode = False
            self.async_schedule_update_ha_state()

    @property
    def name(self):
        """Return the display name of this switch."""
        return f"{self._device.nickname} Music Mode for Effects"

    @property
    def available(self):
        """Return the connection status of this switch."""
        return self._device.available

    @property
    def is_on(self):
        """Return true if switch is on."""
        return self._device.music_mode

    @property
    def unique_id(self):
        """Add a unique ID to the switch."""
        return f"{self._device.mac}-music_mode"

    @callback
    def handle_light_update(self, bulb: Bulb) -> None:
        """Update the switch whenever there is an update."""
        self._device = bulb
        self.async_write_ha_state()

    async def async_added_to_hass(self) -> None:
        """Listen for light updates."""
        self.async_on_remove(
            async_dispatcher_connect(
                self.hass,
                f"{LIGHT_UPDATED}-{self._device.mac}",
                self.handle_light_update,
            )
        )
