from datetime import timedelta

from aiohttp.client_exceptions import ClientResponseError
from injector import inject, singleton

from ..model import Coordinator, Backup
from ..config import Config, Setting
from ..util import GlobalInfo, Backoff, Estimator
from .harequests import HaRequests
from ..time import Time
from ..worker import Worker
from ..const import SOURCE_HA, SOURCE_GOOGLE_DRIVE
from ..logger import getLogger

logger = getLogger(__name__)

NOTIFICATION_TITLE = "Home Assistant Google Drive Backup is Having Trouble"
NOTIFICATION_DESC_LINK = "The add-on is having trouble making backups and needs attention.  Please visit the add-on [status page]({0}) for details."
NOTIFICATION_DESC_STATIC = "The add-on is having trouble making backups and needs attention.  Please visit the add-on status page for details."

MAX_BACKOFF = 60 * 5  # 5 minutes
FIRST_BACKOFF = 60  # 1 minute

# Wait 5 minutes before logging
NOTIFY_DELAY = 60 * 5  # 5 minute

OLD_BACKUP_ENTITY_NAME = "sensor.snapshot_backup"
BACKUP_ENTITY_NAME = "sensor.backup_state"

REASSURING_MESSAGE = "Unable to reach Home Assistant (HTTP {0}).  This is normal if Home Assistant is restarting.  You will probably see some errors in the supervisor logs until it comes back online."


