# -*- coding: utf-8 -*-
"""
Notes and logger decorator to be used on class
This will add a "notes" object to the class and will allow
logging feature at the same time.
Goal is to be able to access quickly to important informations for
the web interface.
"""
import inspect
import logging
import os
import sys
import typing as t

# --- standard Python modules ---
from collections import namedtuple
from datetime import datetime
from logging import FileHandler, Logger
from os.path import expanduser, join

# --- 3rd party modules ---
from ...core.utils.lookfordependency import rich_if_available, pandas_if_available

RICH, rich = rich_if_available()
if RICH:
    from rich.logging import RichHandler

_PANDAS, pd, _, _ = pandas_if_available()


class LogList:
    LOGGERS: t.List[Logger] = []


def convert_level(level):
    if not level:
        return None
    _valid_levels = [
        logging.DEBUG,
        logging.INFO,
        logging.WARNING,
        logging.ERROR,
        logging.CRITICAL,
    ]
    if level in _valid_levels:
        return level
    if level.lower() == "info":
        return logging.INFO
    elif level.lower() == "debug":
        return logging.DEBUG
    elif level.lower() == "warning":
        return logging.WARNING
    elif level.lower() == "error":
        return logging.ERROR
    elif level.lower() == "critical":
        return logging.CRITICAL
    raise ValueError(f"Wrong log level use one of the following : {_valid_levels}")


def update_log_level(
    level=None, *, log_file=None, stderr=None, stdout=None, log_this=True
):
    """
    Typical usage ::
        # Silence (use CRITICAL so not much messages will be sent)
        BAC0.log_level('silence')
        # Verbose
        BAC0.log_level('info')
        # Default, Info on console....but Warning in file
        BAC0.log_level(file='warning', stdout='info', stderr='critical')
        # Debug in file and console... this is a bad idea as the console will be filled
        BAC0.log_level(file='debug', stdout='debug', stderr='critical')

        # Preferably, debug in the file, keep console limited to info
        BAC0.log_level('debug')
        # OR
        BAC0.log_level(file='debug', stdout='info', stderr='critical')


    Giving only one parameter will set file and console to the same level.
    I tend to keep stderr CRITICAL

    """
    update_log_file_lvl = False
    update_stderr_lvl = False
    update_stdout_lvl = False

    if level:
        logging.getLogger("BAC0_Root.BAC0.scripts.Base.Base").disabled = False
        if level.lower() == "silence":
            log_file_lvl = logging.CRITICAL
            stderr_lvl = logging.CRITICAL
            stdout_lvl = logging.CRITICAL
            update_log_file_lvl = True
            update_stderr_lvl = True
            update_stdout_lvl = True
            logging.getLogger("BAC0_Root.BAC0.scripts.Base.Base").disabled = True
        elif level.lower() == "default":
            log_file_lvl = logging.WARNING
            stderr_lvl = logging.CRITICAL
            stdout_lvl = logging.INFO
            update_log_file_lvl = True
            update_stderr_lvl = True
            update_stdout_lvl = True
        elif level.lower() == "debug":
            log_file_lvl = logging.DEBUG
            stdout_lvl = logging.INFO
            update_log_file_lvl = True
            update_stdout_lvl = True
        else:
            level = convert_level(level)
            log_file_lvl = level
            stdout_lvl = level
            update_log_file_lvl = True
            update_stdout_lvl = True

    else:
        if log_file:
            log_file_lvl = convert_level(log_file)
            update_log_file_lvl = True
        if stderr:
            stderr_lvl = convert_level(stderr)
            update_stderr_lvl = True
        if stdout:
            stdout_lvl = convert_level(stdout)
            update_stdout_lvl = True

    # Choose Base as logger for this task
    if log_this:
        BAC0_logger = logging.getLogger("BAC0_Root.BAC0.scripts.Base.Base")

    for each in LogList.LOGGERS:
        for handler in each.handlers:
            if update_log_file_lvl and handler.get_name() == "file_handler":
                handler.setLevel(log_file_lvl)
                if log_this:
                    BAC0_logger.warning(
                        "Changed log level of file to {}".format(
                            logging.getLevelName(log_file_lvl)
                        )
                    )
            elif update_stdout_lvl and handler.get_name() == "stdout":
                handler.setLevel(stdout_lvl)
                if log_this:
                    BAC0_logger.warning(
                        "Changed log level of console stdout to {}".format(
                            logging.getLevelName(stdout_lvl)
                        )
                    )
            elif update_stderr_lvl and handler.get_name() == "stderr":
                handler.setLevel(stderr_lvl)
                if log_this:
                    BAC0_logger.warning(
                        "Changed log level of console stderr to {}".format(
                            logging.getLevelName(stderr_lvl)
                        )
                    )


