"""Integration with the Rachio Iro sprinkler system controller."""
from abc import abstractmethod
from datetime import timedelta
import logging

import voluptuous as vol

from homeassistant.components.switch import SwitchEntity
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.util.dt import as_timestamp, now, parse_datetime, utc_from_timestamp

from .const import (
    CONF_MANUAL_RUN_MINS,
    DEFAULT_MANUAL_RUN_MINS,
    DOMAIN as DOMAIN_RACHIO,
    KEY_CUSTOM_CROP,
    KEY_CUSTOM_SHADE,
    KEY_CUSTOM_SLOPE,
    KEY_DEVICE_ID,
    KEY_DURATION,
    KEY_ENABLED,
    KEY_ID,
    KEY_IMAGE_URL,
    KEY_NAME,
    KEY_ON,
    KEY_RAIN_DELAY,
    KEY_RAIN_DELAY_END,
    KEY_SCHEDULE_ID,
    KEY_SUBTYPE,
    KEY_SUMMARY,
    KEY_TYPE,
    KEY_ZONE_ID,
    KEY_ZONE_NUMBER,
    SCHEDULE_TYPE_FIXED,
    SCHEDULE_TYPE_FLEX,
    SERVICE_SET_ZONE_MOISTURE,
    SERVICE_START_MULTIPLE_ZONES,
    SIGNAL_RACHIO_CONTROLLER_UPDATE,
    SIGNAL_RACHIO_RAIN_DELAY_UPDATE,
    SIGNAL_RACHIO_SCHEDULE_UPDATE,
    SIGNAL_RACHIO_ZONE_UPDATE,
    SLOPE_FLAT,
    SLOPE_MODERATE,
    SLOPE_SLIGHT,
    SLOPE_STEEP,
)
from .entity import RachioDevice
from .webhooks import (
    SUBTYPE_RAIN_DELAY_OFF,
    SUBTYPE_RAIN_DELAY_ON,
    SUBTYPE_SCHEDULE_COMPLETED,
    SUBTYPE_SCHEDULE_STARTED,
    SUBTYPE_SCHEDULE_STOPPED,
    SUBTYPE_SLEEP_MODE_OFF,
    SUBTYPE_SLEEP_MODE_ON,
    SUBTYPE_ZONE_COMPLETED,
    SUBTYPE_ZONE_PAUSED,
    SUBTYPE_ZONE_STARTED,
    SUBTYPE_ZONE_STOPPED,
)

_LOGGER = logging.getLogger(__name__)

ATTR_DURATION = "duration"
ATTR_ID = "id"
ATTR_PERCENT = "percent"
ATTR_SCHEDULE_SUMMARY = "Summary"
ATTR_SCHEDULE_ENABLED = "Enabled"
ATTR_SCHEDULE_DURATION = "Duration"
ATTR_SCHEDULE_TYPE = "Type"
ATTR_SORT_ORDER = "sortOrder"
ATTR_ZONE_NUMBER = "Zone number"
ATTR_ZONE_SHADE = "Shade"
ATTR_ZONE_SLOPE = "Slope"
ATTR_ZONE_SUMMARY = "Summary"
ATTR_ZONE_TYPE = "Type"

START_MULTIPLE_ZONES_SCHEMA = vol.Schema(
    {
        vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
        vol.Required(ATTR_DURATION): cv.ensure_list_csv,
    }
)


async def async_setup_entry(hass, config_entry, async_add_entities):
    """Set up the Rachio switches."""
    zone_entities = []
    has_flex_sched = False
    entities = await hass.async_add_executor_job(_create_entities, hass, config_entry)
    for entity in entities:
        if isinstance(entity, RachioZone):
            zone_entities.append(entity)
        if isinstance(entity, RachioSchedule) and entity.type == SCHEDULE_TYPE_FLEX:
            has_flex_sched = True

    async_add_entities(entities)
    _LOGGER.info("%d Rachio switch(es) added", len(entities))

    def start_multiple(service):
        """Service to start multiple zones in sequence."""
        zones_list = []
        person = hass.data[DOMAIN_RACHIO][config_entry.entry_id]
        entity_id = service.data[ATTR_ENTITY_ID]
        duration = iter(service.data[ATTR_DURATION])
        default_time = service.data[ATTR_DURATION][0]
        entity_to_zone_id = {
            entity.entity_id: entity.zone_id for entity in zone_entities
        }

        for (count, data) in enumerate(entity_id):
            if data in entity_to_zone_id:
                # Time can be passed as a list per zone,
                # or one time for all zones
                time = int(next(duration, default_time)) * 60
                zones_list.append(
                    {
                        ATTR_ID: entity_to_zone_id.get(data),
                        ATTR_DURATION: time,
                        ATTR_SORT_ORDER: count,
                    }
                )

        if len(zones_list) != 0:
            person.start_multiple_zones(zones_list)
            _LOGGER.debug("Starting zone(s) %s", entity_id)
        else:
            raise HomeAssistantError("No matching zones found in given entity_ids")

    hass.services.async_register(
        DOMAIN_RACHIO,
        SERVICE_START_MULTIPLE_ZONES,
        start_multiple,
        schema=START_MULTIPLE_ZONES_SCHEMA,
    )

    if has_flex_sched:
        platform = entity_platform.current_platform.get()
        platform.async_register_entity_service(
            SERVICE_SET_ZONE_MOISTURE,
            {vol.Required(ATTR_PERCENT): cv.positive_int},
            "set_moisture_percent",
        )


