"""SeverityMapper for mapping Python logging levels to OTel severity values.

Maps Python's standard logging levels to OpenTelemetry SeverityNumber ranges
and SeverityText values per the OTel Log Data Model specification.

Mapping table:
    Python Level    | SeverityText | SeverityNumber
    NOTSET (0)      | TRACE        | 1
    DEBUG (10)      | DEBUG        | 5
    INFO (20)       | INFO         | 9
    WARNING (30)    | WARN         | 13
    ERROR (40)      | ERROR        | 17
    CRITICAL (50)   | FATAL        | 21
"""

from __future__ import annotations

import logging
from typing import Tuple

# Standard Python logging levels mapped to (SeverityNumber, SeverityText).
# Each maps to the lowest value in the OTel range for that severity band.
_PYTHON_LEVEL_MAP: dict[int, tuple[int, str]] = {
    logging.NOTSET: (1, "TRACE"),      # NOTSET=0  -> TRACE range 1-4
    logging.DEBUG: (5, "DEBUG"),       # DEBUG=10  -> DEBUG range 5-8
    logging.INFO: (9, "INFO"),         # INFO=20   -> INFO range 9-12
    logging.WARNING: (13, "WARN"),     # WARNING=30 -> WARN range 13-16
    logging.ERROR: (17, "ERROR"),      # ERROR=40  -> ERROR range 17-20
    logging.CRITICAL: (21, "FATAL"),   # CRITICAL=50 -> FATAL range 21-24
}

# Sorted standard levels for closest-match lookup.
_SORTED_LEVELS = sorted(_PYTHON_LEVEL_MAP.keys())

# Recognized severity strings (case-insensitive) to (SeverityNumber, SeverityText).
_SEVERITY_STRING_MAP: dict[str, tuple[int, str]] = {
    "TRACE": (1, "TRACE"),
    "DEBUG": (5, "DEBUG"),
    "INFO": (9, "INFO"),
    "WARN": (13, "WARN"),
    "WARNING": (13, "WARN"),
    "ERROR": (17, "ERROR"),
    "FATAL": (21, "FATAL"),
    "CRITICAL": (21, "FATAL"),
    # Python-specific level names
    "NOTSET": (1, "TRACE"),
}


def _find_closest_level(python_level: int) -> int:
    """Find the closest standard Python logging level to the given value."""
    closest = _SORTED_LEVELS[0]
    min_distance = abs(python_level - closest)
    for level in _SORTED_LEVELS[1:]:
        distance = abs(python_level - level)
        if distance < min_distance:
            min_distance = distance
            closest = level
        elif distance == min_distance and level < closest:
            # Tie-break: prefer the lower standard level
            closest = level
    return closest


class SeverityMapper:
    """Maps between Python logging levels and OTel severity values."""

    @staticmethod
    def to_severity_number(python_level: int) -> int:
        """Map a Python logging level (int) to an OTel SeverityNumber.

        For standard levels (NOTSET, DEBUG, INFO, WARNING, ERROR, CRITICAL),
        returns the lowest SeverityNumber in the corresponding OTel range.
        For non-standard levels, finds the closest standard level and uses
        its mapping.

        Args:
            python_level: A Python logging level integer.

        Returns:
            An OTel SeverityNumber (1-24).
        """
        if python_level in _PYTHON_LEVEL_MAP:
            return _PYTHON_LEVEL_MAP[python_level][0]
        closest = _find_closest_level(python_level)
        return _PYTHON_LEVEL_MAP[closest][0]

    @staticmethod
    def to_severity_text(python_level: int) -> str:
        """Map a Python logging level (int) to an OTel SeverityText.

        For standard levels, returns the corresponding OTel SeverityText.
        For non-standard levels, finds the closest standard level and uses
        its mapping.

        Args:
            python_level: A Python logging level integer.

        Returns:
            An OTel SeverityText string (TRACE, DEBUG, INFO, WARN, ERROR, FATAL).
        """
        if python_level in _PYTHON_LEVEL_MAP:
            return _PYTHON_LEVEL_MAP[python_level][1]
        closest = _find_closest_level(python_level)
        return _PYTHON_LEVEL_MAP[closest][1]

    @staticmethod
    def from_string(severity_str: str) -> Tuple[int, str]:
        """Map a severity string to (SeverityNumber, SeverityText).

        Recognizes standard OTel severity names (TRACE, DEBUG, INFO, WARN,
        ERROR, FATAL) and Python-specific names (WARNING, CRITICAL, NOTSET),
        case-insensitively.

        For unrecognized strings, returns SeverityNumber 0 (UNSPECIFIED)
        and preserves the original string as SeverityText.

        Args:
            severity_str: A severity level name string.

        Returns:
            A tuple of (SeverityNumber, SeverityText).
        """
        normalized = severity_str.strip().upper()
        if normalized in _SEVERITY_STRING_MAP:
            return _SEVERITY_STRING_MAP[normalized]
        return (0, severity_str)
