"""HA entity classes"""

import json
from collections.abc import Iterator
from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.core import ServiceCall
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.util import dt

from homeassistant.const import (
    CONF_STATE,
    CONF_UNTIL,
    STATE_OK,
    STATE_ON,
)

from .irrigation_unlimited import (
    IUCoordinator,
    IUController,
    IUZone,
    IUSequence,
    IUSequenceZone,
    IUAdjustment,
    IUBase,
)

from .const import (
    ATTR_ADJUSTMENT,
    ATTR_CONFIGURATION,
    ATTR_CONTROLLER_COUNT,
    ATTR_ENABLED,
    ATTR_NEXT_TICK,
    ATTR_TICK_LOG,
    ATTR_SUSPENDED,
    CONF_CONTROLLER,
    CONF_CONTROLLERS,
    CONF_ENABLED,
    CONF_INDEX,
    CONF_RESET,
    CONF_SEQUENCE,
    CONF_SEQUENCE_ID,
    CONF_SEQUENCE_ZONE,
    CONF_SEQUENCE_ZONES,
    CONF_SEQUENCES,
    CONF_ZONE,
    CONF_ZONES,
    COORDINATOR,
    ICON,
    SERVICE_ENABLE,
    SERVICE_DISABLE,
    SERVICE_TIME_ADJUST,
    SERVICE_SUSPEND,
    STATUS_INITIALISING,
)


