import json
import logging
import traceback
from datetime import datetime, timezone
from logging.config import dictConfig
from pathlib import Path
from sys import stdout
from typing import Any, Optional

from letta.log_context import get_log_context
from letta.settings import log_settings, settings, telemetry_settings

selected_log_level = logging.DEBUG if settings.debug else logging.INFO


class JSONFormatter(logging.Formatter):
    """
    Custom JSON formatter for structured logging with Datadog integration.

    Outputs logs in JSON format with fields compatible with Datadog log ingestion.
    Automatically includes trace correlation fields when Datadog tracing is enabled.

    Usage:
        Enable JSON logging by setting the environment variable:
            LETTA_LOGGING_JSON_LOGGING=true

        Add custom structured fields to logs using the 'extra' parameter:
            logger.info("User action", extra={"user_id": "123", "action": "login"})

        These fields will be automatically included in the JSON output and
        indexed by Datadog for filtering and analysis.

    Output format:
        {
            "timestamp": "2025-10-23T18:34:24.931739+00:00",
            "level": "INFO",
            "logger": "Letta.module",
            "message": "Log message",
            "module": "module_name",
            "function": "function_name",
            "line": 123,
            "dd.trace_id": "1234567890",  # Added when Datadog tracing is enabled
            "dd.span_id": "9876543210",   # Added when Datadog tracing is enabled
            "custom_field": "custom_value" # Any extra fields you provide
        }
    """

    def format(self, record: logging.LogRecord) -> str:
        """Format log record as JSON with Datadog-compatible fields."""
        # Base log structure
        log_data: dict[str, Any] = {
            "timestamp": datetime.fromtimestamp(record.created, tz=timezone.utc).isoformat(),
            "level": record.levelname,
            "logger": record.name,
            "message": record.getMessage(),
            "module": record.module,
            "function": record.funcName,
            "line": record.lineno,
        }

        # Add Datadog trace correlation if available
        # ddtrace automatically injects these attributes when logging is patched
        if hasattr(record, "dd.trace_id"):
            log_data["dd.trace_id"] = getattr(record, "dd.trace_id")
        if hasattr(record, "dd.span_id"):
            log_data["dd.span_id"] = getattr(record, "dd.span_id")
        if hasattr(record, "dd.service"):
            log_data["dd.service"] = getattr(record, "dd.service")
        if hasattr(record, "dd.env"):
            log_data["dd.env"] = getattr(record, "dd.env")
        if hasattr(record, "dd.version"):
            log_data["dd.version"] = getattr(record, "dd.version")

        # Add OpenTelemetry trace correlation (for OTEL → Datadog integration)
        try:
            from opentelemetry import trace

            span = trace.get_current_span()
            if span and span.get_span_context().is_valid:
                ctx = span.get_span_context()
                # Format trace_id and span_id as Datadog expects (decimal strings)
                log_data["dd.trace_id"] = str(ctx.trace_id)
                log_data["dd.span_id"] = str(ctx.span_id)
        except Exception:
            # Fail silently if OTEL is not available
            pass

        # Add exception info if present
        if record.exc_info:
            log_data["exception"] = {
                "type": record.exc_info[0].__name__ if record.exc_info[0] else None,
                "message": str(record.exc_info[1]) if record.exc_info[1] else None,
                "stacktrace": "".join(traceback.format_exception(*record.exc_info)),
            }

        # Add any extra fields from the log record
        # These are custom fields passed via logging.info("msg", extra={...})
        for key, value in record.__dict__.items():
            if key not in [
                "name",
                "msg",
                "args",
                "created",
                "filename",
                "funcName",
                "levelname",
                "levelno",
                "lineno",
                "module",
                "msecs",
                "message",
                "pathname",
                "process",
                "processName",
                "relativeCreated",
                "thread",
                "threadName",
                "exc_info",
                "exc_text",
                "stack_info",
                "dd_env",
                "dd_service",
            ] and not key.startswith("dd."):
                log_data[key] = value

        return json.dumps(log_data, default=str)


class DatadogEnvFilter(logging.Filter):
    """
    Logging filter that adds Datadog-specific attributes to log records.

    This enables log-trace correlation by injecting environment and service metadata
    that Datadog can use to link logs with traces and other telemetry data.
    """

    def filter(self, record: logging.LogRecord) -> bool:
        """Add Datadog attributes to log record if Datadog is enabled."""
        if telemetry_settings.enable_datadog:
            record.dd_env = settings.environment or "development"
            record.dd_service = telemetry_settings.datadog_service_name
        else:
            # Provide defaults to prevent attribute errors if filter is applied incorrectly
            record.dd_env = ""
            record.dd_service = ""
        return True


