"""Structured logging for Natural Payments SDK.

Supports two modes:
1. Plain text logging (default, for local development)
2. JSON logging with Datadog correlation (when NATURAL_LOG_FORMAT=json)

Features:
- Source info: file, line, function on every log (for error grouping)
- Context binding for request_id, agent_id, instance_id
- Datadog trace correlation fields
"""

from __future__ import annotations

import json
import logging
import os
import sys
from contextvars import ContextVar
from datetime import UTC, datetime
from typing import Any

# SDK version - imported lazily to avoid circular imports
SDK_VERSION = "0.0.4"

# Context variables for request-scoped data
_context_var: ContextVar[dict[str, Any] | None] = ContextVar("natural_context", default=None)


def get_context() -> dict[str, Any]:
    """Get current logging context."""
    ctx = _context_var.get()
    return ctx.copy() if ctx else {}


def _sanitize_log_value(value: Any) -> Any:
    """Sanitize a value for safe logging to prevent log injection.

    Removes/escapes control characters, newlines, and other potentially
    dangerous sequences that could be used for log injection attacks.
    """
    if isinstance(value, str):
        # Remove control characters and newlines that could be used for log injection
        # Keep printable ASCII and common unicode, replace others with escaped form
        sanitized = ""
        for char in value:
            if char in "\r\n":
                sanitized += "\\n"
            elif ord(char) < 32 or ord(char) == 127:
                # Control characters - escape them
                sanitized += f"\\x{ord(char):02x}"
            else:
                sanitized += char
        # Truncate overly long values to prevent log flooding
        max_length = 1000
        if len(sanitized) > max_length:
            sanitized = sanitized[:max_length] + "...[truncated]"
        return sanitized
    return value


def bind_context(**kwargs: Any) -> None:
    """Bind additional context to current scope (e.g., request_id, agent_id).

    Values are sanitized to prevent log injection attacks.

    Example:
        bind_context(request_id="req_123", agent_id="agt_456")
        logger.info("Processing payment")  # Will include request_id and agent_id
    """
    current = _context_var.get() or {}
    sanitized = {k: _sanitize_log_value(v) for k, v in kwargs.items()}
    _context_var.set({**current, **sanitized})


def clear_context() -> None:
    """Clear all bound context."""
    _context_var.set(None)


class StructuredJSONFormatter(logging.Formatter):
    """JSON formatter with source info and context correlation.

    Outputs logs in JSON format with:
    - source: {file, line, function} for error grouping
    - Bound context: request_id, agent_id, instance_id, etc.
    - dd.*: Datadog trace correlation fields
    """

    def __init__(self, service_name: str = "naturalpay-sdk"):
        super().__init__()
        self.service_name = service_name
        self.environment = os.getenv("NATURAL_ENV", os.getenv("DD_ENV", "development"))
        self.version = SDK_VERSION

    def format(self, record: logging.LogRecord) -> str:
        log_record: dict[str, Any] = {
            "timestamp": datetime.now(UTC).isoformat(),
            "level": record.levelname,
            "logger": record.name,
            "message": record.getMessage(),
            "source": {
                "file": record.pathname,
                "line": record.lineno,
                "function": record.funcName,
            },
            "service": self.service_name,
            "environment": self.environment,
            "version": self.version,
        }

        # Add bound context
        context = _context_var.get()
        if context:
            log_record.update(context)

        # Add extra fields from log record (e.g., logger.info("msg", extra={...}))
        if hasattr(record, "__dict__"):
            for key, value in record.__dict__.items():
                if key not in {
                    "name",
                    "msg",
                    "args",
                    "created",
                    "filename",
                    "funcName",
                    "levelname",
                    "levelno",
                    "lineno",
                    "module",
                    "msecs",
                    "pathname",
                    "process",
                    "processName",
                    "relativeCreated",
                    "stack_info",
                    "exc_info",
                    "exc_text",
                    "thread",
                    "threadName",
                    "taskName",
                    "message",
                }:
                    log_record[key] = value

        # Add exception info if present
        if record.exc_info:
            log_record["exception"] = self.formatException(record.exc_info)

        # Datadog trace correlation (if ddtrace is active)
        try:
            import ddtrace  # type: ignore[import-not-found]

            span = ddtrace.tracer.current_span()
            if span:
                log_record["dd.trace_id"] = str(span.trace_id)
                log_record["dd.span_id"] = str(span.span_id)
                log_record["dd.service"] = self.service_name
                log_record["dd.version"] = self.version
                log_record["dd.env"] = self.environment
        except ImportError:
            pass

        return json.dumps(log_record, default=str)


class PlainTextFormatter(logging.Formatter):
    """Human-readable formatter for local development."""

    def format(self, record: logging.LogRecord) -> str:
        # Add context to message if present
        context = _context_var.get()
        context_str = ""
        if context:
            context_parts = [f"{k}={v}" for k, v in context.items()]
            context_str = f" [{', '.join(context_parts)}]"

        # Format: LEVEL logger: message [context]
        base = f"{record.levelname:8s} {record.name}: {record.getMessage()}{context_str}"

        if record.exc_info:
            base += "\n" + self.formatException(record.exc_info)

        return base


