"""
Absfuyu: Logger
---------------
Custom Logger Module

Version: 5.17.0
Date updated: 20/11/2025 (dd/mm/yyyy)

Usage:
------
>>> from absfuyu.logger import logger, LogLevel
>>> logger.setLevel(LogLevel.DEBUG)
>>> logger.debug("This logs!")
"""

# Module level
# ---------------------------------------------------------------------------
__all__ = [
    # logger
    "logger",
    "compress_for_log",
    # log level
    "LogLevel",
]


# Library
# ---------------------------------------------------------------------------
import logging
import math
from logging.handlers import RotatingFileHandler as _RFH
from logging.handlers import TimedRotatingFileHandler as _TRFH
from pathlib import Path
from typing import Any


# Setup
# ---------------------------------------------------------------------------
class LogLevel:
    """
    ``logging``'s log level wrapper + custom log level
    """

    TRACE: int = logging.DEBUG - 5
    DEBUG: int = logging.DEBUG
    INFO: int = logging.INFO
    WARNING: int = logging.WARNING
    ERROR: int = logging.ERROR
    CRITICAL: int = logging.CRITICAL
    EXTREME: int = logging.CRITICAL + 10


class _LogFormat:
    """Some log format styles"""

    FULL = "[%(asctime)s] [%(process)-d] [%(module)s] [%(name)s] [%(funcName)s] [%(levelname)-s] %(message)s"  # Time|ProcessID|Module|Name|Function|LogType|Message
    SHORT = (
        "[%(module)s] [%(name)s] [%(funcName)s] [%(levelname)-s] %(message)s"  # Module|Name|Function|LogType|Message
    )
    CONSOLE = (
        "%(asctime)s [%(levelname)5s] %(funcName)s:%(lineno)3d: %(message)s"  # Time|LogType|Function|LineNumber|Message
    )
    FILE = "%(asctime)s [%(levelname)5s] %(filename)s:%(funcName)s:%(lineno)3d: %(message)s"  # Time|LogType|FileName|Function|LineNumber|Message


# Create a custom logger
logger = logging.getLogger(__name__)
logger.setLevel(logging.WARNING)

# Create handlers
## Console log handler
console_handler = logging.StreamHandler()
console_handler.setLevel(LogLevel.TRACE)  # Minimum log level
console_handler.setFormatter(logging.Formatter(_LogFormat.CONSOLE, datefmt="%Y-%m-%d %H:%M:%S"))
# logger.addHandler(console_handler)
logger.addHandler(logging.NullHandler())


# Functions
# ---------------------------------------------------------------------------
def _compress_list_for_print(iterable: list, max_visible: int | None = 5) -> str:
    """
    Compress the list to be more log-readable

    iterable: list
    max_visible: Maximum items can be printed on screen (Minimum: 3)
    """

    if max_visible is None or max_visible <= 2:
        max_visible = 5

    if len(iterable) <= max_visible:
        return str(iterable)
    else:
        # logger.debug(f"Max vis: {max_visible}")
        if max_visible % 2 == 0:
            cut_idx_1 = math.floor(max_visible / 2) - 1
            cut_idx_2 = math.floor(max_visible / 2)
        else:
            cut_idx_1 = cut_idx_2 = math.floor(max_visible / 2)

        # logger.debug(f"Cut pos: {(cut_idx_1, cut_idx_2)}")
        # temp = [iterable[:cut_idx_1], ["..."], iterable[len(iterable)-cut_idx_2:]]
        # out = list(chain.from_iterable(temp))
        # out = [*iterable[:cut_idx_1], "...", *iterable[len(iterable)-cut_idx_2:]] # Version 2
        out = f"{str(iterable[:cut_idx_1])[:-1]}, ..., {str(iterable[len(iterable) - cut_idx_2 :])[1:]}"  # Version 3
        # logger.debug(out)
        return f"{out} [Len: {len(iterable)}]"