def _create_entities(hass, config_entry):
    entities = []
    person = hass.data[DOMAIN_RACHIO][config_entry.entry_id]
    # Fetch the schedule once at startup
    # in order to avoid every zone doing it
    for controller in person.controllers:
        entities.append(RachioStandbySwitch(controller))
        entities.append(RachioRainDelay(controller))
        zones = controller.list_zones()
        schedules = controller.list_schedules()
        flex_schedules = controller.list_flex_schedules()
        current_schedule = controller.current_schedule
        for zone in zones:
            entities.append(RachioZone(person, controller, zone, current_schedule))
        for sched in schedules + flex_schedules:
            entities.append(RachioSchedule(person, controller, sched, current_schedule))
    _LOGGER.debug("Added %s", entities)
    return entities


class RachioSwitch(RachioDevice, SwitchEntity):
    """Represent a Rachio state that can be toggled."""

    def __init__(self, controller):
        """Initialize a new Rachio switch."""
        super().__init__(controller)
        self._state = None

    @property
    def name(self) -> str:
        """Get a name for this switch."""
        return f"Switch on {self._controller.name}"

    @property
    def is_on(self) -> bool:
        """Return whether the switch is currently on."""
        return self._state

    @callback
    def _async_handle_any_update(self, *args, **kwargs) -> None:
        """Determine whether an update event applies to this device."""
        if args[0][KEY_DEVICE_ID] != self._controller.controller_id:
            # For another device
            return

        # For this device
        self._async_handle_update(args, kwargs)

    @abstractmethod
    def _async_handle_update(self, *args, **kwargs) -> None:
        """Handle incoming webhook data."""


class RachioStandbySwitch(RachioSwitch):
    """Representation of a standby status/button."""

    @property
    def name(self) -> str:
        """Return the name of the standby switch."""
        return f"{self._controller.name} in standby mode"

    @property
    def unique_id(self) -> str:
        """Return a unique id by combining controller id and purpose."""
        return f"{self._controller.controller_id}-standby"

    @property
    def icon(self) -> str:
        """Return an icon for the standby switch."""
        return "mdi:power"

    @callback
    def _async_handle_update(self, *args, **kwargs) -> None:
        """Update the state using webhook data."""
        if args[0][0][KEY_SUBTYPE] == SUBTYPE_SLEEP_MODE_ON:
            self._state = True
        elif args[0][0][KEY_SUBTYPE] == SUBTYPE_SLEEP_MODE_OFF:
            self._state = False

        self.async_write_ha_state()

    def turn_on(self, **kwargs) -> None:
        """Put the controller in standby mode."""
        self._controller.rachio.device.turn_off(self._controller.controller_id)

    def turn_off(self, **kwargs) -> None:
        """Resume controller functionality."""
        self._controller.rachio.device.turn_on(self._controller.controller_id)

    async def async_added_to_hass(self):
        """Subscribe to updates."""
        if KEY_ON in self._controller.init_data:
            self._state = not self._controller.init_data[KEY_ON]

        self.async_on_remove(
            async_dispatcher_connect(
                self.hass,
                SIGNAL_RACHIO_CONTROLLER_UPDATE,
                self._async_handle_any_update,
            )
        )


