import os
from typing import Any, Dict

from aiohttp import ClientSession, ClientTimeout
from aiohttp.client_exceptions import ClientResponseError, ClientConnectorError
from injector import inject
from asyncio.exceptions import TimeoutError

from ..util import AsyncHttpGetter
from ..config import Config, Setting, Version
from ..exceptions import HomeAssistantDeleteError, SupervisorConnectionError, SupervisorPermissionError, SupervisorTimeoutError, SupervisorUnexpectedError
from ..model import HABackup
from ..logger import getLogger
from ..util import DataCache
from backup.time import Time
from backup.const import NECESSARY_OLD_BACKUP_PLURAL_NAME, NECESSARY_OLD_SUPERVISOR_URL
from yarl import URL

logger = getLogger(__name__)

HEADER_TOKEN = "X-Supervisor-Token"

NOTIFICATION_ID = "backup_broken"
EVENT_BACKUP_START = "backup_started"
EVENT_BACKUP_END = "backup_ended"

VERSION_BACKUP_PATH = Version.parse("2021.8")


def supervisor_call(func):
    async def wrap_and_call(*args, **kwargs):
        try:
            return await func(*args, **kwargs)
        except ClientConnectorError:
            raise SupervisorConnectionError()
        except TimeoutError:
            raise SupervisorConnectionError()
        except ClientResponseError as e:
            if e.status == 403:
                raise SupervisorPermissionError()
            raise
    return wrap_and_call