def note_and_log(cls):
    """
    This will be used as a decorator on class to activate
    logging and store messages in the variable cls._notes
    This will allow quick access to events in the web app.
    A note can be added to cls._notes without logging if passing
    the argument log=false to function note()
    Something can be logged without addind a note using function log()
    """
    if hasattr(cls, "DEBUG_LEVEL"):
        if cls.DEBUG_LEVEL == "debug":
            file_level = logging.DEBUG
            console_level = logging.DEBUG
        elif cls.DEBUG_LEVEL == "info":
            file_level = logging.INFO
            console_level = logging.INFO
    else:
        file_level = logging.WARNING
        console_level = logging.INFO
    # Notes object
    cls._notes = namedtuple("_notes", ["timestamp", "notes"])
    cls._notes.timestamp = []
    cls._notes.notes = []

    # Defining log object
    cls.logname = f"{cls.__module__} | {cls.__name__}"
    cls._log = logging.getLogger(f"BAC0_Root.{cls.__module__}.{cls.__name__}")

    # Set level to debug so filter is done by handler
    cls._log.setLevel(logging.DEBUG)

    # Console Handler
    if RICH:
        ch = RichHandler()
        ch2 = RichHandler()
    else:
        ch = logging.StreamHandler(sys.stderr)
        ch2 = logging.StreamHandler(sys.stdout)

    ch.set_name("stderr")
    ch.setLevel(logging.CRITICAL)
    ch2.set_name("stdout")
    ch2.setLevel(console_level)

    formatter = logging.Formatter("{asctime} - {levelname:<8}| {message}", style="{")

    # File Handler
    _PERMISSION_TO_WRITE = True
    logUserPath = expanduser("~")
    logSaveFilePath = join(logUserPath, ".BAC0")

    logFile = join(logSaveFilePath, "BAC0.log")
    try:
        if not os.path.exists(logSaveFilePath):
            os.makedirs(logSaveFilePath)
        fh = FileHandler(logFile)
        fh.set_name("file_handler")
        fh.setLevel(file_level)
        fh.setFormatter(formatter)

    except OSError:
        _PERMISSION_TO_WRITE = False

    ch.setFormatter(formatter)
    ch2.setFormatter(formatter)
    # Add handlers the first time only...
    if not len(cls._log.handlers):
        if _PERMISSION_TO_WRITE:
            cls._log.addHandler(fh)
        cls._log.addHandler(ch)
        cls._log.addHandler(ch2)

    LogList.LOGGERS.append(cls._log)

    def log_title(self, title, args=None, width=35):
        cls._log.debug("")
        cls._log.debug("#" * width)
        cls._log.debug(f"# {title}")
        cls._log.debug("#" * width)
        if args:
            cls._log.debug(f"{args!r}")
            cls._log.debug("#" * 35)

    def log_subtitle(self, subtitle, args=None, width=35):
        cls._log.debug("")
        cls._log.debug("=" * width)
        cls._log.debug(f"{subtitle}")
        cls._log.debug("=" * width)
        if args:
            cls._log.debug(f"{args!r}")
            cls._log.debug("=" * width)

    def log(self, note, *, level: t.Union[str, int] = logging.DEBUG):
        """
        Add a log entry...no note
        """
        if not note:
            raise ValueError("Provide something to log")
        if isinstance(level, str):
            level = convert_level(level)
        if level == logging.INFO:
            note = f"{note}"
        else:
            caller_frame = inspect.stack()[1]
            module = inspect.getmodule(caller_frame[0])
            module_name = module.__name__ if module else "unknown"
            note = f"{cls.logname} | {module_name} | {note}"
        cls._log.log(level, note)

    def note(self, note, *, level=logging.INFO, log=True):
        """
        Add note to the object. By default, the note will also
        be logged
        :param note: (str) The note itself
        :param level: (logging.level)
        :param log: (boolean) Enable or disable logging of note
        """
        if not note:
            raise ValueError("Provide something to log")
        note = f"{cls.logname} | {note}"
        cls._notes.timestamp.append(datetime.now().astimezone())
        cls._notes.notes.append(note)
        if log:
            cls.log(level, note)

    @property
    def notes(self):
        """
        Retrieve notes list as a Pandas Series
        """
        if not _PANDAS:
            return dict(zip(self._notes.timestamp, self._notes.notes))
        return pd.Series(self._notes.notes, index=self._notes.timestamp)

    def clear_notes(self):
        """
        Clear notes object
        """
        cls._notes.timestamp = []
        cls._notes.notes = []

    # Add the functions to the decorated class
    cls.clear_notes = clear_notes
    cls.note = note
    cls.notes = notes
    cls.log = log
    cls.log_title = log_title
    cls.log_subtitle = log_subtitle
    return cls