def configure_logging(
    level: str | int = logging.INFO,
    json_format: bool | None = None,
    service_name: str = "naturalpay-sdk",
) -> None:
    """Configure logging for the Natural SDK.

    Args:
        level: Log level (DEBUG, INFO, WARNING, ERROR, or int)
        json_format: Use JSON format. If None, auto-detect from NATURAL_LOG_FORMAT env var.
        service_name: Service name for structured logs.

    Environment variables:
        NATURAL_LOG_LEVEL: Override log level (DEBUG, INFO, WARNING, ERROR)
        NATURAL_LOG_FORMAT: Set to "json" for JSON output, "text" for plain text
    """
    # Resolve level from env var if set
    env_level = os.getenv("NATURAL_LOG_LEVEL", "").upper()
    if env_level:
        level = getattr(logging, env_level, logging.INFO)
    elif isinstance(level, str):
        level = getattr(logging, level.upper(), logging.INFO)

    # Resolve format from env var if not explicitly set
    if json_format is None:
        env_format = os.getenv("NATURAL_LOG_FORMAT", "").lower()
        json_format = env_format == "json"

    # Get root naturalpay logger
    logger = logging.getLogger("naturalpay")
    logger.setLevel(level)

    # Remove existing handlers
    logger.handlers.clear()

    # Add handler with appropriate formatter
    handler = logging.StreamHandler(sys.stderr)
    handler.setLevel(level)

    if json_format:
        handler.setFormatter(StructuredJSONFormatter(service_name=service_name))
    else:
        handler.setFormatter(PlainTextFormatter())

    logger.addHandler(handler)

    # Don't propagate to root logger
    logger.propagate = False


def get_logger(name: str) -> logging.Logger:
    """Get a logger for the given module name.

    The logger will be a child of the 'naturalpay' logger, ensuring
    consistent formatting and configuration.

    Example:
        logger = get_logger(__name__)
        logger.info("Payment initiated", extra={"amount": 100})
    """
    if not name.startswith("naturalpay"):
        name = f"naturalpay.{name}"
    return logging.getLogger(name)


# Helper functions for common logging patterns


def log_error(
    logger: logging.Logger,
    message: str,
    *,
    error: Exception | None = None,
    **extra: Any,
) -> None:
    """Log an error with full context and exception info.

    Args:
        logger: The logger to use
        message: Error message
        error: The exception (if any) - will be logged with stack trace
        **extra: Additional context fields
    """
    if error:
        extra["error_type"] = type(error).__name__
        extra["error_message"] = str(error)

        # Add Natural error fields if available
        status_code = getattr(error, "status_code", None)
        if status_code:
            extra["status_code"] = status_code
        code = getattr(error, "code", None)
        if code:
            extra["error_code"] = code

    logger.error(message, extra=extra, exc_info=error is not None)


def log_api_call(
    logger: logging.Logger,
    method: str,
    path: str,
    *,
    status_code: int | None = None,
    duration_ms: float | None = None,
    error: Exception | None = None,
    **extra: Any,
) -> None:
    """Log an API call with standard fields.

    Args:
        logger: The logger to use
        method: HTTP method (GET, POST, etc.)
        path: API path
        status_code: Response status code (if completed)
        duration_ms: Request duration in milliseconds
        error: Exception if the request failed
        **extra: Additional context fields
    """
    extra = {**extra, "method": method, "path": path}

    if status_code:
        extra["status_code"] = status_code
    if duration_ms is not None:
        extra["duration_ms"] = round(duration_ms, 2)

    if error:
        log_error(logger, f"API call failed: {method} {path}", error=error, **extra)
    elif status_code and status_code >= 400:
        logger.warning(f"API call error: {method} {path} -> {status_code}", extra=extra)
    else:
        logger.info(f"API call: {method} {path} -> {status_code}", extra=extra)


def log_tool_call(
    logger: logging.Logger,
    tool_name: str,
    *,
    success: bool | None = None,
    duration_ms: float | None = None,
    error: Exception | None = None,
    **extra: Any,
) -> None:
    """Log an MCP tool invocation.

    Args:
        logger: The logger to use
        tool_name: Name of the MCP tool
        success: Whether the tool call succeeded
        duration_ms: Execution duration in milliseconds
        error: Exception if the tool call failed
        **extra: Additional context fields
    """
    extra["tool_name"] = tool_name

    if duration_ms is not None:
        extra["duration_ms"] = round(duration_ms, 2)

    if error:
        log_error(logger, f"Tool call failed: {tool_name}", error=error, **extra)
    elif success is False:
        logger.warning(f"Tool call returned error: {tool_name}", extra=extra)
    else:
        logger.info(f"Tool call: {tool_name}", extra=extra)


# Auto-configure with defaults on import (can be reconfigured later)
configure_logging()
