"""OpenTelemetry configuration for NoteFlow.

Provides optional tracing, metrics, and logging instrumentation.
When the observability optional dependency is not installed,
this module gracefully degrades to no-op behavior.
"""

from __future__ import annotations

from collections.abc import Sequence
from contextlib import AbstractContextManager
from functools import cache
from typing import TYPE_CHECKING, Protocol, TypeAlias, cast

from noteflow.infrastructure.logging import get_logger

if TYPE_CHECKING:
    from opentelemetry.trace import Status, StatusCode
    StatusLike: TypeAlias = Status | StatusCode
else:
    StatusLike: TypeAlias = object

logger = get_logger(__name__)

# Track whether OpenTelemetry is available and configured
_otel_configured: bool = False


class _GrpcInstrumentor(Protocol):
    def instrument(self) -> None: ...


@cache
def _check_otel_available() -> bool:
    """Check if OpenTelemetry packages are installed."""
    import importlib.util

    return importlib.util.find_spec("opentelemetry.sdk.trace") is not None


def check_otel_available() -> bool:
    """Public check for OpenTelemetry availability."""
    return _check_otel_available()


# Public constant for checking OTel availability
OTEL_AVAILABLE: bool = check_otel_available()


def is_observability_enabled() -> bool:
    """Check if observability is enabled and configured.

    Returns:
        True if OpenTelemetry is installed and configured.
    """
    return _otel_configured and check_otel_available()


def _configure_otlp_exporter(
    tracer_provider: object,
    otlp_endpoint: str,
    otlp_insecure: bool | None,
) -> None:
    """Configure OTLP span exporter for trace export.

    Args:
        tracer_provider: The TracerProvider to add the exporter to.
        otlp_endpoint: OTLP endpoint URL.
        otlp_insecure: Use insecure connection. If None, infers from scheme.
    """
    try:
        from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
        from opentelemetry.sdk.trace import TracerProvider as SDKTracerProvider
        from opentelemetry.sdk.trace.export import BatchSpanProcessor

        use_insecure = otlp_insecure if otlp_insecure is not None else otlp_endpoint.startswith("http://")
        otlp_exporter = OTLPSpanExporter(endpoint=otlp_endpoint, insecure=use_insecure)
        # Safe cast: caller always passes TracerProvider from same SDK
        cast(SDKTracerProvider, tracer_provider).add_span_processor(BatchSpanProcessor(otlp_exporter))
        logger.info("OTLP trace exporter configured: %s (insecure=%s)", otlp_endpoint, use_insecure)
    except ImportError:
        logger.warning("OTLP exporter not available, traces will not be exported")


def _configure_grpc_instrumentation() -> None:
    """Configure gRPC server auto-instrumentation."""
    try:
        from opentelemetry.instrumentation.grpc import GrpcInstrumentorServer

        instrumentor = cast(_GrpcInstrumentor, GrpcInstrumentorServer())
        instrumentor.instrument()
        logger.info("gRPC server instrumentation enabled")
    except ImportError:
        logger.warning(
            "gRPC instrumentation not available. Install opentelemetry-instrumentation-grpc"
        )


