"""Structured logging configuration for Katana MCP Server.

This module provides structured logging using structlog with contextual information,
trace IDs, and performance metrics.

Features:
- JSON or text (console) output formats
- Configurable log levels via environment
- Trace ID support for request correlation
- Performance metrics (duration, counts)
- Error context with stack traces
- No sensitive data logging (API keys, credentials)

Environment Variables:
- KATANA_MCP_LOG_LEVEL: Log level (DEBUG, INFO, WARNING, ERROR) - default INFO
- KATANA_MCP_LOG_FORMAT: Output format (json, text) - default json

Example Usage:
    from katana_mcp.logging import get_logger

    logger = get_logger()
    logger.info("tool_executed", tool_name="search_items", result_count=15)
"""

from __future__ import annotations

import logging
import os
import sys
import time
from collections.abc import Callable
from contextvars import ContextVar
from functools import wraps
from typing import Any, TypeVar

import structlog
from structlog.typing import EventDict, WrappedLogger

# Context variable for trace IDs
trace_id_var: ContextVar[str] = ContextVar("trace_id", default="")


def add_trace_id(
    logger: WrappedLogger, method_name: str, event_dict: EventDict
) -> EventDict:
    """Add trace ID to log events if available.

    Args:
        logger: The wrapped logger instance
        method_name: The name of the method being called
        event_dict: The event dictionary to modify

    Returns:
        Modified event dictionary with trace_id if available
    """
    trace_id = trace_id_var.get()
    if trace_id:
        event_dict["trace_id"] = trace_id
    return event_dict


def filter_sensitive_data(
    logger: WrappedLogger, method_name: str, event_dict: EventDict
) -> EventDict:
    """Filter sensitive data from logs.

    Removes or redacts API keys, credentials, and other sensitive information.

    Args:
        logger: The wrapped logger instance
        method_name: The name of the method being called
        event_dict: The event dictionary to modify

    Returns:
        Modified event dictionary with sensitive data filtered
    """
    # List of sensitive keys to redact
    sensitive_keys = {
        "api_key",
        "password",
        "secret",
        "token",
        "auth",
        "credential",
        "authorization",
    }

    # Redact sensitive keys (case-insensitive)
    for key in list(event_dict.keys()):
        if any(sensitive in key.lower() for sensitive in sensitive_keys):
            event_dict[key] = "***REDACTED***"

    return event_dict


def setup_logging(log_level: str | None = None, log_format: str | None = None) -> None:
    """Configure structured logging for MCP server.

    Args:
        log_level: Log level (DEBUG, INFO, WARNING, ERROR).
                  If None, reads from KATANA_MCP_LOG_LEVEL env var.
                  Defaults to INFO.
        log_format: Output format (json or text).
                   If None, reads from KATANA_MCP_LOG_FORMAT env var.
                   Defaults to json.
    """
    # Get configuration from environment or parameters
    level = log_level or os.getenv("KATANA_MCP_LOG_LEVEL", "INFO").upper()
    format_type = log_format or os.getenv("KATANA_MCP_LOG_FORMAT", "json").lower()

    # Validate log level
    valid_levels = {"DEBUG", "INFO", "WARNING", "ERROR"}
    if level not in valid_levels:
        level = "INFO"

    # Configure standard library logging to use structlog
    logging.basicConfig(
        format="%(message)s",
        stream=sys.stderr,
        level=getattr(logging, level),
    )

    # Common processors for all formats
    processors: list[structlog.types.Processor] = [
        structlog.contextvars.merge_contextvars,
        structlog.processors.add_log_level,
        structlog.processors.TimeStamper(fmt="iso", utc=True),
        add_trace_id,
        filter_sensitive_data,
        structlog.processors.StackInfoRenderer(),
    ]

    # Add format-specific processors
    if format_type == "json":
        processors.append(structlog.processors.JSONRenderer())
    else:
        # Text/console format
        processors.extend(
            [
                structlog.processors.ExceptionRenderer(),
                structlog.dev.ConsoleRenderer(),
            ]
        )

    # Configure structlog
    structlog.configure(
        processors=processors,
        wrapper_class=structlog.make_filtering_bound_logger(getattr(logging, level)),
        context_class=dict,
        logger_factory=structlog.stdlib.LoggerFactory(),
        cache_logger_on_first_use=True,
    )


