"""FastAPI server-span instrumentation — the proxy mounts this at app creation.

``opentelemetry-instrumentation-fastapi`` creates the SERVER span for each HTTP
route and extracts inbound ``traceparent`` headers. This module owns the one call
site that attaches it to the proxy app, plus the passthrough span-naming hook, so
``proxy_server`` stays free of OTel details.

The ``FastAPIInstrumentor`` import is kept lazy (inside :func:`instrument_fastapi_app`,
after the gate check) so importing this module never requires the optional
``opentelemetry-instrumentation-fastapi`` package and pulls in nothing OTel-related
when the feature gate is off.
"""

import os
from typing import Any

from litellm._logging import verbose_logger
from litellm.integrations.otel.model.config import is_otel_v2_enabled

# Routes excluded from server-span tracing by default: high-frequency pollers and
# static UI/docs assets, none of which are LLM traffic. Entries are substring-matched
# against the request path (unanchored, so they survive a ``server_root_path`` prefix
# and each entry also covers everything beneath it — e.g. ``/health`` covers
# ``/health/readiness``). Operators override the whole set via the standard
# ``OTEL_PYTHON_FASTAPI_EXCLUDED_URLS`` env var (set "" to trace everything).
_DEFAULT_EXCLUDED_ROUTES = (
    "/health",  # load-balancer liveness/readiness polling
    "/metrics",  # Prometheus scrape (also drops the /model/metrics admin analytics)
    "/litellm-asset-prefix",  # hashed UI asset bundles
    "/_next",  # Next.js static JS/CSS chunks (root-level mount)
    "/ui",  # admin UI single-page app
    "/swagger",  # static Swagger UI assets
    "/docs",  # FastAPI Swagger docs page
    "/redoc",  # FastAPI ReDoc docs page
    "/openapi.json",  # OpenAPI schema
    "favicon",  # /favicon.ico + /get_favicon
    "/.well-known",  # UI config discovery
)
_DEFAULT_EXCLUDED_URLS = ",".join(_DEFAULT_EXCLUDED_ROUTES)

# Passthrough routes are catch-alls (e.g. "/openai/{endpoint:path}"), so the
# default OTel server-span name "{method} {route}" collapses every upstream
# endpoint into "POST /openai/{endpoint:path}". The hook below renames those spans
# to the real request path so each endpoint is distinguishable. Non-catch-all
# routes keep their low-cardinality template name.
PASSTHROUGH_PREFIXES = frozenset(
    {
        "openai",
        "openai_passthrough",
        "anthropic",
        "azure",
        "azure_ai",
        "bedrock",
        "cohere",
        "cursor",
        "gemini",
        "mistral",
        "vllm",
        "vertex_ai",
        "vertex-ai",
        "assemblyai",
        "eu.assemblyai",
        "milvus",
    }
)


def _passthrough_span_name_hook(span: Any, scope: dict) -> None:
    """FastAPI ``server_request_hook``: give passthrough server spans a useful name.

    The instrumentation matches the route at span creation, so both the span name
    and ``http.route`` are set to the catch-all template (``/openai/{endpoint:path}``)
    before this hook runs. Rewrite both to the real request path so each upstream
    endpoint is distinguishable. (The ASGI ``http receive``/``http send`` sub-spans
    can't be renamed from here — their name is captured at creation — so they are
    dropped via ``exclude_spans`` at instrumentation time.)
    """
    try:
        if span is None or not span.is_recording():
            return
        path = scope.get("path") or ""
        method = scope.get("method") or ""
        first_segment = path.lstrip("/").split("/", 1)[0]
        if first_segment in PASSTHROUGH_PREFIXES:
            span.update_name(f"{method} {path}".strip())
            span.set_attribute("http.route", path)
    except Exception:
        pass


def instrument_fastapi_app(app: Any) -> None:
    """Attach OTel server-span instrumentation to the proxy FastAPI app.

    Safe no-op when the V2 gate is off or ``opentelemetry-instrumentation-fastapi``
    is unavailable. This MUST be called at app-creation time — once the lifespan
    runs, the middleware stack is frozen and ``instrument_app`` raises "Cannot add
    middleware after an application has started".

    No ``TracerProvider`` is passed, so the instrumentation binds to the OTel global
    ``ProxyTracerProvider``; the proxy publishes the real provider as the global
    after config load (see ``proxy_startup_event``), and the proxy delegates to it.
    That way server spans and gen-ai spans share one provider and the same trace.
    """
    try:
        if not is_otel_v2_enabled():
            return

        # Lazy: only the V2-enabled path needs the optional
        # ``opentelemetry-instrumentation-fastapi`` package, which is not part of the
        # base ``litellm[proxy]`` install. Importing it at module top would make
        # ``proxy_server``'s unconditional ``import`` of this module crash when the
        # package is absent, even with the gate off.
        from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor

        excluded_urls = (
            os.environ.get("OTEL_PYTHON_FASTAPI_EXCLUDED_URLS")
            if "OTEL_PYTHON_FASTAPI_EXCLUDED_URLS" in os.environ
            else _DEFAULT_EXCLUDED_URLS
        )
        FastAPIInstrumentor.instrument_app(
            app,
            excluded_urls=excluded_urls,
            server_request_hook=_passthrough_span_name_hook,
            # Drop the ASGI "http receive"/"http send" lifecycle sub-spans: they
            # are low-value noise and (for passthrough) carry the catch-all route
            # template in their name, which can't be rewritten from a hook.
            exclude_spans=["receive", "send"],
        )
    except Exception as e:
        verbose_logger.debug("Skipping OTel V2 FastAPI instrumentation: %s", e)
