"""Provides basic logging configuration for alfred experiments.

.. moduleauthor: Johannes Brachem <jbrachem@posteo.de>
"""

import logging
import queue
import threading
from pathlib import Path
from typing import Union


def prepare_file_handler(filepath: Union[str, Path]) -> logging.FileHandler:
    """Returns a :class:`~logging.FileHandler` and creates the necessary
    directories on the fly, if needed.

    Args:
        filepath: Absolute path to the targeted logfile.
    """
    logpath = Path(filepath)

    if not logpath.is_absolute():
        raise ValueError("Value of filepath must be an absolute path.")

    if logpath.is_dir():
        raise ValueError("Value of filepath must point to a file.")

    logdir = logpath.parent
    logdir.mkdir(exist_ok=True)

    return logging.FileHandler(str(logpath))


def prepare_alfred_formatter(exp_id: str) -> logging.Formatter:
    """Returns a :class:`~logging.Formatter` with the standard alfred
    logging format, including experiment id.
    """

    formatter = logging.Formatter(
        f"%(asctime)s - %(name)s - %(levelname)s - experiment id={exp_id} - %(message)s"
    )

    return formatter


def parse_level(level: str) -> int:
    """Parses level definitions in lower case strings and returns the
    approriate level attribute of the Python logging library.
    """
    lvl = level.upper()
    if lvl not in ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]:
        raise ValueError("log level must be debug, info, warning, error or critical")
    return getattr(logging, lvl)


class QueuedLoggingInterface:
    """Interface for handling logging in alfred3.

    This class provides access to two loggers: The base logger and
    the queue logger. The reasoning is the following:

    * We want logging messages to contain experiment information
        (exp_id and session_id) and we want logfiles to potentially be
        experiment specific.

    * The necessary information only becomes available to pages and elements
        once the page or its parent section get appended to the
        experiment.

    * So, logged messages are collected in a queue and logged as soon
        as the experiment becomes available.

    * In order to not lose information in between their queueing
        and their eventual logging with the queue logger, all messages
        get also logged immediately using the base logger to a general
        "admin" log (this is turned off through configuration
        in run.py for local experiments by default, to avoid doubling
        log messages in a single file).

    Args:
        base_logger: Name of the base logger.
        queue_logger: Name of the queue logger.

    Attributes:
        use_base_logger (bool): Indicates, whether the base logger
            should be used. Defaults to *True*, if a *base_logger* is
            defined upon initialization and to *False*, if *base_logger*
            is *None*. You can use this to turn off base logger usage.

        queue_logger (logging.Logger): The queue logger object. You
            can acces the logger this way to apply advanced
            configuration.

        session_id (str): Allows you to set a session ID that will be
            included in logged messages. (Defaults to "NA".)
    """

    def __init__(self, base_logger: str = None, queue_logger: str = None):
        """Constructor method."""
        self.use_base_logger = True if base_logger is not None else False
        self._base_logger = (
            logging.getLogger(base_logger) if base_logger is not None else None
        )
        self._base_logger_storage = None
        self._queue_logger = (
            logging.getLogger(queue_logger) if queue_logger is not None else None
        )
        self._queue = queue.Queue()
        self._level = None
        self.session_id = "NA"

    @property
    def queue_logger(self):
        return self._queue_logger

    @queue_logger.setter
    def queue_logger(self, logger):

        if self.queue_logger is not None:
            self.warning(
                "Queue logger already present. Overriding queue logger "
                f"{self.queue_logger} with {logger}."
            )

        self._queue_logger = logger

    def add_queue_logger(self, obj, module: str):
        name = self.loggername(obj, module)
        self.queue_logger = logging.getLogger(name)
        if "experiment" in obj.__dict__:
            self.session_id = obj.experiment.session_id
        elif "exp" in obj.__dict__:
            self.session_id = obj.exp.session_id
        else:
            self.session_id = obj.experiment.session_id
        self.log_queued_messages()

    def loggername(self, obj, module: str) -> str:
        """Returns a logger name for use in :class:`~alfred3.alfredlog.QueueLoggingInterface.

        The name has the following format::

            exp.exp_id.module_name.class_name.object_identifier

        The identifier is only added if the object has an attribute
        *instance_log* that is set to *True*. If the object
        has a *name* attribute, that is used as the identifier. Else,
        the object's *uid* is used.

        Args:
            obj: The object for which a logger name should be generated.
        """
        # remove "alfred3" from module name
        module_name = module.split(".")
        module_name.pop(0)

        name = []
        name.append("exp")
        if "experiment" in obj.__dict__:
            name.append(obj.experiment.exp_id)
        elif "exp" in obj.__dict__:
            name.append(obj.exp.exp_id)
        else:
            name.append(obj.experiment.exp_id)

        name.append(".".join(module_name))
        name.append(type(obj).__name__)

        if "instance_log" in obj.__dict__:

            try:
                if obj.instance_log:
                    try:
                        name.append(obj.name)
                    except AttributeError:
                        name.append(obj.uid)

            except AttributeError as e:
                self.debug(f"Suppressed exception: {e}")

        return ".".join(name)

    def _unpack_worker(self):
        while not self._queue.empty():
            level, msg = self._queue.get()
            lvl_logger = getattr(self.queue_logger, level)
            lvl_logger(msg)

    def log_queued_messages(self):
        """Logs messages in the queue using the queue logger."""
        if self.queue_logger is None:
            pass

        if self._level:
            self.queue_logger.setLevel(self._level)
        threading.Thread(target=self._unpack_worker, name="alfredlog").start()

    def setLevel(self, level: str):
        """Sets a level for the queue logger. Since the level will be
        stored, this can be done even before a queue logger is defined.
        """

        if level.upper() not in ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]:
            raise ValueError(
                "Level must be one of 'DEBUG', 'INFO', 'WARNING', 'ERROR', or"
                " 'CRITICAL'."
            )

        self._level = getattr(logging, level.upper())

        if self.queue_logger:
            self.queue_logger.setLevel(self._level)

    def _handle_msg(self, msg: str, level: str, *args, **kwargs):

        msg = f"session id={self.session_id} - " + msg

        if self.use_base_logger:
            getattr(self._base_logger, level)(msg, *args, **kwargs)

        if not self.queue_logger:
            self._queue.put((level, msg))
        else:
            logger_lvl = getattr(self.queue_logger, level)
            try:
                logger_lvl(msg, *args, **kwargs)
            except TypeError:
                logger_lvl(*args, **kwargs)

    def deactivate_base_logger(self):
        self._base_logger_storage = self._base_logger
        self._base_logger = None
        self.use_base_logger = False

    def activate_base_logger(self):
        if not self._base_logger:
            self._base_logger = self._base_logger_storage
        self.use_base_logger = True

    def log(self, level: str, msg: str, *args, **kwargs):
        self._handle_msg(msg, level, *args, **kwargs)

    def debug(self, msg: str, *args, **kwargs):
        self._handle_msg(msg, "debug", *args, **kwargs)

    def info(self, msg: str, *args, **kwargs):
        self._handle_msg(msg, "info", *args, **kwargs)

    def warning(self, msg: str, *args, **kwargs):
        self._handle_msg(msg, "warning", *args, **kwargs)

    def error(self, msg: str, *args, **kwargs):
        self._handle_msg(msg, "error", *args, **kwargs)

    def critical(self, msg: str, *args, **kwargs):
        self._handle_msg(msg, "critical", *args, **kwargs)

    def exception(self, msg: str, *args, **kwargs):
        self._handle_msg(msg, "exception", *args, **kwargs)