class RachioRainDelay(RachioSwitch):
    """Representation of a rain delay status/switch."""

    def __init__(self, controller):
        """Set up a Rachio rain delay switch."""
        self._cancel_update = None
        super().__init__(controller)

    @property
    def name(self) -> str:
        """Return the name of the switch."""
        return f"{self._controller.name} rain delay"

    @property
    def unique_id(self) -> str:
        """Return a unique id by combining controller id and purpose."""
        return f"{self._controller.controller_id}-delay"

    @property
    def icon(self) -> str:
        """Return an icon for rain delay."""
        return "mdi:camera-timer"

    @callback
    def _async_handle_update(self, *args, **kwargs) -> None:
        """Update the state using webhook data."""
        if self._cancel_update:
            self._cancel_update()
            self._cancel_update = None

        if args[0][0][KEY_SUBTYPE] == SUBTYPE_RAIN_DELAY_ON:
            endtime = parse_datetime(args[0][0][KEY_RAIN_DELAY_END])
            _LOGGER.debug("Rain delay expires at %s", endtime)
            self._state = True
            self._cancel_update = async_track_point_in_utc_time(
                self.hass, self._delay_expiration, endtime
            )
        elif args[0][0][KEY_SUBTYPE] == SUBTYPE_RAIN_DELAY_OFF:
            self._state = False

        self.async_write_ha_state()

    @callback
    def _delay_expiration(self, *args) -> None:
        """Trigger when a rain delay expires."""
        self._state = False
        self._cancel_update = None
        self.async_write_ha_state()

    def turn_on(self, **kwargs) -> None:
        """Activate a 24 hour rain delay on the controller."""
        self._controller.rachio.device.rain_delay(self._controller.controller_id, 86400)
        _LOGGER.debug("Starting rain delay for 24 hours")

    def turn_off(self, **kwargs) -> None:
        """Resume controller functionality."""
        self._controller.rachio.device.rain_delay(self._controller.controller_id, 0)
        _LOGGER.debug("Canceling rain delay")

    async def async_added_to_hass(self):
        """Subscribe to updates."""
        if KEY_RAIN_DELAY in self._controller.init_data:
            self._state = self._controller.init_data[
                KEY_RAIN_DELAY
            ] / 1000 > as_timestamp(now())

        # If the controller was in a rain delay state during a reboot, this re-sets the timer
        if self._state is True:
            delay_end = utc_from_timestamp(
                self._controller.init_data[KEY_RAIN_DELAY] / 1000
            )
            _LOGGER.debug("Re-setting rain delay timer for %s", delay_end)
            self._cancel_update = async_track_point_in_utc_time(
                self.hass, self._delay_expiration, delay_end
            )

        self.async_on_remove(
            async_dispatcher_connect(
                self.hass,
                SIGNAL_RACHIO_RAIN_DELAY_UPDATE,
                self._async_handle_any_update,
            )
        )


class RachioZone(RachioSwitch):
    """Representation of one zone of sprinklers connected to the Rachio Iro."""

    def __init__(self, person, controller, data, current_schedule):
        """Initialize a new Rachio Zone."""
        self.id = data[KEY_ID]
        self._zone_name = data[KEY_NAME]
        self._zone_number = data[KEY_ZONE_NUMBER]
        self._zone_enabled = data[KEY_ENABLED]
        self._entity_picture = data.get(KEY_IMAGE_URL)
        self._person = person
        self._shade_type = data.get(KEY_CUSTOM_SHADE, {}).get(KEY_NAME)
        self._zone_type = data.get(KEY_CUSTOM_CROP, {}).get(KEY_NAME)
        self._slope_type = data.get(KEY_CUSTOM_SLOPE, {}).get(KEY_NAME)
        self._summary = ""
        self._current_schedule = current_schedule
        super().__init__(controller)

    def __str__(self):
        """Display the zone as a string."""
        return 'Rachio Zone "{}" on {}'.format(self.name, str(self._controller))

    @property
    def zone_id(self) -> str:
        """How the Rachio API refers to the zone."""
        return self.id

    @property
    def name(self) -> str:
        """Return the friendly name of the zone."""
        return self._zone_name

    @property
    def unique_id(self) -> str:
        """Return a unique id by combining controller id and zone number."""
        return f"{self._controller.controller_id}-zone-{self.zone_id}"

    @property
    def icon(self) -> str:
        """Return the icon to display."""
        return "mdi:water"

    @property
    def zone_is_enabled(self) -> bool:
        """Return whether the zone is allowed to run."""
        return self._zone_enabled

    @property
    def entity_picture(self):
        """Return the entity picture to use in the frontend, if any."""
        return self._entity_picture

    @property
    def device_state_attributes(self) -> dict:
        """Return the optional state attributes."""
        props = {ATTR_ZONE_NUMBER: self._zone_number, ATTR_ZONE_SUMMARY: self._summary}
        if self._shade_type:
            props[ATTR_ZONE_SHADE] = self._shade_type
        if self._zone_type:
            props[ATTR_ZONE_TYPE] = self._zone_type
        if self._slope_type:
            if self._slope_type == SLOPE_FLAT:
                props[ATTR_ZONE_SLOPE] = "Flat"
            elif self._slope_type == SLOPE_SLIGHT:
                props[ATTR_ZONE_SLOPE] = "Slight"
            elif self._slope_type == SLOPE_MODERATE:
                props[ATTR_ZONE_SLOPE] = "Moderate"
            elif self._slope_type == SLOPE_STEEP:
                props[ATTR_ZONE_SLOPE] = "Steep"
        return props

    def turn_on(self, **kwargs) -> None:
        """Start watering this zone."""
        # Stop other zones first
        self.turn_off()

        # Start this zone
        manual_run_time = timedelta(
            minutes=self._person.config_entry.options.get(
                CONF_MANUAL_RUN_MINS, DEFAULT_MANUAL_RUN_MINS
            )
        )
        self._controller.rachio.zone.start(self.zone_id, manual_run_time.seconds)
        _LOGGER.debug(
            "Watering %s on %s for %s",
            self.name,
            self._controller.name,
            str(manual_run_time),
        )

    def turn_off(self, **kwargs) -> None:
        """Stop watering all zones."""
        self._controller.stop_watering()

    def set_moisture_percent(self, percent) -> None:
        """Set the zone moisture percent."""
        _LOGGER.debug("Setting %s moisture to %s percent", self._zone_name, percent)
        self._controller.rachio.zone.set_moisture_percent(self.id, percent / 100)

    @callback
    def _async_handle_update(self, *args, **kwargs) -> None:
        """Handle incoming webhook zone data."""
        if args[0][KEY_ZONE_ID] != self.zone_id:
            return

        self._summary = args[0][KEY_SUMMARY]

        if args[0][KEY_SUBTYPE] == SUBTYPE_ZONE_STARTED:
            self._state = True
        elif args[0][KEY_SUBTYPE] in [
            SUBTYPE_ZONE_STOPPED,
            SUBTYPE_ZONE_COMPLETED,
            SUBTYPE_ZONE_PAUSED,
        ]:
            self._state = False

        self.async_write_ha_state()

    async def async_added_to_hass(self):
        """Subscribe to updates."""
        self._state = self.zone_id == self._current_schedule.get(KEY_ZONE_ID)

        self.async_on_remove(
            async_dispatcher_connect(
                self.hass, SIGNAL_RACHIO_ZONE_UPDATE, self._async_handle_update
            )
        )