class IURestore:
    """Restore class"""

    # pylint: disable=too-few-public-methods

    def __init__(self, data: dict, coordinator: IUCoordinator):
        self._coordinator = coordinator
        self._is_on = []

        if data is not None and isinstance(data, dict):
            for c_data in data.get(CONF_CONTROLLERS, []):
                self._restore_controller(c_data)

    @property
    def is_on(self) -> list:
        """Return the list of objects left in on state"""
        return self._is_on

    def _add_to_on_list(
        self,
        controller: IUController,
        zone: IUZone = None,
        sequence: IUSequence = None,
        sequence_zone: IUSequenceZone = None,
    ) -> None:
        # pylint: disable=too-many-arguments
        if sequence_zone is None:
            if sequence is not None:
                for item in self._is_on:
                    if item[CONF_SEQUENCE] == sequence:
                        return
            elif zone is not None:
                for item in self._is_on:
                    if item[CONF_ZONE] == zone:
                        return
            elif controller is not None:
                for item in self._is_on:
                    if item[CONF_CONTROLLER] == controller:
                        return
        self._is_on.append(
            {
                CONF_CONTROLLER: controller,
                CONF_ZONE: zone,
                CONF_SEQUENCE: sequence,
                CONF_SEQUENCE_ZONE: sequence_zone,
            }
        )
        return

    def _check_is_on(
        self,
        data: dict,
        controller: IUController,
        zone: IUZone,
        sequence: IUSequence,
        sequence_zone: IUSequenceZone,
    ) -> None:
        # pylint: disable=too-many-arguments
        if not CONF_STATE in data:
            return
        if data.get(CONF_STATE) == STATE_ON:
            if sequence_zone is not None:
                items = data.get(CONF_ZONES)
                if isinstance(items, str):
                    items = items.split(",")  # Old style 1's based CSV
                for item in items:
                    try:
                        item = int(item)
                        zne = controller.get_zone(item - sequence_zone.ZONE_OFFSET)
                        if zne is not None:
                            self._add_to_on_list(
                                controller, zne, sequence, sequence_zone
                            )
                    except ValueError:
                        pass
            else:
                self._add_to_on_list(controller, zone, sequence, sequence_zone)

    def _restore_enabled(
        self,
        data: dict,
        controller: IUController,
        zone: IUZone,
        sequence: IUSequence,
        sequence_zone: IUSequenceZone,
    ) -> None:
        # pylint: disable=too-many-arguments
        if not CONF_ENABLED in data:
            return
        svc = SERVICE_ENABLE if data.get(CONF_ENABLED) else SERVICE_DISABLE
        svd = {}
        if sequence is not None:
            svd[CONF_SEQUENCE_ID] = sequence.index + 1
            if sequence_zone is not None:
                svd[CONF_ZONES] = [sequence_zone.index + 1]
        self._coordinator.service_call(svc, controller, zone, None, svd)

    def _restore_suspend(
        self,
        data: dict,
        controller: IUController,
        zone: IUZone,
        sequence: IUSequence,
        sequence_zone: IUSequenceZone,
    ) -> None:
        # pylint: disable=too-many-arguments
        if not ATTR_SUSPENDED in data:
            return
        svd = {}
        if data.get(ATTR_SUSPENDED) is not None:
            svd[CONF_UNTIL] = dt.parse_datetime(data.get(ATTR_SUSPENDED))
        else:
            svd[CONF_RESET] = None
        if sequence is not None:
            svd[CONF_SEQUENCE_ID] = sequence.index + 1
            if sequence_zone is not None:
                svd[CONF_ZONES] = [sequence_zone.index + 1]
        self._coordinator.service_call(SERVICE_SUSPEND, controller, zone, None, svd)

    def _restore_adjustment(
        self,
        data: dict,
        controller: IUController,
        zone: IUZone,
        sequence: IUSequence,
        sequence_zone: IUSequenceZone,
    ) -> None:
        # pylint: disable=too-many-arguments
        if not ATTR_ADJUSTMENT in data:
            return
        if (svd := IUAdjustment(data.get(ATTR_ADJUSTMENT)).to_dict()) == {}:
            svd[CONF_RESET] = None
        if sequence is not None:
            svd[CONF_SEQUENCE_ID] = sequence.index + 1
            if sequence_zone is not None:
                svd[CONF_ZONES] = [sequence_zone.index + 1]
        self._coordinator.service_call(SERVICE_TIME_ADJUST, controller, zone, None, svd)

    def _restore_sequence_zone(
        self, data: dict, controller: IUController, sequence: IUSequence
    ) -> None:
        if (sequence_zone := sequence.get_zone(data.get(CONF_INDEX))) is None:
            return
        self._restore_enabled(data, controller, None, sequence, sequence_zone)
        self._restore_suspend(data, controller, None, sequence, sequence_zone)
        self._restore_adjustment(data, controller, None, sequence, sequence_zone)
        self._check_is_on(data, controller, None, sequence, sequence_zone)

    def _restore_sequence(self, data: dict, controller: IUController) -> None:
        if (sequence := controller.get_sequence(data.get(CONF_INDEX))) is None:
            return
        self._restore_enabled(data, controller, None, sequence, None)
        self._restore_suspend(data, controller, None, sequence, None)
        self._restore_adjustment(data, controller, None, sequence, None)
        for sz_data in data.get(CONF_SEQUENCE_ZONES, []):
            self._restore_sequence_zone(sz_data, controller, sequence)
        self._check_is_on(data, controller, None, sequence, None)

    def _restore_zone(self, data: dict, controller: IUController) -> None:
        if (zone := controller.get_zone(data.get(CONF_INDEX))) is None:
            return
        self._restore_enabled(data, controller, zone, None, None)
        self._restore_suspend(data, controller, zone, None, None)
        self._restore_adjustment(data, controller, zone, None, None)
        self._check_is_on(data, controller, zone, None, None)

    def _restore_controller(self, data: dict) -> None:
        if (controller := self._coordinator.get(data.get(CONF_INDEX))) is None:
            return
        self._restore_enabled(data, controller, None, None, None)
        self._restore_suspend(data, controller, None, None, None)
        for sq_data in data.get(CONF_SEQUENCES, []):
            self._restore_sequence(sq_data, controller)
        for z_data in data.get(CONF_ZONES, []):
            self._restore_zone(z_data, controller)
        self._check_is_on(data, controller, None, None, None)

    def report_is_on(self) -> Iterator[str]:
        """Generate a list of incomplete cycles"""
        for item in self._is_on:
            yield ",".join(
                IUBase.idl(
                    [
                        item[CONF_CONTROLLER],
                        item[CONF_ZONE],
                        item[CONF_SEQUENCE],
                        item[CONF_SEQUENCE_ZONE],
                    ],
                    "-",
                )
            )