class BWCompatibleLogger(QueuedLoggingInterface):
    """Offers backwards compatibility for logging.

    This class accepts an *experiment* parameter in its logging methods.
    """

    def process_experiment(self, exp):
        self.queue_logger = logging.getLogger(f"exp.{exp.exp_id}.general")
        self.session_id = exp.session_id

    def log(self, level: str, msg: str, *args, **kwargs):
        for arg in args:
            try:
                self.process_experiment(arg)
                args.remove(arg)
            except AttributeError:
                pass

        try:
            exp = kwargs.pop("experiment")
            self.process_experiment(exp)
        except KeyError:
            pass

        try:
            super().log(level=level, msg=msg, *args, **kwargs)
        except TypeError:
            super().log(level=level, msg=msg)

    def debug(self, msg: str, *args, **kwargs):
        for arg in args:
            try:
                self.process_experiment(arg)
                args.remove(arg)
            except AttributeError:
                pass

        try:
            exp = kwargs.pop("experiment")
            self.process_experiment(exp)
        except KeyError:
            pass

        try:
            super().debug(msg=msg, *args, **kwargs)
        except TypeError:
            super().debug(msg=msg)

    def info(self, msg: str, *args, **kwargs):
        for arg in args:
            try:
                self.process_experiment(arg)
                args.remove(arg)
            except AttributeError:
                pass

        try:
            exp = kwargs.pop("experiment")
            self.process_experiment(exp)
        except KeyError:
            pass

        try:
            super().info(msg=msg, *args, **kwargs)
        except TypeError:
            super().info(msg=msg)

    def warning(self, msg: str, *args, **kwargs):
        for arg in args:
            try:
                self.process_experiment(arg)
                args.remove(arg)
            except AttributeError:
                pass

        try:
            exp = kwargs.pop("experiment")
            self.process_experiment(exp)
        except KeyError:
            pass

        try:
            super().warning(msg=msg, *args, **kwargs)
        except TypeError:
            super().warning(msg=msg)

    def error(self, msg: str, *args, **kwargs):
        for arg in args:
            try:
                self.process_experiment(arg)
                args.remove(arg)
            except AttributeError:
                pass

        try:
            exp = kwargs.pop("experiment")
            self.process_experiment(exp)
        except KeyError:
            pass

        try:
            super().error(msg=msg, *args, **kwargs)
        except TypeError:
            super().error(msg=msg)

    def critical(self, msg: str, *args, **kwargs):
        for arg in args:
            try:
                self.process_experiment(arg)
                args.remove(arg)
            except AttributeError:
                pass

        try:
            exp = kwargs.pop("experiment")
            self.process_experiment(exp)
        except KeyError:
            pass

        try:
            super().critical(msg=msg, *args, **kwargs)
        except TypeError:
            super().critical(msg=msg)

    def exception(self, msg: str, *args, **kwargs):
        for arg in args:
            try:
                self.process_experiment(arg)
                args.remove(arg)
            except AttributeError:
                pass

        try:
            exp = kwargs.pop("experiment")
            self.process_experiment(exp)
        except KeyError:
            pass

        try:
            super().exception(msg=msg, *args, **kwargs)
        except TypeError:
            super().exception(msg=msg)
