"""Provide add-on management."""
from __future__ import annotations

import asyncio
from dataclasses import dataclass
from enum import Enum
from functools import partial
from typing import Any, Callable, TypeVar, cast

from homeassistant.components.hassio import (
    async_create_backup,
    async_get_addon_discovery_info,
    async_get_addon_info,
    async_install_addon,
    async_restart_addon,
    async_set_addon_options,
    async_start_addon,
    async_stop_addon,
    async_uninstall_addon,
    async_update_addon,
)
from homeassistant.components.hassio.handler import HassioAPIError
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.singleton import singleton

from .const import ADDON_SLUG, CONF_ADDON_DEVICE, CONF_ADDON_NETWORK_KEY, DOMAIN, LOGGER

F = TypeVar("F", bound=Callable[..., Any])  # pylint: disable=invalid-name

DATA_ADDON_MANAGER = f"{DOMAIN}_addon_manager"


@singleton(DATA_ADDON_MANAGER)
@callback
def get_addon_manager(hass: HomeAssistant) -> AddonManager:
    """Get the add-on manager."""
    return AddonManager(hass)


def api_error(error_message: str) -> Callable[[F], F]:
    """Handle HassioAPIError and raise a specific AddonError."""

    def handle_hassio_api_error(func: F) -> F:
        """Handle a HassioAPIError."""

        async def wrapper(*args, **kwargs):  # type: ignore
            """Wrap an add-on manager method."""
            try:
                return_value = await func(*args, **kwargs)
            except HassioAPIError as err:
                raise AddonError(f"{error_message}: {err}") from err

            return return_value

        return cast(F, wrapper)

    return handle_hassio_api_error


@dataclass
class AddonInfo:
    """Represent the current add-on info state."""

    options: dict[str, Any]
    state: AddonState
    update_available: bool
    version: str | None


class AddonState(Enum):
    """Represent the current state of the add-on."""

    NOT_INSTALLED = "not_installed"
    INSTALLING = "installing"
    UPDATING = "updating"
    NOT_RUNNING = "not_running"
    RUNNING = "running"