def _compress_string_for_print(text: str, max_visible: int | None = 120) -> str:
    """
    Compress the string to be more log-readable

    text: str
    max_visible: Maximum text can be printed on screen (Minimum: 5)
    """

    if max_visible is None or max_visible <= 5:
        max_visible = 120

    text = text.replace("\n", " ")  # Remove new line
    # logger.debug(text)

    if len(text) <= max_visible:
        return str(text)
    else:
        cut_idx = math.floor((max_visible - 3) / 2)
        temp = f"{text[:cut_idx]}...{text[len(text) - cut_idx :]}"
        return f"{temp} [Len: {len(text)}]"


def compress_for_log(object_: Any, max_visible: int | None = None) -> str:
    """
    Compress the object to be more log-readable

    :param object_: Object
    :param max_visible: Maximum objects can be printed on screen
    :returns: Compressed log output
    :rtype: str
    """

    if isinstance(object_, list):
        return _compress_list_for_print(object_, max_visible)

    elif isinstance(object_, (set, tuple)):
        return _compress_list_for_print(list(object_), max_visible)

    elif isinstance(object_, dict):
        temp = [{k: v} for k, v in object_.items()]
        return _compress_list_for_print(temp, max_visible)

    elif isinstance(object_, str):
        return _compress_string_for_print(object_, max_visible)

    else:
        try:
            return _compress_string_for_print(str(object_), max_visible)
        except Exception:
            return object_  # type: ignore


