use crate::observability::config::{FuzzyRuleKind, LogFilterConfig};
use crate::observability::reload::ObservabilityReloadHandle;
use anyhow::{anyhow, Context, Result};
use parking_lot::RwLock;
use serde::Serialize;
use std::sync::OnceLock;

static GLOBAL_FILTER_EXPORT: OnceLock<RwLock<Option<ObservabilityReloadHandle>>> = OnceLock::new();

/// Version of the JSON schema returned by the JavaScript filter export API.
pub const JS_LOG_FILTER_SCHEMA_VERSION: u32 = 1;
/// Version of the JSON schema returned by the JavaScript trace filter export API.
pub const JS_TRACE_FILTER_SCHEMA_VERSION: u32 = 1;

/// Snapshot of the currently effective ordinary-log filters for JavaScript.
///
/// JavaScript may use this snapshot to reject logs before crossing into Rust.
/// It must only reject a log when every enabled output scope can conclusively
/// reject it. Trace and metrics filters are intentionally not included.
#[derive(Debug, Clone, Serialize)]
pub struct JsLogFilterSnapshot {
    /// Export schema version. Consumers must fail open for unknown versions.
    pub schema_version: u32,
    /// Monotonic process-local revision changed after a successful filter reload.
    pub revision: u64,
    /// Console/local-file output scope, when either output was enabled at startup.
    pub local: Option<JsLogFilterScope>,
    /// Remote OpenTelemetry logs output scope, when enabled at startup.
    pub remote: Option<JsLogFilterScope>,
}

/// Snapshot of the currently effective trace filter for JavaScript.
///
/// JavaScript may use this snapshot to reject trace spans before crossing into
/// Rust. It must fail open for unknown schema versions and unsupported rule
/// shapes. The Rust trace API remains the final authority before creating an
/// OpenTelemetry span.
#[derive(Debug, Clone, Serialize)]
pub struct JsTraceFilterSnapshot {
    /// Export schema version. Consumers must fail open for unknown versions.
    pub schema_version: u32,
    /// Monotonic process-local revision changed after a successful filter reload.
    pub revision: u64,
    /// Trace output scope, when OpenTelemetry trace export was enabled at startup.
    pub trace: Option<JsLogFilterScope>,
}

/// One enabled ordinary-log output scope exported for JavaScript pre-filtering.
#[derive(Debug, Clone, Serialize)]
pub struct JsLogFilterScope {
    /// Indicates whether some rules require information unavailable to a
    /// `level + target` JavaScript pre-filter.
    ///
    /// When true, JavaScript must fail open whenever it cannot conclusively
    /// evaluate a span rule or a target-matched override containing field rules.
    pub requires_rust_evaluation: bool,
    /// Current effective ordered filter configuration for this output scope.
    pub filter: LogFilterConfig,
}

impl JsLogFilterScope {
    fn from_filter(filter: LogFilterConfig) -> Self {
        let requires_rust_evaluation = filter
            .overrides
            .iter()
            .filter(|rule| rule.enabled.unwrap_or(true))
            .any(|rule| {
                !rule.field_rules.is_empty()
                    || rule
                        .fuzzy_rules
                        .iter()
                        .any(|rule| rule.kind == FuzzyRuleKind::Span)
            });
        Self {
            requires_rust_evaluation,
            filter,
        }
    }
}

impl ObservabilityReloadHandle {
    /// Exports the currently effective ordinary-log filters for JavaScript.
    ///
    /// The snapshot contains only enabled local/console and remote-log output
    /// scopes. JavaScript must treat unknown schema versions, span rules,
    /// field-dependent rules, and unsupported operators as "allow into Rust".
    /// The Rust observability filter remains the final authority.
    pub fn export_js_log_filter_snapshot(&self) -> JsLogFilterSnapshot {
        loop {
            let revision = self.current_filter_revision();
            let local = self
                .local_output_enabled()
                .then(|| JsLogFilterScope::from_filter(self.local().current_filter_config()));
            let remote = self
                .remote()
                .ok()
                .map(|scope| JsLogFilterScope::from_filter(scope.current_filter_config()));
            if revision == self.current_filter_revision() {
                return JsLogFilterSnapshot {
                    schema_version: JS_LOG_FILTER_SCHEMA_VERSION,
                    revision,
                    local,
                    remote,
                };
            }
        }
    }

