"""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 'payload' field (default: 10240 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 max chars for payload truncation (10240).
    """

    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 'payload' field.
                If payload exceeds this limit, it will be truncated with
                a '[truncated]' indicator. Defaults to DEFAULT_MAX_CHARS
                (10240). Set to 0 to disable truncation.
        """
        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.

        The 'payload' field is automatically truncated if it exceeds max_chars
        (unless root logger is set to DEBUG).

        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):
            # Build message with context prefix
            base_message = details.get("message") or str(record.msg)
            log_data["message"] = self._build_message_with_context(
                base_message, details
            )

            # 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 key == "payload" and value is not None:
                    # Truncate payload if it exceeds max_chars (when not in DEBUG)
                    log_data[key] = self._maybe_truncate_payload(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
        return json.dumps(log_data, separators=(",", ":"), default=str)

    def _maybe_truncate_payload(self, value: Any) -> Any:
        """Truncate payload value if it exceeds max_chars.

        Only truncates when root logger level is not DEBUG, allowing full
        payloads during development.

        Args:
            value: The payload value to potentially truncate.

        Returns:
            The original value, or a truncated string if oversized.
        """
        # Skip truncation if disabled or in DEBUG mode
        if self._max_chars <= 0:
            return value

        root_level = logging.getLogger().level
        if root_level == logging.DEBUG:
            return value

        # Convert to JSON string to measure size
        payload_str = json.dumps(value, separators=(",", ":"), default=str)

        if len(payload_str) <= self._max_chars:
            return value

        # Truncate and add indicator
        return payload_str[: self._max_chars] + "...[truncated]"

    def _build_message_with_context(
        self, message: str, details: dict[str, Any]
    ) -> str:
        """Build message with subject, dealer_id, and user_id prefix.

        Prepends context information to the message for better log readability.
        Format: [subject] [dealer:xxx] [user:xxx] message

        Args:
            message: The base log message.
            details: The details dictionary containing optional context fields.

        Returns:
            The message with context prefix if any context fields exist.
        """
        prefix_parts: list[str] = []

        # Add subject if present and not default
        subject = details.get("subject")
        if subject and subject != "not_specified":
            prefix_parts.append(subject)

        # Add dealer_id if present
        dealer_id = details.get("dealer_id")
        if dealer_id is not None:
            dealer_str = str(dealer_id)
            prefix_parts.append(f"[dealer:{dealer_str}]")

        # Add user_id if present
        user_id = details.get("user_id")
        if user_id is not None:
            user_str = str(user_id)
            prefix_parts.append(f"[user:{user_str}]")

        if prefix_parts:
            return f"{' - '.join(prefix_parts)} - {message}"
        return message