# Class
# ---------------------------------------------------------------------------
class _CustomLogger:
    """
    Custom logger [W.I.P]

    Create a custom logger
    *Useable but maybe unstable*
    """

    def __init__(
        self,
        name: str,
        cwd: str | Path = ".",
        log_format: str | None = None,
        *,
        save_log_file: bool = False,
        separated_error_file: bool = False,
        timed_log: bool = False,
        date_log_format: str | None = None,
        error_log_size: int = 1_000_000,  # 1 MB
    ) -> None:
        """
        :param name: Custom logger name
        :param cwd: Current working directory
        :param log_format: Log format
        :param save_log_file: Save logs to log file (default: False)
        :param separated_error_file: Save error logs into a separated file (Default: False)
        :param timed_log: Split log file every day. Requirement: `save_log_file = True` (Default: False)
        :param date_log_format: Date format in log
        :param error_log_size: Error log file max size (Default: 1 MB)
        """
        self._cwd = Path(cwd)
        self.log_folder = self._cwd.joinpath("logs")
        self.log_folder.mkdir(exist_ok=True, parents=True)  # Does not throw exception when folder existed
        self.name = name
        self.log_file = self.log_folder.joinpath(f"{name}.log")

        # Create a custom logger
        try:
            self.logger = logging.getLogger(self.name)
        except Exception:
            try:
                self.logger = logging.getLogger(__name__)
            except Exception:
                self.logger = logging.getLogger()
        self.logger.setLevel(logging.DEBUG)

        if date_log_format is None:
            _date_format = "%Y-%m-%d %H:%M:%S"
        else:
            _date_format = date_log_format

        ## Console log handler
        if log_format is None:
            # Time|LogType|Function|LineNumber|Message
            _log_format = "%(asctime)s [%(levelname)5s] %(funcName)s:%(lineno)3d: %(message)s"
        else:
            _log_format = log_format
        console_handler = logging.StreamHandler()
        console_handler.setLevel(logging.DEBUG)  # Minimum log level
        _console_log_format = _log_format  # Create formatters and add it to handlers
        _console_formatter = logging.Formatter(_console_log_format, datefmt=_date_format)
        console_handler.setFormatter(_console_formatter)
        self._console_handler = console_handler
        self.logger.addHandler(self._console_handler)  # Add handlers to the logger

        ## Log file handler
        if save_log_file:
            if log_format is None:
                # Time|LogType|FileName|Function|LineNumber|Message
                _log_format = "%(asctime)s [%(levelname)5s] %(filename)s:%(funcName)s:%(lineno)3d: %(message)s"
            else:
                _log_format = log_format
            file_handler = logging.FileHandler(self.log_file, mode="a", encoding="utf-8")
            file_handler.setLevel(logging.DEBUG)
            _file_log_format = _log_format
            _file_formatter = logging.Formatter(_file_log_format, datefmt=_date_format)
            file_handler.setFormatter(_file_formatter)
            self._file_handler = file_handler
            self.logger.addHandler(self._file_handler)

            if timed_log:
                ## Time handler (split log every day)
                time_handler = _TRFH(
                    self.log_folder.joinpath(f"{self.name}_timed.log"),
                    when="midnight",
                    interval=1,
                    encoding="utf-8",
                )
                time_handler.setLevel(logging.DEBUG)
                time_handler.setFormatter(_file_formatter)
                self._time_handler = time_handler
                self.logger.addHandler(self._time_handler)
                # |   Value  |    Type of interval   |
                # |:--------:|:---------------------:|
                # |     S    |        Seconds        |
                # |     M    |        Minutes        |
                # |     H    |         Hours         |
                # |     D    |          Days         |
                # |     W    |  Week day (0=Monday)  |
                # | midnight | Roll over at midnight |

        ## Error and above log handler
        if separated_error_file:
            if log_format is None:
                # Time|LogType|FileName|Function|LineNumber|Message
                _log_format = "%(asctime)s [%(levelname)5s] %(filename)s:%(funcName)s:%(lineno)3d: %(message)s"
            else:
                _log_format = log_format
            error_handler = _RFH(
                self.log_folder.joinpath(f"{self.name}_error.log"),
                maxBytes=error_log_size,
                backupCount=1,
                encoding="utf-8",
            )
            error_handler.setLevel(logging.ERROR)
            error_handler.setFormatter(_log_format)  # type: ignore
            self._error_handler = error_handler
            self.logger.addHandler(self._error_handler)

    def __str__(self) -> str:
        return f"{self.__class__.__name__}({self.name})"

    def __repr__(self) -> str:
        return self.__str__()

    @staticmethod
    def _add_logging_level(level_name: str, level_num: int, method_name: str | None = None):
        """
        Comprehensively adds a new logging level to the `logging` module and the
        currently configured logging class.

        `level_name` becomes an attribute of the `logging` module with the value
        `level_num`. `method_name` becomes a convenience method for both `logging`
        itself and the class returned by `logging.getLoggerClass()` (usually just
        `logging.Logger`). If `method_name` is not specified, `level_name.lower()` is
        used.

        To avoid accidental clobberings of existing attributes, this method will
        raise an `AttributeError` if the level name is already an attribute of the
        `logging` module or if the method name is already present
        """
        # Original code: https://stackoverflow.com/a/35804945/1691778
        if not method_name:
            method_name = level_name.lower()

        if hasattr(logging, level_name):
            raise AttributeError(f"{level_name} already defined in logging module")
        if hasattr(logging, method_name):
            raise AttributeError(f"{method_name} already defined in logging module")
        if hasattr(logging.getLoggerClass(), method_name):
            raise AttributeError(f"{method_name} already defined in logger class")

        # This method was inspired by the answers to Stack Overflow post
        # http://stackoverflow.com/q/2183233/2988730, especially
        # http://stackoverflow.com/a/13638084/2988730
        def logForLevel(self, message, *args, **kwargs):
            if self.isEnabledFor(level_num):
                self._log(level_num, message, args, **kwargs)

        def logToRoot(message, *args, **kwargs):
            logging.log(level_num, message, *args, **kwargs)

        logging.addLevelName(level_num, level_name)
        setattr(logging, level_name, level_num)
        setattr(logging.getLoggerClass(), method_name, logForLevel)
        setattr(logging, method_name, logToRoot)

    def add_log_level(self, level_name: str, level_num: int):
        __class__._add_logging_level(level_name, level_num)  # type: ignore
        if level_num < logging.DEBUG:
            self._console_handler.setLevel(level_num)
            self.logger.setLevel(level_num)
