"""Harmony data object which contains the Harmony Client."""
from __future__ import annotations

from collections.abc import Iterable
import logging

from aioharmony.const import ClientCallbackType, SendCommandDevice
import aioharmony.exceptions as aioexc
from aioharmony.harmonyapi import HarmonyAPI as HarmonyClient

from .const import ACTIVITY_POWER_OFF
from .subscriber import HarmonySubscriberMixin

_LOGGER = logging.getLogger(__name__)


class HarmonyData(HarmonySubscriberMixin):
    """HarmonyData registers for Harmony hub updates."""

    def __init__(self, hass, address: str, name: str, unique_id: str):
        """Initialize a data object."""
        super().__init__(hass)
        self._name = name
        self._unique_id = unique_id
        self._available = False
        self._client = None
        self._address = address

    @property
    def activities(self):
        """List of all non-poweroff activity objects."""
        activity_infos = self._client.config.get("activity", [])
        return [
            info
            for info in activity_infos
            if info["label"] is not None and info["label"] != ACTIVITY_POWER_OFF
        ]

    @property
    def activity_names(self):
        """Names of all the remotes activities."""
        activity_infos = self.activities
        activities = [activity["label"] for activity in activity_infos]

        return activities

    @property
    def device_names(self):
        """Names of all of the devices connected to the hub."""
        device_infos = self._client.config.get("device", [])
        devices = [device["label"] for device in device_infos]

        return devices

    @property
    def name(self):
        """Return the Harmony device's name."""
        return self._name

    @property
    def unique_id(self):
        """Return the Harmony device's unique_id."""
        return self._unique_id

    @property
    def json_config(self):
        """Return the hub config as json."""
        if self._client.config is None:
            return None
        return self._client.json_config

    @property
    def available(self) -> bool:
        """Return if connected to the hub."""
        return self._available

    @property
    def current_activity(self) -> tuple:
        """Return the current activity tuple."""
        return self._client.current_activity

    def device_info(self, domain: str):
        """Return hub device info."""
        model = "Harmony Hub"
        if "ethernetStatus" in self._client.hub_config.info:
            model = "Harmony Hub Pro 2400"
        return {
            "identifiers": {(domain, self.unique_id)},
            "manufacturer": "Logitech",
            "sw_version": self._client.hub_config.info.get(
                "hubSwVersion", self._client.fw_version
            ),
            "name": self.name,
            "model": model,
        }

    async def connect(self) -> bool:
        """Connect to the Harmony Hub."""
        _LOGGER.debug("%s: Connecting", self._name)

        callbacks = {
            "config_updated": self._config_updated,
            "connect": self._connected,
            "disconnect": self._disconnected,
            "new_activity_starting": self._activity_starting,
            "new_activity": self._activity_started,
        }
        self._client = HarmonyClient(
            ip_address=self._address, callbacks=ClientCallbackType(**callbacks)
        )

        try:
            if not await self._client.connect():
                _LOGGER.warning("%s: Unable to connect to HUB", self._name)
                await self._client.close()
                return False
        except aioexc.TimeOut:
            _LOGGER.warning("%s: Connection timed-out", self._name)
            return False

        return True

    async def shutdown(self):
        """Close connection on shutdown."""
        _LOGGER.debug("%s: Closing Harmony Hub", self._name)
        try:
            await self._client.close()
        except aioexc.TimeOut:
            _LOGGER.warning("%s: Disconnect timed-out", self._name)

    async def async_start_activity(self, activity: str):
        """Start an activity from the Harmony device."""

        if not activity:
            _LOGGER.error("%s: No activity specified with turn_on service", self.name)
            return

        activity_id = None
        activity_name = None

        if activity.isdigit() or activity == "-1":
            _LOGGER.debug("%s: Activity is numeric", self.name)
            activity_name = self._client.get_activity_name(int(activity))
            if activity_name:
                activity_id = activity

        if activity_id is None:
            _LOGGER.debug("%s: Find activity ID based on name", self.name)
            activity_name = str(activity)
            activity_id = self._client.get_activity_id(activity_name)

        if activity_id is None:
            _LOGGER.error("%s: Activity %s is invalid", self.name, activity)
            return

        _, current_activity_name = self.current_activity
        if current_activity_name == activity_name:
            # Automations or HomeKit may turn the device on multiple times
            # when the current activity is already active which will cause
            # harmony to loose state.  This behavior is unexpected as turning
            # the device on when its already on isn't expected to reset state.
            _LOGGER.debug(
                "%s: Current activity is already %s", self.name, activity_name
            )
            return

        await self.async_lock_start_activity()
        try:
            await self._client.start_activity(activity_id)
        except aioexc.TimeOut:
            _LOGGER.error("%s: Starting activity %s timed-out", self.name, activity)
            self.async_unlock_start_activity()

    async def async_power_off(self):
        """Start the PowerOff activity."""
        _LOGGER.debug("%s: Turn Off", self.name)
        try:
            await self._client.power_off()
        except aioexc.TimeOut:
            _LOGGER.error("%s: Powering off timed-out", self.name)

    async def async_send_command(
        self,
        commands: Iterable[str],
        device: str,
        num_repeats: int,
        delay_secs: float,
        hold_secs: float,
    ):
        """Send a list of commands to one device."""
        device_id = None
        if device.isdigit():
            _LOGGER.debug("%s: Device %s is numeric", self.name, device)
            if self._client.get_device_name(int(device)):
                device_id = device

        if device_id is None:
            _LOGGER.debug(
                "%s: Find device ID %s based on device name", self.name, device
            )
            device_id = self._client.get_device_id(str(device).strip())

        if device_id is None:
            _LOGGER.error("%s: Device %s is invalid", self.name, device)
            return

        _LOGGER.debug(
            "Sending commands to device %s holding for %s seconds "
            "with a delay of %s seconds",
            device,
            hold_secs,
            delay_secs,
        )

        # Creating list of commands to send.
        snd_cmnd_list = []
        for _ in range(num_repeats):
            for single_command in commands:
                send_command = SendCommandDevice(
                    device=device_id, command=single_command, delay=hold_secs
                )
                snd_cmnd_list.append(send_command)
                if delay_secs > 0:
                    snd_cmnd_list.append(float(delay_secs))

        _LOGGER.debug("%s: Sending commands", self.name)
        try:
            result_list = await self._client.send_commands(snd_cmnd_list)
        except aioexc.TimeOut:
            _LOGGER.error("%s: Sending commands timed-out", self.name)
            return

        for result in result_list:
            _LOGGER.error(
                "Sending command %s to device %s failed with code %s: %s",
                result.command.command,
                result.command.device,
                result.code,
                result.msg,
            )

    async def change_channel(self, channel: int):
        """Change the channel using Harmony remote."""
        _LOGGER.debug("%s: Changing channel to %s", self.name, channel)
        try:
            await self._client.change_channel(channel)
        except aioexc.TimeOut:
            _LOGGER.error("%s: Changing channel to %s timed-out", self.name, channel)

    async def sync(self) -> bool:
        """Sync the Harmony device with the web service.

        Returns True if the sync was successful.
        """
        _LOGGER.debug("%s: Syncing hub with Harmony cloud", self.name)
        try:
            await self._client.sync()
        except aioexc.TimeOut:
            _LOGGER.error("%s: Syncing hub with Harmony cloud timed-out", self.name)
            return False
        else:
            return True
