"""Custom structlog processors for NoteFlow logging.

Provide context injection, OpenTelemetry integration, and LogBuffer feeding.
"""

from __future__ import annotations

from typing import TYPE_CHECKING, Final

import structlog

# Log field name constants (avoid repeated string literals)
_TRACE_ID: Final = "trace_id"
_SPAN_ID: Final = "span_id"
_PARENT_SPAN_ID: Final = "parent_span_id"
_HEX_32: Final = "032x"
_HEX_16: Final = "016x"

if TYPE_CHECKING:
    from collections.abc import Sequence

    from structlog.typing import EventDict, Processor, WrappedLogger

    from .config import LoggingConfig


def add_noteflow_context(
    logger: WrappedLogger,
    method_name: str,
    event_dict: EventDict,
) -> EventDict:
    """Inject request_id, user_id, workspace_id from context vars.

    Only adds values that are set and not already present in the event.

    Args:
        logger: The wrapped logger instance.
        method_name: Name of the log method called.
        event_dict: Current event dictionary.

    Returns:
        Updated event dictionary with context values.
    """
    from .structured import get_logging_context

    ctx = get_logging_context()
    for key, value in ctx.items():
        if value is not None and key not in event_dict:
            event_dict[key] = value
    return event_dict


def add_otel_trace_context(
    logger: WrappedLogger,
    method_name: str,
    event_dict: EventDict,
) -> EventDict:
    """Inject OpenTelemetry trace/span IDs if available.

    Gracefully handles missing OpenTelemetry installation.

    Args:
        logger: The wrapped logger instance.
        method_name: Name of the log method called.
        event_dict: Current event dictionary.

    Returns:
        Updated event dictionary with trace context.
    """
    try:
        from opentelemetry import trace

        span = trace.get_current_span()
        if span.is_recording():
            ctx = span.get_span_context()
            if ctx.is_valid:
                event_dict[_TRACE_ID] = format(ctx.trace_id, _HEX_32)
                event_dict[_SPAN_ID] = format(ctx.span_id, _HEX_16)
                # Parent span ID if available
                parent = getattr(span, "parent", None)
                if parent is not None:
                    parent_ctx = getattr(parent, _SPAN_ID, None)
                    if parent_ctx is not None:
                        event_dict[_PARENT_SPAN_ID] = format(parent_ctx, _HEX_16)
    except ImportError:
        pass
    except (AttributeError, TypeError):
        # Graceful degradation for edge cases
        pass
    return event_dict


def build_processor_chain(config: LoggingConfig) -> Sequence[Processor]:
    """Build the structlog processor chain based on configuration.

    Args:
        config: Logging configuration.

    Returns:
        Sequence of processors in execution order.
    """
    processors: list[Processor] = [
        # Filter by level early
        structlog.stdlib.filter_by_level,
        # Add standard fields
        structlog.stdlib.add_logger_name,
        structlog.stdlib.add_log_level,
        # Handle %-style formatting from legacy code
        structlog.stdlib.PositionalArgumentsFormatter(),
        # ISO 8601 timestamp
        structlog.processors.TimeStamper(fmt="iso"),
    ]

    # Context injection (optional based on config)
    if config.enable_noteflow_context:
        processors.append(add_noteflow_context)

    if config.enable_otel_context:
        processors.append(add_otel_trace_context)

    # Additional standard processors
    processors.extend([
        # Add callsite information (file, function, line)
        structlog.processors.CallsiteParameterAdder(
            parameters=[
                structlog.processors.CallsiteParameter.FILENAME,
                structlog.processors.CallsiteParameter.FUNC_NAME,
                structlog.processors.CallsiteParameter.LINENO,
            ]
        ),
        # Stack traces if requested
        structlog.processors.StackInfoRenderer(),
        # Exception formatting
        structlog.processors.format_exc_info,
        # Decode bytes to strings
        structlog.processors.UnicodeDecoder(),
    ])

    return processors
