import atexit
import os
import threading
from collections.abc import Callable
from typing import Any, cast
from urllib.parse import urlsplit

from anyio.to_thread import run_sync
from fastapi.exceptions import FastAPIError
from fastapi.logger import logger
from fastapi.telemetry._api import TelemetryConfig, _unconfigured
from opentelemetry import _logs, metrics, trace
from starlette.types import ASGIApp, Message, Receive, Scope, Send

_lock = threading.RLock()
_owned: list[Any] = []
_configured: list[tuple[str, Any]] = []


def _flush() -> None:
    for component in tuple(_owned):
        try:
            component.force_flush()
        except Exception:
            logger.exception("FastAPI telemetry cleanup failed")


def _shutdown() -> None:
    with _lock:
        owned = tuple(_owned)
        _owned.clear()
    for component in owned:
        try:
            component.shutdown()
        except Exception:
            logger.exception("FastAPI telemetry cleanup failed")


atexit.register(_shutdown)


def _export_endpoint(signal: str) -> str | None:
    exporter = (os.getenv(f"OTEL_{signal}_EXPORTER") or "otlp").strip().lower()
    endpoint = os.getenv(f"OTEL_EXPORTER_OTLP_{signal}_ENDPOINT")
    if not endpoint:
        endpoint = os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT")
        if endpoint:
            endpoint = f"{endpoint.removesuffix('/')}/v1/{signal.lower()}"
    if not endpoint or exporter == "none":
        return None
    if exporter != "otlp":
        raise FastAPIError(
            f"FastAPI automatic telemetry supports OTEL_{signal}_EXPORTER=otlp or none. "
            "Configure other exporters explicitly and pass telemetry={'auto_configure': False} to FastAPI()."
        )
    protocol = (
        os.getenv(f"OTEL_EXPORTER_OTLP_{signal}_PROTOCOL")
        or os.getenv("OTEL_EXPORTER_OTLP_PROTOCOL")
        or "http/protobuf"
    )
    if protocol != "http/protobuf":
        raise FastAPIError(
            "FastAPI automatic telemetry requires the OTLP http/protobuf protocol. Configure other transports explicitly and pass telemetry={'auto_configure': False} to FastAPI()."
        )
    parsed = urlsplit(endpoint)
    if parsed.scheme not in {"http", "https"} or not parsed.hostname:
        raise FastAPIError(
            f"Invalid OTLP {signal.lower()} endpoint. Use an absolute HTTP or HTTPS URL."
        )
    return endpoint


def _registration_provider(provider: Any) -> Any:
    # Older Logfire meter wrappers do not expose add_metric_reader(). Attach
    # the reader to their SDK provider. Measurements still use the original wrapper.
    if (
        type(provider).__module__ == "logfire._internal.metrics"
        and type(provider).__name__ == "ProxyMeterProvider"
        and not hasattr(provider, "add_metric_reader")
    ):
        return provider.provider
    return provider