    /// Serializes the current JavaScript log pre-filter snapshot as JSON.
    ///
    /// This is intended for FFI layers that prefer transferring one UTF-8 JSON
    /// string instead of binding every Rust configuration type. Serialization
    /// errors are returned and never replace the active observability filters.
    pub fn export_js_log_filter_json(&self) -> Result<String> {
        serde_json::to_string(&self.export_js_log_filter_snapshot())
            .context("failed to serialize JavaScript log filter snapshot")
    }

    /// Exports the currently effective trace filter for JavaScript.
    ///
    /// The snapshot contains only the trace output scope. JavaScript must treat
    /// unknown schema versions, span rules, field-dependent rules, and
    /// unsupported operators as "allow into Rust". The Rust trace API remains
    /// the final authority before creating an OpenTelemetry span.
    pub fn export_js_trace_filter_snapshot(&self) -> JsTraceFilterSnapshot {
        loop {
            let revision = self.current_filter_revision();
            let trace = self
                .trace()
                .ok()
                .map(|scope| JsLogFilterScope::from_filter(scope.current_filter_config()));
            if revision == self.current_filter_revision() {
                return JsTraceFilterSnapshot {
                    schema_version: JS_TRACE_FILTER_SCHEMA_VERSION,
                    revision,
                    trace,
                };
            }
        }
    }

    /// Serializes the current JavaScript trace pre-filter snapshot as JSON.
    pub fn export_js_trace_filter_json(&self) -> Result<String> {
        serde_json::to_string(&self.export_js_trace_filter_snapshot())
            .context("failed to serialize JavaScript trace filter snapshot")
    }
}

/// Returns the revision of the globally initialized JavaScript log filter.
///
/// This inexpensive function is intended for JavaScript/FFI integrations that
/// do not retain the [`ObservabilityReloadHandle`] returned during
/// initialization. The revision changes after every successful observability
/// filter reload. It returns an error before observability initialization and
/// after [`crate::observability::ObservabilityGuards::shutdown`] clears the
/// global export state.
pub fn current_js_log_filter_revision() -> Result<u64> {
    Ok(global_filter_export()?.current_filter_revision())
}

/// Exports the globally initialized ordinary-log filters for JavaScript.
///
/// The returned snapshot follows the same conservative pre-filtering contract
/// as [`ObservabilityReloadHandle::export_js_log_filter_snapshot`]. JavaScript
/// must fail open for rules it cannot conclusively evaluate. This function
/// returns an error before observability initialization and after shutdown.
pub fn export_js_log_filter_snapshot() -> Result<JsLogFilterSnapshot> {
    Ok(global_filter_export()?.export_js_log_filter_snapshot())
}

/// Serializes the globally initialized JavaScript log pre-filter snapshot.
///
/// This is the primary API for JavaScript/FFI bindings: callers can poll
/// [`current_js_log_filter_revision`] and fetch this JSON only after the
/// revision changes. The JSON snapshot contains its own revision, which should
/// be treated as authoritative if a reload races with polling.
pub fn export_js_log_filter_json() -> Result<String> {
    global_filter_export()?.export_js_log_filter_json()
}

/// Returns the revision of the globally initialized JavaScript trace filter.
///
/// The revision is shared with ordinary log filters because observability
/// reloads update local, remote, and trace filters as one coherent snapshot.
pub fn current_js_trace_filter_revision() -> Result<u64> {
    Ok(global_filter_export()?.current_filter_revision())
}

/// Exports the globally initialized trace filter for JavaScript.
pub fn export_js_trace_filter_snapshot() -> Result<JsTraceFilterSnapshot> {
    Ok(global_filter_export()?.export_js_trace_filter_snapshot())
}

/// Serializes the globally initialized JavaScript trace pre-filter snapshot.
pub fn export_js_trace_filter_json() -> Result<String> {
    global_filter_export()?.export_js_trace_filter_json()
}

/// Returns whether a JavaScript-owned trace span should be recorded.
///
/// This function is the Rust-side authority used by FFI bridges before they
/// create OpenTelemetry spans directly. It evaluates target/span level rules
/// from the active trace filter. If trace export is disabled, the span is
/// rejected. Field-dependent rules cannot be evaluated at span creation time.
pub fn js_trace_enabled(level: &str, target: &str, span: &str) -> Result<bool> {
    let reload = global_filter_export()?;
    let trace = match reload.trace() {
        Ok(trace) => trace,
        Err(_) => return Ok(false),
    };
    trace.enabled_for_target_span(level, target, span)
}

