use serde_json::Value;

use crate::{Schema, SchemaError, SchemaKind};

impl Schema {
    pub fn validate_shape(&self, value: &Value) -> Result<(), SchemaError> {
        validate_value(self, value, "$")
    }
}

fn validate_value(schema: &Schema, value: &Value, path: &str) -> Result<(), SchemaError> {
    if value.is_null() {
        if schema.nullable || schema.kind == SchemaKind::Null {
            return Ok(());
        }

        return Err(SchemaError::invalid_type(
            path,
            schema.expected_type_name(),
            "null",
        ));
    }

    match schema.kind {
        SchemaKind::Object => validate_object(schema, value, path),
        SchemaKind::Array => validate_array(schema, value, path),
        SchemaKind::String => validate_scalar(schema, value, path, Value::is_string),
        SchemaKind::Integer => validate_scalar(schema, value, path, Value::is_i64_or_u64),
        SchemaKind::Number => validate_scalar(schema, value, path, Value::is_number),
        SchemaKind::Boolean => validate_scalar(schema, value, path, Value::is_boolean),
        SchemaKind::Enum => validate_enum(schema, value, path),
        SchemaKind::Null => Err(SchemaError::invalid_type(
            path,
            "null",
            actual_type_name(value),
        )),
        SchemaKind::Oneof => validate_oneof(schema, value, path),
    }
}

fn validate_object(schema: &Schema, value: &Value, path: &str) -> Result<(), SchemaError> {
    let Some(object) = value.as_object() else {
        return Err(SchemaError::invalid_type(
            path,
            "object",
            actual_type_name(value),
        ));
    };

    for (field_name, field_schema) in &schema.fields {
        let field_path = format!("{path}.{field_name}");

        match object.get(field_name) {
            Some(field_value) => validate_value(field_schema, field_value, &field_path)?,
            None if field_schema.required => return Err(SchemaError::missing_field(field_path)),
            None => {}
        }
    }

    Ok(())
}

fn validate_array(schema: &Schema, value: &Value, path: &str) -> Result<(), SchemaError> {
    let Some(items) = value.as_array() else {
        return Err(SchemaError::invalid_type(
            path,
            "array",
            actual_type_name(value),
        ));
    };

    if let Some(item_schema) = schema.items.as_deref() {
        for (index, item) in items.iter().enumerate() {
            let item_path = format!("{path}[{index}]");
            validate_value(item_schema, item, &item_path)?;
        }
    }

    Ok(())
}

fn validate_scalar(
    schema: &Schema,
    value: &Value,
    path: &str,
    is_valid: impl Fn(&Value) -> bool,
) -> Result<(), SchemaError> {
    if is_valid(value) {
        return Ok(());
    }

    Err(SchemaError::invalid_type(
        path,
        schema.expected_type_name(),
        actual_type_name(value),
    ))
}

fn validate_enum(schema: &Schema, value: &Value, path: &str) -> Result<(), SchemaError> {
    let Some(actual) = value.as_str() else {
        return Err(SchemaError::invalid_type(
            path,
            "enum",
            actual_type_name(value),
        ));
    };

    if schema.enum_values.iter().any(|expected| expected == actual) {
        return Ok(());
    }

    Err(SchemaError::invalid_enum_value(
        path,
        schema.enum_values.clone(),
        actual,
    ))
}

fn validate_oneof(schema: &Schema, value: &Value, path: &str) -> Result<(), SchemaError> {
    if schema
        .variants
        .iter()
        .any(|variant| validate_value(variant, value, path).is_ok())
    {
        return Ok(());
    }

    Err(SchemaError::oneof_mismatch(path))
}

fn actual_type_name(value: &Value) -> &'static str {
    match value {
        Value::Null => "null",
        Value::Bool(_) => "boolean",
        Value::Number(number) if number.is_i64() || number.is_u64() => "integer",
        Value::Number(_) => "number",
        Value::String(_) => "string",
        Value::Array(_) => "array",
        Value::Object(_) => "object",
    }
}

trait JsonValueExt {
    fn is_i64_or_u64(&self) -> bool;
}

impl JsonValueExt for Value {
    fn is_i64_or_u64(&self) -> bool {
        self.as_i64().is_some() || self.as_u64().is_some()
    }
}

#[cfg(test)]
mod tests {
    use std::collections::BTreeMap;

    use serde_json::json;

    use crate::{Schema, SchemaError, SchemaKind};

    #[test]
    fn validates_nested_object_shape() {
        let schema = Schema {
            kind: SchemaKind::Object,
            description: None,
            required: true,
            nullable: false,
            default_value: None,
            fields: BTreeMap::from([(
                "lead".to_owned(),
                Schema {
                    kind: SchemaKind::Object,
                    description: None,
                    required: true,
                    nullable: false,
                    default_value: None,
                    fields: BTreeMap::from([(
                        "email".to_owned(),
                        Schema {
                            kind: SchemaKind::String,
                            description: None,
                            required: true,
                            nullable: false,
                            default_value: None,
                            fields: BTreeMap::new(),
                            items: None,
                            enum_values: Vec::new(),
                            variants: Vec::new(),
                        },
                    )]),
                    items: None,
                    enum_values: Vec::new(),
                    variants: Vec::new(),
                },
            )]),
            items: None,
            enum_values: Vec::new(),
            variants: Vec::new(),
        };

        let result = schema.validate_shape(&json!({
            "lead": {
                "email": "user@example.com"
            }
        }));

        assert!(result.is_ok());
    }

    #[test]
    fn rejects_missing_required_field() {
        let schema = Schema {
            kind: SchemaKind::Object,
            description: None,
            required: true,
            nullable: false,
            default_value: None,
            fields: BTreeMap::from([(
                "email".to_owned(),
                Schema {
                    kind: SchemaKind::String,
                    description: None,
                    required: true,
                    nullable: false,
                    default_value: None,
                    fields: BTreeMap::new(),
                    items: None,
                    enum_values: Vec::new(),
                    variants: Vec::new(),
                },
            )]),
            items: None,
            enum_values: Vec::new(),
            variants: Vec::new(),
        };

        let error = schema.validate_shape(&json!({})).unwrap_err();

        assert_eq!(error, SchemaError::missing_field("$.email"));
    }

    #[test]
    fn rejects_invalid_scalar_type() {
        let schema = Schema {
            kind: SchemaKind::Boolean,
            description: None,
            required: true,
            nullable: false,
            default_value: None,
            fields: BTreeMap::new(),
            items: None,
            enum_values: Vec::new(),
            variants: Vec::new(),
        };

        let error = schema.validate_shape(&json!("true")).unwrap_err();

        assert_eq!(error, SchemaError::invalid_type("$", "boolean", "string"));
    }
}
