"""Init file for Supervisor Home Assistant RESTful API."""

import asyncio
from pathlib import Path
from typing import Any, cast

from aiohttp import web
import voluptuous as vol

from ..addons.addon import Addon
from ..addons.manager import AnyAddon
from ..addons.utils import rating_security
from ..api.const import ATTR_SIGNED
from ..api.utils import api_process, api_process_raw, api_validate
from ..const import (
    ATTR_ADDONS,
    ATTR_ADVANCED,
    ATTR_APPARMOR,
    ATTR_ARCH,
    ATTR_AUTH_API,
    ATTR_AVAILABLE,
    ATTR_BACKUP,
    ATTR_BUILD,
    ATTR_DESCRIPTON,
    ATTR_DETACHED,
    ATTR_DOCKER_API,
    ATTR_DOCUMENTATION,
    ATTR_FULL_ACCESS,
    ATTR_HASSIO_API,
    ATTR_HASSIO_ROLE,
    ATTR_HOMEASSISTANT,
    ATTR_HOMEASSISTANT_API,
    ATTR_HOST_NETWORK,
    ATTR_HOST_PID,
    ATTR_ICON,
    ATTR_INGRESS,
    ATTR_INSTALLED,
    ATTR_JOB_ID,
    ATTR_LOGO,
    ATTR_LONG_DESCRIPTION,
    ATTR_MAINTAINER,
    ATTR_NAME,
    ATTR_RATING,
    ATTR_REPOSITORIES,
    ATTR_REPOSITORY,
    ATTR_SLUG,
    ATTR_SOURCE,
    ATTR_STAGE,
    ATTR_UPDATE_AVAILABLE,
    ATTR_URL,
    ATTR_VERSION,
    ATTR_VERSION_LATEST,
    REQUEST_FROM,
)
from ..coresys import CoreSysAttributes
from ..exceptions import APIError, APIForbidden, APINotFound, StoreAddonNotFoundError
from ..resolution.const import ContextType, SuggestionType
from ..store.addon import AddonStore
from ..store.repository import Repository
from ..store.validate import validate_repository
from .const import ATTR_BACKGROUND, CONTENT_TYPE_PNG, CONTENT_TYPE_TEXT
from .utils import background_task

SCHEMA_UPDATE = vol.Schema(
    {
        vol.Optional(ATTR_BACKUP): bool,
        vol.Optional(ATTR_BACKGROUND, default=False): bool,
    }
)

SCHEMA_ADD_REPOSITORY = vol.Schema(
    {vol.Required(ATTR_REPOSITORY): vol.All(str, validate_repository)}
)

SCHEMA_INSTALL = vol.Schema(
    {
        vol.Optional(ATTR_BACKGROUND, default=False): bool,
    }
)


def _read_static_text_file(path: Path) -> Any:
    """Read in a static text file asset for API output.

    Must be run in executor.
    """
    with path.open("r", errors="replace") as asset:
        return asset.read()


def _read_static_binary_file(path: Path) -> Any:
    """Read in a static binary file asset for API output.

    Must be run in executor.
    """
    with path.open("rb") as asset:
        return asset.read()


