// (C) Copyright 2024- ECMWF and individual contributors.
//
// This software is licensed under the terms of the Apache Licence Version 2.0
// which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
// In applying this licence, ECMWF does not waive the privileges and immunities
// granted to it by virtue of its status as an intergovernmental organisation nor
// does it submit to any jurisdiction.

//! Constraint parsing and evaluation helpers for identifier filters.
//!
//! These helpers are schema-agnostic and validate one constraint object at a time.

use anyhow::{Context, Result, bail};
use serde_json::Value;

#[derive(Debug, Clone, PartialEq)]
pub enum NumericConstraint<T> {
    Eq(T),
    In(Vec<T>),
    Gt(T),
    Gte(T),
    Lt(T),
    Lte(T),
    Between(T, T),
}

impl NumericConstraint<i64> {
    pub fn matches(&self, value: i64) -> bool {
        matches_numeric(self, value)
    }
}

impl NumericConstraint<f64> {
    pub fn matches(&self, value: f64) -> bool {
        if !value.is_finite() {
            return false;
        }
        // Exact comparison keeps replay/live filtering deterministic and avoids hidden
        // tolerance behavior when identifiers are canonicalized into topic tokens.
        matches_numeric(self, value)
    }
}

fn matches_numeric<T>(constraint: &NumericConstraint<T>, value: T) -> bool
where
    T: PartialOrd + PartialEq + Copy,
{
    match constraint {
        NumericConstraint::Eq(v) => value == *v,
        NumericConstraint::In(values) => values.contains(&value),
        NumericConstraint::Gt(v) => value > *v,
        NumericConstraint::Gte(v) => value >= *v,
        NumericConstraint::Lt(v) => value < *v,
        NumericConstraint::Lte(v) => value <= *v,
        NumericConstraint::Between(min, max) => value >= *min && value <= *max,
    }
}

#[derive(Debug, Clone, PartialEq)]
pub enum EnumConstraint {
    Eq(String),
    In(Vec<String>),
}

impl EnumConstraint {
    pub fn matches(&self, value: &str) -> bool {
        let canonical = value.to_lowercase();
        match self {
            EnumConstraint::Eq(v) => canonical == *v,
            EnumConstraint::In(values) => values.iter().any(|v| v == &canonical),
        }
    }
}

pub fn parse_int_constraint(
    field_name: &str,
    value: &Value,
    range: Option<&[i64; 2]>,
) -> Result<NumericConstraint<i64>> {
    let object = parse_constraint_object(field_name, value)?;
    let (operator, operand) = single_operator(field_name, object)?;

    let constraint = match operator {
        "eq" => NumericConstraint::Eq(parse_int_operand(field_name, operator, operand)?),
        "in" => NumericConstraint::In(parse_int_array_operand(field_name, operator, operand)?),
        "gt" => NumericConstraint::Gt(parse_int_operand(field_name, operator, operand)?),
        "gte" => NumericConstraint::Gte(parse_int_operand(field_name, operator, operand)?),
        "lt" => NumericConstraint::Lt(parse_int_operand(field_name, operator, operand)?),
        "lte" => NumericConstraint::Lte(parse_int_operand(field_name, operator, operand)?),
        "between" => {
            let (min, max) = parse_int_between_operand(field_name, operator, operand)?;
            NumericConstraint::Between(min, max)
        }
        _ => {
            bail!(
                "Field '{}' has invalid operator '{}'. Allowed: eq,in,gt,gte,lt,lte,between",
                field_name,
                operator
            )
        }
    };

    validate_int_constraint_against_range(field_name, &constraint, range)?;
    Ok(constraint)
}

