"""Supervisor events monitor."""

from __future__ import annotations

import asyncio
from dataclasses import dataclass, field
from datetime import datetime
import logging
from typing import Any, NotRequired, TypedDict
from uuid import UUID

from aiohasupervisor import SupervisorError
from aiohasupervisor.models import ContextType, Issue as SupervisorIssue

from homeassistant.core import HassJob, HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.issue_registry import (
    IssueSeverity,
    async_create_issue,
    async_delete_issue,
)

from .const import (
    ATTR_DATA,
    ATTR_HEALTHY,
    ATTR_SUPPORTED,
    ATTR_UNHEALTHY_REASONS,
    ATTR_UNSUPPORTED_REASONS,
    ATTR_UPDATE_KEY,
    ATTR_WS_EVENT,
    DOMAIN,
    EVENT_HEALTH_CHANGED,
    EVENT_ISSUE_CHANGED,
    EVENT_ISSUE_REMOVED,
    EVENT_SUPERVISOR_EVENT,
    EVENT_SUPERVISOR_UPDATE,
    EVENT_SUPPORTED_CHANGED,
    ISSUE_KEY_ADDON_BOOT_FAIL,
    ISSUE_KEY_ADDON_DETACHED_ADDON_MISSING,
    ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED,
    ISSUE_KEY_SYSTEM_DOCKER_CONFIG,
    PLACEHOLDER_KEY_ADDON,
    PLACEHOLDER_KEY_ADDON_URL,
    PLACEHOLDER_KEY_REFERENCE,
    REQUEST_REFRESH_DELAY,
    UPDATE_KEY_SUPERVISOR,
)
from .coordinator import get_addons_info
from .handler import HassIO, get_supervisor_client

ISSUE_KEY_UNHEALTHY = "unhealthy"
ISSUE_KEY_UNSUPPORTED = "unsupported"
ISSUE_ID_UNHEALTHY = "unhealthy_system"
ISSUE_ID_UNSUPPORTED = "unsupported_system"

INFO_URL_UNHEALTHY = "https://www.home-assistant.io/more-info/unhealthy"
INFO_URL_UNSUPPORTED = "https://www.home-assistant.io/more-info/unsupported"

PLACEHOLDER_KEY_REASON = "reason"

UNSUPPORTED_REASONS = {
    "apparmor",
    "connectivity_check",
    "content_trust",
    "dbus",
    "dns_server",
    "docker_configuration",
    "docker_version",
    "cgroup_version",
    "job_conditions",
    "lxc",
    "network_manager",
    "os",
    "os_agent",
    "restart_policy",
    "software",
    "source_mods",
    "supervisor_version",
    "systemd",
    "systemd_journal",
    "systemd_resolved",
}
# Some unsupported reasons also mark the system as unhealthy. If the unsupported reason
# provides no additional information beyond the unhealthy one then skip that repair.
UNSUPPORTED_SKIP_REPAIR = {"privileged"}
UNHEALTHY_REASONS = {
    "docker",
    "supervisor",
    "setup",
    "privileged",
    "untrusted",
}

# Keys (type + context) of issues that when found should be made into a repair
ISSUE_KEYS_FOR_REPAIRS = {
    ISSUE_KEY_ADDON_BOOT_FAIL,
    "issue_mount_mount_failed",
    "issue_system_multiple_data_disks",
    "issue_system_reboot_required",
    ISSUE_KEY_SYSTEM_DOCKER_CONFIG,
    ISSUE_KEY_ADDON_DETACHED_ADDON_MISSING,
    ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED,
}

_LOGGER = logging.getLogger(__name__)


class SuggestionDataType(TypedDict):
    """Suggestion dictionary as received from supervisor."""

    uuid: str
    type: str
    context: str
    reference: str | None


@dataclass(slots=True, frozen=True)
class Suggestion:
    """Suggestion from Supervisor which resolves an issue."""

    uuid: UUID
    type: str
    context: ContextType
    reference: str | None = None

    @property
    def key(self) -> str:
        """Get key for suggestion (combination of context and type)."""
        return f"{self.context}_{self.type}"

    @classmethod
    def from_dict(cls, data: SuggestionDataType) -> Suggestion:
        """Convert from dictionary representation."""
        return cls(
            uuid=UUID(data["uuid"]),
            type=data["type"],
            context=ContextType(data["context"]),
            reference=data["reference"],
        )


class IssueDataType(TypedDict):
    """Issue dictionary as received from supervisor."""

    uuid: str
    type: str
    context: str
    reference: str | None
    suggestions: NotRequired[list[SuggestionDataType]]


