import logging
import logging.handlers
import sys
from logging import Logger as RootLogger
from logging import LogRecord
from typing import Any, Dict

from . import app_config, util


class ExcludeErrorsFilter:
    """Helper class for the logging filter."""

    @staticmethod
    def filter(record: LogRecord) -> bool:  # noqa a003 (filter name is used bacause of logger filter implementation)
        """Filters all logging events that are errors.

        Args:
            record (LogRecord): log record

        Returns:
            None

        """
        return record.levelno < logging.ERROR


class ExcludeAllFilter:
    """Helper class for the logging filter."""

    @staticmethod
    def filter(_record: LogRecord) -> bool:  # noqa a003 (filter name is used bacause of logger filter implementation)
        """Filters all logging events.

        Currently unused

        Args:
            _record (LogRecord): log record

        Returns:
            None

        """
        return False


class SysLogHandler(logging.handlers.SysLogHandler):
    """Extends main SysLogHandler with additional functionality."""

    def emit(self, record: LogRecord) -> None:
        """Removes newlines and whitespaces and make the syslog output compact.

        Overloads the SysLogHandler.emit() method

        Args:
            record (LogRecord): log record

        Returns:
            None

        """
        self.append_nul = False
        self.ident = ''
        if hasattr(record, 'message') and record.message:
            record.message = util.compact_str(record.message)
        if hasattr(record, 'exc_text') and record.exc_text:
            record.exc_text = util.compact_str(record.exc_text)
            record.msg = ''

        return super().emit(record)


