//! Parameter validation module
//!
//! Validates trigger and action parameters against their declared schemas.
//! Schemas use the flat StackStorm-style format:
//!   { "param_name": { "type": "string", "required": true, "secret": true, ... }, ... }
//!
//! Before validation, flat schemas are converted to standard JSON Schema so we
//! can reuse the `jsonschema` crate. Template-aware: values containing `{{ }}`
//! template expressions are replaced with schema-appropriate placeholders before
//! validation, so template expressions pass type checks while literal values are
//! still validated normally.

use attune_common::models::{action::Action, trigger::Trigger};
use jsonschema::Validator;
use serde_json::Value;

use crate::middleware::ApiError;

/// Convert a flat StackStorm-style parameter schema into a standard JSON Schema
/// object suitable for `jsonschema::Validator`.
///
/// Input (flat):
/// ```json
/// { "url": { "type": "string", "required": true }, "timeout": { "type": "integer", "default": 30 } }
/// ```
///
/// Output (JSON Schema):
/// ```json
/// { "type": "object", "properties": { "url": { "type": "string" }, "timeout": { "type": "integer", "default": 30 } }, "required": ["url"] }
/// ```
fn flat_to_json_schema(flat: &Value) -> Value {
    let Some(map) = flat.as_object() else {
        // Not an object — return a permissive schema
        return serde_json::json!({});
    };

    // If it already looks like a JSON Schema (has "type": "object" + "properties"),
    // pass it through unchanged for backward tolerance.
    if map.get("type").and_then(|v| v.as_str()) == Some("object") && map.contains_key("properties")
    {
        return flat.clone();
    }

    let mut properties = serde_json::Map::new();
    let mut required: Vec<Value> = Vec::new();

    for (key, prop_def) in map {
        let Some(prop_obj) = prop_def.as_object() else {
            // Skip non-object entries (shouldn't happen in valid schemas)
            continue;
        };

        // Clone the property definition, stripping `required` and `secret`
        // (they are not valid JSON Schema keywords).
        let mut clean = prop_obj.clone();
        let is_required = clean
            .remove("required")
            .and_then(|v| v.as_bool())
            .unwrap_or(false);
        clean.remove("secret");
        // `position` is also an Attune extension, not JSON Schema
        clean.remove("position");

        if is_required {
            required.push(Value::String(key.clone()));
        }

        properties.insert(key.clone(), Value::Object(clean));
    }

    let mut schema = serde_json::Map::new();
    schema.insert("type".to_string(), Value::String("object".to_string()));
    schema.insert("properties".to_string(), Value::Object(properties));
    if !required.is_empty() {
        schema.insert("required".to_string(), Value::Array(required));
    }

    Value::Object(schema)
}

/// Check if a JSON value is (or contains) a template expression.
fn is_template_expression(value: &Value) -> bool {
    match value {
        Value::String(s) => s.contains("{{") && s.contains("}}"),
        _ => false,
    }
}