class APIStore(CoreSysAttributes):
    """Handle RESTful API for store functions."""

    def _extract_addon(self, request: web.Request, installed=False) -> AnyAddon:
        """Return add-on, throw an exception it it doesn't exist."""
        addon_slug: str = request.match_info["addon"]

        if not (addon := self.sys_addons.get(addon_slug)):
            raise StoreAddonNotFoundError(addon=addon_slug)

        if installed and not addon.is_installed:
            raise APIError(f"Addon {addon_slug} is not installed")

        if not installed and addon.is_installed:
            addon = cast(Addon, addon)
            if not addon.addon_store:
                raise StoreAddonNotFoundError(addon=addon_slug)
            return addon.addon_store

        return addon

    def _extract_repository(self, request: web.Request) -> Repository:
        """Return repository, throw an exception it it doesn't exist."""
        repository_slug: str = request.match_info["repository"]

        if repository_slug not in self.sys_store.repositories:
            raise APINotFound(
                f"Repository {repository_slug} does not exist in the store"
            )

        return self.sys_store.get(repository_slug)

    async def _generate_addon_information(
        self, addon: AddonStore, extended: bool = False
    ) -> dict[str, Any]:
        """Generate addon information."""

        installed = (
            self.sys_addons.get_local_only(addon.slug) if addon.is_installed else None
        )

        data = {
            ATTR_ADVANCED: addon.advanced,
            ATTR_ARCH: addon.supported_arch,
            ATTR_AVAILABLE: addon.available,
            ATTR_BUILD: addon.need_build,
            ATTR_DESCRIPTON: addon.description,
            ATTR_DOCUMENTATION: addon.with_documentation,
            ATTR_HOMEASSISTANT: addon.homeassistant_version,
            ATTR_ICON: addon.with_icon,
            ATTR_INSTALLED: addon.is_installed,
            ATTR_LOGO: addon.with_logo,
            ATTR_NAME: addon.name,
            ATTR_REPOSITORY: addon.repository,
            ATTR_SLUG: addon.slug,
            ATTR_STAGE: addon.stage,
            ATTR_UPDATE_AVAILABLE: installed.need_update if installed else False,
            ATTR_URL: addon.url,
            ATTR_VERSION_LATEST: addon.latest_version,
            ATTR_VERSION: installed.version if installed else None,
        }
        if extended:
            data.update(
                {
                    ATTR_APPARMOR: addon.apparmor,
                    ATTR_AUTH_API: addon.access_auth_api,
                    ATTR_DETACHED: addon.is_detached,
                    ATTR_DOCKER_API: addon.access_docker_api,
                    ATTR_FULL_ACCESS: addon.with_full_access,
                    ATTR_HASSIO_API: addon.access_hassio_api,
                    ATTR_HASSIO_ROLE: addon.hassio_role,
                    ATTR_HOMEASSISTANT_API: addon.access_homeassistant_api,
                    ATTR_HOST_NETWORK: addon.host_network,
                    ATTR_HOST_PID: addon.host_pid,
                    ATTR_INGRESS: addon.with_ingress,
                    ATTR_LONG_DESCRIPTION: await addon.long_description(),
                    ATTR_RATING: rating_security(addon),
                    ATTR_SIGNED: addon.signed,
                }
            )

        return data

    def _generate_repository_information(
        self, repository: Repository
    ) -> dict[str, Any]:
        """Generate repository information."""
        return {
            ATTR_SLUG: repository.slug,
            ATTR_NAME: repository.name,
            ATTR_SOURCE: repository.source,
            ATTR_URL: repository.url,
            ATTR_MAINTAINER: repository.maintainer,
        }

    @api_process
    async def reload(self, request: web.Request) -> None:
        """Reload all add-on data from store."""
        await asyncio.shield(self.sys_store.reload())

    @api_process
    async def store_info(self, request: web.Request) -> dict[str, Any]:
        """Return store information."""
        return {
            ATTR_ADDONS: await asyncio.gather(
                *[
                    self._generate_addon_information(self.sys_addons.store[addon])
                    for addon in self.sys_addons.store
                ]
            ),
            ATTR_REPOSITORIES: [
                self._generate_repository_information(repository)
                for repository in self.sys_store.all
            ],
        }

    @api_process
    async def addons_list(self, request: web.Request) -> dict[str, Any]:
        """Return all store add-ons."""
        return {
            ATTR_ADDONS: await asyncio.gather(
                *[
                    self._generate_addon_information(self.sys_addons.store[addon])
                    for addon in self.sys_addons.store
                ]
            )
        }

    @api_process
    async def addons_addon_install(self, request: web.Request) -> dict[str, str] | None:
        """Install add-on."""
        addon = self._extract_addon(request)
        body = await api_validate(SCHEMA_INSTALL, request)

        background = body[ATTR_BACKGROUND]

        install_task, job_id = await background_task(
            self, self.sys_addons.install, addon.slug
        )

        if background and not install_task.done():
            return {ATTR_JOB_ID: job_id}

        return await install_task

    @api_process
    async def addons_addon_update(self, request: web.Request) -> dict[str, str] | None:
        """Update add-on."""
        addon = self._extract_addon(request, installed=True)
        if addon == request.get(REQUEST_FROM):
            raise APIForbidden(f"Add-on {addon.slug} can't update itself!")

        body = await api_validate(SCHEMA_UPDATE, request)
        background = body[ATTR_BACKGROUND]

        update_task, job_id = await background_task(
            self,
            self.sys_addons.update,
            addon.slug,
            backup=body.get(ATTR_BACKUP),
        )

        if background and not update_task.done():
            return {ATTR_JOB_ID: job_id}

        if start_task := await update_task:
            await start_task
        return None

    @api_process
    async def addons_addon_info(self, request: web.Request) -> dict[str, Any]:
        """Return add-on information."""
        return await self.addons_addon_info_wrapped(request)

    # Used by legacy routing for addons/{addon}/info, can be refactored out when that is removed (1/2023)
    async def addons_addon_info_wrapped(self, request: web.Request) -> dict[str, Any]:
        """Return add-on information directly (not api)."""
        addon = cast(AddonStore, self._extract_addon(request))
        return await self._generate_addon_information(addon, True)

    @api_process_raw(CONTENT_TYPE_PNG)
    async def addons_addon_icon(self, request: web.Request) -> bytes:
        """Return icon from add-on."""
        addon = self._extract_addon(request)
        if not addon.with_icon:
            raise APIError(f"No icon found for add-on {addon.slug}!")

        return await self.sys_run_in_executor(_read_static_binary_file, addon.path_icon)

    @api_process_raw(CONTENT_TYPE_PNG)
    async def addons_addon_logo(self, request: web.Request) -> bytes:
        """Return logo from add-on."""
        addon = self._extract_addon(request)
        if not addon.with_logo:
            raise APIError(f"No logo found for add-on {addon.slug}!")

        return await self.sys_run_in_executor(_read_static_binary_file, addon.path_logo)

    @api_process_raw(CONTENT_TYPE_TEXT)
    async def addons_addon_changelog(self, request: web.Request) -> str:
        """Return changelog from add-on."""
        # Frontend can't handle error response here, need to return 200 and error as text for now
        try:
            addon = self._extract_addon(request)
        except APIError as err:
            return str(err)

        if not addon.with_changelog:
            return f"No changelog found for add-on {addon.slug}!"

        return await self.sys_run_in_executor(
            _read_static_text_file, addon.path_changelog
        )

    @api_process_raw(CONTENT_TYPE_TEXT)
    async def addons_addon_documentation(self, request: web.Request) -> str:
        """Return documentation from add-on."""
        # Frontend can't handle error response here, need to return 200 and error as text for now
        try:
            addon = self._extract_addon(request)
        except APIError as err:
            return str(err)

        if not addon.with_documentation:
            return f"No documentation found for add-on {addon.slug}!"

        return await self.sys_run_in_executor(
            _read_static_text_file, addon.path_documentation
        )

    @api_process
    async def addons_addon_availability(self, request: web.Request) -> None:
        """Check add-on availability for current system."""
        addon = cast(AddonStore, self._extract_addon(request))
        addon.validate_availability()

    @api_process
    async def repositories_list(self, request: web.Request) -> list[dict[str, Any]]:
        """Return all repositories."""
        return [
            self._generate_repository_information(repository)
            for repository in self.sys_store.all
        ]

    @api_process
    async def repositories_repository_info(
        self, request: web.Request
    ) -> dict[str, Any]:
        """Return repository information."""
        repository: Repository = self._extract_repository(request)
        return self._generate_repository_information(repository)

    @api_process
    async def add_repository(self, request: web.Request) -> None:
        """Add repository to the store."""
        body = await api_validate(SCHEMA_ADD_REPOSITORY, request)
        await asyncio.shield(self.sys_store.add_repository(body[ATTR_REPOSITORY]))

    @api_process
    async def remove_repository(self, request: web.Request) -> None:
        """Remove repository from the store."""
        repository: Repository = self._extract_repository(request)
        await asyncio.shield(self.sys_store.remove_repository(repository))

    @api_process
    async def repositories_repository_repair(self, request: web.Request) -> None:
        """Repair repository."""
        repository: Repository = self._extract_repository(request)
        await asyncio.shield(repository.reset())

        # If we have an execute reset suggestion on this repository, dismiss it and the issue
        for suggestion in self.sys_resolution.suggestions:
            if (
                suggestion.type == SuggestionType.EXECUTE_RESET
                and suggestion.context == ContextType.STORE
                and suggestion.reference == repository.slug
            ):
                for issue in self.sys_resolution.issues_for_suggestion(suggestion):
                    self.sys_resolution.dismiss_issue(issue)
                return
