//! Action validation — precondition checking and invariant enforcement.

use car_ir::precondition;
use car_ir::{Action, Precondition, ToolSchema};
use car_state::StateStore;
use std::collections::HashMap;

/// A single validation failure.
#[derive(Debug, Clone)]
pub struct ValidationError {
    pub action_id: String,
    pub reason: String,
}

/// Aggregate result of validating an action.
#[derive(Debug)]
pub struct ValidationResult {
    pub action_id: String,
    pub errors: Vec<ValidationError>,
}

impl ValidationResult {
    pub fn valid(&self) -> bool {
        self.errors.is_empty()
    }
}

/// Check a single precondition against state.
pub fn check_precondition(pre: &Precondition, state: &StateStore) -> Option<String> {
    precondition::check_precondition(pre, state)
}

/// Validate `action.parameters` against a tool's registered schema.
///
/// Two paths run side-by-side for backwards compatibility with the
/// schemaless `register_tool(name)` registration path:
///
/// 1. **Required-key check** — kept for the legacy case where the
///    schema is an empty object (no constraints) but a sibling
///    `required` array is somehow present. Cheap, no-op when no
///    `required` field exists.
/// 2. **Full JSON Schema validation** via `jsonschema` — runs only
///    when the schema is a non-trivial object (has `type` /
///    `properties` / `required` / `enum` / etc.). Catches type
///    mismatches like `{path: 42}` for a tool wanting
///    `{path: string}` — which the legacy required-only check let
///    through.
///
/// Tools registered via the schemaless API have an empty `{}` schema
/// → both checks are no-ops → existing callers see no behavior change.
fn validate_parameters(
    action: &car_ir::Action,
    tool: &str,
    schema: &ToolSchema,
    result: &mut ValidationResult,
) {
    // 1. Cheap required-key check (LLM may send unexpected extras —
    // we intentionally do NOT reject unknown properties).
    if let Some(required) = schema.parameters.get("required") {
        if let Some(required_arr) = required.as_array() {
            for req in required_arr {
                if let Some(param_name) = req.as_str() {
                    if !action.parameters.contains_key(param_name) {
                        result.errors.push(ValidationError {
                            action_id: action.id.clone(),
                            reason: format!(
                                "missing required parameter '{}' for tool '{}'",
                                param_name, tool
                            ),
                        });
                    }
                }
            }
        }
    }

    // 2. Full JSON Schema validation when the schema actually carries
    // constraints. An empty object schema (the schemaless registration
    // case) trivially validates everything — skip the work.
    if !schema_is_empty_object(&schema.parameters) {
        let params_value = parameters_to_value(&action.parameters);
        match jsonschema::validator_for(&schema.parameters) {
            Ok(validator) => {
                for err in validator.iter_errors(&params_value) {
                    result.errors.push(ValidationError {
                        action_id: action.id.clone(),
                        reason: format!(
                            "tool '{}' parameter validation: {} (at {})",
                            tool, err, err.instance_path
                        ),
                    });
                }
            }
            Err(e) => {
                result.errors.push(ValidationError {
                    action_id: action.id.clone(),
                    reason: format!(
                        "tool '{}' has an invalid registered JSON Schema: {}",
                        tool, e
                    ),
                });
            }
        }
    }
}

fn schema_is_empty_object(schema: &serde_json::Value) -> bool {
    match schema {
        serde_json::Value::Object(map) => map.is_empty(),
        _ => true,
    }
}

fn parameters_to_value(params: &HashMap<String, serde_json::Value>) -> serde_json::Value {
    let map: serde_json::Map<String, serde_json::Value> =
        params.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
    serde_json::Value::Object(map)
}