class IUEntity(BinarySensorEntity, RestoreEntity):
    """Base class for entities"""

    def __init__(
        self,
        coordinator: IUCoordinator,
        controller: IUController,
        zone: IUZone,
        sequence: IUSequence,
    ):
        """Base entity class"""
        self._coordinator = coordinator
        self._controller = controller
        self._zone = zone  # This will be None if it belongs to a Master/Controller
        self._sequence = sequence
        if self._sequence is not None:
            self.entity_id = self._sequence.entity_id
        elif self._zone is not None:
            self.entity_id = self._zone.entity_id
        else:
            self.entity_id = self._controller.entity_id

    async def async_added_to_hass(self):
        self._coordinator.register_entity(
            self._controller, self._zone, self._sequence, self
        )

        # This code should be removed in future update. Moved to coordinator JSON configuration.
        if not self._coordinator.restored_from_configuration:
            state = await self.async_get_last_state()
            if state is None:
                return
            service = (
                SERVICE_ENABLE
                if state.attributes.get(ATTR_ENABLED, True)
                else SERVICE_DISABLE
            )
            self._coordinator.service_call(
                service, self._controller, self._zone, None, {}
            )
        return

    async def async_will_remove_from_hass(self):
        self._coordinator.deregister_entity(
            self._controller, self._zone, self._sequence, self
        )
        return

    def dispatch(self, service: str, call: ServiceCall) -> None:
        """Dispatcher for service calls"""
        self._coordinator.service_call(
            service, self._controller, self._zone, self._sequence, call.data
        )


class IUComponent(RestoreEntity):
    """Representation of IrrigationUnlimitedCoordinator"""

    def __init__(self, coordinator: IUCoordinator):
        self._coordinator = coordinator
        self.entity_id = self._coordinator.entity_id

    async def async_added_to_hass(self):
        self._coordinator.register_entity(None, None, None, self)
        state = await self.async_get_last_state()
        if state is None or ATTR_CONFIGURATION not in state.attributes:
            return
        json_data = state.attributes.get(ATTR_CONFIGURATION, {})
        try:
            data = json.loads(json_data)
            for item in IURestore(data, self._coordinator).is_on:
                controller: IUController = item[CONF_CONTROLLER]
                zone: IUZone = item[CONF_ZONE]
                sequence: IUSequence = item[CONF_SEQUENCE]
                sequence_zone: IUSequenceZone = item[CONF_SEQUENCE_ZONE]
                self._coordinator.logger.log_incomplete_cycle(
                    controller,
                    zone,
                    sequence,
                    sequence_zone,
                )
            self._coordinator.restored_from_configuration = True
        # pylint: disable=broad-except
        except Exception as exc:
            self._coordinator.logger.log_invalid_restore_data(repr(exc), json_data)
        return

    async def async_will_remove_from_hass(self):
        self._coordinator.deregister_entity(None, None, None, self)
        return

    def dispatch(self, service: str, call: ServiceCall) -> None:
        """Service call dispatcher"""
        self._coordinator.service_call(service, None, None, None, call.data)

    @property
    def should_poll(self):
        """If entity should be polled"""
        return False

    @property
    def unique_id(self):
        """Return a unique ID."""
        return COORDINATOR

    @property
    def name(self):
        """Return the name of the integration."""
        return "Irrigation Unlimited Coordinator"

    @property
    def state(self):
        """Return the state of the entity."""
        if not self._coordinator.initialised:
            return STATUS_INITIALISING
        return STATE_OK

    @property
    def icon(self):
        """Return the icon to be used for this entity"""
        return ICON

    @property
    def state_attributes(self):
        """Return the state attributes."""
        attr = {}
        attr[ATTR_CONTROLLER_COUNT] = len(self._coordinator.controllers)
        attr[ATTR_CONFIGURATION] = self._coordinator.configuration
        if self._coordinator.clock.show_log:
            next_tick = self._coordinator.clock.next_tick
            attr[ATTR_NEXT_TICK] = (
                dt.as_local(next_tick) if next_tick is not None else None
            )
            attr[ATTR_TICK_LOG] = list(
                dt.as_local(tick) for tick in self._coordinator.clock.tick_log
            )
        return attr
