"""
Base logger configuration class
"""

import sys
import re
from abc import ABC
from datetime import timedelta
from pathlib import Path
from typing import (
    Dict,
    Any,
    Union,
    Optional,
    ClassVar,
    cast,
    TYPE_CHECKING,
    IO,
    Callable,
    Protocol,
    runtime_checkable,
    Type,
    TypedDict,
    NotRequired,
)
from enum import StrEnum
from pydantic import BaseModel, field_validator, Field, ConfigDict

from loguru import logger

if TYPE_CHECKING:
    from ..patera import Patera


@runtime_checkable
# pylint: disable=R0903
class Writable(Protocol):
    def write(self, s: str) -> Any: ...


SinkType = Union[str, Path, IO[str], IO[bytes], Callable[[str], Any]]
SinkInput = Union[str, Path, IO[str], IO[bytes], Callable[[str], Any], Writable]
SinkAccepted = Union[str, Writable, Callable[[str], None]]
RotationType = Union[str, int, timedelta, Callable[[str, Any], Any]]
RetentionType = Union[str, int, timedelta, Callable[[str, Any], Any]]
CompressionType = Optional[str]
FilterType = Union[None, str, Dict[str, str], Callable[[Dict[str, Any]], bool]]


class OutputSink(StrEnum):
    STDERR = "STDERR"
    STDOUT = "STDOUT"


class LogLevel(StrEnum):
    TRACE = "TRACE"
    DEBUG = "DEBUG"
    INFO = "INFO"
    WARNING = "WARNING"
    ERROR = "ERROR"
    CRITICAL = "CRITICAL"


class _LoggerConfigBase(BaseModel):
    """
    Base config model for loggers
    """

    model_config = ConfigDict(extra="allow")

    SINK: Optional[str | Path] = Field(
        OutputSink.STDERR.value, description="Output of the logger"
    )
    LEVEL: Optional[LogLevel] = Field(
        LogLevel.TRACE, description="Log level for logger implementation"
    )
    FORMAT: Optional[str] = Field(
        "<green>{time:HH:mm:ss}</green> | <level>{level}</level> | {extra[logger_name]} | <level>{message}</level>",
        description="Output format",
    )
    ENQUEUE: Optional[bool] = Field(
        False,
        description="Use enqueue or not. For thread, process and coroutine safe logging.",
    )
    BACKTRACE: Optional[bool] = Field(True, description="Use backtrace")
    DIAGNOSE: Optional[bool] = Field(True, description="Diagnose mode")
    COLORIZE: Optional[bool] = Field(True, description="Colorize output")
    DELAY: Optional[bool] = Field(True, description="Use delay")
    ROTATION: Optional[RotationType] = Field(
        "5 MB", description="Size of log file before rotating."
    )
    RETENTION: Optional[RetentionType] = Field(
        "10 days", description="How long log files are kept before automatic erasure."
    )
    COMPRESSION: CompressionType = Field(
        "zip", description="If log files should be compressed. No compression = None"
    )
    SERIALIZE: Optional[bool] = Field(False)
    ENCODING: Optional[str] = Field(
        "utf-8", description="Encoding of logged messages - for file logging."
    )
    MODE: Optional[str] = Field(
        "a", description="If log messages should be appended to the file."
    )

    @field_validator("SINK", mode="before")
    @classmethod
    def normalize_sink(cls, v):
        """Accept case-insensitive strings and convert to canonical form."""
        s = v.strip().upper()
        if s in {"STDERR", "SYS.ERR", "CONSOLE", "ERR"}:
            return "STDERR"
        if s in {"STDOUT", "SYS.OUT", "OUT"}:
            return "STDOUT"
        if s in {"NULL", "DEVNULL", "NONE"}:
            return "NULL"
        # Otherwise treat it as a path
        return Path(v)