def get_logger(name: str | None = None) -> structlog.stdlib.BoundLogger:
    """Get a structured logger instance.

    Args:
        name: Optional logger name. If None, uses the calling module's name.

    Returns:
        Configured structlog logger instance
    """
    return structlog.get_logger(name)


def set_trace_id(trace_id: str) -> None:
    """Set trace ID for current context.

    Args:
        trace_id: Unique identifier for request tracing
    """
    trace_id_var.set(trace_id)


def clear_trace_id() -> None:
    """Clear trace ID from current context."""
    trace_id_var.set("")


def observe_tool[F: Callable[..., Any]](func: F) -> F:
    """Decorator to add observability to MCP tool functions.

    Logs:
    - Tool invocation with parameters
    - Execution duration
    - Success/failure status
    - Error details if failed

    Usage:
        @observe_tool
        @mcp.tool()
        async def my_tool(param: str, ctx: Context) -> str:
            return "result"
    """

    @wraps(func)
    async def wrapper(*args: Any, **kwargs: Any) -> Any:
        tool_name = func.__name__
        start_time = time.perf_counter()

        # Get logger
        logger = get_logger(__name__)

        # Extract parameters (excluding ctx/context)
        params = {k: v for k, v in kwargs.items() if k not in ("ctx", "context")}

        logger.info(
            "tool_invoked",
            tool_name=tool_name,
            params=params,
        )

        try:
            result = await func(*args, **kwargs)
            duration_ms = (time.perf_counter() - start_time) * 1000

            logger.info(
                "tool_completed",
                tool_name=tool_name,
                duration_ms=round(duration_ms, 2),
                success=True,
            )

            return result

        except Exception as e:
            duration_ms = (time.perf_counter() - start_time) * 1000

            logger.error(
                "tool_failed",
                tool_name=tool_name,
                duration_ms=round(duration_ms, 2),
                error=str(e),
                error_type=type(e).__name__,
                success=False,
            )

            raise

    return wrapper  # type: ignore[return-value]


# Type variable for observe_service decorator
F = TypeVar("F", bound=Callable[..., Any])


def observe_service(operation: str) -> Callable[[F], F]:
    """Decorator to add observability to service layer operations.

    Args:
        operation: Name of the operation (e.g., "get_product", "create_order")

    Usage:
        class MyService:
            @observe_service("get_item")
            async def get(self, item_id: int) -> Item:
                return item
    """

    def decorator(func: F) -> F:
        @wraps(func)
        async def wrapper(*args: Any, **kwargs: Any) -> Any:
            start_time = time.perf_counter()

            # Get logger
            logger = get_logger(__name__)

            # Get class name if available
            class_name = args[0].__class__.__name__ if args else "unknown"

            logger.debug(
                "service_operation_started",
                service=class_name,
                operation=operation,
                params=kwargs,
            )

            try:
                result = await func(*args, **kwargs)
                duration_ms = (time.perf_counter() - start_time) * 1000

                logger.debug(
                    "service_operation_completed",
                    service=class_name,
                    operation=operation,
                    duration_ms=round(duration_ms, 2),
                    success=True,
                )

                return result

            except Exception as e:
                duration_ms = (time.perf_counter() - start_time) * 1000

                logger.error(
                    "service_operation_failed",
                    service=class_name,
                    operation=operation,
                    duration_ms=round(duration_ms, 2),
                    error=str(e),
                    error_type=type(e).__name__,
                    success=False,
                )

                raise

        return wrapper  # type: ignore[return-value]

    return decorator


__all__ = [
    "clear_trace_id",
    "get_logger",
    "observe_service",
    "observe_tool",
    "set_trace_id",
    "setup_logging",
    "trace_id_var",
]