pub(crate) fn install_global_filter_export(reload: ObservabilityReloadHandle) {
    *global_filter_export_registry().write() = Some(reload);
}

pub(crate) fn clear_global_filter_export() {
    *global_filter_export_registry().write() = None;
}

fn global_filter_export() -> Result<ObservabilityReloadHandle> {
    global_filter_export_registry()
        .read()
        .clone()
        .ok_or_else(|| anyhow!("global observability filter export is not initialized"))
}

fn global_filter_export_registry() -> &'static RwLock<Option<ObservabilityReloadHandle>> {
    GLOBAL_FILTER_EXPORT.get_or_init(|| RwLock::new(None))
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::observability::config::{
        FieldRuleConfig, FieldRuleOp, FuzzyMatchType, FuzzyRuleConfig, LogFilterOverrideConfig,
    };
    use crate::observability::filter::SharedOrderedFilter;
    use crate::observability::reload::FilterReloadHandle;
    use serde_json::json;

    #[test]
    fn snapshot_exports_only_enabled_log_outputs() {
        let reload = handle(true, false);
        let snapshot = reload.export_js_log_filter_snapshot();

        assert_eq!(snapshot.schema_version, JS_LOG_FILTER_SCHEMA_VERSION);
        assert!(snapshot.local.is_some());
        assert!(snapshot.remote.is_none());
    }

    #[test]
    fn snapshot_marks_span_and_field_rules_for_rust_evaluation() {
        let filter = LogFilterConfig {
            default_level: "info".to_string(),
            overrides: vec![LogFilterOverrideConfig {
                name: "field_rule".to_string(),
                enabled: Some(true),
                level: "debug".to_string(),
                priority: 100,
                fuzzy_rules: vec![FuzzyRuleConfig {
                    kind: FuzzyRuleKind::Target,
                    match_type: FuzzyMatchType::Exact,
                    pattern: "app::decoder".to_string(),
                    level: None,
                }],
                field_rules: vec![FieldRuleConfig {
                    field: "id".to_string(),
                    op: FieldRuleOp::Eq,
                    value: json!(100),
                }],
            }],
        };
        let reload = ObservabilityReloadHandle::new(
            FilterReloadHandle::new(true, "local", SharedOrderedFilter::new(filter).unwrap()),
            None,
            None,
            false,
            true,
        );

        assert!(
            reload
                .export_js_log_filter_snapshot()
                .local
                .unwrap()
                .requires_rust_evaluation
        );
    }

    #[test]
    fn successful_reload_changes_export_revision() {
        let reload = handle(true, false);
        let initial = reload.current_filter_revision();

        reload.local().reload_default_level("debug").unwrap();

        assert!(reload.current_filter_revision() > initial);
        let json: serde_json::Value =
            serde_json::from_str(&reload.export_js_log_filter_json().unwrap()).unwrap();
        assert_eq!(json["local"]["filter"]["default_level"], "debug");
    }

    #[test]
    fn failed_reload_keeps_export_revision() {
        let reload = handle(true, false);
        let initial = reload.current_filter_revision();

        assert!(reload.local().reload_default_level("invalid").is_err());

        assert_eq!(reload.current_filter_revision(), initial);
    }

    #[test]
    fn global_functions_follow_registration_lifecycle() {
        clear_global_filter_export();
        assert!(current_js_log_filter_revision().is_err());

        let reload = handle(true, false);
        install_global_filter_export(reload.clone());

        assert_eq!(
            current_js_log_filter_revision().unwrap(),
            reload.current_filter_revision()
        );
        let snapshot = export_js_log_filter_snapshot().unwrap();
        assert!(snapshot.local.is_some());
        assert_eq!(
            serde_json::from_str::<serde_json::Value>(&export_js_log_filter_json().unwrap())
                .unwrap()["revision"],
            snapshot.revision
        );

        clear_global_filter_export();
        assert!(export_js_log_filter_json().is_err());
    }

    fn handle(local_output_enabled: bool, remote_enabled: bool) -> ObservabilityReloadHandle {
        let remote = remote_enabled.then(|| FilterReloadHandle::new(true, "remote", filter("off")));
        ObservabilityReloadHandle::new(
            FilterReloadHandle::new(true, "local", filter("info")),
            remote,
            None,
            false,
            local_output_enabled,
        )
    }

    fn filter(level: &str) -> SharedOrderedFilter {
        SharedOrderedFilter::new(LogFilterConfig {
            default_level: level.to_string(),
            overrides: Vec::new(),
        })
        .unwrap()
    }
}