@dataclass(slots=True, frozen=True)
class Issue:
    """Issue from Supervisor."""

    uuid: UUID
    type: str
    context: ContextType
    reference: str | None = None
    suggestions: list[Suggestion] = field(default_factory=list, compare=False)

    @property
    def key(self) -> str:
        """Get key for issue (combination of context and type)."""
        return f"issue_{self.context}_{self.type}"

    @classmethod
    def from_dict(cls, data: IssueDataType) -> Issue:
        """Convert from dictionary representation."""
        suggestions: list[SuggestionDataType] = data.get("suggestions", [])
        return cls(
            uuid=UUID(data["uuid"]),
            type=data["type"],
            context=ContextType(data["context"]),
            reference=data["reference"],
            suggestions=[
                Suggestion.from_dict(suggestion) for suggestion in suggestions
            ],
        )


class SupervisorIssues:
    """Create issues from supervisor events."""

    def __init__(self, hass: HomeAssistant, client: HassIO) -> None:
        """Initialize supervisor issues."""
        self._hass = hass
        self._client = client
        self._unsupported_reasons: set[str] = set()
        self._unhealthy_reasons: set[str] = set()
        self._issues: dict[UUID, Issue] = {}
        self._supervisor_client = get_supervisor_client(hass)

    @property
    def unhealthy_reasons(self) -> set[str]:
        """Get unhealthy reasons. Returns empty set if system is healthy."""
        return self._unhealthy_reasons

    @unhealthy_reasons.setter
    def unhealthy_reasons(self, reasons: set[str]) -> None:
        """Set unhealthy reasons. Create or delete repairs as necessary."""
        for unhealthy in reasons - self.unhealthy_reasons:
            if unhealthy in UNHEALTHY_REASONS:
                translation_key = f"{ISSUE_KEY_UNHEALTHY}_{unhealthy}"
                translation_placeholders = None
            else:
                translation_key = ISSUE_KEY_UNHEALTHY
                translation_placeholders = {PLACEHOLDER_KEY_REASON: unhealthy}

            async_create_issue(
                self._hass,
                DOMAIN,
                f"{ISSUE_ID_UNHEALTHY}_{unhealthy}",
                is_fixable=False,
                learn_more_url=f"{INFO_URL_UNHEALTHY}/{unhealthy}",
                severity=IssueSeverity.CRITICAL,
                translation_key=translation_key,
                translation_placeholders=translation_placeholders,
            )

        for fixed in self.unhealthy_reasons - reasons:
            async_delete_issue(self._hass, DOMAIN, f"{ISSUE_ID_UNHEALTHY}_{fixed}")

        self._unhealthy_reasons = reasons

    @property
    def unsupported_reasons(self) -> set[str]:
        """Get unsupported reasons. Returns empty set if system is supported."""
        return self._unsupported_reasons

    @unsupported_reasons.setter
    def unsupported_reasons(self, reasons: set[str]) -> None:
        """Set unsupported reasons. Create or delete repairs as necessary."""
        for unsupported in reasons - UNSUPPORTED_SKIP_REPAIR - self.unsupported_reasons:
            if unsupported in UNSUPPORTED_REASONS:
                translation_key = f"{ISSUE_KEY_UNSUPPORTED}_{unsupported}"
                translation_placeholders = None
            else:
                translation_key = ISSUE_KEY_UNSUPPORTED
                translation_placeholders = {PLACEHOLDER_KEY_REASON: unsupported}

            async_create_issue(
                self._hass,
                DOMAIN,
                f"{ISSUE_ID_UNSUPPORTED}_{unsupported}",
                is_fixable=False,
                learn_more_url=f"{INFO_URL_UNSUPPORTED}/{unsupported}",
                severity=IssueSeverity.WARNING,
                translation_key=translation_key,
                translation_placeholders=translation_placeholders,
            )

        for fixed in self.unsupported_reasons - (reasons - UNSUPPORTED_SKIP_REPAIR):
            async_delete_issue(self._hass, DOMAIN, f"{ISSUE_ID_UNSUPPORTED}_{fixed}")

        self._unsupported_reasons = reasons

    @property
    def issues(self) -> set[Issue]:
        """Get issues."""
        return set(self._issues.values())

    def add_issue(self, issue: Issue) -> None:
        """Add or update an issue in the list. Create or update a repair if necessary."""
        if issue.key in ISSUE_KEYS_FOR_REPAIRS:
            placeholders: dict[str, str] | None = None
            if issue.reference:
                placeholders = {PLACEHOLDER_KEY_REFERENCE: issue.reference}

                if issue.key == ISSUE_KEY_ADDON_DETACHED_ADDON_MISSING:
                    placeholders[PLACEHOLDER_KEY_ADDON_URL] = (
                        f"/hassio/addon/{issue.reference}"
                    )
                    addons = get_addons_info(self._hass)
                    if addons and issue.reference in addons:
                        placeholders[PLACEHOLDER_KEY_ADDON] = addons[issue.reference][
                            "name"
                        ]
                    else:
                        placeholders[PLACEHOLDER_KEY_ADDON] = issue.reference

            async_create_issue(
                self._hass,
                DOMAIN,
                issue.uuid.hex,
                is_fixable=bool(issue.suggestions),
                severity=IssueSeverity.WARNING,
                translation_key=issue.key,
                translation_placeholders=placeholders,
            )

        self._issues[issue.uuid] = issue

    async def add_issue_from_data(self, data: SupervisorIssue) -> None:
        """Add issue from data to list after getting latest suggestions."""
        try:
            suggestions = (
                await self._supervisor_client.resolution.suggestions_for_issue(
                    data.uuid
                )
            )
        except SupervisorError:
            _LOGGER.error(
                "Could not get suggestions for supervisor issue %s, skipping it",
                data.uuid.hex,
            )
            return
        self.add_issue(
            Issue(
                uuid=data.uuid,
                type=str(data.type),
                context=data.context,
                reference=data.reference,
                suggestions=[
                    Suggestion(
                        uuid=suggestion.uuid,
                        type=str(suggestion.type),
                        context=suggestion.context,
                        reference=suggestion.reference,
                    )
                    for suggestion in suggestions
                ],
            )
        )

    def remove_issue(self, issue: Issue) -> None:
        """Remove an issue from the list. Delete a repair if necessary."""
        if issue.uuid not in self._issues:
            return

        if issue.key in ISSUE_KEYS_FOR_REPAIRS:
            async_delete_issue(self._hass, DOMAIN, issue.uuid.hex)

        del self._issues[issue.uuid]

    def get_issue(self, issue_id: str) -> Issue | None:
        """Get issue from key."""
        return self._issues.get(UUID(issue_id))

    async def setup(self) -> None:
        """Create supervisor events listener."""
        await self._update()

        async_dispatcher_connect(
            self._hass, EVENT_SUPERVISOR_EVENT, self._supervisor_events_to_issues
        )

    async def _update(self, _: datetime | None = None) -> None:
        """Update issues from Supervisor resolution center."""
        try:
            data = await self._supervisor_client.resolution.info()
        except SupervisorError as err:
            _LOGGER.error("Failed to update supervisor issues: %r", err)
            async_call_later(
                self._hass,
                REQUEST_REFRESH_DELAY,
                HassJob(self._update, cancel_on_shutdown=True),
            )
            return
        self.unhealthy_reasons = set(data.unhealthy)
        self.unsupported_reasons = set(data.unsupported)

        # Remove any cached issues that weren't returned
        for issue_id in set(self._issues) - {issue.uuid for issue in data.issues}:
            self.remove_issue(self._issues[issue_id])

        # Add/update any issues that came back
        await asyncio.gather(
            *[self.add_issue_from_data(issue) for issue in data.issues]
        )

    @callback
    def _supervisor_events_to_issues(self, event: dict[str, Any]) -> None:
        """Create issues from supervisor events."""
        if ATTR_WS_EVENT not in event:
            return

        if (
            event[ATTR_WS_EVENT] == EVENT_SUPERVISOR_UPDATE
            and event.get(ATTR_UPDATE_KEY) == UPDATE_KEY_SUPERVISOR
        ):
            self._hass.async_create_task(self._update())

        elif event[ATTR_WS_EVENT] == EVENT_HEALTH_CHANGED:
            self.unhealthy_reasons = (
                set()
                if event[ATTR_DATA][ATTR_HEALTHY]
                else set(event[ATTR_DATA][ATTR_UNHEALTHY_REASONS])
            )

        elif event[ATTR_WS_EVENT] == EVENT_SUPPORTED_CHANGED:
            self.unsupported_reasons = (
                set()
                if event[ATTR_DATA][ATTR_SUPPORTED]
                else set(event[ATTR_DATA][ATTR_UNSUPPORTED_REASONS])
            )

        elif event[ATTR_WS_EVENT] == EVENT_ISSUE_CHANGED:
            self.add_issue(Issue.from_dict(event[ATTR_DATA]))

        elif event[ATTR_WS_EVENT] == EVENT_ISSUE_REMOVED:
            self.remove_issue(Issue.from_dict(event[ATTR_DATA]))