/// Given a JSON Schema property definition, produce a placeholder value that
/// satisfies the schema's type constraint. This is used to replace template
/// expressions so that JSON Schema validation passes for the remaining
/// (non-template) parts of the parameters.
fn placeholder_for_schema(property_schema: &Value) -> Value {
    // Handle anyOf / oneOf by picking the first variant
    if let Some(any_of) = property_schema.get("anyOf").and_then(|v| v.as_array()) {
        if let Some(first) = any_of.first() {
            return placeholder_for_schema(first);
        }
    }
    if let Some(one_of) = property_schema.get("oneOf").and_then(|v| v.as_array()) {
        if let Some(first) = one_of.first() {
            return placeholder_for_schema(first);
        }
    }

    let type_value = property_schema.get("type").and_then(|t| t.as_str());

    match type_value {
        Some("integer") => {
            // Use minimum if set, else default if set, else 0
            if let Some(default) = property_schema.get("default") {
                return default.clone();
            }
            if let Some(min) = property_schema.get("minimum").and_then(|v| v.as_i64()) {
                return Value::Number(min.into());
            }
            Value::Number(0.into())
        }
        Some("number") => {
            if let Some(default) = property_schema.get("default") {
                return default.clone();
            }
            if let Some(min) = property_schema.get("minimum").and_then(|v| v.as_f64()) {
                return serde_json::Number::from_f64(min)
                    .map(Value::Number)
                    .unwrap_or(Value::Number(0.into()));
            }
            serde_json::Number::from_f64(0.0)
                .map(Value::Number)
                .unwrap_or(Value::Number(0.into()))
        }
        Some("boolean") => {
            if let Some(default) = property_schema.get("default") {
                return default.clone();
            }
            Value::Bool(true)
        }
        Some("array") => {
            if let Some(default) = property_schema.get("default") {
                return default.clone();
            }
            Value::Array(vec![])
        }
        Some("object") => {
            if let Some(default) = property_schema.get("default") {
                return default.clone();
            }
            Value::Object(serde_json::Map::new())
        }
        Some("string") | None => {
            // For enum fields, use the first valid value so enum validation passes
            if let Some(enum_values) = property_schema.get("enum").and_then(|v| v.as_array()) {
                if let Some(first) = enum_values.first() {
                    return first.clone();
                }
            }
            if let Some(default) = property_schema.get("default") {
                return default.clone();
            }
            Value::String("__template_placeholder__".to_string())
        }
        Some(_) => Value::Null,
    }
}

/// Walk a parameters object and replace any template expression values with
/// schema-appropriate placeholders. Only replaces leaf values that match
/// `{{ ... }}`; non-template values are left untouched for normal validation.
///
/// `schema` must be a standard JSON Schema object (with `properties`, `type`, etc).
/// Call `flat_to_json_schema` first if starting from flat format.
fn replace_templates_with_placeholders(params: &Value, schema: &Value) -> Value {
    match params {
        Value::Object(map) => {
            let properties = schema.get("properties").and_then(|p| p.as_object());

            let mut result = serde_json::Map::new();
            for (key, value) in map {
                let prop_schema = properties.and_then(|p| p.get(key));

                if is_template_expression(value) {
                    // Replace with a type-appropriate placeholder
                    if let Some(ps) = prop_schema {
                        result.insert(key.clone(), placeholder_for_schema(ps));
                    } else {
                        // No schema for this property — keep as string placeholder
                        result.insert(
                            key.clone(),
                            Value::String("__template_placeholder__".to_string()),
                        );
                    }
                } else if value.is_object() {
                    // Recurse into nested objects
                    let empty_schema = Value::Object(serde_json::Map::new());
                    let nested_schema = prop_schema.unwrap_or(&empty_schema);
                    result.insert(
                        key.clone(),
                        replace_templates_with_placeholders(value, nested_schema),
                    );
                } else if value.is_array() {
                    // Recurse into arrays — check each element
                    if let Some(arr) = value.as_array() {
                        let empty_items_schema = Value::Object(serde_json::Map::new());
                        let item_schema = prop_schema
                            .and_then(|ps| ps.get("items"))
                            .unwrap_or(&empty_items_schema);
                        let new_arr: Vec<Value> = arr
                            .iter()
                            .map(|item| {
                                if is_template_expression(item) {
                                    placeholder_for_schema(item_schema)
                                } else if item.is_object() || item.is_array() {
                                    replace_templates_with_placeholders(item, item_schema)
                                } else {
                                    item.clone()
                                }
                            })
                            .collect();
                        result.insert(key.clone(), Value::Array(new_arr));
                    } else {
                        result.insert(key.clone(), value.clone());
                    }
                } else {
                    result.insert(key.clone(), value.clone());
                }
            }
            Value::Object(result)
        }
        other => other.clone(),
    }
}

