//! Contract Validator and Analyzer
//!
//! Validates Nickel contracts and predicates against JSON values.
//! Analyzes contracts to infer conditional expressions for form generation.
//!
//! Supported validation predicates:
//! - `std.string.NonEmpty` - Non-empty string
//! - `std.string.length.min N` - Minimum string length
//! - `std.string.length.max N` - Maximum string length
//! - `std.string.Email` - Valid email address format
//! - `std.string.Url` - Valid URL format (http/https/ftp/ftps)
//! - `std.number.between A B` - Number in range [A, B]
//! - `std.number.greater_than N` - Number > N
//! - `std.number.less_than N` - Number < N

use super::schema_ir::{NickelFieldIR, NickelSchemaIR, NickelType};
use crate::error::ErrorWrapper;
use crate::Result;
use serde_json::Value;

/// Validator for Nickel contracts and predicates
pub struct ContractValidator;

impl ContractValidator {
    /// Validate a value against a Nickel contract
    ///
    /// # Arguments
    ///
    /// * `value` - The JSON value to validate
    /// * `contract` - The Nickel contract string (e.g., "String | std.string.NonEmpty")
    ///
    /// # Returns
    ///
    /// Ok if validation succeeds, Err with descriptive message if it fails
    pub fn validate(value: &Value, contract: &str) -> Result<()> {
        // Extract the predicate from the contract (after the pipe)
        let predicate = contract
            .rfind('|')
            .map(|i| contract[i + 1..].trim())
            .unwrap_or(contract);

        // Match common predicates
        if predicate.contains("std.string.NonEmpty") {
            return Self::validate_non_empty_string(value);
        }

        if predicate.contains("std.string.length.min") {
            if let Some(n) = Self::extract_number(predicate, "std.string.length.min") {
                return Self::validate_min_length(value, n);
            }
        }

        if predicate.contains("std.string.length.max") {
            if let Some(n) = Self::extract_number(predicate, "std.string.length.max") {
                return Self::validate_max_length(value, n);
            }
        }

        if predicate.contains("std.number.between") {
            if let Some((a, b)) = Self::extract_range(predicate) {
                return Self::validate_between(value, a, b);
            }
        }

        if predicate.contains("std.number.greater_than") {
            if let Some(n) = Self::extract_number(predicate, "std.number.greater_than") {
                return Self::validate_greater_than(value, n as f64);
            }
        }

        if predicate.contains("std.number.less_than") {
            if let Some(n) = Self::extract_number(predicate, "std.number.less_than") {
                return Self::validate_less_than(value, n as f64);
            }
        }

        if predicate.contains("std.string.Email") {
            return Self::validate_email(value);
        }

        if predicate.contains("std.string.Url") {
            return Self::validate_url(value);
        }

        // Unknown predicate - pass validation
        Ok(())
    }

    /// Validate that string is non-empty
    fn validate_non_empty_string(value: &Value) -> Result<()> {
        match value {
            Value::String(s) => {
                if s.is_empty() {
                    Err(ErrorWrapper::new(
                        "String must not be empty (std.string.NonEmpty)".to_string(),
                    ))
                } else {
                    Ok(())
                }
            }
            _ => Err(ErrorWrapper::new("Expected string value".to_string())),
        }
    }

    /// Validate minimum string length
    fn validate_min_length(value: &Value, min: usize) -> Result<()> {
        match value {
            Value::String(s) => {
                if s.len() < min {
                    Err(ErrorWrapper::new(format!(
                        "String must be at least {} characters (std.string.length.min {})",
                        min, min
                    )))
                } else {
                    Ok(())
                }
            }
            _ => Err(ErrorWrapper::new("Expected string value".to_string())),
        }
    }

    /// Validate maximum string length
    fn validate_max_length(value: &Value, max: usize) -> Result<()> {
        match value {
            Value::String(s) => {
                if s.len() > max {
                    Err(ErrorWrapper::new(format!(
                        "String must be at most {} characters (std.string.length.max {})",
                        max, max
                    )))
                } else {
                    Ok(())
                }
            }
            _ => Err(ErrorWrapper::new("Expected string value".to_string())),
        }
    }

    /// Validate number is in range [a, b]
    fn validate_between(value: &Value, a: f64, b: f64) -> Result<()> {
        match value {
            Value::Number(n) => {
                if let Some(num) = n.as_f64() {
                    if num >= a && num <= b {
                        Ok(())
                    } else {
                        Err(ErrorWrapper::new(format!(
                            "Number must be between {} and {} (std.number.between {} {})",
                            a, b, a, b
                        )))
                    }
                } else {
                    Err(ErrorWrapper::new("Invalid number value".to_string()))
                }
            }
            _ => Err(ErrorWrapper::new("Expected number value".to_string())),
        }
    }

