//! Conditional expression evaluation for form fields
//!
//! Provides logic for evaluating `when` conditions on form elements.

use std::collections::HashMap;
use std::path::Path;

use super::types::FormDefinition;

/// Extract field name from a condition string
///
/// Examples:
/// - "enable_prometheus == true" → Some("enable_prometheus")
/// - "provider == lxd" → Some("provider")
/// - "grafana_port >= 3000" → Some("grafana_port")
/// - "rust in detected_languages" → Some("detected_languages")
pub(super) fn extract_field_from_condition(condition: &str) -> Option<String> {
    let condition = condition.trim();

    // String operators: contains, startswith, endswith, in
    let string_operators = ["contains", "startswith", "endswith", "in"];
    for op_str in &string_operators {
        if let Some(pos) = condition.find(op_str) {
            let before_ok = pos == 0
                || !condition[..pos]
                    .chars()
                    .last()
                    .unwrap_or(' ')
                    .is_alphanumeric();
            let after_ok = pos + op_str.len() >= condition.len()
                || !condition[pos + op_str.len()..]
                    .chars()
                    .next()
                    .unwrap_or(' ')
                    .is_alphanumeric();

            if before_ok && after_ok {
                // For "in" operator, the field is on the right side
                let field_name = if *op_str == "in" {
                    condition[pos + op_str.len()..].trim()
                } else {
                    condition[..pos].trim()
                };
                return Some(field_name.to_string());
            }
        }
    }

    // Comparison operators: ==, !=, <=, >=, <, >
    let operators = ["<=", ">=", "==", "!=", ">", "<"];
    for op_str in &operators {
        if let Some(pos) = condition.find(op_str) {
            let field_name = condition[..pos].trim();
            return Some(field_name.to_string());
        }
    }

    None
}

/// Identify all selector fields in the form
///
/// Scans all group conditionals and returns field names that are used in `when` expressions.
/// These are the fields that determine which fragments to load.
pub fn identify_selector_fields(form: &FormDefinition) -> Vec<String> {
    let mut selectors = std::collections::HashSet::new();
    let mut selector_list: Vec<String> = Vec::new();

    // Scan all elements for items with conditional visibility
    // Preserve insertion order (order of appearance in elements)
    for element in &form.elements {
        if let Some(condition) = element.when() {
            if let Some(field_name) = extract_field_from_condition(condition) {
                // Only add if not already seen (maintain first occurrence order)
                if !selectors.contains(&field_name) {
                    selectors.insert(field_name.clone());
                    selector_list.push(field_name);
                }
            }
        }
    }

    // Also scan legacy items array for backward compatibility
    for item in &form.items {
        if let Some(ref condition) = item.when {
            if let Some(field_name) = extract_field_from_condition(condition) {
                if !selectors.contains(&field_name) {
                    selectors.insert(field_name.clone());
                    selector_list.push(field_name);
                }
            }
        }
    }

    // Return in order of first appearance, not alphabetically sorted
    selector_list
}

/// Check if a fragment should be loaded based on its condition
///
/// Returns true if:
/// - No condition is specified (fragment always loads)
/// - Condition evaluates to true with current results
///
/// Returns false if:
/// - Condition evaluates to false with current results
pub fn should_load_fragment(
    condition: Option<&str>,
    results: &HashMap<String, serde_json::Value>,
) -> bool {
    match condition {
        None => true, // No condition = always load
        Some(cond) => evaluate_condition(cond, results),
    }
}

