"""Helpers for the logger integration."""

from collections import defaultdict
from collections.abc import Mapping
import contextlib
from dataclasses import asdict, dataclass
from enum import StrEnum
from functools import lru_cache
import logging
from typing import Any

from homeassistant.const import EVENT_LOGGING_CHANGED
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.storage import Store
from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import IntegrationNotFound, async_get_integration
from homeassistant.util.hass_dict import HassKey

from .const import (
    DOMAIN,
    LOGGER_DEFAULT,
    LOGGER_LOGS,
    LOGSEVERITY,
    LOGSEVERITY_NOTSET,
    STORAGE_KEY,
    STORAGE_LOG_KEY,
    STORAGE_VERSION,
)

DATA_LOGGER: HassKey[LoggerDomainConfig] = HassKey(DOMAIN)

SAVE_DELAY = 15.0
# At startup, we want to save after a long delay to avoid
# saving while the system is still starting up. If the system
# for some reason restarts quickly, it will still be written
# at the final write event. In most cases we expect startup
# to happen in less than 180 seconds, but if it takes longer
# it's likely delayed because of remote I/O and not local
# I/O so it's fine to save at that point.
SAVE_DELAY_LONG = 180.0


@callback
def set_default_log_level(hass: HomeAssistant, level: int) -> None:
    """Set the default log level for components."""
    _set_log_level(logging.getLogger(""), level)
    hass.bus.async_fire(EVENT_LOGGING_CHANGED)


@callback
def set_log_levels(hass: HomeAssistant, logpoints: Mapping[str, int]) -> None:
    """Set the specified log levels."""
    hass.data[DATA_LOGGER].overrides.update(logpoints)
    for key, value in logpoints.items():
        _set_log_level(logging.getLogger(key), value)
    hass.bus.async_fire(EVENT_LOGGING_CHANGED)


def _set_log_level(logger: logging.Logger, level: int) -> None:
    """Set the log level.

    Any logger fetched before this integration is loaded will use old class.
    """
    getattr(logger, "orig_setLevel", logger.setLevel)(level)


def _chattiest_log_level(level1: int, level2: int) -> int:
    """Return the chattiest log level."""
    if level1 == logging.NOTSET:
        return level2
    if level2 == logging.NOTSET:
        return level1
    return min(level1, level2)


@callback
def _clear_logger_overwrites(hass: HomeAssistant) -> None:
    """Clear logger overwrites. Used for testing."""
    hass.data[DATA_LOGGER].overrides.clear()


async def get_integration_loggers(hass: HomeAssistant, domain: str) -> set[str]:
    """Get loggers for an integration."""
    loggers: set[str] = {f"homeassistant.components.{domain}"}
    with contextlib.suppress(IntegrationNotFound):
        integration = await async_get_integration(hass, domain)
        loggers.add(integration.pkg_path)
        if integration.loggers:
            loggers.update(integration.loggers)
    return loggers


@dataclass(slots=True)
class LoggerSetting:
    """Settings for a single module or integration."""

    level: str
    persistence: str
    type: str


@dataclass(slots=True)
class LoggerDomainConfig:
    """Logger domain config."""

    overrides: dict[str, Any]
    settings: LoggerSettings


class LogPersistance(StrEnum):
    """Log persistence."""

    NONE = "none"
    ONCE = "once"
    PERMANENT = "permanent"


class LogSettingsType(StrEnum):
    """Log settings type."""

    INTEGRATION = "integration"
    MODULE = "module"


