# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0.

"""Python logging integration for the AWS CRT."""

import logging
import threading
from enum import IntEnum

import _awscrt
from awscrt.io import LogLevel

__all__ = ['init_logging', 'set_log_level', 'logf', 'LogSubject', 'CRT_LOG_FORMAT']

_CRT_TO_PY_LEVEL = {
    0: logging.NOTSET,
    1: logging.CRITICAL,
    2: logging.ERROR,
    3: logging.WARNING,
    4: logging.INFO,
    5: logging.DEBUG,
    6: logging.DEBUG,
}

_PY_TO_CRT_LEVEL = {
    logging.NOTSET: LogLevel.NoLogs,
    logging.CRITICAL: LogLevel.Fatal,
    logging.ERROR: LogLevel.Error,
    logging.WARNING: LogLevel.Warn,
    logging.INFO: LogLevel.Info,
    logging.DEBUG: LogLevel.Trace,
}

# Default Format supported by native loggers.
CRT_LOG_FORMAT = '%(asctime)s [%(threadName)s] %(levelname)s %(name)s - %(message)s'


class LogSubject(IntEnum):
    """Log subject identifiers for CRT subsystems."""
    # aws-c-common (package ID 0)
    COMMON_GENERAL = 0x000
    COMMON_TASK_SCHEDULER = 0x001

    # aws-c-io (package ID 1)
    IO_GENERAL = 0x400
    IO_EVENT_LOOP = 0x401
    IO_SOCKET = 0x402
    IO_SOCKET_HANDLER = 0x403
    IO_TLS = 0x404
    IO_ALPN = 0x405
    IO_DNS = 0x406
    IO_PKI = 0x407
    IO_CHANNEL = 0x408
    IO_CHANNEL_BOOTSTRAP = 0x409
    IO_FILE_UTILS = 0x40A
    IO_SHARED_LIBRARY = 0x40B

    # aws-c-http (package ID 2)
    HTTP_GENERAL = 0x800
    HTTP_CONNECTION = 0x801
    HTTP_SERVER = 0x802
    HTTP_STREAM = 0x803
    HTTP_CONNECTION_MANAGER = 0x804
    HTTP_WEBSOCKET = 0x805
    HTTP_WEBSOCKET_SETUP = 0x806

    # aws-c-event-stream (package ID 4)
    EVENT_STREAM_GENERAL = 0x1000
    EVENT_STREAM_CHANNEL_HANDLER = 0x1001
    EVENT_STREAM_RPC_SERVER = 0x1002
    EVENT_STREAM_RPC_CLIENT = 0x1003

    # aws-c-mqtt (package ID 5)
    MQTT_GENERAL = 0x1400
    MQTT_CLIENT = 0x1401
    MQTT_TOPIC_TREE = 0x1402

    # aws-c-auth (package ID 6)
    AUTH_GENERAL = 0x1800
    AUTH_PROFILE = 0x1801
    AUTH_CREDENTIALS_PROVIDER = 0x1802
    AUTH_SIGNING = 0x1803

    # aws-c-cal (package ID 7)
    CAL_GENERAL = 0x1C00
    CAL_ECC = 0x1C01
    CAL_HASH = 0x1C02
    CAL_HMAC = 0x1C03
    CAL_DER = 0x1C04
    CAL_LIBCRYPTO_RESOLVE = 0x1C05
    CAL_RSA = 0x1C06
    CAL_ED25519 = 0x1C07

    # aws-c-s3 (package ID 14)
    S3_GENERAL = 0x3800
    S3_CLIENT = 0x3801

    # aws-c-sdkutils (package ID 15)
    SDKUTILS_GENERAL = 0x3C00
    SDKUTILS_PROFILE = 0x3C01
    SDKUTILS_ENDPOINTS_PARSING = 0x3C02
    SDKUTILS_ENDPOINTS_RESOLVE = 0x3C03
    SDKUTILS_ENDPOINTS_GENERAL = 0x3C04
    SDKUTILS_PARTITIONS_PARSING = 0x3C05
    SDKUTILS_ENDPOINTS_REGEX = 0x3C06


