use crate::observability::config::{FuzzyRuleKind, LogFilterConfig};
use crate::observability::reload::ObservabilityReloadHandle;
use anyhow::{Context, Result};
use serde::Serialize;

/// Version of the JSON schema returned by the JavaScript filter export API.
pub const JS_LOG_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>,
}

/// 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")
    }
}

#[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);
    }

    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()
    }
}