class LoggerConfig(TypedDict):
    SINK: NotRequired[str | Path]
    LEVEL: NotRequired[LogLevel]
    FORMAT: NotRequired[str]
    ENQUEUE: NotRequired[bool]
    BACKTRACE: NotRequired[bool]
    DIAGNOSE: NotRequired[bool]
    COLORIZE: NotRequired[bool]
    DELAY: NotRequired[bool]
    ROTATION: NotRequired[RotationType]
    RETENTION: NotRequired[RetentionType]
    COMPRESSION: NotRequired[CompressionType]
    SERIALIZE: NotRequired[bool]
    ENCODING: NotRequired[str]
    MODE: NotRequired[str]


class LoggerBase(ABC):
    """
    Base template for configuring a Loguru sink.
    Subclasses implement get_sink() and optionally override getters below.
    """

    configs_model: ClassVar[Optional[Type[_LoggerConfigBase]]] = cast(
        Type[_LoggerConfigBase], None
    )

    def __init__(self, app: "Patera"):
        self.app: "Patera" = app
        # loads configs for the logger from application configurations
        # by the config class name as upper-case
        # example: CustomLoggerConfig -> CUSTOM_LOGGER_CONFIG
        self.conf: Dict[str, Any] = app.get_conf(self.logger_name, None) or {}  # type: ignore
        self.conf = self.validate_configs(self.conf)

    def validate_configs(self, configs: dict[str, Any]) -> dict[str, Any]:
        if self.configs_model is not None and issubclass(
            self.configs_model, _LoggerConfigBase
        ):
            return self.configs_model.model_validate(configs).model_dump()
        return _LoggerConfigBase.model_validate(configs).model_dump()

    @property
    def logger_name(self) -> str:
        """Returns class name as upper snake case"""
        name = self.__class__.__name__
        return re.sub(r"(?<!^)(?=[A-Z])", "_", name).upper()

    def get_sink(self) -> SinkInput:
        sink = self.conf.get("SINK", "STDERR")
        if sink == "STDERR":
            return sys.stderr
        if sink == "STDOUT":
            return sys.stdout
        if sink == "NULL":
            return "NUL" if sys.platform.startswith("win") else "/dev/null"
        p = Path(sink)
        p.parent.mkdir(parents=True, exist_ok=True)
        return p

    def get_conf_value(self, key: str, default: Any = None) -> Any:
        return self.conf.get(key, default)

    def get_level(self) -> Union[str, int]:
        return self.get_conf_value("LEVEL", LogLevel.INFO)

    def get_format(self) -> str:
        # includes the logger name as a constant-like tag
        return self.get_conf_value(
            "FORMAT",
            "<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | "
            "<level>{level: <8}</level> | {extra[logger_name]} | "
            "{name}:{function}:{line} - <cyan>{message}</cyan>",
        )

    def dict_to_format_string(self, fmt_dict: dict[str, str]) -> str:
        """
        Converts a dictionary of Loguru placeholders into a JSON-like
        format string suitable for loguru.add(format=...).

        CURRENTLY DOESN'T WORK AS INTENDED
        """
        items = []
        for key, value in fmt_dict.items():
            items.append(f'"{key}": "{value}"')
        return "{{{}}}".format(", ".join(items))

    def get_rotation(self) -> Optional[RotationType]:
        return self.get_conf_value("ROTATION", None)

    def get_retention(self) -> Optional[RetentionType]:
        return self.get_conf_value("RETENTION", None)

    def get_compression(self) -> CompressionType:
        return self.get_conf_value("COMPRESSION", None)

    def get_filter(self) -> FilterType:
        return None

    def _wrap_filter_with_logger_name(
        self, original_filter: FilterType
    ) -> Callable[[Dict[str, Any]], bool]:
        def _wrapped(record: Dict[str, Any]) -> bool:
            # Injects the logger name as extra information
            record["extra"].setdefault("logger_name", self.logger_name)

            if original_filter is None:
                return True
            if callable(original_filter):
                return bool(original_filter(record))
            if isinstance(original_filter, str):
                return record.get("name") == original_filter
            if isinstance(original_filter, dict):
                return all(
                    record["extra"].get(k) == v for k, v in original_filter.items()
                )
            return True

        return _wrapped

    def get_enqueue(self) -> bool:
        return self.get_conf_value("ENQUEUE", True)

    def get_backtrace(self) -> bool:
        return self.get_conf_value("BACKTRACE", False)

    def get_diagnose(self) -> bool:
        return self.get_conf_value("DIAGNOSE", False)

    def get_colorize(self) -> Optional[bool]:
        return self.get_conf_value("COLORIZE", None)

    def get_serialize(self) -> bool:
        return self.get_conf_value("SERIALIZE", False)

    def get_encoding(self) -> Optional[str]:
        return self.get_conf_value("ENCODING", "utf-8")

    def get_mode(self) -> Optional[str]:
        return self.get_conf_value("MODE", "a")

    def get_delay(self) -> bool:
        return self.get_conf_value("DELAY", True)

    def remove_existing_handlers(self) -> bool:
        return self.get_conf_value("remove_existing_handlers", False)

    def _build_handler_kwargs(self) -> Dict[str, Any]:
        return {
            "level": self.get_level(),
            "format": self.get_format(),
            "rotation": self.get_rotation(),
            "retention": self.get_retention(),
            "compression": self.get_compression(),
            "filter": self.get_filter(),
            "enqueue": self.get_enqueue(),
            "backtrace": self.get_backtrace(),
            "diagnose": self.get_diagnose(),
            "colorize": self.get_colorize(),
            "serialize": self.get_serialize(),
            "encoding": self.get_encoding(),
            "mode": self.get_mode(),
            "delay": self.get_delay(),
        }

    def _is_path_sink(self, sink: SinkAccepted) -> bool:
        return isinstance(sink, str)

    def _is_stream_sink(self, sink: SinkAccepted) -> bool:
        return (not isinstance(sink, str)) and hasattr(sink, "write")

    def _is_callable_sink(self, sink: SinkAccepted) -> bool:
        return callable(sink) and not hasattr(sink, "write")

    def _normalize_sink(self, sink: SinkInput) -> SinkAccepted:
        if isinstance(sink, Path):
            return sink.as_posix()
        return sink  # type: ignore[return-value]

    def _filter_kwargs_for_sink(
        self, sink: SinkAccepted, kwargs: Dict[str, Any]
    ) -> Dict[str, Any]:
        # Common kwargs accepted by all sinks
        common = {
            "level",
            "format",
            "filter",
            "colorize",
            "serialize",
            "backtrace",
            "diagnose",
            "enqueue",
            "catch",
        }
        # File-path only kwargs (Loguru opens the file for you)
        file_only = {
            "rotation",
            "retention",
            "compression",
            "mode",
            "delay",
            "encoding",
            "buffering",
        }
        # Streams (TextIO / Writable): no rotation/retention/compression/encoding/mode/delay
        stream_only: set = set()  # (no extra)
        # Callables: also no file-only kwargs
        callable_only: set = set()

        if self._is_path_sink(sink):
            allowed = common | file_only
        elif self._is_stream_sink(sink):
            allowed = common | stream_only
        else:  # callable sink
            allowed = common | callable_only

        return {k: v for k, v in kwargs.items() if k in allowed and v is not None}

    def configure(self) -> int:

        raw_sink = self.get_sink()
        sink = self._normalize_sink(raw_sink)

        base_kwargs = self._build_handler_kwargs()
        # Wrap filter to inject extra["logger_name"] for every record to this sink
        base_kwargs["filter"] = self._wrap_filter_with_logger_name(
            base_kwargs.get("filter")
        )

        kwargs = self._filter_kwargs_for_sink(sink, base_kwargs)
        logger_sink_id: int = logger.add(sink, **kwargs)

        return logger_sink_id
