//! Optional OTLP span exporter, gated behind the `otlp` cargo feature.
//!
//! The default build ships no network exporter (the deliberate
//! "deterministic, no egress unless asked" stance documented in
//! `docs/otel.md`). Building with `--features otlp` and passing
//! `--otel-endpoint <url>` installs an OpenTelemetry batch exporter that
//! drains the `mcptest.run` / `mcp.request` spans this crate already emits
//! to an OTLP/HTTP collector.
//!
//! Lifecycle matters for a short-lived CLI: the batch processor flushes on
//! its own interval, so a run that exits first would drop its spans. The
//! returned [`OtelGuard`] holds the provider and shuts it down (flushing)
//! when it drops, which the binary entry point keeps alive for the whole
//! run.

use anyhow::{Context, Result};
use opentelemetry::trace::TracerProvider as _;
use opentelemetry_otlp::{SpanExporter, WithExportConfig};
use opentelemetry_sdk::trace::SdkTracerProvider;
use opentelemetry_sdk::Resource;
use tracing_subscriber::registry::LookupSpan;
use tracing_subscriber::Layer;

/// Keeps the tracer provider alive for the process and flushes batched spans
/// on drop. Without an explicit shutdown a CLI that exits before the batch
/// interval would silently export nothing.
pub struct OtelGuard {
    provider: SdkTracerProvider,
}

impl Drop for OtelGuard {
    fn drop(&mut self) {
        // Best-effort: the run already produced its report, so a flush failure
        // is a diagnostic warning, not a run failure.
        if let Err(err) = self.provider.shutdown() {
            tracing::warn!(error = %err, "OTLP exporter shutdown (flush) failed");
        }
    }
}

/// Build an OTLP/HTTP tracing layer and its lifecycle guard for `endpoint`.
///
/// `endpoint` is the collector base URL (the standard
/// `OTEL_EXPORTER_OTLP_ENDPOINT` value); the exporter appends `/v1/traces`.
/// Compose the returned layer into a `tracing_subscriber::registry()` and
/// hold the guard until the process exits.
pub fn build_layer<S>(endpoint: &str) -> Result<(impl Layer<S>, OtelGuard)>
where
    S: tracing::Subscriber + for<'a> LookupSpan<'a>,
{
    // `--otel-endpoint` is the OTEL_EXPORTER_OTLP_ENDPOINT base URL, so the
    // signal path is ours to append: the programmatic `.with_endpoint()` is
    // treated as the full traces endpoint and does NOT append `/v1/traces`
    // the way the SDK does for the base env var. Append it (idempotently) so
    // `http://collector:4318` reaches `/v1/traces`, not the collector root.
    let base = endpoint.trim_end_matches('/');
    let traces_endpoint = if base.ends_with("/v1/traces") {
        base.to_string()
    } else {
        format!("{base}/v1/traces")
    };

    let exporter = SpanExporter::builder()
        .with_http()
        .with_endpoint(traces_endpoint)
        .build()
        .context("building OTLP/HTTP span exporter")?;

    let provider = SdkTracerProvider::builder()
        .with_batch_exporter(exporter)
        .with_resource(Resource::builder().with_service_name("mcptest").build())
        .build();

    let tracer = provider.tracer("mcptest");
    let layer = tracing_opentelemetry::layer().with_tracer(tracer);
    Ok((layer, OtelGuard { provider }))
}
