"""
A connection between object logger and background k8s-event poster.

Everything logged to the object logger (except for debug information)
is also posted as the k8s-event -- in the background: `kopf.engines.posting`.

This eliminates the need to log & post the same messages, which complicates
the operators' code, and can lead to information loss or mismatch
(e.g. when logging call is added, but posting is forgotten).
"""
import asyncio
import copy
import logging
from typing import Tuple, MutableMapping, Any, Optional

from kopf.engines import posting
from kopf.structs import bodies
from kopf.structs import configuration


class ObjectPrefixingFormatter(logging.Formatter):
    """ An utility to prefix the per-object log messages. """

    def format(self, record: logging.LogRecord) -> str:
        if hasattr(record, 'k8s_ref'):
            ref = getattr(record, 'k8s_ref')
            prefix = f"[{ref.get('namespace', '')}/{ref.get('name', '')}]"
            record = copy.copy(record)  # shallow
            record.msg = f"{prefix} {record.msg}"
        return super().format(record)


class K8sPoster(logging.Handler):
    """
    A handler to post all log messages as K8s events.
    """

    def createLock(self) -> None:
        # Save some time on unneeded locks. Events are posted in the background.
        # We only put events to the queue, which is already lock-protected.
        self.lock = None

    def filter(self, record: logging.LogRecord) -> bool:
        # Only those which have a k8s object referred (see: `ObjectLogger`).
        # Otherwise, we have nothing to post, and nothing to do.
        settings: Optional[configuration.OperatorSettings]
        settings = getattr(record, 'settings', None)
        level_ok = settings is not None and record.levelno >= settings.posting.level
        enabled = settings is not None and settings.posting.enabled
        has_ref = hasattr(record, 'k8s_ref')
        skipped = hasattr(record, 'k8s_skip') and getattr(record, 'k8s_skip')
        return enabled and level_ok and has_ref and not skipped and super().filter(record)

    def emit(self, record: logging.LogRecord) -> None:
        # Same try-except as in e.g. `logging.StreamHandler`.
        try:
            ref = getattr(record, 'k8s_ref')
            type = (
                "Debug" if record.levelno <= logging.DEBUG else
                "Normal" if record.levelno <= logging.INFO else
                "Warning" if record.levelno <= logging.WARNING else
                "Error" if record.levelno <= logging.ERROR else
                "Fatal" if record.levelno <= logging.FATAL else
                logging.getLevelName(record.levelno).capitalize())
            reason = 'Logging'
            message = self.format(record)
            posting.enqueue(
                ref=ref,
                type=type,
                reason=reason,
                message=message)
        except Exception:
            self.handleError(record)


class ObjectLogger(logging.LoggerAdapter):
    """
    A logger/adapter to carry the object identifiers for formatting.

    The identifiers are then used both for formatting the per-object messages
    in `ObjectPrefixingFormatter`, and when posting the per-object k8s-events.

    Constructed in event handling of each individual object.

    The internal structure is made the same as an object reference in K8s API,
    but can change over time to anything needed for our internal purposes.
    However, as little information should be carried as possible,
    and the information should be protected against the object modification
    (e.g. in case of background posting via the queue; see `K8sPoster`).
    """

    def __init__(self, *, body: bodies.Body, settings: configuration.OperatorSettings) -> None:
        super().__init__(logger, dict(
            settings=settings,
            k8s_skip=False,
            k8s_ref=dict(
                apiVersion=body.get('apiVersion'),
                kind=body.get('kind'),
                name=body.get('metadata', {}).get('name'),
                uid=body.get('metadata', {}).get('uid'),
                namespace=body.get('metadata', {}).get('namespace'),
            ),
        ))

    def process(
            self,
            msg: str,
            kwargs: MutableMapping[str, Any],
    ) -> Tuple[str, MutableMapping[str, Any]]:
        # Native logging overwrites the message's extra with the adapter's extra.
        # We merge them, so that both message's & adapter's extras are available.
        kwargs["extra"] = dict(self.extra, **kwargs.get('extra', {}))
        return msg, kwargs


class LocalObjectLogger(ObjectLogger):
    """
    Same as `ObjectLogger`, but does not post the messages as k8s-events.

    Used in the resource-watching handlers to log the handler's invocation
    successes/failures without overloading K8s with excessively many k8s-events.

    This class is used internally only and is not exposed publicly in any way.
    """

    def log(self, *args: Any, **kwargs: Any) -> None:
        kwargs['extra'] = dict(kwargs.pop('extra', {}), k8s_skip=True)
        return super().log(*args, **kwargs)


logger = logging.getLogger('kopf.objects')
logger.addHandler(K8sPoster())

format = '[%(asctime)s] %(name)-20.20s [%(levelname)-8.8s] %(message)s'


def configure(
        debug: Optional[bool] = None,
        verbose: Optional[bool] = None,
        quiet: Optional[bool] = None,
) -> None:
    log_level = 'DEBUG' if debug or verbose else 'WARNING' if quiet else 'INFO'

    logger = logging.getLogger()
    handler = logging.StreamHandler()
    formatter = ObjectPrefixingFormatter(format)
    handler.setFormatter(formatter)
    logger.addHandler(handler)
    logger.setLevel(log_level)

    # Configure the Kubernetes client defaults according to our settings.
    try:
        import kubernetes
    except ImportError:
        pass
    else:
        config = kubernetes.client.configuration.Configuration()
        config.logger_format = format
        config.logger_file = None  # once again after the constructor to re-apply the formatter
        config.debug = debug
        kubernetes.client.configuration.Configuration.set_default(config)

    # Kubernetes client is as buggy as hell: it adds its own stream handlers even in non-debug mode,
    # does not respect the formatting, and dumps too much of the low-level info.
    if not debug:
        logger = logging.getLogger("urllib3")
        del logger.handlers[1:]  # everything except the default NullHandler

    # Prevent the low-level logging unless in the debug verbosity mode. Keep only the operator's messages.
    # For no-propagation loggers, add a dummy null handler to prevent printing the messages.
    for name in ['urllib3', 'asyncio', 'kubernetes']:
        logger = logging.getLogger(name)
        logger.propagate = bool(debug)
        if not debug:
            logger.handlers[:] = [logging.NullHandler()]

    loop = asyncio.get_event_loop()
    loop.set_debug(bool(debug))