/// Evaluate a conditional expression against previous results
/// Supports formats like:
/// - "field_name == value"
/// - "field_name != value"
/// - "field_name contains value"
/// - "field_name startswith value"
/// - "value in field_name" (array membership)
/// - "file_exists(path)" (file existence check)
/// - "!file_exists(path)" (negated file existence check)
pub fn evaluate_condition(condition: &str, results: &HashMap<String, serde_json::Value>) -> bool {
    let condition = condition.trim();

    // Check for function calls first (file_exists, etc.)
    if let Some(result) = evaluate_function_call(condition) {
        return result;
    }

    // Check string operators first (word boundaries)
    let string_operators = ["contains", "startswith", "endswith", "in"];
    for op_str in &string_operators {
        if let Some(pos) = condition.find(op_str) {
            // Make sure it's word-bounded (not part of another word)
            let before_ok = pos == 0
                || !condition[..pos]
                    .chars()
                    .last()
                    .unwrap_or(' ')
                    .is_alphanumeric();
            let after_ok = pos + op_str.len() >= condition.len()
                || !condition[pos + op_str.len()..]
                    .chars()
                    .next()
                    .unwrap_or(' ')
                    .is_alphanumeric();

            if before_ok && after_ok {
                let left = condition[..pos].trim();
                let right = condition[pos + op_str.len()..].trim();

                match *op_str {
                    "in" => {
                        // For "in" operator: "value in array_field"
                        // left = value to search for
                        // right = field name containing array
                        let field_value = results
                            .get(right)
                            .cloned()
                            .unwrap_or(serde_json::Value::Null);
                        let search_value = left;

                        // Handle array membership check
                        match &field_value {
                            serde_json::Value::Array(arr) => {
                                // Check if any array element matches the search value
                                return arr.iter().any(|v| {
                                    let v_str = value_to_string(v);
                                    v_str == search_value
                                });
                            }
                            serde_json::Value::String(s) => {
                                // Handle comma-separated string as array (for multiselect defaults)
                                return s
                                    .split(',')
                                    .map(|item| item.trim())
                                    .any(|item| item == search_value);
                            }
                            _ => return false,
                        }
                    }
                    _ => {
                        // For other operators: "field_name operator value"
                        let field_value = results
                            .get(left)
                            .cloned()
                            .unwrap_or(serde_json::Value::Null);
                        let field_str = value_to_string(&field_value);
                        let expected = parse_condition_value(right);
                        let expected_str = value_to_string(&expected);

                        match *op_str {
                            "contains" => return field_str.contains(&expected_str),
                            "startswith" => return field_str.starts_with(&expected_str),
                            "endswith" => return field_str.ends_with(&expected_str),
                            _ => {}
                        }
                    }
                }
            }
        }
    }

    // Parse numeric/comparison operators in order of precedence (longest first to avoid partial matches)
    let operators = [
        ("<=", "le"),
        (">=", "ge"),
        ("==", "eq"),
        ("!=", "ne"),
        (">", "gt"),
        ("<", "lt"),
    ];

    for (op_str, _op_name) in &operators {
        if let Some(pos) = condition.find(op_str) {
            let left = condition[..pos].trim();
            let right = condition[pos + op_str.len()..].trim();

            // Get the field value from results
            let field_value = results
                .get(left)
                .cloned()
                .unwrap_or(serde_json::Value::Null);

            // Parse the right side as value (handle quoted strings and raw values)
            let expected = parse_condition_value(right);

            // Perform comparison
            match *op_str {
                "==" => return values_equal(&field_value, &expected),
                "!=" => return !values_equal(&field_value, &expected),
                ">" => {
                    return compare_values(&field_value, &expected) == std::cmp::Ordering::Greater
                }
                "<" => return compare_values(&field_value, &expected) == std::cmp::Ordering::Less,
                ">=" => {
                    let cmp = compare_values(&field_value, &expected);
                    return cmp == std::cmp::Ordering::Greater || cmp == std::cmp::Ordering::Equal;
                }
                "<=" => {
                    let cmp = compare_values(&field_value, &expected);
                    return cmp == std::cmp::Ordering::Less || cmp == std::cmp::Ordering::Equal;
                }
                _ => {}
            }
        }
    }

    // If no valid condition found, default to true
    true
}

/// Evaluate function calls in conditions
/// Returns Some(bool) if a function call was found and evaluated
/// Returns None if no function call was found
fn evaluate_function_call(condition: &str) -> Option<bool> {
    let condition = condition.trim();

    // Handle negation prefix
    let (is_negated, clean_condition) = if let Some(stripped) = condition.strip_prefix('!') {
        (true, stripped.trim())
    } else {
        (false, condition)
    };

    // Check for file_exists() function
    if clean_condition.starts_with("file_exists(") && clean_condition.ends_with(')') {
        let start = "file_exists(".len();
        let end = clean_condition.len() - 1;
        let path_arg = clean_condition[start..end].trim();

        // Remove quotes if present
        let path = if (path_arg.starts_with('"') && path_arg.ends_with('"'))
            || (path_arg.starts_with('\'') && path_arg.ends_with('\''))
        {
            &path_arg[1..path_arg.len() - 1]
        } else {
            path_arg
        };

        // Check if file exists
        let exists = Path::new(path).exists();

        // Apply negation if needed
        return Some(if is_negated { !exists } else { exists });
    }

    None
}