pub fn parse_float_constraint(
    field_name: &str,
    value: &Value,
    range: Option<&[f64; 2]>,
) -> Result<NumericConstraint<f64>> {
    let object = parse_constraint_object(field_name, value)?;
    let (operator, operand) = single_operator(field_name, object)?;

    let constraint = match operator {
        "eq" => NumericConstraint::Eq(parse_float_operand(field_name, operator, operand)?),
        "in" => NumericConstraint::In(parse_float_array_operand(field_name, operator, operand)?),
        "gt" => NumericConstraint::Gt(parse_float_operand(field_name, operator, operand)?),
        "gte" => NumericConstraint::Gte(parse_float_operand(field_name, operator, operand)?),
        "lt" => NumericConstraint::Lt(parse_float_operand(field_name, operator, operand)?),
        "lte" => NumericConstraint::Lte(parse_float_operand(field_name, operator, operand)?),
        "between" => {
            let (min, max) = parse_float_between_operand(field_name, operator, operand)?;
            NumericConstraint::Between(min, max)
        }
        _ => {
            bail!(
                "Field '{}' has invalid operator '{}'. Allowed: eq,in,gt,gte,lt,lte,between",
                field_name,
                operator
            )
        }
    };

    validate_float_constraint_against_range(field_name, &constraint, range)?;
    Ok(constraint)
}

pub fn parse_enum_constraint(
    field_name: &str,
    value: &Value,
    allowed_values: &[String],
) -> Result<EnumConstraint> {
    let object = parse_constraint_object(field_name, value)?;
    let (operator, operand) = single_operator(field_name, object)?;
    let normalized_allowed: Vec<String> = allowed_values.iter().map(|v| v.to_lowercase()).collect();

    let constraint = match operator {
        "eq" => {
            let parsed = parse_enum_operand(field_name, operator, operand)?;
            validate_enum_value(field_name, &parsed, &normalized_allowed)?;
            EnumConstraint::Eq(parsed)
        }
        "in" => {
            let parsed = parse_enum_array_operand(field_name, operator, operand)?;
            for value in &parsed {
                validate_enum_value(field_name, value, &normalized_allowed)?;
            }
            EnumConstraint::In(parsed)
        }
        _ => {
            bail!(
                "Field '{}' has invalid operator '{}'. Allowed for enum constraints: eq,in",
                field_name,
                operator
            )
        }
    };

    Ok(constraint)
}

fn parse_constraint_object<'a>(
    field_name: &str,
    value: &'a Value,
) -> Result<&'a serde_json::Map<String, Value>> {
    value.as_object().ok_or_else(|| {
        anyhow::anyhow!(
            "Field '{}' constraint must be an object (for example {{\"gte\": 4}})",
            field_name
        )
    })
}

fn single_operator<'a>(
    field_name: &str,
    object: &'a serde_json::Map<String, Value>,
) -> Result<(&'a str, &'a Value)> {
    if object.len() != 1 {
        bail!(
            "Field '{}' constraint must contain exactly one operator",
            field_name
        );
    }
    let (operator, operand) = object
        .iter()
        .next()
        .context("constraint object unexpectedly empty")?;
    Ok((operator.as_str(), operand))
}

fn parse_int_operand(field_name: &str, operator: &str, value: &Value) -> Result<i64> {
    value.as_i64().ok_or_else(|| {
        anyhow::anyhow!(
            "Field '{}' operator '{}' expects integer value",
            field_name,
            operator
        )
    })
}

fn parse_int_array_operand(field_name: &str, operator: &str, value: &Value) -> Result<Vec<i64>> {
    let values = value.as_array().ok_or_else(|| {
        anyhow::anyhow!(
            "Field '{}' operator '{}' expects array of integers",
            field_name,
            operator
        )
    })?;
    if values.is_empty() {
        bail!(
            "Field '{}' operator '{}' must contain at least one value",
            field_name,
            operator
        );
    }
    values
        .iter()
        .map(|v| parse_int_operand(field_name, operator, v))
        .collect()
}

fn parse_int_between_operand(
    field_name: &str,
    operator: &str,
    value: &Value,
) -> Result<(i64, i64)> {
    let values = value.as_array().ok_or_else(|| {
        anyhow::anyhow!(
            "Field '{}' operator '{}' expects [min,max]",
            field_name,
            operator
        )
    })?;
    if values.len() != 2 {
        bail!(
            "Field '{}' operator '{}' expects exactly two values [min,max]",
            field_name,
            operator
        );
    }
    let min = parse_int_operand(field_name, operator, &values[0])?;
    let max = parse_int_operand(field_name, operator, &values[1])?;
    if min > max {
        bail!(
            "Field '{}' operator '{}' expects min <= max",
            field_name,
            operator
        );
    }
    Ok((min, max))
}

