"""
Logging configuration and log-directory setup for daemon entrypoints.

The supported startup flow is explicit:
1. construct ``LoggingManager`` with runtime config
2. optionally register PV handlers via ``create_log_pv_publisher_handler``
3. call ``start()``
"""

import logging
from enum import IntEnum
from logging.handlers import QueueHandler, QueueListener, RotatingFileHandler
from pathlib import Path
from queue import Queue
from typing import Sequence, Union

from .log_pv_publisher import LogPvPublisherHandler
from .pv import PV

logger = logging.getLogger(__name__)

__all__ = [
    "LogLevel",
    "LoggingManager",
]


class LogLevel(IntEnum):
    """EPICS-friendly runtime log levels used by control PV enums."""

    DEBUG = 0
    INFO = 1
    WARNING = 2
    ERROR = 3


class LoggingManager:
    """Runtime logging manager with lifecycle and reconfiguration controls."""

    DEFAULT_THIRD_PARTY_LOG_FLOOR = logging.WARNING
    DEFAULT_NOISY_THIRD_PARTY_LOGGERS: tuple[str, ...] = ("p4p",)
    DEFAULT_FILE_CONSOLE_LOG_FORMAT = (
        "%(asctime)s [%(levelname)s] %(name)s: %(message)s"
    )
    DEFAULT_PV_LOG_FORMAT = "%(levelname).1s:%(message)s"
    DEFAULT_MAX_BYTES = 1024 * 1024 * 1024
    DEFAULT_BACKUP_COUNT = 5
    LOG_LEVEL_BY_ENUM: dict[LogLevel, int] = {
        LogLevel.DEBUG: logging.DEBUG,
        LogLevel.INFO: logging.INFO,
        LogLevel.WARNING: logging.WARNING,
        LogLevel.ERROR: logging.ERROR,
    }

    def __init__(
        self,
        *,
        daemon_log_dir: str | None = None,
        daemon_log_level: Union[str, int] = logging.INFO,
        noisy_third_party_loggers: Sequence[str] | None = None,
        extra_handlers: Sequence[logging.Handler] | None = None,
    ) -> None:
        self.third_party_log_floor = self.DEFAULT_THIRD_PARTY_LOG_FLOOR
        self.default_noisy_third_party_loggers = self.DEFAULT_NOISY_THIRD_PARTY_LOGGERS
        self._listener: QueueListener | None = None
        self._queue_handler: QueueHandler | None = None
        self._managed_handlers: list[logging.Handler] = []
        self._pending_extra_handlers: list[logging.Handler] = []
        self._configured_extra_handlers = list(extra_handlers or [])
        self._noisy_third_party_loggers = tuple(
            noisy_third_party_loggers
            if noisy_third_party_loggers is not None
            else self.default_noisy_third_party_loggers
        )
        self._daemon_log_dir = Path(daemon_log_dir) if daemon_log_dir else None
        self._daemon_log_level = daemon_log_level

    def parse_log_level(self, level_name: str) -> int:
        """
        Return the logging level constant for the given name.

        Args:
            level_name: e.g. 'INFO', 'DEBUG'.

        Returns:
            logging.INFO etc.

        Raises:
            ValueError: If level_name is not a valid logging level.
        """
        normalized = level_name.strip().upper()
        enum_level = LogLevel.__members__.get(normalized)
        if enum_level is not None:
            return self.LOG_LEVEL_BY_ENUM[enum_level]

        level = getattr(logging, normalized, None)
        if not isinstance(level, int):
            raise ValueError(f"Invalid log level: {level_name}")
        return level

    def _apply_noisy_loggers(self) -> None:
        for name in self._noisy_third_party_loggers:
            logging.getLogger(name).setLevel(self.third_party_log_floor)

    def normalize_noisy_third_party_loggers(
        self, logger_names: Sequence[str] | str | None
    ) -> tuple[str, ...]:
        """
        Normalize logger-name inputs from CLI/runtime PV values.

        Accepts either a sequence of names (e.g. waveform payload), a comma-
        separated string, or ``None``.
        """
        if logger_names is None:
            return ()

        if isinstance(logger_names, str):
            items = logger_names.split(",")
        else:
            items = list(logger_names)

        normalized: list[str] = []
        for item in items:
            if not isinstance(item, str):
                continue
            name = item.strip()
            if not name:
                continue
            if name not in normalized:
                normalized.append(name)
        return tuple(normalized)

    def set_noisy_third_party_loggers(self, logger_names: Sequence[str]) -> None:
        """Update noisy third-party loggers and apply clamp immediately."""
        self._noisy_third_party_loggers = self.normalize_noisy_third_party_loggers(
            logger_names
        )
        self._apply_noisy_loggers()

    def set_level(self, log_level: Union[str, int]) -> int:
        """Set root logger level at runtime and return effective integer value."""
        effective_level = self._resolve_runtime_log_level(log_level)
        logging.getLogger().setLevel(effective_level)
        return effective_level

    def _resolve_runtime_log_level(self, log_level: Union[str, int]) -> int:
        """
        Resolve runtime log level from EPICS enum or logging constants.

        String values accept both enum names (DEBUG/INFO/WARNING/ERROR) and
        Python logging names. Integer values accept either EPICS enum values
        (0..3) or Python logging constants (10/20/30/40).
        """
        if isinstance(log_level, str):
            return self.parse_log_level(log_level)
        if isinstance(log_level, LogLevel):
            return self.LOG_LEVEL_BY_ENUM[log_level]
        enum_level = LogLevel._value2member_map_.get(log_level)
        if enum_level is not None:
            return self.LOG_LEVEL_BY_ENUM[enum_level]
        return log_level

    def _ensure_not_started(self) -> None:
        if self._listener is not None:
            raise RuntimeError("LoggingManager is already started")

    def _resolve_log_directory(self) -> Path:
        resolved_log_dir = self._daemon_log_dir
        if resolved_log_dir is None:
            raise ValueError("daemon_log_dir must be configured before start()")
        resolved_log_dir.mkdir(parents=True, exist_ok=True)
        return resolved_log_dir

    def _resolve_effective_log_level(self) -> int:
        resolved_log_level = self._daemon_log_level
        if resolved_log_level is None:
            raise ValueError("daemon_log_level must be configured before start()")
        return (
            self.parse_log_level(resolved_log_level)
            if isinstance(resolved_log_level, str)
            else resolved_log_level
        )

    def _configure_log_levels(self, effective_level: int) -> None:
        self.set_level(effective_level)
        self._apply_noisy_loggers()

    def _build_managed_handlers(self, resolved_log_dir: Path) -> list[logging.Handler]:
        log_file = resolved_log_dir / "daemon.log"
        log_formatter = logging.Formatter(self.DEFAULT_FILE_CONSOLE_LOG_FORMAT)
        file_handler = RotatingFileHandler(
            log_file,
            maxBytes=self.DEFAULT_MAX_BYTES,
            backupCount=self.DEFAULT_BACKUP_COUNT,
        )
        file_handler.setFormatter(log_formatter)
        console_handler = logging.StreamHandler()
        console_handler.setFormatter(log_formatter)
        managed_handlers: list[logging.Handler] = [file_handler, console_handler]

        all_extra_handlers: list[logging.Handler] = list(
            self._configured_extra_handlers
        )
        all_extra_handlers.extend(self._pending_extra_handlers)
        if all_extra_handlers:
            managed_handlers.extend(all_extra_handlers)
        return managed_handlers

    def _install_queue_handler(self, log_queue: Queue) -> QueueHandler:
        queue_handler = QueueHandler(log_queue)
        root_logger = logging.getLogger()
        if root_logger.hasHandlers():
            root_logger.handlers.clear()
        root_logger.addHandler(queue_handler)
        return queue_handler

    def _start_listener(
        self, log_queue: Queue, managed_handlers: list[logging.Handler]
    ) -> QueueListener:
        listener = QueueListener(
            log_queue, *managed_handlers, respect_handler_level=True
        )
        listener.start()
        return listener

    def _store_start_state(
        self,
        listener: QueueListener,
        queue_handler: QueueHandler,
        managed_handlers: list[logging.Handler],
    ) -> None:
        self._managed_handlers = managed_handlers
        self._queue_handler = queue_handler
        self._listener = listener
        self._pending_extra_handlers = []

    def start(self) -> QueueListener:
        """
        Configure RotatingFileHandler and StreamHandler via QueueListener.

        Replaces root logger handlers with a QueueHandler; the listener runs file
        and console handlers in a background thread.

        Third-party loggers listed in ``noisy_third_party_loggers`` are clamped
        to WARNING regardless of the requested log_level so their internal
        DEBUG/INFO output does not flood daemon logs. By default, only ``p4p``
        is clamped.

        Returns:
            Started QueueListener. Caller must call listener.stop() in a finally
            block (or at shutdown).
        """
        self._ensure_not_started()
        resolved_log_dir = self._resolve_log_directory()
        effective_level = self._resolve_effective_log_level()
        self._configure_log_levels(effective_level)
        managed_handlers = self._build_managed_handlers(resolved_log_dir)
        log_queue = Queue(-1)
        queue_handler = self._install_queue_handler(log_queue)
        listener = self._start_listener(log_queue, managed_handlers)
        self._store_start_state(listener, queue_handler, managed_handlers)
        return listener

    def create_log_pv_publisher_handler(
        self,
        warning_pv: PV,
        error_pv: PV,
        *,
        buffer_size: int = 100,
    ) -> LogPvPublisherHandler:
        """Create a warning/error EPICS log publisher handler."""
        pv_formatter = logging.Formatter(self.DEFAULT_PV_LOG_FORMAT)
        pv_handler = LogPvPublisherHandler(
            warning_pv=warning_pv,
            error_pv=error_pv,
            buffer_size=buffer_size,
        )
        pv_handler.setFormatter(pv_formatter)
        self._pending_extra_handlers.append(pv_handler)
        return pv_handler

    def stop(self) -> None:
        """Stop active listener and close owned handlers."""
        if self._listener is not None:
            self._listener.stop()
        for handler in self._managed_handlers:
            try:
                handler.close()
            except Exception:
                continue
        root_logger = logging.getLogger()
        if (
            self._queue_handler is not None
            and self._queue_handler in root_logger.handlers
        ):
            root_logger.removeHandler(self._queue_handler)
        self._listener = None
        self._queue_handler = None
        self._managed_handlers = []
