"""Extend the basic Accessory and Bridge functions."""
from datetime import timedelta
from functools import partial, wraps
from inspect import getmodule
import logging

from pyhap.accessory import Accessory, Bridge
from pyhap.accessory_driver import AccessoryDriver
from pyhap.const import CATEGORY_OTHER

from homeassistant.const import (
    ATTR_BATTERY_CHARGING,
    ATTR_BATTERY_LEVEL,
    ATTR_ENTITY_ID,
    ATTR_SERVICE,
    __version__,
)
from homeassistant.core import callback as ha_callback, split_entity_id
from homeassistant.helpers.event import (
    async_track_state_change,
    track_point_in_utc_time,
)
from homeassistant.util import dt as dt_util

from .const import (
    ATTR_DISPLAY_NAME,
    ATTR_VALUE,
    BRIDGE_MODEL,
    BRIDGE_SERIAL_NUMBER,
    CHAR_BATTERY_LEVEL,
    CHAR_CHARGING_STATE,
    CHAR_STATUS_LOW_BATTERY,
    CONF_LINKED_BATTERY_SENSOR,
    CONF_LOW_BATTERY_THRESHOLD,
    DEBOUNCE_TIMEOUT,
    DEFAULT_LOW_BATTERY_THRESHOLD,
    EVENT_HOMEKIT_CHANGED,
    MANUFACTURER,
    SERV_BATTERY_SERVICE,
)
from .util import convert_to_float, dismiss_setup_message, show_setup_message

_LOGGER = logging.getLogger(__name__)


def debounce(func):
    """Decorate function to debounce callbacks from HomeKit."""

    @ha_callback
    def call_later_listener(self, *args):
        """Handle call_later callback."""
        debounce_params = self.debounce.pop(func.__name__, None)
        if debounce_params:
            self.hass.async_add_executor_job(func, self, *debounce_params[1:])

    @wraps(func)
    def wrapper(self, *args):
        """Start async timer."""
        debounce_params = self.debounce.pop(func.__name__, None)
        if debounce_params:
            debounce_params[0]()  # remove listener
        remove_listener = track_point_in_utc_time(
            self.hass,
            partial(call_later_listener, self),
            dt_util.utcnow() + timedelta(seconds=DEBOUNCE_TIMEOUT),
        )
        self.debounce[func.__name__] = (remove_listener, *args)
        logger.debug(
            "%s: Start %s timeout", self.entity_id, func.__name__.replace("set_", "")
        )

    name = getmodule(func).__name__
    logger = logging.getLogger(name)
    return wrapper