fn parse_float_operand(field_name: &str, operator: &str, value: &Value) -> Result<f64> {
    let parsed = value.as_f64().ok_or_else(|| {
        anyhow::anyhow!(
            "Field '{}' operator '{}' expects numeric value",
            field_name,
            operator
        )
    })?;

    if !parsed.is_finite() {
        bail!(
            "Field '{}' operator '{}' expects finite numeric value",
            field_name,
            operator
        );
    }

    Ok(parsed)
}

fn parse_float_array_operand(field_name: &str, operator: &str, value: &Value) -> Result<Vec<f64>> {
    let values = value.as_array().ok_or_else(|| {
        anyhow::anyhow!(
            "Field '{}' operator '{}' expects array of numbers",
            field_name,
            operator
        )
    })?;
    if values.is_empty() {
        bail!(
            "Field '{}' operator '{}' must contain at least one value",
            field_name,
            operator
        );
    }
    values
        .iter()
        .map(|v| parse_float_operand(field_name, operator, v))
        .collect()
}

fn parse_float_between_operand(
    field_name: &str,
    operator: &str,
    value: &Value,
) -> Result<(f64, f64)> {
    let values = value.as_array().ok_or_else(|| {
        anyhow::anyhow!(
            "Field '{}' operator '{}' expects [min,max]",
            field_name,
            operator
        )
    })?;
    if values.len() != 2 {
        bail!(
            "Field '{}' operator '{}' expects exactly two values [min,max]",
            field_name,
            operator
        );
    }
    let min = parse_float_operand(field_name, operator, &values[0])?;
    let max = parse_float_operand(field_name, operator, &values[1])?;
    if min > max {
        bail!(
            "Field '{}' operator '{}' expects min <= max",
            field_name,
            operator
        );
    }
    Ok((min, max))
}

fn parse_enum_operand(field_name: &str, operator: &str, value: &Value) -> Result<String> {
    value.as_str().map(|v| v.to_lowercase()).ok_or_else(|| {
        anyhow::anyhow!(
            "Field '{}' operator '{}' expects string value",
            field_name,
            operator
        )
    })
}

fn parse_enum_array_operand(
    field_name: &str,
    operator: &str,
    value: &Value,
) -> Result<Vec<String>> {
    let values = value.as_array().ok_or_else(|| {
        anyhow::anyhow!(
            "Field '{}' operator '{}' expects array of strings",
            field_name,
            operator
        )
    })?;
    if values.is_empty() {
        bail!(
            "Field '{}' operator '{}' must contain at least one value",
            field_name,
            operator
        );
    }
    values
        .iter()
        .map(|v| parse_enum_operand(field_name, operator, v))
        .collect()
}

fn validate_enum_value(field_name: &str, value: &str, allowed_values: &[String]) -> Result<()> {
    if !allowed_values.iter().any(|allowed| allowed == value) {
        bail!(
            "Field '{}' has invalid enum value '{}'. Allowed: [{}]",
            field_name,
            value,
            allowed_values.join(", ")
        );
    }
    Ok(())
}

fn validate_int_constraint_against_range(
    field_name: &str,
    constraint: &NumericConstraint<i64>,
    range: Option<&[i64; 2]>,
) -> Result<()> {
    let Some([min, max]) = range else {
        return Ok(());
    };

    validate_constraint_against_range(constraint, |value| {
        if value < *min || value > *max {
            bail!(
                "Field '{}' constraint value {} is outside allowed range [{}, {}]",
                field_name,
                value,
                min,
                max
            );
        }
        Ok(())
    })
}

fn validate_float_constraint_against_range(
    field_name: &str,
    constraint: &NumericConstraint<f64>,
    range: Option<&[f64; 2]>,
) -> Result<()> {
    let Some([min, max]) = range else {
        return Ok(());
    };

    // Float operands are validated as finite during parse_*_constraint.
    validate_constraint_against_range(constraint, |value| {
        if value < *min || value > *max {
            bail!(
                "Field '{}' constraint value {} is outside allowed range [{}, {}]",
                field_name,
                value,
                min,
                max
            );
        }
        Ok(())
    })
}