class Logger:
    """Logger class own implementation.

    Uses the logger module
    but does not inherit it. Instead it creates and uses logger handlers
    for logging.

    Attributes:
        _logger (Logger): base logger instance
        _config (Dict[str, Any]): config options

    """
    _logger: RootLogger
    _config: Dict[str, Any] = {
        'filters': {
            'exclude_errors': ExcludeErrorsFilter,
            'exclude_all': ExcludeAllFilter,
        },
        'formatters': {
            'basic': logging.BASIC_FORMAT,
            'test': logging.Formatter(
                '(%(process)d) %(asctime)s %(name)s (line %(lineno)s) | %(levelname)s %(message)s'  # noqa: E501
            ),
            'console': logging.Formatter(
                '%(asctime)s [%(process)d] %(levelname)s: (%(name)s) %(message)s'  # noqa: E501
            ),
            'syslog': logging.Formatter(
                app_config.app_name + '[%(process)d] %(levelname)s: (%(name)s) %(message)s'  # noqa: E501
            ),
        },
        'handlers': {
            'stderr': {
                'class': logging.StreamHandler,
                'level': logging.ERROR,
                'stream': sys.stderr,
                'formatter': 'console',
                'run_with_systemd': False,
            },
            'stdout': {
                'class': logging.StreamHandler,
                'level': logging.DEBUG,
                'filters': ['exclude_errors'],
                'stream': sys.stdout,
                'formatter': 'console',
                'run_with_systemd': False,
            },
            'syslog': {
                'class': SysLogHandler,
                'level': logging.DEBUG,
                'address': '/dev/log',
                'formatter': 'syslog',
                'run_with_systemd': True,
            }
        },
        'root': {
            'level': logging.DEBUG,
            'handlers': ['stdout', 'stderr', 'syslog'],
            # 'filter': 'exclude_all',
        },
    }

    def __init__(self, cls: str) -> None:
        """Constructor for Logger class.

        Args:
            cls (str): class name to use in the log instance

        Returns:
            None

        """
        self._logger = logging.getLogger(cls)
        cfg = self._config
        logger = self._logger
        logger.setLevel(cfg['root']['level'])
        if 'filter' in cfg['root']:
            logger.addFilter(cfg['filters'][cfg['root']['filter']])
        for handler_name in cfg['root']['handlers']:
            if handler_name not in cfg['handlers']:
                continue
            handler_config: Dict[str, Any] = cfg['handlers'][handler_name]
            run_with_systemd = handler_config.get('run_with_systemd', False)
            if app_config.systemd_controlled and not run_with_systemd:
                continue
            handler = self._create_handler(handler_config)
            self._setup_handler(handler, handler_config)
            logger.addHandler(handler)

    def _create_handler(self, handler_config: Any) -> logging.Handler:
        """Creates a handler from the provided handler config.

        Args:
            handler_config (Any): handler config

        Returns:
            logging.Handler: logging handler

        """
        cls = handler_config['class']
        handler: logging.Handler
        if cls == SysLogHandler:
            handler = SysLogHandler(address=handler_config.get('address'))
        elif cls == logging.handlers.SysLogHandler:
            handler = logging.handlers.SysLogHandler(
                address=handler_config.get('address')
            )
        elif cls == logging.StreamHandler:
            handler = logging.StreamHandler(handler_config.get('stream'))
        else:
            raise ValueError(f'Unknown handler class {cls}')
        return handler

    def _setup_handler(self,
                       handler: logging.Handler,
                       handler_config: Any) -> None:
        """Assigns filters, formatting for the provided handler.

        Args:
            handler (logging.Handler): handler
            handler_config (Any): handler config

        Returns:
            None

        """
        if 'filters' in handler_config:
            for l_filter in handler_config['filters']:
                if l_filter in self._config['filters']:
                    handler.addFilter(self._config['filters'][l_filter])
                else:
                    raise ValueError(f'Unknown filter {l_filter}')
        if 'formatter' in handler_config:
            handler.setFormatter(
                self._config['formatters'][handler_config['formatter']]
            )
        handler.setLevel(handler_config['level'])

    def debug(self, msg: str, *args: Any, **kwargs: Any) -> None:
        """Log debug message.

        Args:
            msg (str): message
            *args (Any): args
            **kwargs (Any): kwargs

        Returns:
            None

        """
        self._logger.debug(msg, *args, **kwargs)

    def info(self, msg: str, *args: Any, **kwargs: Any) -> None:
        """Log info message.

        Args:
            msg (str): message
            *args (Any): args
            **kwargs (Any): kwargs

        Returns:
            None

        """
        self._logger.info(msg, *args, **kwargs)

    def warning(self, msg: str, *args: Any, **kwargs: Any) -> None:
        """Log warning message.

        Args:
            msg (str): message
            *args (Any): args
            **kwargs (Any): kwargs

        Returns:
            None

        """
        self._logger.warning(msg, *args, **kwargs)

    def warn(self, msg: str, *args: Any, **kwargs: Any) -> None:
        """Alias for self.warning.

        Args:
            msg (str): message
            *args (Any): args
            **kwargs (Any): kwargs

        Returns:
            None

        """
        self.warning(msg, *args, **kwargs)

    def error(self, msg: str, *args: Any, **kwargs: Any) -> None:
        """Log error message.

        Args:
            msg (str): message
            *args (Any): args
            **kwargs (Any): kwargs

        Returns:
            None

        """
        self._logger.error(msg, *args, **kwargs)

    def exception(self, msg: str, *args: Any, exc_info: bool = True,
                  **kwargs: Any) -> None:
        """Log exception message.

        Args:
            msg (str): message
            *args (Any): args
            exc_info (bool = True): print stacktrace. Default is True
            **kwargs (Any): kwargs

        Returns:
            None

        """
        self._logger.exception(msg, *args, exc_info, **kwargs)

    def critical(self, msg: str, *args: Any, **kwargs: Any) -> None:
        """Log critical message.

        Args:
            msg (str): message
            *args (Any): args
            **kwargs (Any): kwargs

        Returns:
            None

        """
        if 'exc_info' not in kwargs:
            kwargs['exc_info'] = True
        self._logger.critical(msg, *args, **kwargs)

    def fatal(self, msg: str, *args: Any, **kwargs: Any) -> None:
        """Alias for self.critical.

        Args:
            msg (str): message
            *args (Any): args
            **kwargs (Any): kwargs

        Returns:
            None

        """
        self.critical(msg, *args, **kwargs)