    /// Validate number is greater than n
    fn validate_greater_than(value: &Value, n: f64) -> Result<()> {
        match value {
            Value::Number(num) => {
                if let Some(val) = num.as_f64() {
                    if val > n {
                        Ok(())
                    } else {
                        Err(ErrorWrapper::new(format!(
                            "Number must be greater than {} (std.number.greater_than {})",
                            n, n
                        )))
                    }
                } else {
                    Err(ErrorWrapper::new("Invalid number value".to_string()))
                }
            }
            _ => Err(ErrorWrapper::new("Expected number value".to_string())),
        }
    }

    /// Validate number is less than n
    fn validate_less_than(value: &Value, n: f64) -> Result<()> {
        match value {
            Value::Number(num) => {
                if let Some(val) = num.as_f64() {
                    if val < n {
                        Ok(())
                    } else {
                        Err(ErrorWrapper::new(format!(
                            "Number must be less than {} (std.number.less_than {})",
                            n, n
                        )))
                    }
                } else {
                    Err(ErrorWrapper::new("Invalid number value".to_string()))
                }
            }
            _ => Err(ErrorWrapper::new("Expected number value".to_string())),
        }
    }

    /// Extract a single number from predicate string
    fn extract_number(predicate: &str, pattern: &str) -> Option<usize> {
        let start = predicate.find(pattern)? + pattern.len();
        let rest = &predicate[start..];

        // Extract digits after the pattern
        rest.split_whitespace()
            .next()
            .and_then(|s| s.trim_matches(|c: char| !c.is_ascii_digit()).parse().ok())
    }

    /// Extract a range (a, b) from between predicate
    fn extract_range(predicate: &str) -> Option<(f64, f64)> {
        // Parse patterns like "std.number.between 0 100" or "std.number.between 0.5 99.9"
        let start = predicate.find("std.number.between")? + "std.number.between".len();
        let rest = predicate[start..].trim();

        let parts: Vec<&str> = rest.split_whitespace().collect();
        if parts.len() < 2 {
            return None;
        }

        let a = parts[0].parse::<f64>().ok()?;
        let b = parts[1].parse::<f64>().ok()?;

        Some((a, b))
    }

    /// Validate email address format
    ///
    /// Uses a simple regex pattern to check basic email format.
    /// Pattern: local@domain where local can contain alphanumeric, dots, hyphens, underscores
    /// and domain must have at least one dot.
    pub fn validate_email(value: &Value) -> Result<()> {
        match value {
            Value::String(s) => {
                if s.is_empty() {
                    return Err(ErrorWrapper::new(
                        "Email address cannot be empty".to_string(),
                    ));
                }

                let at_count = s.matches('@').count();
                if at_count != 1 {
                    return Err(ErrorWrapper::new(
                        "Email must contain exactly one @ symbol".to_string(),
                    ));
                }

                let parts: Vec<&str> = s.split('@').collect();
                let local = parts[0];
                let domain = parts[1];

                if local.is_empty() {
                    return Err(ErrorWrapper::new(
                        "Email local part cannot be empty".to_string(),
                    ));
                }

                if domain.is_empty() {
                    return Err(ErrorWrapper::new(
                        "Email domain cannot be empty".to_string(),
                    ));
                }

                if !domain.contains('.') {
                    return Err(ErrorWrapper::new(
                        "Email domain must contain at least one dot".to_string(),
                    ));
                }

                if domain.starts_with('.') || domain.ends_with('.') {
                    return Err(ErrorWrapper::new(
                        "Email domain cannot start or end with a dot".to_string(),
                    ));
                }

                Ok(())
            }
            _ => Err(ErrorWrapper::new(
                "Expected string value for email".to_string(),
            )),
        }
    }

    /// Validate URL format
    ///
    /// Checks for basic URL structure: scheme://host with optional path.
    /// Accepted schemes: http, https, ftp, ftps.
    pub fn validate_url(value: &Value) -> Result<()> {
        match value {
            Value::String(s) => {
                if s.is_empty() {
                    return Err(ErrorWrapper::new("URL cannot be empty".to_string()));
                }

                let valid_schemes = ["http://", "https://", "ftp://", "ftps://"];
                let has_valid_scheme = valid_schemes.iter().any(|scheme| s.starts_with(scheme));

                if !has_valid_scheme {
                    return Err(ErrorWrapper::new(format!(
                        "URL must start with one of: {}",
                        valid_schemes.join(", ")
                    )));
                }

                let scheme_end = s.find("://").unwrap() + 3;
                let rest = &s[scheme_end..];

                if rest.is_empty() {
                    return Err(ErrorWrapper::new(
                        "URL must contain a host after the scheme".to_string(),
                    ));
                }

                Ok(())
            }
            _ => Err(ErrorWrapper::new(
                "Expected string value for URL".to_string(),
            )),
        }
    }
}