_PACKAGE_ID_TO_MODULE = {
    0: 'common',
    1: 'io',
    2: 'http',
    4: 'event-stream',
    5: 'mqtt',
    6: 'auth',
    7: 'cal',
    14: 's3',
    15: 'sdkutils',
}


def _python_logging_callback(crt_level, message, subject_id, subject_name, thread_name):
    """Called from C for each CRT log message."""
    module = _PACKAGE_ID_TO_MODULE.get(subject_id >> 10, 'unknown')
    logger = logging.getLogger('awscrt.{}.{}'.format(module, subject_name))
    py_level = _CRT_TO_PY_LEVEL.get(crt_level, logging.DEBUG)
    # `There is the possibility that “dummy thread objects” are created. These are
    # thread objects corresponding to “alien threads”, which are threads of control
    # started outside the threading module, such as directly from C code.`
    # https://docs.python.org/3/library/threading.html#thread-objects
    #
    # As per current conventions, dummy thread objects have thread names
    # starting with "Dummy". Eg. Dummy-1, Dummy-8
    if threading.current_thread().name.startswith("Dummy") and thread_name is not None:
        threading.current_thread().name = thread_name
    logger.log(py_level, '%s', message)


def init_logging(level: int = logging.DEBUG):
    """Initialize CRT logging, routing output through Python's logging module.

    Log messages appear under the ``awscrt`` logger hierarchy, with each CRT
    subsystem as a child logger (e.g. ``awscrt.io.event-loop``,
    ``awscrt.s3.S3Client``).

    This is mutually exclusive with :func:`~awscrt.io.init_logging` -- use one
    or the other, not both. Can only be called once.

    Example usage::

        import logging
        from awscrt.logging import init_logging

        logging.basicConfig(level=logging.DEBUG)
        init_logging(logging.DEBUG)

    Args:
        level (int): Python logging level (e.g. ``logging.DEBUG``,
            ``logging.WARNING``).

    Raises:
        RuntimeError: If CRT logging has already been initialized.
    """
    root_logger = logging.getLogger('awscrt')

    try:
        crt_level = _PY_TO_CRT_LEVEL[level]
    except KeyError:
        raise ValueError(f"Invalid log level: {level}. Use logging.DEBUG, INFO, WARNING, ERROR, or CRITICAL")

    if root_logger.level == logging.NOTSET:
        root_logger.setLevel(_CRT_TO_PY_LEVEL.get(int(crt_level), logging.DEBUG))

    _awscrt.init_python_logging(int(crt_level), _python_logging_callback)


def set_log_level(level: int):
    """Change the CRT log level. :func:`init_logging` must have been called first.

    Set log level to logging.NOTSET to disable the logger. Cleaning up the logger
    is dangerous in a multi-threaded environment.

    Args:
        level (int): Python logging level (e.g. ``logging.DEBUG``,
            ``logging.WARNING``).
    """
    try:
        crt_level = _PY_TO_CRT_LEVEL[level]
    except KeyError:
        raise ValueError(f"Invalid log level: {level}. Use logging.DEBUG, INFO, WARNING, ERROR, or CRITICAL")
    _awscrt.set_log_level(int(crt_level))


def log(level: int, subject: LogSubject, message: str):
    """Log a message through the CRT's configured logger.

    Args:
        level (int): Python logging level (e.g. logging.DEBUG, logging.INFO).
        subject (LogSubject): Log subject identifying the subsystem.
        message (str): The message to log.
    """
    try:
        crt_level = _PY_TO_CRT_LEVEL[level]
    except KeyError:
        raise ValueError(f"Invalid log level: {level}. Use logging.DEBUG, INFO, WARNING, ERROR, or CRITICAL")
    _awscrt.logger_log(int(crt_level), int(subject), message)