def configure_observability(
    service_name: str = "noteflow",
    *,
    enable_grpc_instrumentation: bool = True,
    otlp_endpoint: str | None = None,
    otlp_insecure: bool | None = None,
) -> bool:
    """Initialize OpenTelemetry trace and metrics providers.

    This function is idempotent and safe to call multiple times.
    If OpenTelemetry packages are not installed, it logs a warning
    and returns False.

    Args:
        service_name: Service name for resource identification.
        enable_grpc_instrumentation: Whether to auto-instrument gRPC.
        otlp_endpoint: Optional OTLP endpoint for exporting telemetry.
        otlp_insecure: Use insecure connection. If None, infers from endpoint
            scheme (http:// = insecure, https:// = secure).

    Returns:
        True if configuration succeeded, False if OTel is not available.
    """
    global _otel_configured

    if _otel_configured:
        logger.debug("OpenTelemetry already configured")
        return True

    if not _check_otel_available():
        logger.info(
            "OpenTelemetry packages not installed. "
            "Install with: pip install noteflow[observability]"
        )
        return False

    try:
        from opentelemetry import metrics, trace
        from opentelemetry.sdk.metrics import MeterProvider
        from opentelemetry.sdk.resources import Resource
        from opentelemetry.sdk.trace import TracerProvider

        resource = Resource.create({"service.name": service_name})
        tracer_provider = TracerProvider(resource=resource)

        if otlp_endpoint:
            _configure_otlp_exporter(tracer_provider, otlp_endpoint, otlp_insecure)

        trace.set_tracer_provider(tracer_provider)
        metrics.set_meter_provider(MeterProvider(resource=resource))

        if enable_grpc_instrumentation:
            _configure_grpc_instrumentation()

        _otel_configured = True
        logger.info("OpenTelemetry configured for service: %s", service_name)
        return True

    except Exception:
        logger.exception("Failed to configure OpenTelemetry")
        return False


def get_tracer(name: str) -> TracerProtocol:
    """Get a tracer instance for the given instrumentation scope.

    If OpenTelemetry is not configured, returns a no-op tracer.

    Args:
        name: Instrumentation scope name (typically module name).

    Returns:
        A Tracer instance (real or no-op).
    """
    if not _check_otel_available():
        # Return a stub that does nothing
        return _NoOpTracer()

    from opentelemetry import trace

    # Cast is safe: OTel Tracer implements start_span and start_as_current_span
    return cast(TracerProtocol, trace.get_tracer(name))


AttributeValue = str | bool | int | float | Sequence[str] | Sequence[bool] | Sequence[int] | Sequence[float]


class SpanProtocol(Protocol):
    """Protocol for span operations used by NoteFlow."""

    def set_attribute(self, key: str, value: AttributeValue) -> None:
        """Set a span attribute."""
        ...

    def add_event(
        self, name: str, attributes: dict[str, AttributeValue] | None = None
    ) -> None:
        """Add an event to the span."""
        ...

    def record_exception(self, exception: BaseException) -> None:
        """Record an exception on the span."""
        ...

    def set_status(self, status: StatusLike) -> None:
        """Set the span status."""
        ...


class TracerProtocol(Protocol):
    """Protocol for tracer operations used by NoteFlow."""

    def start_as_current_span(
        self, name: str, **kwargs: object
    ) -> AbstractContextManager[SpanProtocol]:
        """Start a span as the current span in context."""
        ...

    def start_span(self, name: str, **kwargs: object) -> SpanProtocol:
        """Start a new span."""
        ...


class _NoOpSpan:
    """No-op span for when OTel is not available."""

    def __enter__(self) -> _NoOpSpan:
        return self

    def __exit__(self, *args: object) -> None:
        pass

    def set_attribute(self, key: str, value: AttributeValue) -> None:
        """No-op."""

    def add_event(
        self, name: str, attributes: dict[str, AttributeValue] | None = None
    ) -> None:
        """No-op."""

    def record_exception(self, exception: BaseException) -> None:
        """No-op."""

    def set_status(self, status: StatusLike) -> None:
        """No-op."""


class _NoOpSpanContext:
    """Context manager that yields a no-op span."""

    def __init__(self) -> None:
        self._span = _NoOpSpan()

    def __enter__(self) -> _NoOpSpan:
        return self._span

    def __exit__(self, *args: object) -> None:
        pass


class _NoOpTracer:
    """No-op tracer for when OTel is not available."""

    def start_as_current_span(self, name: str, **kwargs: object) -> _NoOpSpanContext:
        """Return a no-op span context manager."""
        return _NoOpSpanContext()

    def start_span(self, name: str, **kwargs: object) -> _NoOpSpan:
        """Return a no-op span."""
        return _NoOpSpan()