/// Parse a value from condition right-hand side
fn parse_condition_value(s: &str) -> serde_json::Value {
    let s = s.trim();

    // Remove quotes if present
    if (s.starts_with('"') && s.ends_with('"')) || (s.starts_with('\'') && s.ends_with('\'')) {
        return serde_json::json!(s[1..s.len() - 1].to_string());
    }

    // Try to parse as number
    if let Ok(n) = s.parse::<i64>() {
        return serde_json::json!(n);
    }
    if let Ok(n) = s.parse::<f64>() {
        return serde_json::json!(n);
    }

    // Parse as boolean
    match s.to_lowercase().as_str() {
        "true" | "yes" | "1" => serde_json::json!(true),
        "false" | "no" | "0" => serde_json::json!(false),
        _ => serde_json::json!(s.to_string()), // Default to string
    }
}

/// Compare two values for equality, handling different types
fn values_equal(a: &serde_json::Value, b: &serde_json::Value) -> bool {
    match (a, b) {
        (serde_json::Value::String(s1), serde_json::Value::String(s2)) => s1 == s2,
        (serde_json::Value::Number(n1), serde_json::Value::Number(n2)) => n1 == n2,
        (serde_json::Value::Bool(b1), serde_json::Value::Bool(b2)) => b1 == b2,
        (serde_json::Value::Null, serde_json::Value::Null) => true,
        // Try numeric comparison if one is string and other is number
        (serde_json::Value::String(s), serde_json::Value::Number(n))
        | (serde_json::Value::Number(n), serde_json::Value::String(s)) => {
            if let Ok(parsed) = s.parse::<f64>() {
                if let Some(num_val) = n.as_f64() {
                    return (parsed - num_val).abs() < 1e-10;
                }
            }
            false
        }
        // String to bool comparison
        (serde_json::Value::String(s), serde_json::Value::Bool(b))
        | (serde_json::Value::Bool(b), serde_json::Value::String(s)) => {
            matches!(
                (s.to_lowercase().as_str(), b),
                ("true" | "yes" | "1", true) | ("false" | "no" | "0", false)
            )
        }
        _ => false,
    }
}

/// Compare two values numerically
fn compare_values(a: &serde_json::Value, b: &serde_json::Value) -> std::cmp::Ordering {
    use std::cmp::Ordering;

    // Extract numeric values
    let a_num = extract_numeric(a);
    let b_num = extract_numeric(b);

    match (a_num, b_num) {
        (Some(an), Some(bn)) => {
            if (an - bn).abs() < 1e-10 {
                Ordering::Equal
            } else if an > bn {
                Ordering::Greater
            } else {
                Ordering::Less
            }
        }
        _ => {
            // Fall back to string comparison
            let a_str = value_to_string(a);
            let b_str = value_to_string(b);
            a_str.cmp(&b_str)
        }
    }
}

/// Extract numeric value from JSON value
fn extract_numeric(v: &serde_json::Value) -> Option<f64> {
    match v {
        serde_json::Value::Number(n) => n.as_f64(),
        serde_json::Value::String(s) => s.parse::<f64>().ok(),
        serde_json::Value::Bool(b) => Some(if *b { 1.0 } else { 0.0 }),
        _ => None,
    }
}

