"""
**File:** ``config.py``
**Region:** ``ds_common_logger_py_lib``

Description
-----------
Defines the ``LoggerConfig`` class for application-level logger configuration.
"""

from __future__ import annotations

import logging
import sys
from collections.abc import Callable
from typing import ClassVar

from .formatter import ExtraFieldsFormatter

DEFAULT_FORMAT = "[%(asctime)s][%(name)s][%(levelname)s][%(filename)s:%(lineno)d]: %(message)s"
DEFAULT_FORMAT_WITH_PREFIX = "[%(asctime)s][{prefix}][%(name)s][%(levelname)s][%(filename)s:%(lineno)d]: %(message)s"
DEFAULT_DATE_FORMAT = "%Y-%m-%dT%H:%M:%S"


class LoggerFilter(logging.Filter):
    """
    Filter to only allow logs from specified logger name prefixes.

    Simple whitelist approach: if a logger name starts with (or equals) any
    allowed prefix, it's allowed. Everything else is filtered out.

    Loggers created via Logger.get_logger() or LoggingMixin are automatically
    allowed, regardless of allowed_prefixes.
    """

    def __init__(self, allowed_prefixes: set[str] | None = None) -> None:
        """
        Initialize the filter.

        Args:
            allowed_prefixes: Set of logger name prefixes to allow.
                            If None or empty set, only library-created loggers are allowed.
                            Otherwise, library loggers plus logs matching the prefixes are allowed.
        """
        super().__init__()
        self.allowed_prefixes = allowed_prefixes

    def filter(self, record: logging.LogRecord) -> bool:
        """
        Filter log records - return True to allow, False to exclude.

        Loggers created via Logger.get_logger() or LoggingMixin are automatically allowed.
        If allowed_prefixes is None or empty, only library-created loggers are allowed.
        Otherwise, library loggers plus logs whose name starts with (or equals) an allowed prefix are allowed.

        Args:
            record: The log record to filter.

        Returns:
            True if the log should be shown, False if it should be excluded.
        """
        logger_name = record.name

        if logger_name in LoggerConfig._library_loggers:
            return True

        if self.allowed_prefixes is None or not self.allowed_prefixes:
            return False

        return any(logger_name.startswith(prefix) or logger_name == prefix for prefix in self.allowed_prefixes)