class RachioSchedule(RachioSwitch):
    """Representation of one fixed schedule on the Rachio Iro."""

    def __init__(self, person, controller, data, current_schedule):
        """Initialize a new Rachio Schedule."""
        self._schedule_id = data[KEY_ID]
        self._schedule_name = data[KEY_NAME]
        self._duration = data[KEY_DURATION]
        self._schedule_enabled = data[KEY_ENABLED]
        self._summary = data[KEY_SUMMARY]
        self.type = data.get(KEY_TYPE, SCHEDULE_TYPE_FIXED)
        self._current_schedule = current_schedule
        super().__init__(controller)

    @property
    def name(self) -> str:
        """Return the friendly name of the schedule."""
        return f"{self._schedule_name} Schedule"

    @property
    def unique_id(self) -> str:
        """Return a unique id by combining controller id and schedule."""
        return f"{self._controller.controller_id}-schedule-{self._schedule_id}"

    @property
    def icon(self) -> str:
        """Return the icon to display."""
        return "mdi:water" if self.schedule_is_enabled else "mdi:water-off"

    @property
    def device_state_attributes(self) -> dict:
        """Return the optional state attributes."""
        return {
            ATTR_SCHEDULE_SUMMARY: self._summary,
            ATTR_SCHEDULE_ENABLED: self.schedule_is_enabled,
            ATTR_SCHEDULE_DURATION: f"{round(self._duration / 60)} minutes",
            ATTR_SCHEDULE_TYPE: self.type,
        }

    @property
    def schedule_is_enabled(self) -> bool:
        """Return whether the schedule is allowed to run."""
        return self._schedule_enabled

    def turn_on(self, **kwargs) -> None:
        """Start this schedule."""
        self._controller.rachio.schedulerule.start(self._schedule_id)
        _LOGGER.debug(
            "Schedule %s started on %s",
            self.name,
            self._controller.name,
        )

    def turn_off(self, **kwargs) -> None:
        """Stop watering all zones."""
        self._controller.stop_watering()

    @callback
    def _async_handle_update(self, *args, **kwargs) -> None:
        """Handle incoming webhook schedule data."""
        # Schedule ID not passed when running individual zones, so we catch that error
        try:
            if args[0][KEY_SCHEDULE_ID] == self._schedule_id:
                if args[0][KEY_SUBTYPE] in [SUBTYPE_SCHEDULE_STARTED]:
                    self._state = True
                elif args[0][KEY_SUBTYPE] in [
                    SUBTYPE_SCHEDULE_STOPPED,
                    SUBTYPE_SCHEDULE_COMPLETED,
                ]:
                    self._state = False
        except KeyError:
            pass

        self.async_write_ha_state()

    async def async_added_to_hass(self):
        """Subscribe to updates."""
        self._state = self._schedule_id == self._current_schedule.get(KEY_SCHEDULE_ID)

        self.async_on_remove(
            async_dispatcher_connect(
                self.hass, SIGNAL_RACHIO_SCHEDULE_UPDATE, self._async_handle_update
            )
        )