@singleton
class HaUpdater(Worker):
    @inject
    def __init__(self, requests: HaRequests, coordinator: Coordinator, config: Config, time: Time, global_info: GlobalInfo):
        self._config = config
        super().__init__("Sensor Updater", self.update, time, self.getInterval)
        self._time = time
        self._coordinator = coordinator
        self._requests: HaRequests = requests
        self._info = global_info
        self._notified = False
        self._backoff = Backoff(max=MAX_BACKOFF, base=FIRST_BACKOFF)
        self._first_error = None
        self._trigger_once = False

        self._last_backup_update = None
        self.last_backup_update_time = time.now() - timedelta(days=1)
        self._config.subscribe(self.config_updated)
        self._last_interval = self.getInterval()

    def config_updated(self):
        if self._last_interval != self.getInterval():
            self._wait_event.set()
            self._last_interval = self.getInterval()

    def getInterval(self):
        return self._config.get(Setting.HA_REPORTING_INTERVAL_SECONDS)

    async def update(self):
        try:
            if self._config.get(Setting.ENABLE_BACKUP_STALE_SENSOR):
                await self._requests.updateBackupStaleSensor('on' if self._stale() else 'off')
            if self._config.get(Setting.ENABLE_BACKUP_STATE_SENSOR):
                await self._maybeSendBackupUpdate()
            if self._config.get(Setting.NOTIFY_FOR_STALE_BACKUPS):
                if self._stale() and not self._notified:
                    if self._info.url is None or len(self._info.url) == 0:
                        message = NOTIFICATION_DESC_STATIC
                    else:
                        message = NOTIFICATION_DESC_LINK.format(self._info.url)
                    await self._requests.sendNotification(NOTIFICATION_TITLE, message)
                    self._notified = True
                elif not self._stale() and self._notified:
                    await self._requests.dismissNotification()
                    self._notified = False
            self._backoff.reset()
            self._first_error = None
            self._trigger_once = False
        except ClientResponseError as e:
            if self._first_error is None:
                self._first_error = self._time.now()
            if int(e.status / 100) == 5:
                if self._time.now() > self._first_error + timedelta(seconds=NOTIFY_DELAY):
                    logger.error(
                        "Unable to reach Home Assistant (HTTP {0}).  This is normal if Home Assistant is restarting.  You will probably see some errors in the supervisor logs until it comes back online.".format(e.status))
            else:
                logger.error("Trouble updating Home Assistant sensors.")
            self._last_backup_update = None
            await self._time.sleepAsync(self._backoff.backoff(e))
        except Exception as e:
            self._last_backup_update = None
            logger.error("Trouble updating Home Assistant sensors.")
            logger.printException(e)
            await self._time.sleepAsync(self._backoff.backoff(e))

    async def _maybeSendBackupUpdate(self):
        update = self._buildBackupUpdate()
        if self._trigger_once or update != self._last_backup_update or self._time.now() > self.last_backup_update_time + timedelta(hours=1):
            if self._config.get(Setting.CALL_BACKUP_SNAPSHOT):
                await self._requests.updateEntity(OLD_BACKUP_ENTITY_NAME, update)
            else:
                await self._requests.updateEntity(BACKUP_ENTITY_NAME, update)
            self._last_backup_update = update
            self.last_backup_update_time = self._time.now()

    def _stale(self):
        if self._info._first_sync:
            return False
        if self._info._last_error:
            return self._time.now() > self._info._last_success + timedelta(seconds=self._config.get(Setting.BACKUP_STALE_SECONDS))
        else:
            next_backup = self._coordinator.nextBackupTime(include_pending=False)
            if not next_backup:
                # no backups are configured
                return False

            # Determine if a lot of time has passed since the last backup "should" have been made.
            warn_after = next_backup + timedelta(seconds=self._config.get(Setting.LONG_TERM_STALE_BACKUP_SECONDS))
            return self._time.now() >= warn_after

    def _state(self):
        if self._stale():
            return "error"
        else:
            return "waiting" if self._info._first_sync else "backed_up"

    def triggerRefresh(self):
        self._trigger_once = True

    def _buildBackupUpdate(self):
        backups = list(filter(lambda s: not s.ignore(), self._coordinator.backups()))
        last = "Never"
        if len(backups) > 0:
            last = max(backups, key=lambda s: s.date()).date().isoformat()

        def makeBackupData(backup: Backup):
            return {
                "name": backup.name(),
                "date": str(backup.date().isoformat()),
                "state": backup.status(),
                "size": backup.sizeString(),
                "slug": backup.slug()
            }
        ha_backups = list(filter(lambda s: s.getSource(SOURCE_HA) is not None, backups))
        drive_backups = list(filter(lambda s: s.getSource(SOURCE_GOOGLE_DRIVE) is not None, backups))

        last_uploaded = "Never"
        if len(drive_backups) > 0:
            last_uploaded = max(drive_backups, key=lambda s: s.date()).date().isoformat()
        if self._config.get(Setting.CALL_BACKUP_SNAPSHOT):
            return {
                "state": self._state(),
                "attributes": {
                    "friendly_name": "Snapshot State",
                    "last_snapshot": last,  # type: ignore
                    "snapshots_in_google_drive": len(drive_backups),
                    "snapshots_in_hassio": len(ha_backups),
                    "snapshots_in_home_assistant": len(ha_backups),
                    "size_in_google_drive": Estimator.asSizeString(sum(map(lambda v: v.sizeInt(), drive_backups))),
                    "size_in_home_assistant": Estimator.asSizeString(sum(map(lambda v: v.sizeInt(), ha_backups))),
                    "snapshots": list(map(makeBackupData, backups))
                }
            }
        else:
            source_metrics = self._coordinator.buildBackupMetrics()
            next = self._coordinator.nextBackupTime()
            if next is not None:
                next = next.isoformat()
            attr = {
                "friendly_name": "Backup State",
                "last_backup": last,  # type: ignore
                "next_backup": next,
                "last_uploaded": last_uploaded,
                "backups_in_google_drive": len(drive_backups),
                "backups_in_home_assistant": len(ha_backups),
                "size_in_google_drive": Estimator.asSizeString(sum(map(lambda v: v.sizeInt(), drive_backups))),
                "size_in_home_assistant": Estimator.asSizeString(sum(map(lambda v: v.sizeInt(), ha_backups))),
                "backups": list(map(makeBackupData, backups))
            }
            if SOURCE_GOOGLE_DRIVE in source_metrics and 'free_space' in source_metrics[SOURCE_GOOGLE_DRIVE]:
                attr["free_space_in_google_drive"] = source_metrics[SOURCE_GOOGLE_DRIVE]['free_space']
            else:
                attr["free_space_in_google_drive"] = ""
            return {
                "state": self._state(),
                "attributes": attr
            }
