use crate::config::Settings;

/// Guard that flushes remaining spans on drop.
///
/// Must be held alive for the duration of the program.
#[cfg(feature = "telemetry")]
pub struct TelemetryGuard(opentelemetry_sdk::trace::SdkTracerProvider);

#[cfg(feature = "telemetry")]
impl Drop for TelemetryGuard {
    fn drop(&mut self) {
        let _ = self.0.shutdown();
    }
}

/// Build an OpenTelemetry tracing layer that exports spans via OTLP.
///
/// Returns `None` when telemetry is disabled, the endpoint is not configured,
/// or the exporter fails to build (graceful degradation).
///
/// The returned layer includes a per-layer `Targets` filter that only exports
/// spans from the `mika::otel` target. This ensures only LLM-relevant spans
/// (tagged with `target: "mika::otel"`) reach the OTLP endpoint (e.g. Langfuse),
/// filtering out infrastructure spans from tower-http, reqwest, etc.
#[cfg(feature = "telemetry")]
pub fn build_otel_layer(
    settings: &Settings,
) -> Option<(
    impl tracing_subscriber::Layer<tracing_subscriber::Registry> + Send + Sync + use<>,
    TelemetryGuard,
)> {
    use opentelemetry::KeyValue;
    use opentelemetry::trace::TracerProvider;
    use opentelemetry_otlp::{SpanExporter, WithExportConfig, WithHttpConfig};
    use opentelemetry_sdk::Resource;
    use opentelemetry_sdk::trace::SdkTracerProvider;
    use tracing_subscriber::Layer;
    use tracing_subscriber::filter;

    if !settings.telemetry_enabled {
        return None;
    }

    let endpoint = match settings.otlp_endpoint.as_ref() {
        Some(ep) if !ep.trim().is_empty() => ep.clone(),
        _ => {
            tracing::warn!("telemetry_enabled=true but otlp_endpoint is not set, skipping OTel");
            return None;
        }
    };

    // Build OTLP HTTP exporter
    let mut exporter_builder = SpanExporter::builder().with_http().with_endpoint(&endpoint);

    // Add auth header if configured (Langfuse uses Basic auth encoded as Base64).
    // Auto-detect raw "publicKey:secretKey" vs pre-encoded base64: if the value
    // contains ':' it's raw (base64 alphabet never includes ':'), so encode it.
    if let Some(ref auth) = settings.otlp_auth_header {
        use secrecy::ExposeSecret;
        let encoded = normalize_auth_header(auth.expose_secret());
        let mut headers = std::collections::HashMap::new();
        headers.insert("Authorization".to_string(), format!("Basic {encoded}"));
        exporter_builder = exporter_builder.with_headers(headers);
    }

    let exporter = match exporter_builder.build() {
        Ok(e) => e,
        Err(err) => {
            tracing::warn!("failed to build OTLP exporter: {err}, continuing without telemetry");
            return None;
        }
    };

    let provider = SdkTracerProvider::builder()
        .with_batch_exporter(exporter)
        .with_resource(
            Resource::builder()
                .with_attributes([KeyValue::new("service.name", "mika-agent")])
                .build(),
        )
        .build();

    let tracer = provider.tracer("mika");
    let layer = tracing_opentelemetry::layer().with_tracer(tracer);

    // Per-layer filter: only export spans from the "mika::otel" target.
    // This prevents infrastructure spans (tower-http, reqwest, etc.) from
    // reaching Langfuse while allowing LLM call spans and their parent
    // agent_turn/process_message spans through.
    let filtered_layer =
        layer.with_filter(filter::Targets::new().with_target("mika::otel", tracing::Level::INFO));

    Some((filtered_layer, TelemetryGuard(provider)))
}

/// Normalize an OTLP auth header value: if it contains `:` it's a raw
/// `publicKey:secretKey` pair and needs base64 encoding; otherwise it's
/// already encoded and is returned as-is.
#[cfg(feature = "telemetry")]
fn normalize_auth_header(value: &str) -> String {
    use base64::Engine;
    if value.contains(':') {
        base64::engine::general_purpose::STANDARD.encode(value)
    } else {
        value.to_string()
    }
}

/// Convenience wrapper that builds the OTel layer + guard pair, returning
/// uniform types regardless of the `telemetry` feature flag.
///
/// When `telemetry` is enabled: builds the OTLP export layer from settings,
/// returns both the layer and guard as `Option`s.
/// When `telemetry` is disabled: returns `(None, None)` with concrete types
/// that satisfy the generic bounds on `logging::init` / `logging::init_pretty`.
///
/// Callers replace the ~10-line `#[cfg(feature)]` / `#[cfg(not)]` block with:
/// ```ignore
/// let (otel_layer, _telemetry_guard) = mika_common::telemetry::try_init_otel(&settings);
/// ```
#[cfg(feature = "telemetry")]
pub fn try_init_otel(
    settings: &Settings,
) -> (
    Option<impl tracing_subscriber::Layer<tracing_subscriber::Registry> + Send + Sync + use<>>,
    Option<TelemetryGuard>,
) {
    match build_otel_layer(settings) {
        Some((layer, guard)) => (Some(layer), Some(guard)),
        None => (None, None),
    }
}

#[cfg(not(feature = "telemetry"))]
pub fn try_init_otel(_settings: &Settings) -> (Option<crate::logging::NoopLayer>, Option<()>) {
    (None, None)
}

/// No-op when the telemetry feature is not enabled.
#[cfg(not(feature = "telemetry"))]
pub fn build_otel_layer(_settings: &Settings) -> Option<()> {
    None
}

#[cfg(test)]
mod tests {
    use super::*;
    use serial_test::serial;

    #[cfg(feature = "telemetry")]
    #[test]
    fn test_normalize_auth_header_raw_keys() {
        let raw = "pk-lf-abc123:sk-lf-xyz789";
        let result = super::normalize_auth_header(raw);
        // Should be base64-encoded
        assert!(!result.contains(':'));
        // Decode and verify round-trip
        use base64::Engine;
        let decoded = base64::engine::general_purpose::STANDARD
            .decode(&result)
            .unwrap();
        assert_eq!(String::from_utf8(decoded).unwrap(), raw);
    }

    #[cfg(feature = "telemetry")]
    #[test]
    fn test_normalize_auth_header_already_encoded() {
        // Pre-encoded base64 of "pk:sk" — no colon in the encoded form
        use base64::Engine;
        let pre_encoded = base64::engine::general_purpose::STANDARD.encode("pk:sk");
        let result = super::normalize_auth_header(&pre_encoded);
        assert_eq!(result, pre_encoded);
    }

    #[test]
    #[serial]
    fn test_build_otel_layer_disabled() {
        // Safety: tests manipulate env vars; no production thread reads these.
        unsafe {
            std::env::remove_var("MIKA_TELEMETRY_ENABLED");
            std::env::remove_var("MIKA_OTLP_ENDPOINT");
            std::env::remove_var("MIKA_OTLP_AUTH_HEADER");
        }
        let tmp = tempfile::tempdir().unwrap();
        let settings = Settings::load(tmp.path()).unwrap();
        // telemetry_enabled defaults to false
        #[cfg(feature = "telemetry")]
        assert!(build_otel_layer(&settings).is_none());
        #[cfg(not(feature = "telemetry"))]
        assert!(build_otel_layer(&settings).is_none());
    }
}