class AddonManager:
    """Manage the add-on.

    Methods may raise AddonError.
    Only one instance of this class may exist
    to keep track of running add-on tasks.
    """

    def __init__(self, hass: HomeAssistant) -> None:
        """Set up the add-on manager."""
        self._hass = hass
        self._install_task: asyncio.Task | None = None
        self._restart_task: asyncio.Task | None = None
        self._start_task: asyncio.Task | None = None
        self._update_task: asyncio.Task | None = None

    def task_in_progress(self) -> bool:
        """Return True if any of the add-on tasks are in progress."""
        return any(
            task and not task.done()
            for task in (
                self._install_task,
                self._start_task,
                self._update_task,
            )
        )

    @api_error("Failed to get Z-Wave JS add-on discovery info")
    async def async_get_addon_discovery_info(self) -> dict:
        """Return add-on discovery info."""
        discovery_info = await async_get_addon_discovery_info(self._hass, ADDON_SLUG)

        if not discovery_info:
            raise AddonError("Failed to get Z-Wave JS add-on discovery info")

        discovery_info_config: dict = discovery_info["config"]
        return discovery_info_config

    @api_error("Failed to get the Z-Wave JS add-on info")
    async def async_get_addon_info(self) -> AddonInfo:
        """Return and cache Z-Wave JS add-on info."""
        addon_info: dict = await async_get_addon_info(self._hass, ADDON_SLUG)
        addon_state = self.async_get_addon_state(addon_info)
        return AddonInfo(
            options=addon_info["options"],
            state=addon_state,
            update_available=addon_info["update_available"],
            version=addon_info["version"],
        )

    @callback
    def async_get_addon_state(self, addon_info: dict[str, Any]) -> AddonState:
        """Return the current state of the Z-Wave JS add-on."""
        addon_state = AddonState.NOT_INSTALLED

        if addon_info["version"] is not None:
            addon_state = AddonState.NOT_RUNNING
        if addon_info["state"] == "started":
            addon_state = AddonState.RUNNING
        if self._install_task and not self._install_task.done():
            addon_state = AddonState.INSTALLING
        if self._update_task and not self._update_task.done():
            addon_state = AddonState.UPDATING

        return addon_state

    @api_error("Failed to set the Z-Wave JS add-on options")
    async def async_set_addon_options(self, config: dict) -> None:
        """Set Z-Wave JS add-on options."""
        options = {"options": config}
        await async_set_addon_options(self._hass, ADDON_SLUG, options)

    @api_error("Failed to install the Z-Wave JS add-on")
    async def async_install_addon(self) -> None:
        """Install the Z-Wave JS add-on."""
        await async_install_addon(self._hass, ADDON_SLUG)

    @callback
    def async_schedule_install_addon(self, catch_error: bool = False) -> asyncio.Task:
        """Schedule a task that installs the Z-Wave JS add-on.

        Only schedule a new install task if the there's no running task.
        """
        if not self._install_task or self._install_task.done():
            LOGGER.info("Z-Wave JS add-on is not installed. Installing add-on")
            self._install_task = self._async_schedule_addon_operation(
                self.async_install_addon, catch_error=catch_error
            )
        return self._install_task

    @callback
    def async_schedule_install_setup_addon(
        self, usb_path: str, network_key: str, catch_error: bool = False
    ) -> asyncio.Task:
        """Schedule a task that installs and sets up the Z-Wave JS add-on.

        Only schedule a new install task if the there's no running task.
        """
        if not self._install_task or self._install_task.done():
            LOGGER.info("Z-Wave JS add-on is not installed. Installing add-on")
            self._install_task = self._async_schedule_addon_operation(
                self.async_install_addon,
                partial(self.async_configure_addon, usb_path, network_key),
                self.async_start_addon,
                catch_error=catch_error,
            )
        return self._install_task

    @api_error("Failed to uninstall the Z-Wave JS add-on")
    async def async_uninstall_addon(self) -> None:
        """Uninstall the Z-Wave JS add-on."""
        await async_uninstall_addon(self._hass, ADDON_SLUG)

    @api_error("Failed to update the Z-Wave JS add-on")
    async def async_update_addon(self) -> None:
        """Update the Z-Wave JS add-on if needed."""
        addon_info = await self.async_get_addon_info()

        if addon_info.version is None:
            raise AddonError("Z-Wave JS add-on is not installed")

        if not addon_info.update_available:
            return

        await self.async_create_backup()
        await async_update_addon(self._hass, ADDON_SLUG)

    @callback
    def async_schedule_update_addon(self, catch_error: bool = False) -> asyncio.Task:
        """Schedule a task that updates and sets up the Z-Wave JS add-on.

        Only schedule a new update task if the there's no running task.
        """
        if not self._update_task or self._update_task.done():
            LOGGER.info("Trying to update the Z-Wave JS add-on")
            self._update_task = self._async_schedule_addon_operation(
                self.async_update_addon,
                catch_error=catch_error,
            )
        return self._update_task

    @api_error("Failed to start the Z-Wave JS add-on")
    async def async_start_addon(self) -> None:
        """Start the Z-Wave JS add-on."""
        await async_start_addon(self._hass, ADDON_SLUG)

    @api_error("Failed to restart the Z-Wave JS add-on")
    async def async_restart_addon(self) -> None:
        """Restart the Z-Wave JS add-on."""
        await async_restart_addon(self._hass, ADDON_SLUG)

    @callback
    def async_schedule_start_addon(self, catch_error: bool = False) -> asyncio.Task:
        """Schedule a task that starts the Z-Wave JS add-on.

        Only schedule a new start task if the there's no running task.
        """
        if not self._start_task or self._start_task.done():
            LOGGER.info("Z-Wave JS add-on is not running. Starting add-on")
            self._start_task = self._async_schedule_addon_operation(
                self.async_start_addon, catch_error=catch_error
            )
        return self._start_task

    @callback
    def async_schedule_restart_addon(self, catch_error: bool = False) -> asyncio.Task:
        """Schedule a task that restarts the Z-Wave JS add-on.

        Only schedule a new restart task if the there's no running task.
        """
        if not self._restart_task or self._restart_task.done():
            LOGGER.info("Restarting Z-Wave JS add-on")
            self._restart_task = self._async_schedule_addon_operation(
                self.async_restart_addon, catch_error=catch_error
            )
        return self._restart_task

    @api_error("Failed to stop the Z-Wave JS add-on")
    async def async_stop_addon(self) -> None:
        """Stop the Z-Wave JS add-on."""
        await async_stop_addon(self._hass, ADDON_SLUG)

    async def async_configure_addon(self, usb_path: str, network_key: str) -> None:
        """Configure and start Z-Wave JS add-on."""
        addon_info = await self.async_get_addon_info()

        new_addon_options = {
            CONF_ADDON_DEVICE: usb_path,
            CONF_ADDON_NETWORK_KEY: network_key,
        }

        if new_addon_options != addon_info.options:
            await self.async_set_addon_options(new_addon_options)

    @callback
    def async_schedule_setup_addon(
        self, usb_path: str, network_key: str, catch_error: bool = False
    ) -> asyncio.Task:
        """Schedule a task that configures and starts the Z-Wave JS add-on.

        Only schedule a new setup task if the there's no running task.
        """
        if not self._start_task or self._start_task.done():
            LOGGER.info("Z-Wave JS add-on is not running. Starting add-on")
            self._start_task = self._async_schedule_addon_operation(
                partial(self.async_configure_addon, usb_path, network_key),
                self.async_start_addon,
                catch_error=catch_error,
            )
        return self._start_task

    @api_error("Failed to create a backup of the Z-Wave JS add-on.")
    async def async_create_backup(self) -> None:
        """Create a partial backup of the Z-Wave JS add-on."""
        addon_info = await self.async_get_addon_info()
        name = f"addon_{ADDON_SLUG}_{addon_info.version}"

        LOGGER.debug("Creating backup: %s", name)
        await async_create_backup(
            self._hass,
            {"name": name, "addons": [ADDON_SLUG]},
            partial=True,
        )

    @callback
    def _async_schedule_addon_operation(
        self, *funcs: Callable, catch_error: bool = False
    ) -> asyncio.Task:
        """Schedule an add-on task."""

        async def addon_operation() -> None:
            """Do the add-on operation and catch AddonError."""
            for func in funcs:
                try:
                    await func()
                except AddonError as err:
                    if not catch_error:
                        raise
                    LOGGER.error(err)
                    break

        return self._hass.async_create_task(addon_operation())


class AddonError(HomeAssistantError):
    """Represent an error with Z-Wave JS add-on."""