/// Validate a single action against state and registered tools.
pub fn validate_action(
    action: &Action,
    state: &StateStore,
    registered_tools: &HashMap<String, ToolSchema>,
) -> ValidationResult {
    let mut result = ValidationResult {
        action_id: action.id.clone(),
        errors: Vec::new(),
    };

    // Check tool exists and validate parameters against schema
    if let Some(ref tool) = action.tool {
        if let Some(schema) = registered_tools.get(tool) {
            validate_parameters(action, tool, schema, &mut result);
        } else {
            result.errors.push(ValidationError {
                action_id: action.id.clone(),
                reason: format!("tool '{}' is not registered", tool),
            });
        }
    }

    // Check preconditions
    for pre in &action.preconditions {
        if let Some(error) = check_precondition(pre, state) {
            result.errors.push(ValidationError {
                action_id: action.id.clone(),
                reason: error,
            });
        }
    }

    // Check state dependencies
    for dep in &action.state_dependencies {
        if !state.exists(dep) {
            result.errors.push(ValidationError {
                action_id: action.id.clone(),
                reason: format!("state dependency '{}' not found", dep),
            });
        }
    }

    result
}

#[cfg(test)]
mod tests {
    use super::*;
    use car_ir::{ActionType, FailureBehavior, Precondition, ToolSchema};
    use serde_json::Value;
    use std::collections::HashMap;

    fn make_tool_call(tool: &str) -> Action {
        Action {
            id: "test".to_string(),
            action_type: ActionType::ToolCall,
            tool: Some(tool.to_string()),
            parameters: HashMap::new(),
            preconditions: vec![],
            expected_effects: HashMap::new(),
            state_dependencies: vec![],
            idempotent: false,
            max_retries: 3,
            failure_behavior: FailureBehavior::Abort,
            timeout_ms: None,
            metadata: HashMap::new(),
        }
    }

    fn simple_schema(name: &str) -> ToolSchema {
        ToolSchema {
            name: name.to_string(),
            description: String::new(),
            parameters: Value::Object(Default::default()),
            returns: None,
            idempotent: false,
            cache_ttl_secs: None,
            rate_limit: None,
        }
    }

    fn tools_map(names: &[&str]) -> HashMap<String, ToolSchema> {
        names
            .iter()
            .map(|n| (n.to_string(), simple_schema(n)))
            .collect()
    }

    #[test]
    fn unknown_tool_rejected() {
        let state = StateStore::new();
        let tools = tools_map(&["echo"]);
        let action = make_tool_call("nonexistent");
        let result = validate_action(&action, &state, &tools);
        assert!(!result.valid());
        assert!(result.errors[0].reason.contains("not registered"));
    }

    #[test]
    fn known_tool_passes() {
        let state = StateStore::new();
        let tools = tools_map(&["echo"]);
        let action = make_tool_call("echo");
        let result = validate_action(&action, &state, &tools);
        assert!(result.valid());
    }

    #[test]
    fn precondition_eq_fails() {
        let state = StateStore::new();
        let pre = Precondition {
            key: "auth".to_string(),
            operator: "eq".to_string(),
            value: Value::Bool(true),
            description: String::new(),
        };
        assert!(check_precondition(&pre, &state).is_some());
    }

    #[test]
    fn precondition_eq_passes() {
        let state = StateStore::new();
        state.set("auth", Value::Bool(true), "setup");
        let pre = Precondition {
            key: "auth".to_string(),
            operator: "eq".to_string(),
            value: Value::Bool(true),
            description: String::new(),
        };
        assert!(check_precondition(&pre, &state).is_none());
    }

    #[test]
    fn precondition_exists() {
        let state = StateStore::new();
        let pre = Precondition {
            key: "x".to_string(),
            operator: "exists".to_string(),
            value: Value::Null,
            description: String::new(),
        };
        assert!(check_precondition(&pre, &state).is_some());

        state.set("x", Value::from(1), "setup");
        assert!(check_precondition(&pre, &state).is_none());
    }

    #[test]
    fn state_dependency_missing() {
        let state = StateStore::new();
        let tools: HashMap<String, ToolSchema> = HashMap::new();
        let mut action = make_tool_call("echo");
        action.tool = None;
        action.action_type = ActionType::StateRead;
        action.state_dependencies = vec!["missing".to_string()];
        let result = validate_action(&action, &state, &tools);
        assert!(!result.valid());
        assert!(result.errors[0].reason.contains("not found"));
    }