def _configure_from_environment(config: TelemetryConfig) -> None:
    """Add OTLP exporters from the environment to the selected providers.

    Called automatically before ASGI lifespan startup. Existing providers and
    their exporters are preserved. FastAPI registers its own export components once
    per provider. It does not inspect or deduplicate other components' exporters.
    Enable with `FASTAPI_OTEL_AUTO_CONFIGURE=true` or pass
    `telemetry={"auto_configure": True}` to `FastAPI()`.
    """
    if (
        not config["auto_configure"]
        or os.getenv("OTEL_SDK_DISABLED", "").lower() == "true"
    ):
        return
    with _lock:
        signals = [
            ("TRACES", config["tracing"], config["tracer_provider"]),
            ("METRICS", config["metrics"], config["meter_provider"]),
            ("LOGS", config["logs"], config["logger_provider"]),
        ]
        requested = [
            (signal, provider, endpoint)
            for signal, enabled, provider in signals
            if enabled and (endpoint := _export_endpoint(signal)) is not None
        ]
        if not requested:
            return
        try:
            from opentelemetry.exporter.otlp.proto.http._log_exporter import (
                OTLPLogExporter,
            )
            from opentelemetry.exporter.otlp.proto.http.metric_exporter import (
                OTLPMetricExporter,
            )
            from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
                OTLPSpanExporter,
            )
            from opentelemetry.sdk._logs import LoggerProvider
            from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
            from opentelemetry.sdk.metrics import MeterProvider
            from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
            from opentelemetry.sdk.trace import TracerProvider
            from opentelemetry.sdk.trace.export import BatchSpanProcessor
        except ImportError as exc:
            raise FastAPIError(
                "Automatic OpenTelemetry export requires fastapi[opentelemetry] or fastapi[standard]. Install the extra, configure providers yourself, or pass telemetry={'auto_configure': False} to FastAPI()."
            ) from exc
        # The SDK's registration methods also allow vendor wrappers to forward
        # additions. Keep a record of our own registrations, without examining
        # another component's exporters or their destinations.
        providers = {
            "TRACES": ("tracer", trace.get_tracer_provider),
            "METRICS": ("meter", metrics.get_meter_provider),
            "LOGS": ("logger", _logs.get_logger_provider),
        }

        def create_component(*, signal: str, endpoint: str) -> Any:
            if signal == "TRACES":
                return BatchSpanProcessor(OTLPSpanExporter(endpoint=endpoint))
            if signal == "METRICS":
                return PeriodicExportingMetricReader(
                    OTLPMetricExporter(endpoint=endpoint)
                )
            return BatchLogRecordProcessor(OTLPLogExporter(endpoint=endpoint))

        for signal, explicit, endpoint in requested:
            name, get_provider = providers[signal]
            provider = _registration_provider(
                explicit if explicit is not None else get_provider()
            )
            if any(s == signal and p is provider for s, p in _configured):
                continue
            if explicit is None and _unconfigured(provider):
                component = create_component(signal=signal, endpoint=endpoint)
                if signal == "TRACES":
                    provider = TracerProvider(shutdown_on_exit=False)
                    provider.add_span_processor(component)
                    trace.set_tracer_provider(provider)
                elif signal == "METRICS":
                    provider = MeterProvider(
                        metric_readers=[component], shutdown_on_exit=False
                    )
                    metrics.set_meter_provider(provider)
                else:
                    provider = LoggerProvider(shutdown_on_exit=False)
                    provider.add_log_record_processor(component)
                    _logs.set_logger_provider(provider)
                current = get_provider()
                if current is provider:
                    _configured.append((signal, provider))
                    _owned.append(provider)
                    continue
                # Another component configured the global provider concurrently.
                # Close the unused pipeline and attach a new component to theirs.
                provider.shutdown()
                provider = _registration_provider(current)
            # Vendor wrappers can expose the SDK registration methods without
            # inheriting its provider classes. Check each method against its SDK
            # class statically and its availability on the provider at runtime.
            register: Callable[[Any], None] | None
            try:
                if signal == "TRACES":
                    register = cast(TracerProvider, provider).add_span_processor
                elif signal == "METRICS":
                    register = cast(MeterProvider, provider).add_metric_reader
                else:
                    register = cast(LoggerProvider, provider).add_log_record_processor
            except AttributeError:
                register = None
            if not callable(register):
                raise FastAPIError(
                    f"The configured OpenTelemetry {name} provider does not support "
                    "adding an OTLP exporter. Configure environment export through "
                    "that provider and pass telemetry={'auto_configure': False} to FastAPI()."
                )
            component = create_component(signal=signal, endpoint=endpoint)
            try:
                register(component)
            except Exception:
                component.shutdown()
                raise
            _configured.append((signal, provider))
            _owned.append(component)


async def lifespan(
    *, config: TelemetryConfig, app: ASGIApp, scope: Scope, receive: Receive, send: Send
) -> None:
    async def wrapped_receive() -> Message:
        message = await receive()
        if message["type"] == "lifespan.startup":
            try:
                _configure_from_environment(config)
            except Exception as exc:
                # Optional telemetry setup must not prevent application startup.
                logger.warning(
                    "FastAPI automatic telemetry configuration failed: %s", exc
                )
        return message

    async def wrapped_send(message: Message) -> None:
        if (
            message["type"]
            in {
                "lifespan.shutdown.complete",
                "lifespan.shutdown.failed",
                "lifespan.startup.failed",
            }
            and _owned
        ):
            await run_sync(_flush)
        await send(message)

    await app(scope, wrapped_receive, wrapped_send)