class LogContextFilter(logging.Filter):
    """
    Logging filter that enriches log records with request context.

    Injects context-specific attributes like actor_id, agent_id, org_id, etc.
    into log records. These attributes are stored in a context variable
    and automatically included in all log messages within that context.

    This enables correlation of logs with specific requests, agents, and users
    in monitoring systems like Datadog.

    Usage:
        from letta.log_context import set_log_context, update_log_context

        # Set a single context value
        set_log_context("agent_id", "agent-123")

        # Set multiple context values
        update_log_context(agent_id="agent-123", actor_id="user-456")

        # All subsequent logs will include these attributes
        logger.info("Processing request")
        # Output: {"message": "Processing request", "agent_id": "agent-123", "actor_id": "user-456", ...}
    """

    def filter(self, record: logging.LogRecord) -> bool:
        """Add request context attributes to log record."""
        context = get_log_context()
        for key, value in context.items():
            if not hasattr(record, key):
                setattr(record, key, value)
        return True


def _setup_logfile() -> "Path":
    """ensure the logger filepath is in place

    Returns: the logfile Path
    """
    logfile = Path(settings.letta_dir / "logs" / "Letta.log")
    logfile.parent.mkdir(parents=True, exist_ok=True)
    logfile.touch(exist_ok=True)
    return logfile


# Determine which formatter to use based on configuration
def _get_console_formatter() -> str:
    """Determine the appropriate console formatter based on settings."""
    if log_settings.json_logging:
        return "json"
    elif telemetry_settings.enable_datadog:
        return "datadog"
    else:
        return "no_datetime"


def _get_file_formatter() -> str:
    """Determine the appropriate file formatter based on settings."""
    if log_settings.json_logging:
        return "json"
    elif telemetry_settings.enable_datadog:
        return "datadog"
    else:
        return "standard"


# Logging configuration with optional Datadog integration and JSON support
DEVELOPMENT_LOGGING = {
    "version": 1,
    "disable_existing_loggers": False,  # Allow capturing from all loggers
    "formatters": {
        "standard": {"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"},
        "no_datetime": {"format": "%(name)s - %(levelname)s - %(message)s"},
        "datadog": {
            # Datadog-compatible format with key=value pairs for better parsing
            # ddtrace's log injection will add dd.trace_id, dd.span_id automatically when logging is patched
            "format": "%(asctime)s - %(name)s - %(levelname)s - [dd.env=%(dd_env)s dd.service=%(dd_service)s] - %(message)s"
        },
        "json": {
            # JSON formatter for structured logging with full Datadog integration
            "()": JSONFormatter,
        },
    },
    "filters": {
        "datadog_env": {
            "()": DatadogEnvFilter,
        },
        "log_context": {
            "()": LogContextFilter,
        },
    },
    "handlers": {
        "console": {
            "level": selected_log_level,
            "class": "logging.StreamHandler",
            "stream": stdout,
            "formatter": _get_console_formatter(),
            "filters": (["datadog_env"] if telemetry_settings.enable_datadog and not log_settings.json_logging else []) + ["log_context"],
        },
        "file": {
            "level": "DEBUG",
            "class": "logging.handlers.RotatingFileHandler",
            "filename": _setup_logfile(),
            "maxBytes": 1024**2 * 10,  # 10 MB per file
            "backupCount": 3,  # Keep 3 backup files
            "formatter": _get_file_formatter(),
            "filters": (["datadog_env"] if telemetry_settings.enable_datadog and not log_settings.json_logging else []) + ["log_context"],
        },
    },
    "root": {  # Root logger handles all logs
        "level": logging.DEBUG if settings.debug else logging.INFO,
        "handlers": ["console", "file"],
    },
    "loggers": {
        "Letta": {
            "level": logging.DEBUG if settings.debug else logging.INFO,
            "propagate": True,  # Let logs bubble up to root
        },
        "uvicorn": {
            "level": "CRITICAL",
            "handlers": ["console"],
            "propagate": True,
        },
        # Reduce noise from ddtrace internal logging
        "ddtrace": {
            "level": "WARNING",
            "propagate": True,
        },
    },
}

# Configure logging once at module initialization to avoid performance overhead
dictConfig(DEVELOPMENT_LOGGING)


def get_logger(name: Optional[str] = None) -> "logging.Logger":
    """returns the project logger, scoped to a child name if provided
    Args:
        name: will define a child logger
    """
    parent_logger = logging.getLogger("Letta")
    if name:
        return parent_logger.getChild(name)
    return parent_logger
