"""Consume the august activity stream."""
import asyncio
import logging

from aiohttp import ClientError

from homeassistant.core import callback
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.event import async_call_later
from homeassistant.util.dt import utcnow

from .const import ACTIVITY_UPDATE_INTERVAL
from .subscriber import AugustSubscriberMixin

_LOGGER = logging.getLogger(__name__)

ACTIVITY_STREAM_FETCH_LIMIT = 10
ACTIVITY_CATCH_UP_FETCH_LIMIT = 2500


class ActivityStream(AugustSubscriberMixin):
    """August activity stream handler."""

    def __init__(self, hass, api, august_gateway, house_ids, pubnub):
        """Init August activity stream object."""
        super().__init__(hass, ACTIVITY_UPDATE_INTERVAL)
        self._hass = hass
        self._schedule_updates = {}
        self._august_gateway = august_gateway
        self._api = api
        self._house_ids = house_ids
        self._latest_activities = {}
        self._last_update_time = None
        self.pubnub = pubnub
        self._update_debounce = {}

    async def async_setup(self):
        """Token refresh check and catch up the activity stream."""
        for house_id in self._house_ids:
            self._update_debounce[house_id] = self._async_create_debouncer(house_id)

        await self._async_refresh(utcnow())

    @callback
    def _async_create_debouncer(self, house_id):
        """Create a debouncer for the house id."""

        async def _async_update_house_id():
            await self._async_update_house_id(house_id)

        return Debouncer(
            self._hass,
            _LOGGER,
            cooldown=ACTIVITY_UPDATE_INTERVAL.total_seconds(),
            immediate=True,
            function=_async_update_house_id,
        )

    @callback
    def async_stop(self):
        """Cleanup any debounces."""
        for debouncer in self._update_debounce.values():
            debouncer.async_cancel()
        for house_id, updater in self._schedule_updates.items():
            if updater is not None:
                updater()
                self._schedule_updates[house_id] = None

    def get_latest_device_activity(self, device_id, activity_types):
        """Return latest activity that is one of the activity_types."""
        if device_id not in self._latest_activities:
            return None

        latest_device_activities = self._latest_activities[device_id]
        latest_activity = None

        for activity_type in activity_types:
            if activity_type in latest_device_activities:
                if (
                    latest_activity is not None
                    and latest_device_activities[activity_type].activity_start_time
                    <= latest_activity.activity_start_time
                ):
                    continue
                latest_activity = latest_device_activities[activity_type]

        return latest_activity

    async def _async_refresh(self, time):
        """Update the activity stream from August."""
        # This is the only place we refresh the api token
        await self._august_gateway.async_refresh_access_token_if_needed()
        if self.pubnub.connected:
            _LOGGER.debug("Skipping update because pubnub is connected")
            return
        await self._async_update_device_activities(time)

    async def _async_update_device_activities(self, time):
        _LOGGER.debug("Start retrieving device activities")
        await asyncio.gather(
            *(
                self._update_debounce[house_id].async_call()
                for house_id in self._house_ids
            )
        )
        self._last_update_time = time

    @callback
    def async_schedule_house_id_refresh(self, house_id):
        """Update for a house activities now and once in the future."""
        if self._schedule_updates.get(house_id):
            self._schedule_updates[house_id]()
            self._schedule_updates[house_id] = None

        async def _update_house_activities(_):
            await self._update_debounce[house_id].async_call()

        self._hass.async_create_task(self._update_debounce[house_id].async_call())
        # Schedule an update past the debounce to ensure
        # we catch the case where the lock operator is
        # not updated or the lock failed
        self._schedule_updates[house_id] = async_call_later(
            self._hass,
            ACTIVITY_UPDATE_INTERVAL.total_seconds() + 1,
            _update_house_activities,
        )

    async def _async_update_house_id(self, house_id):
        """Update device activities for a house."""
        if self._last_update_time:
            limit = ACTIVITY_STREAM_FETCH_LIMIT
        else:
            limit = ACTIVITY_CATCH_UP_FETCH_LIMIT

        _LOGGER.debug("Updating device activity for house id %s", house_id)
        try:
            activities = await self._api.async_get_house_activities(
                self._august_gateway.access_token, house_id, limit=limit
            )
        except ClientError as ex:
            _LOGGER.error(
                "Request error trying to retrieve activity for house id %s: %s",
                house_id,
                ex,
            )
            # Make sure we process the next house if one of them fails
            return

        _LOGGER.debug(
            "Completed retrieving device activities for house id %s", house_id
        )

        updated_device_ids = self.async_process_newer_device_activities(activities)

        if not updated_device_ids:
            return

        for device_id in updated_device_ids:
            _LOGGER.debug(
                "async_signal_device_id_update (from activity stream): %s",
                device_id,
            )
            self.async_signal_device_id_update(device_id)

    def async_process_newer_device_activities(self, activities):
        """Process activities if they are newer than the last one."""
        updated_device_ids = set()
        for activity in activities:
            device_id = activity.device_id
            activity_type = activity.activity_type
            device_activities = self._latest_activities.setdefault(device_id, {})
            lastest_activity = device_activities.get(activity_type)

            # Ignore activities that are older than the latest one
            if (
                lastest_activity
                and lastest_activity.activity_start_time >= activity.activity_start_time
            ):
                continue

            device_activities[activity_type] = activity

            updated_device_ids.add(device_id)

        return updated_device_ids
