"""
Structured logging for the runner using structlog.

Both local and cloud runners are production. Output format is controlled by
the `json_output` flag:
- Cloud runner (Fly.io): JSON to stdout, captured by fly-log-shipper → Axiom
- Local runner (user machine): JSON to stdout, shipped to /api/logs by log_shipper module
  Falls back to colored console if SKOP_LOG_FORMAT=console is set (for debugging)

Usage:
    from skop_runner.structured_log import configure_logging, get_logger, bind_trace

    configure_logging(level="INFO", json_output=True)
    log = get_logger("file_manager")
    bind_trace("abc-123")
    log.info("file.read", path="/foo/bar.py", size=1234)
"""

import os
import logging
import structlog


def configure_logging(
    level: str = "INFO",
    json_output: bool = False,
    extra_processors: list | None = None,
) -> None:
    """
    Configure structlog for the runner.

    Args:
        level: Log level (DEBUG, INFO, WARNING, ERROR).
        json_output: True for JSON (production), False for colored console (debugging).
        extra_processors: Additional processors inserted before the renderer
                         (e.g., LogShipper.capture for HTTP log shipping).
    """
    # Choose renderer based on mode
    if json_output:
        renderer = structlog.processors.JSONRenderer()
    else:
        renderer = structlog.dev.ConsoleRenderer()

    processors: list = [
        structlog.contextvars.merge_contextvars,
        _add_service,
        structlog.processors.add_log_level,
        structlog.processors.TimeStamper(fmt="iso"),
        structlog.processors.StackInfoRenderer(),
        structlog.processors.format_exc_info,
        *(extra_processors or []),
        renderer,
    ]

    # Configure structlog
    structlog.configure(
        processors=processors,
        wrapper_class=structlog.make_filtering_bound_logger(
            getattr(logging, level.upper(), logging.INFO)
        ),
        logger_factory=structlog.PrintLoggerFactory(),
        cache_logger_on_first_use=True,
    )

    # Also configure stdlib logging so existing logger.info() calls
    # continue to work and go through structlog
    logging.basicConfig(
        format="%(message)s",
        level=getattr(logging, level.upper(), logging.INFO),
    )


def _add_service(
    logger: object, method_name: str, event_dict: dict
) -> dict:
    """Processor that adds service='runner' to every log entry."""
    event_dict["service"] = "runner"
    return event_dict


def get_logger(module: str) -> structlog.BoundLogger:
    """
    Get a logger bound to a specific module name.

    Args:
        module: Module name (e.g., "file_manager", "rpc", "jupyter_backend").

    Returns:
        A structlog BoundLogger with the module field pre-bound.
    """
    return structlog.get_logger(module=module)


def bind_trace(trace_id: str) -> None:
    """
    Bind a trace ID to the current execution context (asyncio task / thread).

    All subsequent log calls in this context will include trace_id.
    Uses structlog's contextvars integration, which is automatically
    isolated per asyncio.Task when using create_task().

    Args:
        trace_id: Trace ID (UUID string) from the client.
    """
    structlog.contextvars.bind_contextvars(trace_id=trace_id)


def unbind_trace() -> None:
    """Remove trace_id from the current execution context."""
    structlog.contextvars.unbind_contextvars("trace_id")


def clear_contextvars() -> None:
    """Clear all contextvars (call at the start of a new request)."""
    structlog.contextvars.clear_contextvars()