class LoggerConfig:
    """
    Application-level logger configuration manager.

    This class allows applications to configure logging at startup with their own
    prefix, format, and handlers. Configuration set via LoggerConfig takes precedence
    over package-level Logger() calls.

    Example:
        >>> from ds_common_logger_py_lib import LoggerConfig, Logger
        >>> import logging
        >>>
        >>> LoggerConfig.configure(
        ...     prefix="MyApp",
        ...     format_string="[%(asctime)s][{prefix}][%(name)s][%(levelname)s]: %(message)s",
        ...     level=logging.INFO
        ... )
        >>> logger = Logger.get_logger(__name__)
        >>> logger.info("Application started")
        [2024-01-15T10:30:45][MyApp][__main__][INFO]: Application started
        >>>
        >>> LoggerConfig.set_prefix("MyApp-session123")
        >>> logger.info("Session initialized")
        [2024-01-15T10:30:46][MyApp-session123][__main__][INFO]: Session initialized
    """

    _configured: bool = False
    _prefix: str = ""
    _format_string: str = DEFAULT_FORMAT_WITH_PREFIX
    _date_format: str = DEFAULT_DATE_FORMAT
    _level: int = logging.INFO
    _handlers: ClassVar[list[logging.Handler]] = []
    _default_handler: logging.Handler | None = None
    _filter: LoggerFilter = LoggerFilter()
    _original_add_handler: ClassVar[Callable[[logging.Logger, logging.Handler], None] | None] = None
    _library_loggers: ClassVar[set[str]] = set()

    @classmethod
    def configure(
        cls,
        prefix: str = "",
        format_string: str = DEFAULT_FORMAT_WITH_PREFIX,
        date_format: str = DEFAULT_DATE_FORMAT,
        level: int = logging.INFO,
        handlers: list[logging.Handler] | None = None,
        default_handler: logging.Handler | None = None,
        allowed_prefixes: set[str] | None = None,
        force: bool = False,
    ) -> None:
        """
        Configure application-level logging settings.

        This should be called once at application startup, before any packages
        start using the logger. The configuration will be applied to all loggers
        created via Logger.get_logger().

        Args:
            prefix: Prefix to inject into log messages (via {prefix} in format).
                   Can be updated later with set_prefix().
            format_string: Format string for log messages. Uses {prefix} to include the prefix.
                          Uses DEFAULT_FORMAT_WITH_PREFIX by default.
            date_format: Date format string. Uses DEFAULT_DATE_FORMAT by default.
            level: Default logging level.
            handlers: List of handlers to add to all loggers. If None, uses default StreamHandler.
            default_handler: Single default handler to use for all loggers. If provided,
                           this replaces the default StreamHandler.
            allowed_prefixes: Set of logger name prefixes to allow in addition to
                            library-created loggers. Default is None, which means only
                            loggers created via Logger.get_logger() or LoggingMixin are allowed.
                            To include third-party library logs, add their prefixes:
                            {"sqlalchemy", "boto3"} to see SQLAlchemy and boto3 logs.
            force: If True, force reconfiguration even if already configured.

        Example:
            >>> from ds_common_logger_py_lib import LoggerConfig, Logger
            >>> import logging
            >>> LoggerConfig.configure(
            ...     prefix="MyService",
            ...     format_string="[%(asctime)s][{prefix}][%(name)s]: %(message)s",
            ...     level=logging.DEBUG
            ... )
            >>> logger = Logger.get_logger(__name__)
            >>> logger.info("Service started")
            [2024-01-15T10:30:45][MyService][__main__]: Service started
        """
        if cls._configured and not force:
            return

        was_configured = cls._configured
        cls._configured = True

        if not was_configured or prefix:
            cls._prefix = prefix

        if format_string is not None:
            cls._format_string = format_string
        if date_format is not None:
            cls._date_format = date_format

        cls._level = level
        cls._filter = LoggerFilter(allowed_prefixes=allowed_prefixes)

        if default_handler is not None:
            cls._default_handler = default_handler
        elif handlers is not None:
            cls._handlers = list(handlers)
            cls._default_handler = None
        else:
            cls._default_handler = logging.StreamHandler(sys.stdout)
            cls._default_handler.setLevel(level)
            cls._handlers = []

        cls._setup_filter()

        root_logger = logging.getLogger()
        root_logger.setLevel(level)

        if force:
            root_logger.handlers.clear()

        if cls._default_handler:
            root_logger.addHandler(cls._default_handler)
        else:
            for handler in cls._handlers:
                root_logger.addHandler(handler)

        cls._update_existing_loggers()

    @classmethod
    def set_prefix(cls, prefix: str) -> None:
        """
        Update the prefix at runtime.

        This allows you to change the prefix dynamically, for example when a
        session starts or when context changes. The new prefix will be applied
        to all existing and future loggers.

        If LoggerConfig hasn't been configured yet, this will automatically
        configure it with default settings that include {prefix} in the format
        (using the provided prefix).

        Args:
            prefix: New prefix value to use in log messages.

        Example:
            >>> from ds_common_logger_py_lib import LoggerConfig, Logger
            >>> import logging
            >>> LoggerConfig.set_prefix("MyApp")
            >>> logger = Logger.get_logger(__name__)
            >>> logger.info("Log with MyApp prefix")
            [2024-01-15T10:30:45][MyApp][__main__][INFO][config.py:158]: Log with MyApp prefix
            >>>
            >>> session_id = "session_12345"
            >>> LoggerConfig.set_prefix(f"[{session_id}]")
            >>> logger.info("Log with session prefix")
            [2024-01-15T10:30:46][session_12345][__main__][INFO][config.py:162]: Log with session prefix
        """
        if not cls._configured:
            cls.configure(prefix=prefix, format_string=DEFAULT_FORMAT_WITH_PREFIX)

        cls._prefix = prefix
        cls._update_existing_loggers()

    @classmethod
    def add_handler(cls, handler: logging.Handler) -> None:
        """
        Add a handler to all existing and future loggers.

        Args:
            handler: Handler to add.

        Example:
            >>> from ds_common_logger_py_lib import LoggerConfig, Logger
            >>> import logging
            >>> LoggerConfig.configure()
            >>> file_handler = logging.FileHandler("app.log")
            >>> LoggerConfig.add_handler(file_handler)
            >>> logger = Logger.get_logger(__name__)
            >>> len(logger.handlers) > 1
            True
        """
        if not cls._configured:
            raise RuntimeError("LoggerConfig must be configured before adding handlers")

        handler.addFilter(cls._filter)

        cls._handlers.append(handler)
        root_logger = logging.getLogger()
        root_logger.addHandler(handler)

        for logger_name in logging.Logger.manager.loggerDict:
            logger = logging.getLogger(logger_name)
            if logger is not root_logger:
                logger.addHandler(handler)

    @classmethod
    def remove_handler(cls, handler: logging.Handler) -> None:
        """
        Remove a handler from all loggers.

        Args:
            handler: Handler to remove.

        Example:
            >>> from ds_common_logger_py_lib import LoggerConfig, Logger
            >>> import logging
            >>> LoggerConfig.configure()
            >>> file_handler = logging.FileHandler("app.log")
            >>> LoggerConfig.add_handler(file_handler)
            >>> logger = Logger.get_logger(__name__)
            >>> file_handler in logger.handlers
            True
            >>> LoggerConfig.remove_handler(file_handler)
            >>> file_handler in logger.handlers
            False
        """
        if not cls._configured:
            return

        if handler in cls._handlers:
            cls._handlers.remove(handler)

        root_logger = logging.getLogger()
        if handler in root_logger.handlers:
            root_logger.removeHandler(handler)

        for logger_name in logging.Logger.manager.loggerDict:
            logger = logging.getLogger(logger_name)
            if handler in logger.handlers:
                logger.removeHandler(handler)

    @classmethod
    def set_default_handler(cls, handler: logging.Handler) -> None:
        """
        Set the default handler for all loggers, replacing the current default.

        Args:
            handler: Handler to use as default.

        Example:
            >>> from ds_common_logger_py_lib import LoggerConfig, Logger
            >>> import logging
            >>> import sys
            >>> LoggerConfig.configure()
            >>> custom_handler = logging.StreamHandler(sys.stderr)
            >>> LoggerConfig.set_default_handler(custom_handler)
            >>> logger = Logger.get_logger(__name__)
            >>> custom_handler in logger.handlers
            True
        """
        if not cls._configured:
            raise RuntimeError("LoggerConfig must be configured before setting default handler")

        if cls._default_handler:
            cls.remove_handler(cls._default_handler)

        cls._default_handler = handler
        handler.setLevel(cls._level)

        handler.addFilter(cls._filter)

        formatter = cls._create_formatter()
        handler.setFormatter(formatter)

        root_logger = logging.getLogger()
        root_logger.addHandler(handler)

        for logger_name in logging.Logger.manager.loggerDict:
            logger = logging.getLogger(logger_name)
            if logger is not root_logger and not logger.handlers:
                logger.addHandler(handler)

    @classmethod
    def _setup_filter(cls) -> None:
        """Apply filter to existing handlers and patch Logger.addHandler to auto-apply filter to new handlers."""
        if cls._default_handler:
            cls._default_handler.addFilter(cls._filter)
        for handler in cls._handlers:
            handler.addFilter(cls._filter)

        if cls._original_add_handler is None:
            cls._original_add_handler = logging.Logger.addHandler

            def add_handler_with_filter(self: logging.Logger, hdlr: logging.Handler) -> None:
                """Wrapper that applies filter to handlers when they're added."""
                hdlr.filters = [f for f in hdlr.filters if not isinstance(f, LoggerFilter)]
                hdlr.addFilter(cls._filter)
                if cls._original_add_handler:
                    cls._original_add_handler(self, hdlr)

            logging.Logger.addHandler = add_handler_with_filter  # type: ignore[method-assign]

    @classmethod
    def _create_formatter(cls) -> ExtraFieldsFormatter:
        """Create a formatter with current configuration.

        Returns:
            ExtraFieldsFormatter instance with current configuration.
        """
        format_string = cls._format_string or DEFAULT_FORMAT
        date_format = cls._date_format or DEFAULT_DATE_FORMAT

        template_vars: dict[str, str] = {"prefix": cls._prefix}

        return ExtraFieldsFormatter(
            fmt=format_string,
            datefmt=date_format,
            template_vars=template_vars,
        )

    @classmethod
    def _update_existing_loggers(cls) -> None:
        """Update all existing loggers with current configuration."""
        formatter = cls._create_formatter()

        for logger_name in logging.Logger.manager.loggerDict:
            logger = logging.getLogger(logger_name)
            logger.setLevel(cls._level)

            for handler in logger.handlers:
                handler.setFormatter(formatter)
                handler.setLevel(cls._level)
                handler.filters = [f for f in handler.filters if not isinstance(f, LoggerFilter)]
                handler.addFilter(cls._filter)

    @classmethod
    def is_configured(cls) -> bool:
        """Check if LoggerConfig has been configured.

        Returns:
            True if LoggerConfig has been configured, False otherwise.
        """
        return cls._configured

    @classmethod
    def register_library_logger(cls, logger_name: str) -> None:
        """
        Register a logger name as being created by the library.

        This ensures that loggers created via Logger.get_logger() or LoggingMixin
        are automatically allowed by the filter, regardless of allowed_prefixes.

        Args:
            logger_name: The name of the logger to register.
        """
        cls._library_loggers.add(logger_name)

    @classmethod
    def get_prefix(cls) -> str:
        """Get the configured prefix.

        Returns:
            The configured prefix.
        """
        return cls._prefix

    @classmethod
    def get_format_string(cls) -> str | None:
        """Get the configured format string.

        Returns:
            The configured format string.
        """
        return cls._format_string

    @classmethod
    def get_date_format(cls) -> str | None:
        """Get the configured date format.

        Returns:
            The configured date format.
        """
        return cls._date_format