class LoggerSettings:
    """Manage log settings."""

    _stored_config: dict[str, dict[str, LoggerSetting]]

    def __init__(self, hass: HomeAssistant, yaml_config: ConfigType) -> None:
        """Initialize log settings."""

        self._yaml_config = yaml_config
        self._default_level = logging.INFO
        if DOMAIN in yaml_config and LOGGER_DEFAULT in yaml_config[DOMAIN]:
            self._default_level = yaml_config[DOMAIN][LOGGER_DEFAULT]
        self._store: Store[dict[str, dict[str, dict[str, Any]]]] = Store(
            hass, STORAGE_VERSION, STORAGE_KEY
        )

    async def async_load(self) -> None:
        """Load stored settings."""
        stored_config = await self._store.async_load()
        if not stored_config:
            self._stored_config = {STORAGE_LOG_KEY: {}}
            return

        def reset_persistence(settings: LoggerSetting) -> LoggerSetting:
            """Reset persistence."""
            if settings.persistence == LogPersistance.ONCE:
                settings.persistence = LogPersistance.NONE
            return settings

        stored_log_config = stored_config[STORAGE_LOG_KEY]
        # Reset domains for which the overrides should only be applied once
        self._stored_config = {
            STORAGE_LOG_KEY: {
                domain: reset_persistence(LoggerSetting(**settings))
                for domain, settings in stored_log_config.items()
            }
        }
        self.async_save(SAVE_DELAY_LONG)

    @callback
    def _async_data_to_save(self) -> dict[str, dict[str, dict[str, str]]]:
        """Generate data to be saved."""
        stored_log_config = self._stored_config[STORAGE_LOG_KEY]
        return {
            STORAGE_LOG_KEY: {
                domain: asdict(settings)
                for domain, settings in stored_log_config.items()
                if settings.persistence
                in (LogPersistance.ONCE, LogPersistance.PERMANENT)
            }
        }

    @callback
    def async_save(self, delay: float = SAVE_DELAY) -> None:
        """Save settings."""
        self._store.async_delay_save(self._async_data_to_save, delay)

    @callback
    def async_get_integration_domains(self) -> set[str]:
        """Get domains that have integration-level log settings."""
        stored_log_config = self._stored_config[STORAGE_LOG_KEY]
        return {
            domain
            for domain, setting in stored_log_config.items()
            if setting.type == LogSettingsType.INTEGRATION
        }

    @callback
    def _async_get_logger_logs(self) -> dict[str, int]:
        """Get the logger logs."""
        logger_logs: dict[str, int] = self._yaml_config.get(DOMAIN, {}).get(
            LOGGER_LOGS, {}
        )
        return logger_logs

    async def async_update(
        self, hass: HomeAssistant, domain: str, settings: LoggerSetting
    ) -> None:
        """Update settings."""
        stored_log_config = self._stored_config[STORAGE_LOG_KEY]
        if settings.level == LOGSEVERITY_NOTSET:
            stored_log_config.pop(domain, None)
        else:
            stored_log_config[domain] = settings

        self.async_save()

        if settings.type == LogSettingsType.INTEGRATION:
            loggers = await get_integration_loggers(hass, domain)
        else:
            loggers = {domain}

        combined_logs = dict.fromkeys(loggers, LOGSEVERITY[settings.level])
        # Don't override the log levels with the ones from YAML
        # since we want whatever the user is asking for to be honored.

        set_log_levels(hass, combined_logs)

    async def async_get_levels(self, hass: HomeAssistant) -> dict[str, int]:
        """Get combination of levels from yaml and storage."""
        combined_logs = defaultdict(lambda: logging.CRITICAL)
        for domain, settings in self._stored_config[STORAGE_LOG_KEY].items():
            if settings.type == LogSettingsType.INTEGRATION:
                loggers = await get_integration_loggers(hass, domain)
            else:
                loggers = {domain}

            for logger in loggers:
                combined_logs[logger] = LOGSEVERITY[settings.level]

        if yaml_log_settings := self._async_get_logger_logs():
            for domain, level in yaml_log_settings.items():
                combined_logs[domain] = _chattiest_log_level(
                    combined_logs[domain], level
                )

        return dict(combined_logs)


get_logger = lru_cache(maxsize=256)(logging.getLogger)
"""Get a logger.

getLogger uses a threading.RLock, so we cache the result to avoid
locking the threads every time the integrations page is loaded.
"""
