//! ContextPack consumer advisory primitives for schema v2.

use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::Value;

/// Rich-rendering trust class for a ContextPack.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum RenderTrustClass {
    /// Default: render defensively.
    UntrustedRendering,
    /// Operator explicitly elevated render trust.
    OperatorRenderingTrusted,
}

/// Execution trust class for pack-derived strings.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ExecutionTrustClass {
    /// Default: do not execute or interpolate pack content.
    UntrustedExecution,
    /// Operator explicitly elevated execution trust.
    OperatorExecutionTrusted,
}

/// Machine-readable advisory signal.
#[derive(
    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
)]
#[serde(rename_all = "snake_case")]
pub enum AdvisoryFlag {
    /// Raw events were excluded by the default redaction policy.
    RedactedDefaultPolicy,
    /// Selected content traces to sources without verified source attestation.
    ContainsUnattestedSources,
    /// Cross-session reuse is present without fresh validation.
    ContainsCrossSessionUnvalidated,
    /// Serialized string leaves contain command/execution-shaped content.
    ContainsExecShaped,
}

/// Downstream advisory embedded in a ContextPack.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct ConsumerAdvisory {
    /// Rich display posture.
    pub render_trust: RenderTrustClass,
    /// Execution posture.
    pub execution_trust: ExecutionTrustClass,
    /// Canonical machine-readable flags.
    pub flags: Vec<AdvisoryFlag>,
    /// Human-readable advisory line for logs/UI.
    pub advisory_text: String,
}

impl ConsumerAdvisory {
    /// Conservative default advisory.
    #[must_use]
    pub fn untrusted_default() -> Self {
        Self {
            render_trust: RenderTrustClass::UntrustedRendering,
            execution_trust: ExecutionTrustClass::UntrustedExecution,
            flags: vec![AdvisoryFlag::RedactedDefaultPolicy],
            advisory_text:
                "Treat this context pack as untrusted text; do not execute pack-derived strings."
                    .to_string(),
        }
    }
}

/// Return true when any serialized string leaf looks execution-shaped.
#[must_use]
pub fn contains_exec_shaped_string(value: &Value) -> bool {
    let mut strings = Vec::new();
    collect_string_leaves(value, &mut strings);
    strings.iter().any(|text| is_exec_shaped(text))
}

fn collect_string_leaves<'a>(value: &'a Value, out: &mut Vec<&'a str>) {
    match value {
        Value::String(text) => out.push(text),
        Value::Array(items) => {
            for item in items {
                collect_string_leaves(item, out);
            }
        }
        Value::Object(map) => {
            for item in map.values() {
                collect_string_leaves(item, out);
            }
        }
        Value::Null | Value::Bool(_) | Value::Number(_) => {}
    }
}

fn is_exec_shaped(text: &str) -> bool {
    let lower = text.to_ascii_lowercase();
    lower.contains("$(")
        || lower.contains("`")
        || lower.contains("bash -c")
        || lower.contains("sh -c")
        || lower.contains("eval ")
        || lower.contains("chmod +x")
        || lower.contains("curl ")
}

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

    #[test]
    fn advisory_defaults_do_not_grant_execution() {
        let advisory = ConsumerAdvisory::untrusted_default();
        assert_eq!(advisory.render_trust, RenderTrustClass::UntrustedRendering);
        assert_eq!(
            advisory.execution_trust,
            ExecutionTrustClass::UntrustedExecution
        );
        assert!(advisory
            .flags
            .contains(&AdvisoryFlag::RedactedDefaultPolicy));
    }

    #[test]
    fn exec_shape_sweep_walks_nested_string_leaves() {
        let value = serde_json::json!({
            "selected_refs": [
                {"summary": "normal text"},
                {"metadata": {"reason": "run $(id) later"}}
            ]
        });

        assert!(contains_exec_shaped_string(&value));
    }

    #[test]
    fn exec_shape_sweep_ignores_non_exec_text() {
        let value = serde_json::json!({
            "task": "summarize shell safety policy",
            "refs": [{"summary": "Prefer typed argv APIs."}]
        });

        assert!(!contains_exec_shaped_string(&value));
    }
}