    #[test]
    fn precondition_gt() {
        let state = StateStore::new();
        state.set("count", Value::from(10), "setup");
        let pre = Precondition {
            key: "count".to_string(),
            operator: "gt".to_string(),
            value: Value::from(5),
            description: String::new(),
        };
        assert!(check_precondition(&pre, &state).is_none());

        let pre_fail = Precondition {
            key: "count".to_string(),
            operator: "gt".to_string(),
            value: Value::from(20),
            description: String::new(),
        };
        assert!(check_precondition(&pre_fail, &state).is_some());
    }

    #[test]
    fn missing_required_parameter_rejected() {
        let state = StateStore::new();
        let mut schema = simple_schema("add");
        schema.parameters = serde_json::json!({
            "type": "object",
            "properties": {
                "a": {"type": "number"},
                "b": {"type": "number"}
            },
            "required": ["a", "b"]
        });
        let tools: HashMap<String, ToolSchema> =
            [("add".to_string(), schema)].into_iter().collect();

        let action = make_tool_call("add"); // no parameters
        let result = validate_action(&action, &state, &tools);
        assert!(!result.valid());
        assert!(result
            .errors
            .iter()
            .any(|e| e.reason.contains("missing required parameter 'a'")));
        assert!(result
            .errors
            .iter()
            .any(|e| e.reason.contains("missing required parameter 'b'")));
    }

    #[test]
    fn required_parameters_provided_passes() {
        let state = StateStore::new();
        let mut schema = simple_schema("add");
        schema.parameters = serde_json::json!({
            "type": "object",
            "properties": {
                "a": {"type": "number"},
                "b": {"type": "number"}
            },
            "required": ["a", "b"]
        });
        let tools: HashMap<String, ToolSchema> =
            [("add".to_string(), schema)].into_iter().collect();

        let mut action = make_tool_call("add");
        action.parameters = [
            ("a".to_string(), Value::from(1)),
            ("b".to_string(), Value::from(2)),
        ]
        .into();
        let result = validate_action(&action, &state, &tools);
        assert!(result.valid());
    }

    #[test]
    fn type_mismatch_rejected_when_schema_registered() {
        let state = StateStore::new();
        let mut schema = simple_schema("read");
        schema.parameters = serde_json::json!({
            "type": "object",
            "properties": {
                "path": {"type": "string"}
            },
            "required": ["path"]
        });
        let tools: HashMap<String, ToolSchema> =
            [("read".to_string(), schema)].into_iter().collect();

        let mut action = make_tool_call("read");
        action.parameters = [("path".to_string(), Value::from(42))].into();
        let result = validate_action(&action, &state, &tools);
        assert!(!result.valid(), "type mismatch should be rejected");
        assert!(
            result
                .errors
                .iter()
                .any(|e| e.reason.contains("parameter validation")),
            "expected jsonschema parameter validation failure, got: {:?}",
            result.errors
        );
    }

    #[test]
    fn empty_object_schema_is_treated_as_legacy() {
        // Defense-in-depth: a future refactor that "improves"
        // schema_is_empty_object must not silently turn the legacy
        // schemaless registration into a hard rejection.
        assert!(schema_is_empty_object(&Value::Object(Default::default())));
    }

    #[test]
    fn legacy_schemaless_tool_accepts_any_parameters() {
        let state = StateStore::new();
        let tools = tools_map(&["echo"]); // simple_schema → empty object
        let mut action = make_tool_call("echo");
        action.parameters = [
            ("anything".to_string(), Value::from(42)),
            ("else".to_string(), Value::from("string")),
        ]
        .into();
        let result = validate_action(&action, &state, &tools);
        assert!(
            result.valid(),
            "schemaless registration must accept anything"
        );
    }

    #[test]
    fn extra_parameters_allowed() {
        let state = StateStore::new();
        let mut schema = simple_schema("echo");
        schema.parameters = serde_json::json!({
            "type": "object",
            "properties": {
                "message": {"type": "string"}
            },
            "required": ["message"]
        });
        let tools: HashMap<String, ToolSchema> =
            [("echo".to_string(), schema)].into_iter().collect();

        let mut action = make_tool_call("echo");
        action.parameters = [
            ("message".to_string(), Value::from("hi")),
            ("unexpected_extra".to_string(), Value::from(true)),
        ]
        .into();
        let result = validate_action(&action, &state, &tools);
        assert!(result.valid()); // extra params should NOT cause rejection
    }
}