/// Validate trigger parameters against the trigger's parameter schema.
/// Template expressions (`{{ ... }}`) are accepted for any field type.
///
/// The schema is expected in flat StackStorm format and is converted to
/// JSON Schema internally for validation.
pub fn validate_trigger_params(trigger: &Trigger, params: &Value) -> Result<(), ApiError> {
    // If no schema is defined, accept any parameters
    let Some(flat_schema) = &trigger.param_schema else {
        return Ok(());
    };

    // Convert flat format to JSON Schema for validation
    let schema = flat_to_json_schema(flat_schema);

    // Replace template expressions with schema-appropriate placeholders
    let sanitized = replace_templates_with_placeholders(params, &schema);

    // Compile the JSON schema
    let compiled_schema = Validator::new(&schema).map_err(|e| {
        ApiError::InternalServerError(format!(
            "Invalid parameter schema for trigger '{}': {}",
            trigger.r#ref, e
        ))
    })?;

    // Validate the sanitized parameters
    let errors: Vec<String> = compiled_schema
        .iter_errors(&sanitized)
        .map(|e| {
            let path = e.instance_path().to_string();
            if path.is_empty() {
                e.to_string()
            } else {
                format!("{} at {}", e, path)
            }
        })
        .collect();

    if !errors.is_empty() {
        return Err(ApiError::ValidationError(format!(
            "Invalid parameters for trigger '{}': {}",
            trigger.r#ref,
            errors.join(", ")
        )));
    }

    Ok(())
}