/// Convert JSON value to string for comparison
/// Convert a serde_json::Value to a String suitable for form field defaults
pub fn value_to_string(v: &serde_json::Value) -> String {
    match v {
        serde_json::Value::String(s) => s.clone(),
        serde_json::Value::Number(n) => n.to_string(),
        serde_json::Value::Bool(b) => b.to_string(),
        serde_json::Value::Null => String::new(),
        other => other.to_string(),
    }
}

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

    #[test]
    fn test_in_operator_with_array() {
        let mut results = HashMap::new();
        results.insert(
            "detected_languages".to_string(),
            serde_json::json!(["rust", "python", "go"]),
        );

        // Should find rust
        assert!(evaluate_condition("rust in detected_languages", &results));

        // Should find python
        assert!(evaluate_condition("python in detected_languages", &results));

        // Should not find javascript
        assert!(!evaluate_condition(
            "javascript in detected_languages",
            &results
        ));
    }

    #[test]
    fn test_in_operator_with_comma_separated_string() {
        let mut results = HashMap::new();
        results.insert(
            "detected_languages".to_string(),
            serde_json::json!("rust,python,go"),
        );

        // Should find rust (with proper trimming)
        assert!(evaluate_condition("rust in detected_languages", &results));

        // Should find python
        assert!(evaluate_condition("python in detected_languages", &results));

        // Should not find javascript
        assert!(!evaluate_condition(
            "javascript in detected_languages",
            &results
        ));
    }

    #[test]
    fn test_in_operator_with_spaces() {
        let mut results = HashMap::new();
        results.insert(
            "detected_languages".to_string(),
            serde_json::json!(["rust", "nushell", "nickel"]),
        );

        // Should work with spaces around "in"
        assert!(evaluate_condition("rust in detected_languages", &results));
        assert!(evaluate_condition(
            "  rust   in   detected_languages  ",
            &results
        ));
    }

    #[test]
    fn test_in_operator_not_found() {
        let mut results = HashMap::new();
        results.insert(
            "detected_languages".to_string(),
            serde_json::json!(["rust", "python"]),
        );

        // Should not find when value not in array
        assert!(!evaluate_condition(
            "javascript in detected_languages",
            &results
        ));
    }

    #[test]
    fn test_in_operator_with_empty_array() {
        let mut results = HashMap::new();
        results.insert("detected_languages".to_string(), serde_json::json!([]));

        // Should not find anything in empty array
        assert!(!evaluate_condition("rust in detected_languages", &results));
    }

    #[test]
    fn test_in_operator_with_missing_field() {
        let results = HashMap::new();

        // Should not find when field doesn't exist
        assert!(!evaluate_condition("rust in detected_languages", &results));
    }

    #[test]
    fn test_extract_field_from_in_condition() {
        // For "in" operator, should extract the array field name (right side)
        assert_eq!(
            extract_field_from_condition("rust in detected_languages"),
            Some("detected_languages".to_string())
        );

        assert_eq!(
            extract_field_from_condition("python in languages"),
            Some("languages".to_string())
        );
    }

    #[test]
    fn test_existing_operators_still_work() {
        let mut results = HashMap::new();
        results.insert("enable_feature".to_string(), serde_json::json!(true));
        results.insert("name".to_string(), serde_json::json!("test"));

        // Existing operators should still work
        assert!(evaluate_condition("enable_feature == true", &results));
        assert!(evaluate_condition("name == test", &results));
    }

    #[test]
    fn test_file_exists_with_existing_file() {
        let results = HashMap::new();

        // Create a temporary file
        let temp_dir = std::env::temp_dir();
        let test_file = temp_dir.join("typedialog_test_file.txt");
        std::fs::write(&test_file, "test content").unwrap();

        // Test with double quotes
        let condition = format!("file_exists(\"{}\")", test_file.display());
        assert!(evaluate_condition(&condition, &results));

        // Test with single quotes
        let condition = format!("file_exists('{}')", test_file.display());
        assert!(evaluate_condition(&condition, &results));

        // Test without quotes
        let condition = format!("file_exists({})", test_file.display());
        assert!(evaluate_condition(&condition, &results));

        // Clean up
        std::fs::remove_file(&test_file).unwrap();
    }

    #[test]
    fn test_file_exists_with_nonexistent_file() {
        let results = HashMap::new();

        // Use a path that definitely doesn't exist
        let nonexistent = "/tmp/typedialog_nonexistent_file_12345.txt";

        let condition = format!("file_exists(\"{}\")", nonexistent);
        assert!(!evaluate_condition(&condition, &results));
    }

    #[test]
    fn test_file_exists_negated() {
        let results = HashMap::new();

        // Create a temporary file
        let temp_dir = std::env::temp_dir();
        let test_file = temp_dir.join("typedialog_test_negation.txt");
        std::fs::write(&test_file, "test").unwrap();

        // Negated: should return false for existing file
        let condition = format!("!file_exists(\"{}\")", test_file.display());
        assert!(!evaluate_condition(&condition, &results));

        // Clean up
        std::fs::remove_file(&test_file).unwrap();

        // After removal, negated should return true
        let condition = format!("!file_exists(\"{}\")", test_file.display());
        assert!(evaluate_condition(&condition, &results));
    }

    #[test]
    fn test_file_exists_with_directory() {
        let results = HashMap::new();

        // Test with an existing directory
        let temp_dir = std::env::temp_dir();
        let condition = format!("file_exists(\"{}\")", temp_dir.display());

        // Path::exists() returns true for both files and directories
        assert!(evaluate_condition(&condition, &results));
    }

    #[test]
    fn test_file_exists_with_relative_path() {
        let results = HashMap::new();

        // Create a file in current directory
        std::fs::write("typedialog_test_relative.txt", "test").unwrap();

        let condition = "file_exists(\"typedialog_test_relative.txt\")";
        assert!(evaluate_condition(condition, &results));

        // Clean up
        std::fs::remove_file("typedialog_test_relative.txt").unwrap();
    }
}
