/// Sear compatibility layer — converts jsdet observations to Sear's SandboxSummary format.
///
/// This module exists to make the Sear migration seamless:
/// 1. Replace rquickjs + fork/seccomp with jsdet
/// 2. Convert jsdet observations to SandboxSummary
/// 3. All existing Sear rules continue to work unchanged
///
/// Once Sear's rules are refactored to consume jsdet observations directly,
/// this module can be removed.
///
use jsdet_core::Observation;
use jsdet_core::observation::{CookieOp, DomMutationKind};

/// Sear-compatible sandbox summary.
/// This mirrors Sear's `SandboxSummary` struct field by field.
#[derive(Debug, Clone, Default)]
pub struct SandboxSummary {
    pub redirect_targets: Vec<String>,
    pub network_requests: Vec<(String, String)>,
    pub attribute_sets: Vec<(String, String, String)>,
    pub cookie_read: bool,
    pub cookie_written: bool,
    pub document_write_called: bool,
    pub elements_created: Vec<String>,
    pub exposed_credential_forms: Vec<String>,
    pub dom_form_actions: Vec<String>,
    pub dom_password_form_actions: Vec<String>,
    pub fingerprint_apis: Vec<String>,
    pub dynamic_code_used: bool,
    pub has_password_inputs: bool,
    pub timed_out: bool,
    pub observation_count: usize,
    pub execution_us: u64,
    pub scripts_executed: usize,
    pub external_resource_urls: Vec<String>,
    pub css_url_targets: Vec<String>,
}

/// Convert jsdet execution results into a Sear-compatible SandboxSummary.
pub fn to_sandbox_summary(
    observations: &[Observation],
    scripts_executed: usize,
    duration_us: u64,
    timed_out: bool,
) -> SandboxSummary {
    let mut summary = SandboxSummary {
        observation_count: observations.len(),
        execution_us: duration_us,
        scripts_executed,
        timed_out,
        ..Default::default()
    };

    for obs in observations {
        match obs {
            Observation::ApiCall { api, args, .. } => {
                // Redirect targets.
                if (api.contains("location.assign")
                    || api.contains("location.replace")
                    || api == "window.open")
                    && let Some(url) = first_string_arg(args)
                {
                    summary.redirect_targets.push(url);
                }

                // Network requests.
                if api == "fetch" || api.contains("xhr.send") {
                    let url = first_string_arg(args).unwrap_or_default();
                    let method = nth_string_arg(args, 1).unwrap_or_else(|| "GET".into());
                    summary.network_requests.push((url, method));
                }

                // Cookie access.
                if api.contains("cookie.get") || api.contains("cookie") && !api.contains("set") {
                    summary.cookie_read = true;
                }
                if api.contains("cookie.set") {
                    summary.cookie_written = true;
                }

                // Element creation.
                if api == "document.createElement"
                    && let Some(tag) = first_string_arg(args)
                {
                    summary.elements_created.push(tag);
                }

                // Document.write.
                if api.contains("document.write") {
                    summary.document_write_called = true;
                }

                // Storage setItem — track as external resource if URL-like.
                if api.contains("Storage.setItem")
                    && let Some(val) = nth_string_arg(args, 1)
                    && (val.starts_with("http://") || val.starts_with("https://"))
                {
                    summary.external_resource_urls.push(val);
                }
            }

            Observation::CookieAccess { operation, .. } => match operation {
                CookieOp::Read => summary.cookie_read = true,
                CookieOp::Write => summary.cookie_written = true,
                CookieOp::Delete => summary.cookie_written = true,
            },

            Observation::NetworkRequest { url, method, .. } => {
                summary.network_requests.push((url.clone(), method.clone()));
            }

            Observation::DynamicCodeExec { .. } => {
                summary.dynamic_code_used = true;
            }

            Observation::FingerprintAccess { api, .. } => {
                summary.fingerprint_apis.push(api.clone());
            }

            Observation::DomMutation {
                kind: DomMutationKind::AttributeSet,
                target,
                detail,
            } => {
                // Parse "name=value" from detail.
                let (attr, value) = detail.split_once('=').unwrap_or((detail, ""));
                summary
                    .attribute_sets
                    .push((target.clone(), attr.to_string(), value.to_string()));
            }

            Observation::CssExfiltration { url, .. } => {
                summary.css_url_targets.push(url.clone());
            }

            Observation::ResourceLimit { .. } => {
                summary.timed_out = true;
            }

            _ => {}
        }
    }

    summary
}

fn first_string_arg(args: &[jsdet_core::observation::Value]) -> Option<String> {
    args.first().and_then(|v| match v {
        jsdet_core::observation::Value::String(s, _) => {
            // May be a JSON array like '["url", "method"]'.
            if s.starts_with('[') {
                serde_json::from_str::<Vec<String>>(s)
                    .ok()
                    .and_then(|arr| arr.into_iter().next())
            } else {
                Some(s.clone())
            }
        }
        _ => None,
    })
}

fn nth_string_arg(args: &[jsdet_core::observation::Value], n: usize) -> Option<String> {
    args.first().and_then(|v| match v {
        jsdet_core::observation::Value::String(s, _) if s.starts_with('[') => {
            serde_json::from_str::<Vec<String>>(s)
                .ok()
                .and_then(|arr| arr.into_iter().nth(n))
        }
        _ => args.get(n).and_then(|v| match v {
            jsdet_core::observation::Value::String(s, _) => Some(s.clone()),
            _ => None,
        }),
    })
}

#[cfg(test)]
mod tests {
    use super::*;
    use jsdet_core::observation::{DynamicCodeSource, Value};

    #[test]
    fn converts_fetch_to_network_request() {
        let observations = vec![Observation::ApiCall {
            api: "fetch".into(),
            args: vec![Value::string("[\"https://evil.com/exfil\",\"POST\"]")],
            result: Value::Undefined,
        }];
        let summary = to_sandbox_summary(&observations, 1, 1000, false);
        assert_eq!(summary.network_requests.len(), 1);
        assert_eq!(summary.network_requests[0].0, "https://evil.com/exfil");
        assert_eq!(summary.network_requests[0].1, "POST");
    }

    #[test]
    fn detects_cookie_read() {
        let observations = vec![Observation::ApiCall {
            api: "document.cookie.get".into(),
            args: vec![],
            result: Value::string("session=abc"),
        }];
        let summary = to_sandbox_summary(&observations, 1, 1000, false);
        assert!(summary.cookie_read);
    }

    #[test]
    fn detects_dynamic_code() {
        let observations = vec![Observation::DynamicCodeExec {
            source: DynamicCodeSource::Eval,
            code_preview: "alert(1)".into(),
        }];
        let summary = to_sandbox_summary(&observations, 1, 1000, false);
        assert!(summary.dynamic_code_used);
    }
}