/// Validate action parameters against the action's parameter schema.
/// Template expressions (`{{ ... }}`) are accepted for any field type.
///
/// The schema is expected in flat StackStorm format and is converted to
/// JSON Schema internally for validation.
pub fn validate_action_params(action: &Action, params: &Value) -> Result<(), ApiError> {
    // If no schema is defined, accept any parameters
    let Some(flat_schema) = &action.param_schema else {
        return Ok(());
    };

    // Convert flat format to JSON Schema for validation
    let schema = flat_to_json_schema(flat_schema);

    // Replace template expressions with schema-appropriate placeholders
    let sanitized = replace_templates_with_placeholders(params, &schema);

    // Compile the JSON schema
    let compiled_schema = Validator::new(&schema).map_err(|e| {
        ApiError::InternalServerError(format!(
            "Invalid parameter schema for action '{}': {}",
            action.r#ref, e
        ))
    })?;

    // Validate the sanitized parameters
    let errors: Vec<String> = compiled_schema
        .iter_errors(&sanitized)
        .map(|e| {
            let path = e.instance_path().to_string();
            if path.is_empty() {
                e.to_string()
            } else {
                format!("{} at {}", e, path)
            }
        })
        .collect();

    if !errors.is_empty() {
        return Err(ApiError::ValidationError(format!(
            "Invalid parameters for action '{}': {}",
            action.r#ref,
            errors.join(", ")
        )));
    }

    Ok(())
}

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

    // ── Helper builders ──────────────────────────────────────────────

    fn make_trigger(schema: Option<Value>) -> Trigger {
        Trigger {
            id: 1,
            r#ref: "test.trigger".to_string(),
            pack: Some(1),
            pack_ref: Some("test".to_string()),
            label: "Test Trigger".to_string(),
            description: None,
            enabled: true,
            param_schema: schema,
            out_schema: None,
            webhook_enabled: false,
            webhook_key: None,
            webhook_config: None,
            is_adhoc: false,
            created: chrono::Utc::now(),
            updated: chrono::Utc::now(),
        }
    }

    fn make_action(schema: Option<Value>) -> Action {
        Action {
            id: 1,
            r#ref: "test.action".to_string(),
            pack: 1,
            pack_ref: "test".to_string(),
            label: "Test Action".to_string(),
            description: Some("Test action".to_string()),
            entrypoint: "test.sh".to_string(),
            runtime: Some(1),
            runtime_version_constraint: None,
            param_schema: schema,
            out_schema: None,
            workflow_def: None,
            is_adhoc: false,
            parameter_delivery: attune_common::models::ParameterDelivery::default(),
            parameter_format: attune_common::models::ParameterFormat::default(),
            output_format: attune_common::models::OutputFormat::default(),
            created: chrono::Utc::now(),
            updated: chrono::Utc::now(),
        }
    }

    // ── No schema ────────────────────────────────────────────────────

    #[test]
    fn test_validate_trigger_params_with_no_schema() {
        let trigger = make_trigger(None);
        let params = json!({ "any": "value" });
        assert!(validate_trigger_params(&trigger, &params).is_ok());
    }

    // ── Basic trigger validation (no templates) ──────────────────────

    // ── flat_to_json_schema unit tests ───────────────────────────────

    #[test]
    fn test_flat_to_json_schema_basic() {
        let flat = json!({
            "url": { "type": "string", "required": true },
            "timeout": { "type": "integer", "default": 30 }
        });
        let result = flat_to_json_schema(&flat);
        assert_eq!(result["type"], "object");
        assert_eq!(result["properties"]["url"]["type"], "string");
        // `required` should be stripped from individual properties
        assert!(result["properties"]["url"].get("required").is_none());
        assert_eq!(result["properties"]["timeout"]["default"], 30);
        // Top-level required array should contain "url"
        let req = result["required"].as_array().unwrap();
        assert!(req.contains(&json!("url")));
        assert!(!req.contains(&json!("timeout")));
    }

    #[test]
    fn test_flat_to_json_schema_strips_secret_and_position() {
        let flat = json!({
            "token": { "type": "string", "secret": true, "position": 0, "required": true }
        });
        let result = flat_to_json_schema(&flat);
        let token = &result["properties"]["token"];
        assert!(token.get("secret").is_none());
        assert!(token.get("position").is_none());
        assert!(token.get("required").is_none());
    }

    #[test]
    fn test_flat_to_json_schema_empty() {
        let flat = json!({});
        let result = flat_to_json_schema(&flat);
        assert_eq!(result["type"], "object");
        assert!(result.get("required").is_none());
    }

    #[test]
    fn test_flat_to_json_schema_passthrough_json_schema() {
        // If already JSON Schema format, pass through unchanged
        let js = json!({
            "type": "object",
            "properties": { "x": { "type": "string" } },
            "required": ["x"]
        });
        let result = flat_to_json_schema(&js);
        assert_eq!(result, js);
    }

    // ── Basic trigger validation (flat format) ──────────────────────

    #[test]
    fn test_validate_trigger_params_with_valid_params() {
        let schema = json!({
            "unit": { "type": "string", "enum": ["seconds", "minutes", "hours"], "required": true },
            "delta": { "type": "integer", "minimum": 1, "required": true }
        });

        let trigger = make_trigger(Some(schema));
        let params = json!({ "unit": "seconds", "delta": 10 });
        assert!(validate_trigger_params(&trigger, &params).is_ok());
    }

    #[test]
    fn test_validate_trigger_params_with_invalid_params() {
        let schema = json!({
            "unit": { "type": "string", "enum": ["seconds", "minutes", "hours"], "required": true },
            "delta": { "type": "integer", "minimum": 1, "required": true }
        });

        let trigger = make_trigger(Some(schema));

        // Missing required field 'delta'
        let params = json!({ "unit": "seconds" });
        assert!(validate_trigger_params(&trigger, &params).is_err());

        // Invalid enum value for 'unit'
        let params = json!({ "unit": "days", "delta": 10 });
        assert!(validate_trigger_params(&trigger, &params).is_err());

        // Invalid type for 'delta'
        let params = json!({ "unit": "seconds", "delta": "10" });
        assert!(validate_trigger_params(&trigger, &params).is_err());
    }

    // ── Basic action validation (flat format) ───────────────────────

    #[test]
    fn test_validate_action_params_with_valid_params() {
        let schema = json!({
            "message": { "type": "string", "required": true }
        });

        let action = make_action(Some(schema));
        let params = json!({ "message": "Hello, world!" });
        assert!(validate_action_params(&action, &params).is_ok());
    }

    #[test]
    fn test_validate_action_params_with_empty_params_but_required_fields() {
        let schema = json!({
            "message": { "type": "string", "required": true }
        });

        let action = make_action(Some(schema));
        let params = json!({});
        assert!(validate_action_params(&action, &params).is_err());
    }

    // ── Template-aware validation (flat format) ──────────────────────

    #[test]
    fn test_template_in_integer_field_passes() {
        let schema = json!({
            "counter": { "type": "integer", "required": true }
        });

        let action = make_action(Some(schema));
        let params = json!({ "counter": "{{ event.payload.counter }}" });
        assert!(validate_action_params(&action, &params).is_ok());
    }

    #[test]
    fn test_template_in_boolean_field_passes() {
        let schema = json!({
            "verbose": { "type": "boolean", "required": true }
        });

        let action = make_action(Some(schema));
        let params = json!({ "verbose": "{{ event.payload.debug }}" });
        assert!(validate_action_params(&action, &params).is_ok());
    }

    #[test]
    fn test_template_in_number_field_passes() {
        let schema = json!({
            "threshold": { "type": "number", "minimum": 0.0, "required": true }
        });

        let action = make_action(Some(schema));
        let params = json!({ "threshold": "{{ event.payload.threshold }}" });
        assert!(validate_action_params(&action, &params).is_ok());
    }

    #[test]
    fn test_template_in_enum_field_passes() {
        let schema = json!({
            "level": { "type": "string", "enum": ["info", "warn", "error"], "required": true }
        });

        let action = make_action(Some(schema));
        let params = json!({ "level": "{{ event.payload.severity }}" });
        assert!(validate_action_params(&action, &params).is_ok());
    }

    #[test]
    fn test_template_in_array_field_passes() {
        let schema = json!({
            "recipients": { "type": "array", "items": { "type": "string" }, "required": true }
        });

        let action = make_action(Some(schema));
        let params = json!({ "recipients": "{{ event.payload.emails }}" });
        assert!(validate_action_params(&action, &params).is_ok());
    }

    #[test]
    fn test_template_in_object_field_passes() {
        let schema = json!({
            "metadata": { "type": "object", "required": true }
        });

        let action = make_action(Some(schema));
        let params = json!({ "metadata": "{{ event.payload.meta }}" });
        assert!(validate_action_params(&action, &params).is_ok());
    }

    #[test]
    fn test_mixed_template_and_literal_values() {
        let schema = json!({
            "message": { "type": "string", "required": true },
            "count": { "type": "integer", "required": true },
            "verbose": { "type": "boolean", "required": true }
        });

        let action = make_action(Some(schema));

        // Mix of literal and template values
        let params = json!({
            "message": "Hello",
            "count": "{{ event.payload.count }}",
            "verbose": true
        });
        assert!(validate_action_params(&action, &params).is_ok());
    }

    // ── Secret fields are ignored during validation ──────────────────

    #[test]
    fn test_secret_field_validated_normally() {
        let schema = json!({
            "api_key": { "type": "string", "required": true, "secret": true },
            "endpoint": { "type": "string" }
        });

        let action = make_action(Some(schema));

        // Valid: secret field provided
        let params = json!({ "api_key": "sk-1234", "endpoint": "https://api.example.com" });
        assert!(validate_action_params(&action, &params).is_ok());

        // Invalid: secret field missing but required
        let params = json!({ "endpoint": "https://api.example.com" });
        assert!(validate_action_params(&action, &params).is_err());
    }

    #[test]
    fn test_literal_values_still_validated() {
        let schema = json!({
            "type": "object",
            "properties": {
                "message": { "type": "string" },
                "count": { "type": "integer" }
            },
            "required": ["message", "count"]
        });

        let action = make_action(Some(schema));

        // Template for message is fine, but literal "not_a_number" for integer is not
        let params = json!({
            "message": "{{ event.payload.msg }}",
            "count": "not_a_number"
        });
        assert!(validate_action_params(&action, &params).is_err());
    }

    #[test]
    fn test_required_field_still_enforced_with_templates() {
        let schema = json!({
            "type": "object",
            "properties": {
                "message": { "type": "string" },
                "count": { "type": "integer" }
            },
            "required": ["message", "count"]
        });

        let action = make_action(Some(schema));

        // Only message provided (even as template), count is missing
        let params = json!({ "message": "{{ event.payload.msg }}" });
        assert!(validate_action_params(&action, &params).is_err());
    }

    #[test]
    fn test_pack_config_template_passes() {
        let schema = json!({
            "type": "object",
            "properties": {
                "api_key": { "type": "string" },
                "timeout": { "type": "integer" }
            },
            "required": ["api_key", "timeout"]
        });

        let action = make_action(Some(schema));
        let params = json!({
            "api_key": "{{ pack.config.api_key }}",
            "timeout": "{{ pack.config.default_timeout }}"
        });
        assert!(validate_action_params(&action, &params).is_ok());
    }

    #[test]
    fn test_system_template_passes() {
        let schema = json!({
            "type": "object",
            "properties": {
                "timestamp": { "type": "string" },
                "rule_id": { "type": "integer" }
            },
            "required": ["timestamp", "rule_id"]
        });

        let action = make_action(Some(schema));
        let params = json!({
            "timestamp": "{{ system.timestamp }}",
            "rule_id": "{{ system.rule.id }}"
        });
        assert!(validate_action_params(&action, &params).is_ok());
    }

    #[test]
    fn test_trigger_params_template_aware() {
        let schema = json!({
            "type": "object",
            "properties": {
                "unit": { "type": "string", "enum": ["seconds", "minutes", "hours"] },
                "delta": { "type": "integer", "minimum": 1 }
            },
            "required": ["unit", "delta"]
        });

        let trigger = make_trigger(Some(schema));

        // Both fields as templates
        let params = json!({
            "unit": "{{ pack.config.timer_unit }}",
            "delta": "{{ pack.config.timer_delta }}"
        });
        assert!(validate_trigger_params(&trigger, &params).is_ok());
    }

    // ── Placeholder generation ───────────────────────────────────────

    #[test]
    fn test_is_template_expression() {
        assert!(is_template_expression(&json!("{{ event.payload.x }}")));
        assert!(is_template_expression(&json!("{{ pack.config.key }}")));
        assert!(is_template_expression(&json!(
            "prefix {{ system.ts }} suffix"
        )));
        assert!(!is_template_expression(&json!("no braces here")));
        assert!(!is_template_expression(&json!(42)));
        assert!(!is_template_expression(&json!(true)));
        assert!(!is_template_expression(&json!("{ single braces }")));
    }

    #[test]
    fn test_placeholder_for_schema_types() {
        assert_eq!(
            placeholder_for_schema(&json!({"type": "integer"})),
            json!(0)
        );
        assert_eq!(
            placeholder_for_schema(&json!({"type": "number"})),
            json!(0.0)
        );
        assert_eq!(
            placeholder_for_schema(&json!({"type": "boolean"})),
            json!(true)
        );
        assert_eq!(placeholder_for_schema(&json!({"type": "array"})), json!([]));
        assert_eq!(
            placeholder_for_schema(&json!({"type": "object"})),
            json!({})
        );
        assert_eq!(
            placeholder_for_schema(&json!({"type": "string"})),
            json!("__template_placeholder__")
        );
    }

    #[test]
    fn test_placeholder_respects_enum() {
        let schema = json!({"type": "string", "enum": ["a", "b", "c"]});
        assert_eq!(placeholder_for_schema(&schema), json!("a"));
    }

    #[test]
    fn test_placeholder_respects_default() {
        let schema = json!({"type": "integer", "default": 42});
        assert_eq!(placeholder_for_schema(&schema), json!(42));
    }

    #[test]
    fn test_placeholder_respects_minimum() {
        let schema = json!({"type": "integer", "minimum": 5});
        assert_eq!(placeholder_for_schema(&schema), json!(5));
    }

    #[test]
    fn test_nested_object_template_replacement() {
        let schema = json!({
            "type": "object",
            "properties": {
                "outer": {
                    "type": "object",
                    "properties": {
                        "inner_count": { "type": "integer" }
                    }
                }
            }
        });

        let params = json!({
            "outer": {
                "inner_count": "{{ event.payload.count }}"
            }
        });

        let sanitized = replace_templates_with_placeholders(&params, &schema);
        // The inner template should be replaced with an integer placeholder
        assert!(sanitized["outer"]["inner_count"].is_number());
    }

    #[test]
    fn test_array_element_template_replacement() {
        let schema = json!({
            "type": "object",
            "properties": {
                "tags": {
                    "type": "array",
                    "items": { "type": "string" }
                }
            }
        });

        let params = json!({
            "tags": ["literal", "{{ event.payload.tag }}"]
        });

        let sanitized = replace_templates_with_placeholders(&params, &schema);
        let tags = sanitized["tags"].as_array().unwrap();
        assert_eq!(tags[0], "literal");
        assert!(tags[1].is_string());
        assert_ne!(tags[1], "{{ event.payload.tag }}");
    }
}