fn validate_constraint_against_range<T, F>(
    constraint: &NumericConstraint<T>,
    mut validate_operand: F,
) -> Result<()>
where
    T: Copy,
    F: FnMut(T) -> Result<()>,
{
    match constraint {
        NumericConstraint::Eq(v)
        | NumericConstraint::Gt(v)
        | NumericConstraint::Gte(v)
        | NumericConstraint::Lt(v)
        | NumericConstraint::Lte(v) => validate_operand(*v),
        NumericConstraint::In(values) => {
            for value in values {
                validate_operand(*value)?;
            }
            Ok(())
        }
        NumericConstraint::Between(from, to) => {
            validate_operand(*from)?;
            validate_operand(*to)
        }
    }
}

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

    #[test]
    fn int_constraint_parsing_and_matching_works() {
        let constraint = parse_int_constraint("severity", &json!({"gte": 4}), Some(&[1, 7]))
            .expect("constraint should parse");
        assert!(constraint.matches(4));
        assert!(constraint.matches(7));
        assert!(!constraint.matches(3));
    }

    #[test]
    fn int_constraint_rejects_unknown_operator() {
        let result = parse_int_constraint("severity", &json!({"ge": 4}), Some(&[1, 7]));
        assert!(result.is_err());
    }

    #[test]
    fn int_constraint_rejects_out_of_range_operand() {
        let result = parse_int_constraint("severity", &json!({"gte": 9}), Some(&[1, 7]));
        assert!(result.is_err());
    }

    #[test]
    fn int_between_requires_ordered_bounds() {
        let result = parse_int_constraint("severity", &json!({"between": [6, 2]}), Some(&[1, 7]));
        assert!(result.is_err());
    }

    #[test]
    fn enum_constraint_in_works_case_insensitive() {
        let constraint = parse_enum_constraint(
            "alert_level",
            &json!({"in": ["High", "Critical"]}),
            &[
                "low".to_string(),
                "high".to_string(),
                "critical".to_string(),
            ],
        )
        .expect("enum constraint should parse");
        assert!(constraint.matches("high"));
        assert!(constraint.matches("CRITICAL"));
        assert!(!constraint.matches("low"));
    }

    #[test]
    fn enum_constraint_rejects_invalid_members() {
        let result = parse_enum_constraint(
            "alert_level",
            &json!({"in": ["high", "urgent"]}),
            &[
                "low".to_string(),
                "high".to_string(),
                "critical".to_string(),
            ],
        );
        assert!(result.is_err());
    }

    #[test]
    fn float_constraint_parses_and_matches() {
        let constraint =
            parse_float_constraint("temperature", &json!({"between": [10.5, 20.5]}), None)
                .expect("float constraint should parse");
        assert!(constraint.matches(15.0));
        assert!(!constraint.matches(22.0));
    }

    #[test]
    fn float_eq_uses_exact_comparison() {
        let constraint =
            parse_float_constraint("temperature", &json!({"eq": 0.3}), None).expect("valid eq");
        assert!(constraint.matches(0.3));
        assert!(!constraint.matches(0.1 + 0.2));
    }

    #[test]
    fn float_in_uses_exact_comparison() {
        let constraint = parse_float_constraint("temperature", &json!({"in": [0.3, 1.5]}), None)
            .expect("valid in");
        assert!(constraint.matches(0.3));
        assert!(!constraint.matches(0.1 + 0.2));
    }

    #[test]
    fn float_between_is_inclusive() {
        let constraint =
            parse_float_constraint("temperature", &json!({"between": [10.5, 20.5]}), None)
                .expect("float between should parse");
        assert!(constraint.matches(10.5));
        assert!(constraint.matches(20.5));
        assert!(!constraint.matches(20.5000001));
    }

    #[test]
    fn float_constraint_rejects_non_finite_notification_values() {
        let constraint =
            parse_float_constraint("temperature", &json!({"gt": 3.0}), None).expect("valid gt");
        assert!(!constraint.matches(f64::NAN));
        assert!(!constraint.matches(f64::INFINITY));
        assert!(!constraint.matches(f64::NEG_INFINITY));
    }

    #[test]
    fn float_constraint_rejects_out_of_range_operand() {
        let result =
            parse_float_constraint("temperature", &json!({"gt": 21.0}), Some(&[10.0, 20.0]));
        assert!(result.is_err());
    }
}