class HaRequests():
    """
    Stores logic for interacting with the supervisor add-on API
    """
    @inject
    def __init__(self, config: Config, session: ClientSession, time: Time, data_cache: DataCache):
        self.config: Config = config
        self.cache = {}
        self.session = session
        self._time = time
        self._data_cache = data_cache

        # default the supervisor versio to using the "most featured" when it can't be parsed.
        self._super_version = VERSION_BACKUP_PATH

    def getSupervisorURL(self) -> URL:
        if len(self.config.get(Setting.SUPERVISOR_URL)) > 0:
            return URL(self.config.get(Setting.SUPERVISOR_URL))
        if 'SUPERVISOR_TOKEN' in os.environ:
            return URL("http://supervisor")
        else:
            return URL(NECESSARY_OLD_SUPERVISOR_URL)

    def _getBackupPath(self):
        if self.supportsBackupPaths():
            return "backups"
        return NECESSARY_OLD_BACKUP_PLURAL_NAME

    def supportsBackupPaths(self):
        return not self._super_version or self._super_version >= VERSION_BACKUP_PATH

    @supervisor_call
    async def createBackup(self, info):
        if 'folders' in info or 'addons' in info:
            url = self.getSupervisorURL().with_path("{0}/new/partial".format(self._getBackupPath()))
        else:
            url = self.getSupervisorURL().with_path("{0}/new/full".format(self._getBackupPath()))
        return await self._postHassioData(url, info, timeout=ClientTimeout(total=self.config.get(Setting.PENDING_BACKUP_TIMEOUT_SECONDS)))

    @supervisor_call
    async def auth(self, user: str, password: str) -> None:
        await self._postHassioData(self.getSupervisorURL().with_path("auth"), {"username": user, "password": password}, headers=self._altAuthHeaders())

    @supervisor_call
    async def upload(self, stream):
        url = self.getSupervisorURL().with_path("{0}/new/upload".format(self._getBackupPath()))
        return await self._postHassioData(url, data=stream)

    @supervisor_call
    async def delete(self, slug) -> None:
        if slug in self.cache:
            del self.cache[slug]
        try:
            if self.supportsBackupPaths():
                delete_url = self.getSupervisorURL().with_path("{1}/{0}".format(slug, self._getBackupPath()))
                await self._sendHassioData("delete", delete_url, {})
            else:
                delete_url = self.getSupervisorURL().with_path("{1}/{0}/remove".format(slug, self._getBackupPath()))
                await self._sendHassioData("post", delete_url, {})
        except ClientResponseError as e:
            if e.status == 400:
                raise HomeAssistantDeleteError()
            raise e

    @supervisor_call
    async def startAddon(self, slug) -> None:
        url = self.getSupervisorURL().with_path("addons/{0}/start".format(slug))
        await self._postHassioData(url, {})

    @supervisor_call
    async def stopAddon(self, slug) -> None:
        url = self.getSupervisorURL().with_path("addons/{0}/stop".format(slug))
        await self._postHassioData(url, {})

    @supervisor_call
    async def backup(self, slug):
        if slug in self.cache:
            info = self.cache[slug]
        else:
            info = await self._getHassioData(self.getSupervisorURL().with_path("{1}/{0}/info".format(slug, self._getBackupPath())))
            self.cache[slug] = info
        return HABackup(info, self._data_cache, self.config, self.config.isRetained(slug))

    @supervisor_call
    async def backups(self):
        return await self._getHassioData(self.getSupervisorURL().with_path(self._getBackupPath()))

    @supervisor_call
    async def haInfo(self):
        return await self._getHassioData(self.getSupervisorURL().with_path("core/info"))

    @supervisor_call
    async def selfInfo(self) -> Dict[str, Any]:
        return await self.getAddonInfo("self")

    @supervisor_call
    async def getAddonInfo(self, addon_slug) -> Dict[str, Any]:
        return await self._getHassioData(self.getSupervisorURL().with_path("addons/{0}/info".format(addon_slug)))

    @supervisor_call
    async def getAddons(self) -> Dict[str, Any]:
        return await self._getHassioData(self.getSupervisorURL().with_path("addons"))

    @supervisor_call
    async def hassosInfo(self) -> Dict[str, Any]:
        return await self._getHassioData(self.getSupervisorURL().with_path("hassos/info"))

    @supervisor_call
    async def info(self) -> Dict[str, Any]:
        return await self._getHassioData(self.getSupervisorURL().with_path("info"))

    @supervisor_call
    async def refreshBackups(self):
        url = self.getSupervisorURL().with_path("{0}/reload".format(self._getBackupPath()))
        return await self._postHassioData(url)

    @supervisor_call
    async def supervisorInfo(self):
        url = self.getSupervisorURL().with_path("supervisor/info")
        info = await self._getHassioData(url)

        # parse the supervisor version
        if 'version' in info:
            self._super_version = Version.parse(info['version'])
        return info

    @supervisor_call
    async def restore(self, slug: str, password: str = None) -> None:
        url = self.getSupervisorURL().with_path("{1}/{0}/restore/full".format(slug, self._getBackupPath()))
        if password:
            await self._postHassioData(url, {'password': password})
        else:
            await self._postHassioData(url, {})

    @supervisor_call
    async def download(self, slug) -> AsyncHttpGetter:
        url = self.getSupervisorURL().with_path("{1}/{0}/download".format(slug, self._getBackupPath()))
        ret = AsyncHttpGetter(url,
                              self._getAuthHeaders(),
                              self.session,
                              timeoutFactory=SupervisorTimeoutError.factory,
                              otherErrorFactory=SupervisorUnexpectedError.factory,
                              timeout=ClientTimeout(sock_connect=self.config.get(Setting.DOWNLOAD_TIMEOUT_SECONDS),
                                                    sock_read=self.config.get(Setting.DOWNLOAD_TIMEOUT_SECONDS)),
                              time=self._time)
        return ret

    @supervisor_call
    async def getSuperLogs(self):
        url = self.getSupervisorURL().with_path("supervisor/logs")
        async with self.session.get(url, headers=self._getAuthHeaders()) as resp:
            resp.raise_for_status()
            return await resp.text()

    @supervisor_call
    async def getCoreLogs(self):
        url = self.getSupervisorURL().with_path("core/logs")
        async with self.session.get(url, headers=self._getAuthHeaders()) as resp:
            resp.raise_for_status()
            return await resp.text()

    async def _validateHassioReply(self, resp) -> Dict[str, Any]:
        async with resp:
            resp.raise_for_status()
            details: Dict[str, Any] = await resp.json()
            if "result" not in details or details["result"] != "ok":
                if "result" in details:
                    raise Exception("Hassio said: " + details["result"])
                else:
                    raise Exception(
                        "Malformed response from Hassio: " + str(details))

            if "data" not in details:
                return {}
            if self.config.get(Setting.TRACE_REQUESTS):
                logger.trace("Hassio replied: %s", details)
            return details["data"]

    async def getAddonLogo(self, slug: str):
        url = self.getSupervisorURL().with_path("addons/{0}/icon".format(slug))
        async with self.session.get(url, headers=self._getAuthHeaders()) as resp:
            resp.raise_for_status()
            return (resp.headers['Content-Type'], await resp.read())

    def _getToken(self):
        configured = self.config.get(Setting.SUPERVISOR_TOKEN)
        if configured and len(configured) > 0:
            return configured
        if "SUPERVISOR_TOKEN" in os.environ:
            return os.environ.get("SUPERVISOR_TOKEN")
        # Older versions of the supervisor use a different name for the token.
        return os.environ.get("HASSIO_TOKEN")

    def _getAuthHeaders(self):
        return {
            'Authorization': 'Bearer ' + self._getToken()
        }

    def _altAuthHeaders(self):
        return {
            HEADER_TOKEN: self._getToken()
        }

    @supervisor_call
    async def _getHassioData(self, url: URL) -> Dict[str, Any]:
        if self.config.get(Setting.TRACE_REQUESTS):
            logger.trace("Making Hassio request: " + str(url))
        return await self._validateHassioReply(await self.session.get(url, headers=self._getAuthHeaders()))

    async def _postHassioData(self, url: URL, json=None, file=None, data=None, timeout=None, headers=None) -> Dict[str, Any]:
        return await self._sendHassioData("post", url, json, file, data, timeout, headers)

    @supervisor_call
    async def _sendHassioData(self, method: str, url: URL, json=None, file=None, data=None, timeout=None, headers=None) -> Dict[str, Any]:
        if headers is None:
            headers = self._getAuthHeaders()
        if self.config.get(Setting.TRACE_REQUESTS):
            logger.trace("Making Hassio request: " + str(url))
        return await self._validateHassioReply(await self.session.request(method, url, headers=headers, json=json, data=data, timeout=timeout))

    async def _postHaData(self, path: str, data: Dict[str, Any]) -> None:
        url = self.getSupervisorURL().with_path("/core/api/" + path)
        async with self.session.post(url, headers=self._getAuthHeaders(), json=data) as resp:
            resp.raise_for_status()

    async def sendNotification(self, title: str, message: str) -> None:
        data: Dict[str, str] = {
            "title": title,
            "message": message,
            "notification_id": NOTIFICATION_ID
        }
        await self._postHaData("services/persistent_notification/create", data)

    async def eventBackupStart(self, name, time):
        await self._sendEvent(EVENT_BACKUP_START, {
            'backup_name': name,
            'backup_time': str(time)
        })

    async def eventBackupEnd(self, name, time, completed):
        await self._sendEvent(EVENT_BACKUP_END, {
            'completed': completed,
            'backup_name': name,
            'backup_time': str(time)
        })

    async def _sendEvent(self, event_name: str, data: Dict[str, str]) -> None:
        await self._postHaData("events/" + event_name, data)

    async def dismissNotification(self) -> None:
        data: Dict[str, str] = {
            "notification_id": NOTIFICATION_ID
        }
        await self._postHaData("services/persistent_notification/dismiss", data)

    async def updateBackupStaleSensor(self, state: bool) -> None:
        if self.config.get(Setting.CALL_BACKUP_SNAPSHOT):
            data: Dict[str, Any] = {
                "state": state,
                "attributes": {
                    "friendly_name": "Snapshots Stale",
                    "device_class": "problem"
                }
            }
            await self._postHaData("states/binary_sensor." + NECESSARY_OLD_BACKUP_PLURAL_NAME + "_stale", data)
        else:
            data: Dict[str, Any] = {
                "state": state,
                "attributes": {
                    "friendly_name": "Backups Stale",
                    "device_class": "problem"
                }
            }
            await self._postHaData("states/binary_sensor.backups_stale", data)

    @supervisor_call
    async def updateConfig(self, config) -> None:
        return await self._postHassioData(self.getSupervisorURL().with_path("addons/self/options"), {'options': config})

    @supervisor_call
    async def updateAddonOptions(self, slug, options):
        return await self._postHassioData(self.getSupervisorURL().with_path("addons/{0}/options".format(slug)), options)

    async def updateEntity(self, entity, data):
        await self._postHaData("states/" + entity, data)
