"""Supervisor resolution center."""

import errno
import logging
from typing import Any

import attr

from ..bus import EventListener
from ..coresys import CoreSys, CoreSysAttributes
from ..exceptions import (
    ResolutionError,
    ResolutionIssueNotFound,
    ResolutionSuggestionNotFound,
)
from ..homeassistant.const import WSEvent
from ..utils.common import FileConfiguration
from .check import ResolutionCheck
from .const import (
    FILE_CONFIG_RESOLUTION,
    SCHEDULED_HEALTHCHECK,
    ContextType,
    IssueType,
    SuggestionType,
    UnhealthyReason,
    UnsupportedReason,
)
from .data import HealthChanged, Issue, Suggestion, SupportedChanged
from .evaluate import ResolutionEvaluation
from .fixup import ResolutionFixup
from .validate import SCHEMA_RESOLUTION_CONFIG

_LOGGER: logging.Logger = logging.getLogger(__name__)


class ResolutionManager(FileConfiguration, CoreSysAttributes):
    """Resolution manager for supervisor."""

    def __init__(self, coresys: CoreSys):
        """Initialize Resolution manager."""
        super().__init__(FILE_CONFIG_RESOLUTION, SCHEMA_RESOLUTION_CONFIG)

        self.coresys: CoreSys = coresys
        self._evaluate = ResolutionEvaluation(coresys)
        self._check = ResolutionCheck(coresys)
        self._fixup = ResolutionFixup(coresys)

        self._suggestions: list[Suggestion] = []
        self._issues: list[Issue] = []
        self._unsupported: list[UnsupportedReason] = []
        self._unhealthy: list[UnhealthyReason] = []

        # Map suggestion UUID to event listeners (list)
        self._suggestion_listeners: dict[str, list[EventListener]] = {}

    async def load_modules(self):
        """Load resolution evaluation, check and fixup modules."""

        def _load_modules():
            """Load and setup all resolution modules."""
            self._evaluate.load_modules()
            self._check.load_modules()
            self._fixup.load_modules()

        await self.sys_run_in_executor(_load_modules)

    @property
    def data(self) -> dict[str, Any]:
        """Return data."""
        return self._data

    @property
    def evaluate(self) -> ResolutionEvaluation:
        """Return the ResolutionEvaluation class."""
        return self._evaluate

    @property
    def check(self) -> ResolutionCheck:
        """Return the ResolutionCheck class."""
        return self._check

    @property
    def fixup(self) -> ResolutionFixup:
        """Return the ResolutionFixup class."""
        return self._fixup

    @property
    def issues(self) -> list[Issue]:
        """Return a list of issues."""
        return self._issues

    @property
    def suggestions(self) -> list[Suggestion]:
        """Return a list of suggestions that can handled."""
        return self._suggestions

    def add_suggestion(self, suggestion: Suggestion) -> None:
        """Add suggestion."""
        if suggestion in self._suggestions:
            return

        _LOGGER.info(
            "Create new suggestion %s - %s / %s",
            suggestion.type,
            suggestion.context,
            suggestion.reference,
        )
        self._suggestions.append(suggestion)

        # Register event listeners if fixups have a bus_event
        listeners: list[EventListener] = []
        for fixup in self.fixup.fixes_for_suggestion(suggestion):
            if fixup.auto and fixup.bus_event:

                def event_callback(reference, fixup=fixup):
                    return fixup(suggestion)

                listener = self.sys_bus.register_event(fixup.bus_event, event_callback)
                listeners.append(listener)
        if listeners:
            self._suggestion_listeners[suggestion.uuid] = listeners

        # Event on suggestion added to issue
        for issue in self.issues_for_suggestion(suggestion):
            self.sys_homeassistant.websocket.supervisor_event(
                WSEvent.ISSUE_CHANGED, self._make_issue_message(issue)
            )

    @property
    def unsupported(self) -> list[UnsupportedReason]:
        """Return a list of unsupported reasons."""
        return self._unsupported

    def add_unsupported_reason(self, reason: UnsupportedReason) -> None:
        """Add a reason for unsupported."""
        if reason not in self._unsupported:
            self._unsupported.append(reason)
            self.sys_homeassistant.websocket.supervisor_event(
                WSEvent.SUPPORTED_CHANGED,
                attr.asdict(SupportedChanged(False, self.unsupported)),
            )

    @property
    def unhealthy(self) -> list[UnhealthyReason]:
        """Return a list of unhealthy reasons."""
        return self._unhealthy

    def add_unhealthy_reason(self, reason: UnhealthyReason) -> None:
        """Add a reason for unhealthy."""
        if reason not in self._unhealthy:
            self._unhealthy.append(reason)
            self.sys_homeassistant.websocket.supervisor_event(
                WSEvent.HEALTH_CHANGED,
                attr.asdict(HealthChanged(False, self.unhealthy)),
            )

    _OSERROR_UNHEALTHY_REASONS: dict[int, UnhealthyReason] = {
        errno.EBADMSG: UnhealthyReason.OSERROR_BAD_MESSAGE,
    }

    def check_oserror(self, err: OSError) -> None:
        """Check OSError for known filesystem issues and mark system unhealthy.

        Must only be used on OSErrors that are caused by file operation on a
        local path.
        """
        if err.errno in self._OSERROR_UNHEALTHY_REASONS:
            self.add_unhealthy_reason(self._OSERROR_UNHEALTHY_REASONS[err.errno])

    def _make_issue_message(self, issue: Issue) -> dict[str, Any]:
        """Make issue into message for core."""
        return attr.asdict(issue) | {
            "suggestions": [
                attr.asdict(suggestion)
                for suggestion in self.suggestions_for_issue(issue)
            ]
        }

    def get_suggestion_by_id(self, uuid: str) -> Suggestion:
        """Return suggestion with uuid."""
        for suggestion in self._suggestions:
            if suggestion.uuid != uuid:
                continue
            return suggestion
        raise ResolutionSuggestionNotFound(uuid=uuid)

    def get_suggestion_if_present(self, suggestion: Suggestion) -> Suggestion | None:
        """Get suggestion matching provided one if it exists in resolution manager."""
        for s in self._suggestions:
            if s != suggestion:
                continue
            return s
        return None

    def get_issue_by_id(self, uuid: str) -> Issue:
        """Return issue with uuid."""
        for issue in self._issues:
            if issue.uuid != uuid:
                continue
            return issue
        raise ResolutionIssueNotFound(uuid=uuid)

    def get_issue_if_present(self, issue: Issue) -> Issue | None:
        """Get issue matching provided one if it exists in resolution manager."""
        for i in self._issues:
            if i != issue:
                continue
            return i
        return None

    def create_issue(
        self,
        issue: IssueType,
        context: ContextType,
        reference: str | None = None,
        suggestions: list[SuggestionType] | None = None,
    ) -> None:
        """Create issues and suggestion."""
        self.add_issue(Issue(issue, context, reference), suggestions)

    def add_issue(
        self, issue: Issue, suggestions: list[SuggestionType] | None = None
    ) -> None:
        """Add an issue and suggestions."""
        if suggestions:
            for suggestion in suggestions:
                self.add_suggestion(
                    Suggestion(suggestion, issue.context, issue.reference)
                )

        if issue in self._issues:
            return
        _LOGGER.info(
            "Create new issue %s - %s / %s", issue.type, issue.context, issue.reference
        )
        self._issues.append(issue)

        # Event on issue creation
        self.sys_homeassistant.websocket.supervisor_event(
            WSEvent.ISSUE_CHANGED, self._make_issue_message(issue)
        )

    async def load(self):
        """Load the resoulution manager."""
        # Initial healthcheck check
        await self.healthcheck()

        # Schedule the healthcheck
        self.sys_scheduler.register_task(self.healthcheck, SCHEDULED_HEALTHCHECK)

    async def healthcheck(self):
        """Scheduled task to check for known issues."""
        await self.check.check_system()
        await self.evaluate.evaluate_system()

        # Run autofix if possible
        await self.fixup.run_autofix()

    async def apply_suggestion(self, suggestion: Suggestion) -> None:
        """Apply suggested action."""
        suggestion = self.get_suggestion_by_id(suggestion.uuid)
        await self.fixup.apply_fixup(suggestion)
        await self.healthcheck()

    def dismiss_suggestion(self, suggestion: Suggestion) -> None:
        """Dismiss suggested action."""
        suggestion = self.get_suggestion_by_id(suggestion.uuid)
        self._suggestions.remove(suggestion)

        # Remove event listeners if present
        listeners = self._suggestion_listeners.pop(suggestion.uuid, [])
        for listener in listeners:
            self.sys_bus.remove_listener(listener)

        # Event on suggestion removed from issues
        for issue in self.issues_for_suggestion(suggestion):
            self.sys_homeassistant.websocket.supervisor_event(
                WSEvent.ISSUE_CHANGED, self._make_issue_message(issue)
            )

    def dismiss_issue(self, issue: Issue) -> None:
        """Dismiss suggested action."""
        issue = self.get_issue_by_id(issue.uuid)
        self._issues.remove(issue)

        # Event on issue removal
        self.sys_homeassistant.websocket.supervisor_event(
            WSEvent.ISSUE_REMOVED, attr.asdict(issue)
        )

        # Clean up any orphaned suggestions
        for suggestion in self.suggestions_for_issue(issue):
            if not self.issues_for_suggestion(suggestion):
                self.dismiss_suggestion(suggestion)

    def dismiss_unsupported(self, reason: UnsupportedReason) -> None:
        """Dismiss a reason for unsupported."""
        if reason not in self._unsupported:
            raise ResolutionError(f"The reason {reason} is not active", _LOGGER.warning)
        self._unsupported.remove(reason)
        self.sys_homeassistant.websocket.supervisor_event(
            WSEvent.SUPPORTED_CHANGED,
            attr.asdict(
                SupportedChanged(self.sys_core.supported, self.unsupported or None)
            ),
        )

    def suggestions_for_issue(self, issue: Issue) -> set[Suggestion]:
        """Get suggestions that fix an issue."""
        return {
            suggestion
            for fix in self.fixup.fixes_for_issue(issue)
            for suggestion in fix.all_suggestions
            if suggestion.reference == issue.reference
        }

    def issues_for_suggestion(self, suggestion: Suggestion) -> set[Issue]:
        """Get issues fixed by a suggestion."""
        return {
            issue
            for fix in self.fixup.fixes_for_suggestion(suggestion)
            for issue in fix.all_issues
            if issue.reference == suggestion.reference
        }
