"""Custom log formatter for structured logging output."""

import json
import logging
from typing import Any

__all__ = ("CustomFormatter", "StructuredJsonFormatter")


# Level mapping from Python logging levels to JSON log levels
# Maps to common log levels: debug, info, warn, error
_JSON_LEVEL_MAP: dict[int, str] = {
    logging.DEBUG: "debug",
    logging.INFO: "info",
    logging.WARNING: "warn",
    logging.ERROR: "error",
    logging.CRITICAL: "error",  # CRITICAL maps to error for broad compatibility
}


class CustomFormatter(logging.Formatter):
    """A custom log formatter that adapts to structured 'details'.

    This formatter checks if a log record has a 'details' attribute. If
    it does, it formats the log message to include the entire 'details'
    dictionary. Otherwise, it falls back to the standard message format.
    """

    def __init__(self) -> None:
        """Initialize the CustomFormatter with a default date format."""
        super().__init__(None, "%Y-%m-%d %H:%M:%S")

    def format(self, record: logging.LogRecord) -> str:
        """Format the specified log record as text.

        Dynamically adjusts the format string based on whether the record
        contains a structured 'details' attribute or not. This allows
        structured logs to display their full details dictionary while
        standard logs use the basic message format.

        Args:
            record: The log record to format.

        Returns:
            The formatted log string with timestamp, level, and message or details.
        """
        if hasattr(record, "details"):
            self._style._fmt = "%(asctime)s - %(levelname)s - %(details)s"
        else:
            self._style._fmt = "%(asctime)s - %(levelname)s - %(message)s"
        return super().format(record)


class StructuredJsonFormatter(logging.Formatter):
    """JSON formatter for structured logging output.

    This formatter outputs logs as single-line JSON objects suitable for
    cloud logging platforms and log aggregation services. When used with
    footprint.leave(), all metadata fields become top-level JSON keys.

    Format characteristics:
        - Single-line JSON (no pretty printing)
        - Required 'message' field
        - 'level' field (debug, info, warn, error)
        - Custom attributes as top-level keys for querying
        - Automatic truncation of oversized payloads (default: 8000 chars)
        - Truncation disabled when root logger level is DEBUG

    Example output:
        {"level":"info","message":"User logged in","controller":"auth","user_id":"abc-123"}

    Attributes:
        DEFAULT_MAX_CHARS: Default maximum character limit for log output (8000).
    """

    DEFAULT_MAX_CHARS: int = 10240

    def __init__(self, max_chars: int | None = None) -> None:
        """Initialize the StructuredJsonFormatter.

        Args:
            max_chars: Maximum characters allowed for the JSON output.
                If the output exceeds this limit, large fields will be
                removed to prevent breaking log aggregation services.
                Defaults to DEFAULT_MAX_CHARS (8000). Set to 0 to disable.
        """
        super().__init__()
        self._max_chars = (
            max_chars if max_chars is not None else self.DEFAULT_MAX_CHARS
        )

    def format(self, record: logging.LogRecord) -> str:
        """Format the log record as a single-line JSON string.

        Extracts structured 'details' from footprint.leave() calls and flattens
        them into top-level JSON keys. For standard log calls, creates a minimal
        JSON object with just level and message.

        If the resulting JSON exceeds max_chars and the root logger level is not
        DEBUG, large fields are progressively removed and replaced with a
        truncation notice.

        Args:
            record: The log record to format.

        Returns:
            A single-line JSON string for structured logging.
        """
        # Get JSON-compatible level
        level = _JSON_LEVEL_MAP.get(record.levelno, "info")

        # Build the JSON object
        log_data: dict[str, Any] = {
            "level": level,
        }

        # Check for structured details from footprint.leave()
        # Note: 'details' is added dynamically via logging's extra mechanism
        details = getattr(record, "details", None)
        if details is not None and isinstance(details, dict):
            # Use the message from details, or fallback to record.msg
            log_data["message"] = details.get("message") or str(record.msg)

            # Add all other details as top-level keys for querying
            for key, value in details.items():
                if key == "message":
                    continue  # Already handled
                # Convert UUIDs to strings for JSON serialization
                if hasattr(value, "hex"):  # UUID check
                    log_data[key] = str(value)
                elif value is not None:
                    log_data[key] = value
        else:
            # Standard log without structured details
            log_data["message"] = record.getMessage()

        # Add exception info if present
        if record.exc_info and not record.exc_text:
            record.exc_text = self.formatException(record.exc_info)
        if record.exc_text:
            log_data["exception"] = record.exc_text
        if record.stack_info:
            log_data["stack_info"] = record.stack_info

        # Output as compact single-line JSON
        output = json.dumps(log_data, separators=(",", ":"), default=str)

        # Enforce character limit to prevent breaking log aggregation
        # Skip truncation when root logger is set to DEBUG (for development)
        root_level = logging.getLogger().level
        should_truncate = (
            self._max_chars > 0
            and len(output) > self._max_chars
            and root_level != logging.DEBUG
        )
        if should_truncate:
            output = self._truncate_payload(log_data)

        return output

    def _truncate_payload(self, log_data: dict[str, Any]) -> str:
        """Truncate oversized log payload by removing large fields.

        Progressively removes the largest non-essential fields until the
        output fits within max_chars. Essential fields (level, message)
        are preserved.

        Args:
            log_data: The log data dictionary to truncate.

        Returns:
            A truncated JSON string within the character limit.
        """
        # Fields that should never be removed
        protected_keys = {"level", "message"}

        # Work with a copy to avoid mutating original
        data = dict(log_data)
        removed_keys: list[str] = []

        while True:
            output = json.dumps(data, separators=(",", ":"), default=str)
            if len(output) <= self._max_chars:
                break

            # Find the largest non-protected field
            largest_key: str | None = None
            largest_size = 0

            for key, value in data.items():
                if key in protected_keys or key == "_truncated":
                    continue
                value_str = json.dumps(value, separators=(",", ":"), default=str)
                if len(value_str) > largest_size:
                    largest_size = len(value_str)
                    largest_key = key

            if largest_key is None:
                # No more fields to remove, truncate message as last resort
                if "message" in data and len(str(data["message"])) > 100:
                    data["message"] = str(data["message"])[:100] + "...[truncated]"
                break

            # Remove the largest field
            del data[largest_key]
            removed_keys.append(largest_key)

        # Add truncation notice if fields were removed
        if removed_keys:
            data["_truncated"] = f"removed fields: {', '.join(removed_keys)}"

        return json.dumps(data, separators=(",", ":"), default=str)