/// Analyzer for inferring conditional expressions from Nickel contracts
pub struct ContractAnalyzer;

impl ContractAnalyzer {
    /// Infer a conditional expression from a field based on optional status and schema context
    ///
    /// Returns a `when` expression (e.g., "tls_enabled == true") if the field appears to be
    /// conditionally dependent on another field's value, typically a boolean enable flag.
    pub fn infer_condition(field: &NickelFieldIR, schema: &NickelSchemaIR) -> Option<String> {
        // Only optional fields might have conditionals
        if !field.optional {
            return None;
        }

        // Try to find a boolean field that enables this optional field
        Self::find_boolean_dependency(field, schema)
    }

    /// Find a boolean field that enables this optional field based on naming convention
    fn find_boolean_dependency(field: &NickelFieldIR, schema: &NickelSchemaIR) -> Option<String> {
        // Extract prefix: "tls_cert_path" -> "tls"
        let field_prefix = Self::extract_prefix(&field.flat_name)?;

        // Look for a boolean field like "tls_enabled"
        for candidate in &schema.fields {
            if candidate.nickel_type == NickelType::Bool {
                let candidate_prefix = Self::extract_prefix(&candidate.flat_name)?;

                // Match pattern: same prefix + "_enabled" suffix
                if candidate_prefix == field_prefix && candidate.flat_name.ends_with("_enabled") {
                    return Some(format!("{} == true", candidate.flat_name));
                }
            }
        }

        None
    }

    /// Extract the common prefix from a field name
    /// "tls_cert_path" -> "tls"
    /// "database_url" -> "database"
    fn extract_prefix(flat_name: &str) -> Option<String> {
        flat_name
            .split('_')
            .next()
            .filter(|s| !s.is_empty())
            .map(|s| s.to_string())
    }
}

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

    #[test]
    fn test_validate_non_empty_string() {
        let result = ContractValidator::validate(&json!("hello"), "String | std.string.NonEmpty");
        assert!(result.is_ok());

        let result = ContractValidator::validate(&json!(""), "String | std.string.NonEmpty");
        assert!(result.is_err());
    }

    #[test]
    fn test_validate_min_length() {
        let result =
            ContractValidator::validate(&json!("hello"), "String | std.string.length.min 3");
        assert!(result.is_ok());

        let result = ContractValidator::validate(&json!("hi"), "String | std.string.length.min 3");
        assert!(result.is_err());
    }

    #[test]
    fn test_validate_max_length() {
        let result = ContractValidator::validate(&json!("hi"), "String | std.string.length.max 3");
        assert!(result.is_ok());

        let result =
            ContractValidator::validate(&json!("hello"), "String | std.string.length.max 3");
        assert!(result.is_err());
    }

    #[test]
    fn test_validate_between() {
        let result = ContractValidator::validate(&json!(50), "Number | std.number.between 0 100");
        assert!(result.is_ok());

        let result = ContractValidator::validate(&json!(150), "Number | std.number.between 0 100");
        assert!(result.is_err());
    }

    #[test]
    fn test_validate_greater_than() {
        let result = ContractValidator::validate(&json!(50), "Number | std.number.greater_than 10");
        assert!(result.is_ok());

        let result = ContractValidator::validate(&json!(5), "Number | std.number.greater_than 10");
        assert!(result.is_err());
    }

    #[test]
    fn test_validate_less_than() {
        let result = ContractValidator::validate(&json!(5), "Number | std.number.less_than 10");
        assert!(result.is_ok());

        let result = ContractValidator::validate(&json!(50), "Number | std.number.less_than 10");
        assert!(result.is_err());
    }

    #[test]
    fn test_extract_range() {
        let range = ContractValidator::extract_range("std.number.between 0 100");
        assert_eq!(range, Some((0.0, 100.0)));

        let range = ContractValidator::extract_range("std.number.between 0.5 99.9");
        assert_eq!(range, Some((0.5, 99.9)));
    }

    #[test]
    fn test_unknown_predicate_passes() {
        let result =
            ContractValidator::validate(&json!("anything"), "String | some.unknown.predicate");
        assert!(result.is_ok());
    }
}
