#   Licensed under the Apache License, Version 2.0 (the "License"); you may
#   not use this file except in compliance with the License. You may obtain
#   a copy of the License at
#
#        http://www.apache.org/licenses/LICENSE-2.0
#
#   Unless required by applicable law or agreed to in writing, software
#   distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#   WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#   License for the specific language governing permissions and limitations
#   under the License.
#

"""Application logging"""

import argparse
from collections.abc import Mapping
import logging
import sys
from typing import Any
import warnings

from openstack.config import cloud_region


def get_loggers() -> dict[str, str]:
    loggers = {}
    for logkey in logging.Logger.manager.loggerDict.keys():
        loggers[logkey] = logging.getLevelName(logging.getLogger(logkey).level)
    return loggers


def log_level_from_options(options: argparse.Namespace) -> int:
    # if --debug, --quiet or --verbose is not specified,
    # the default logging level is warning
    log_level = logging.WARNING
    if options.verbose_level == 0:
        # --quiet
        log_level = logging.ERROR
    elif options.verbose_level == 2:
        # One --verbose
        log_level = logging.INFO
    elif options.verbose_level >= 3:
        # Two or more --verbose
        log_level = logging.DEBUG
    return log_level


def log_level_from_string(level_string: str) -> int:
    log_level = {
        'critical': logging.CRITICAL,
        'error': logging.ERROR,
        'warning': logging.WARNING,
        'info': logging.INFO,
        'debug': logging.DEBUG,
    }.get(level_string, logging.WARNING)
    return log_level


def log_level_from_config(config: Mapping[str, Any]) -> int:
    # Check the command line option
    verbose_level_from_config = config.get('verbose_level')
    if config.get('debug', False):
        verbose_level_from_config = 3

    match verbose_level_from_config:
        case 0:
            verbose_level = 'error'
        case 1:
            # If a command line option has not been specified, check the
            # configuration file
            verbose_level = config.get('log_level', 'warning')
        case 2:
            verbose_level = 'info'
        case _:
            verbose_level = 'debug'

    return log_level_from_string(verbose_level)


def set_warning_filter(log_level: int) -> None:
    if log_level == logging.ERROR:
        warnings.simplefilter("ignore")
    elif log_level == logging.WARNING:
        warnings.simplefilter("ignore")
    elif log_level == logging.INFO:
        warnings.simplefilter("once")


class _FileFormatter(logging.Formatter):
    """Customize the logging format for logging handler"""

    _LOG_MESSAGE_BEGIN = (
        '%(asctime)s.%(msecs)03d %(process)d %(levelname)s %(name)s '
    )
    _LOG_MESSAGE_CONTEXT = '[%(cloud)s %(username)s %(project)s] '
    _LOG_MESSAGE_END = '%(message)s'
    _LOG_DATE_FORMAT = '%Y-%m-%d %H:%M:%S'

    def __init__(
        self,
        options: argparse.Namespace | None = None,
        config: cloud_region.CloudRegion | None = None,
        **kwargs: Any,
    ) -> None:
        context = {}
        if options:
            context = {
                'cloud': getattr(options, 'cloud', ''),
                'project': getattr(options, 'os_project_name', ''),
                'username': getattr(options, 'username', ''),
            }
        elif config:
            context = {
                'cloud': config.config.get('cloud', ''),
                'project': config.auth.get('project_name', ''),
                'username': config.auth.get('username', ''),
            }
        if context:
            self.fmt = (
                self._LOG_MESSAGE_BEGIN
                + (self._LOG_MESSAGE_CONTEXT % context)
                + self._LOG_MESSAGE_END
            )
        else:
            self.fmt = self._LOG_MESSAGE_BEGIN + self._LOG_MESSAGE_END
        logging.Formatter.__init__(self, self.fmt, self._LOG_DATE_FORMAT)


class LogConfigurator:
    _CONSOLE_MESSAGE_FORMAT = '%(message)s'

    def __init__(self, options: argparse.Namespace) -> None:
        self.root_logger = logging.getLogger('')
        self.root_logger.setLevel(logging.DEBUG)

        # Force verbose_level 3 on --debug
        self.dump_trace = False
        if options.debug:
            options.verbose_level = 3
            self.dump_trace = True

        # Always send higher-level messages to the console via stderr
        self.console_logger = logging.StreamHandler(sys.stderr)
        log_level = log_level_from_options(options)
        self.console_logger.setLevel(log_level)
        formatter = logging.Formatter(self._CONSOLE_MESSAGE_FORMAT)
        self.console_logger.setFormatter(formatter)
        self.root_logger.addHandler(self.console_logger)

        # Set the warning filter now
        set_warning_filter(log_level)

        # Set up logging to a file
        self.file_logger = None
        log_file = options.log_file
        if log_file:
            self.file_logger = logging.FileHandler(filename=log_file)
            self.file_logger.setFormatter(_FileFormatter(options=options))
            self.file_logger.setLevel(log_level)
            self.root_logger.addHandler(self.file_logger)

        # Requests logs some stuff at INFO that we don't want
        # unless we have DEBUG
        requests_log = logging.getLogger("requests")

        # Other modules we don't want DEBUG output for
        cliff_log = logging.getLogger('cliff')
        stevedore_log = logging.getLogger('stevedore')
        iso8601_log = logging.getLogger("iso8601")

        if options.debug:
            # --debug forces traceback
            requests_log.setLevel(logging.DEBUG)
        else:
            requests_log.setLevel(logging.ERROR)

        cliff_log.setLevel(logging.ERROR)
        stevedore_log.setLevel(logging.ERROR)
        iso8601_log.setLevel(logging.ERROR)

    def configure(self, cloud_config: cloud_region.CloudRegion) -> None:
        log_level = log_level_from_config(cloud_config.config)
        set_warning_filter(log_level)
        self.dump_trace = cloud_config.config.get('debug', self.dump_trace)
        self.console_logger.setLevel(log_level)

        log_file = cloud_config.config.get('log_file')
        if log_file:
            if not self.file_logger:
                self.file_logger = logging.FileHandler(filename=log_file)
            self.file_logger.setFormatter(_FileFormatter(config=cloud_config))
            self.file_logger.setLevel(log_level)
            self.root_logger.addHandler(self.file_logger)

        logconfig = cloud_config.config.get('logging')
        if logconfig:
            highest_level = logging.NOTSET
            for k in logconfig.keys():
                level = log_level_from_string(logconfig[k])
                logging.getLogger(k).setLevel(level)
                if highest_level < level:
                    highest_level = level
            self.console_logger.setLevel(highest_level)
            if self.file_logger:
                self.file_logger.setLevel(highest_level)
            # loggers that are not set will use the handler level, so we
            # need to set the global level for all the loggers
            for logkey in logging.Logger.manager.loggerDict.keys():
                logger = logging.getLogger(logkey)
                if logger.level == logging.NOTSET:
                    logger.setLevel(log_level)