class HomeAccessory(Accessory):
    """Adapter class for Accessory."""

    def __init__(
        self, hass, driver, name, entity_id, aid, config, category=CATEGORY_OTHER
    ):
        """Initialize a Accessory object."""
        super().__init__(driver, name, aid=aid)
        model = split_entity_id(entity_id)[0].replace("_", " ").title()
        self.set_info_service(
            firmware_revision=__version__,
            manufacturer=MANUFACTURER,
            model=model,
            serial_number=entity_id,
        )
        self.category = category
        self.config = config or {}
        self.entity_id = entity_id
        self.hass = hass
        self.debounce = {}
        self._support_battery_level = False
        self._support_battery_charging = True
        self.linked_battery_sensor = self.config.get(CONF_LINKED_BATTERY_SENSOR)
        self.low_battery_threshold = self.config.get(
            CONF_LOW_BATTERY_THRESHOLD, DEFAULT_LOW_BATTERY_THRESHOLD
        )

        """Add battery service if available"""
        battery_found = self.hass.states.get(self.entity_id).attributes.get(
            ATTR_BATTERY_LEVEL
        )
        if self.linked_battery_sensor:
            battery_found = self.hass.states.get(self.linked_battery_sensor).state

        if battery_found is None:
            return
        _LOGGER.debug("%s: Found battery level", self.entity_id)
        self._support_battery_level = True
        serv_battery = self.add_preload_service(SERV_BATTERY_SERVICE)
        self._char_battery = serv_battery.configure_char(CHAR_BATTERY_LEVEL, value=0)
        self._char_charging = serv_battery.configure_char(CHAR_CHARGING_STATE, value=2)
        self._char_low_battery = serv_battery.configure_char(
            CHAR_STATUS_LOW_BATTERY, value=0
        )

    async def run(self):
        """Handle accessory driver started event.

        Run inside the HAP-python event loop.
        """
        self.hass.add_job(self.run_handler)

    async def run_handler(self):
        """Handle accessory driver started event.

        Run inside the Home Assistant event loop.
        """
        state = self.hass.states.get(self.entity_id)
        self.hass.async_add_job(self.update_state_callback, None, None, state)
        async_track_state_change(self.hass, self.entity_id, self.update_state_callback)

        if self.linked_battery_sensor:
            battery_state = self.hass.states.get(self.linked_battery_sensor)
            self.hass.async_add_job(
                self.update_linked_battery, None, None, battery_state
            )
            async_track_state_change(
                self.hass, self.linked_battery_sensor, self.update_linked_battery
            )

    @ha_callback
    def update_state_callback(self, entity_id=None, old_state=None, new_state=None):
        """Handle state change listener callback."""
        _LOGGER.debug("New_state: %s", new_state)
        if new_state is None:
            return
        if self._support_battery_level and not self.linked_battery_sensor:
            self.hass.async_add_executor_job(self.update_battery, new_state)
        self.hass.async_add_executor_job(self.update_state, new_state)

    @ha_callback
    def update_linked_battery(self, entity_id=None, old_state=None, new_state=None):
        """Handle linked battery sensor state change listener callback."""
        self.hass.async_add_executor_job(self.update_battery, new_state)

    def update_battery(self, new_state):
        """Update battery service if available.

        Only call this function if self._support_battery_level is True.
        """
        battery_level = convert_to_float(new_state.attributes.get(ATTR_BATTERY_LEVEL))
        if self.linked_battery_sensor:
            battery_level = convert_to_float(new_state.state)
        if battery_level is None:
            return
        self._char_battery.set_value(battery_level)
        self._char_low_battery.set_value(battery_level < self.low_battery_threshold)
        _LOGGER.debug("%s: Updated battery level to %d", self.entity_id, battery_level)
        if not self._support_battery_charging:
            return
        charging = new_state.attributes.get(ATTR_BATTERY_CHARGING)
        if charging is None:
            self._support_battery_charging = False
            return
        hk_charging = 1 if charging is True else 0
        self._char_charging.set_value(hk_charging)
        _LOGGER.debug("%s: Updated battery charging to %d", self.entity_id, hk_charging)

    def update_state(self, new_state):
        """Handle state change to update HomeKit value.

        Overridden by accessory types.
        """
        raise NotImplementedError()

    def call_service(self, domain, service, service_data, value=None):
        """Fire event and call service for changes from HomeKit."""
        self.hass.add_job(self.async_call_service, domain, service, service_data, value)

    async def async_call_service(self, domain, service, service_data, value=None):
        """Fire event and call service for changes from HomeKit.

        This method must be run in the event loop.
        """
        event_data = {
            ATTR_ENTITY_ID: self.entity_id,
            ATTR_DISPLAY_NAME: self.display_name,
            ATTR_SERVICE: service,
            ATTR_VALUE: value,
        }

        self.hass.bus.async_fire(EVENT_HOMEKIT_CHANGED, event_data)
        await self.hass.services.async_call(domain, service, service_data)


class HomeBridge(Bridge):
    """Adapter class for Bridge."""

    def __init__(self, hass, driver, name):
        """Initialize a Bridge object."""
        super().__init__(driver, name)
        self.set_info_service(
            firmware_revision=__version__,
            manufacturer=MANUFACTURER,
            model=BRIDGE_MODEL,
            serial_number=BRIDGE_SERIAL_NUMBER,
        )
        self.hass = hass

    def setup_message(self):
        """Prevent print of pyhap setup message to terminal."""
        pass


class HomeDriver(AccessoryDriver):
    """Adapter class for AccessoryDriver."""

    def __init__(self, hass, **kwargs):
        """Initialize a AccessoryDriver object."""
        super().__init__(**kwargs)
        self.hass = hass

    def pair(self, client_uuid, client_public):
        """Override super function to dismiss setup message if paired."""
        success = super().pair(client_uuid, client_public)
        if success:
            dismiss_setup_message(self.hass)
        return success

    def unpair(self, client_uuid):
        """Override super function to show setup message if unpaired."""
        super().unpair(client_uuid)
        show_setup_message(self.hass, self.state.pincode)
