use serde_json::{Map, Number, Value};

use crate::{
    JsonPath, JsonPathSegment, MappingError, MappingSet, MappingTargetContext, TransformKind,
};

impl MappingSet {
    pub fn apply(&self, source: &Value) -> Result<Value, MappingError> {
        self.validate_paths()?;

        let mut target = empty_target_context(self.target_context());

        for rule in &self.rules {
            if let Some(condition) = &rule.condition {
                let condition_path = JsonPath::parse(&condition.source)?;
                let condition_value = read_path(source, &condition_path);

                if condition_value != Some(&condition.equals) {
                    continue;
                }
            }

            let source_path = JsonPath::parse(&rule.source)?;
            let target_path = JsonPath::parse(&rule.target)?;

            let Some(source_value) = read_path(source, &source_path)
                .cloned()
                .or(rule.default_value.clone())
            else {
                if rule.required {
                    return Err(MappingError::MissingRequiredValue {
                        path: source_path.to_string_path(),
                    });
                }

                continue;
            };

            let value = apply_transform(
                source_value,
                rule.transform.as_ref().map(|value| &value.kind),
            )?;
            write_path(&mut target, &target_path, value)?;
        }

        Ok(target)
    }
}

fn empty_target_context(context: MappingTargetContext) -> Value {
    match context {
        MappingTargetContext::Input => Value::Object(Map::from_iter([(
            "request".to_owned(),
            Value::Object(Map::from_iter([
                ("path".to_owned(), Value::Object(Map::new())),
                ("query".to_owned(), Value::Object(Map::new())),
                ("headers".to_owned(), Value::Object(Map::new())),
                ("body".to_owned(), Value::Object(Map::new())),
                ("variables".to_owned(), Value::Object(Map::new())),
                ("grpc".to_owned(), Value::Object(Map::new())),
            ])),
        )])),
        MappingTargetContext::Output => Value::Object(Map::from_iter([(
            "output".to_owned(),
            Value::Object(Map::new()),
        )])),
        _ => Value::Object(Map::new()),
    }
}

fn read_path<'a>(value: &'a Value, path: &JsonPath) -> Option<&'a Value> {
    path.read(value)
}

fn write_path(target: &mut Value, path: &JsonPath, value: Value) -> Result<(), MappingError> {
    let mut segments = path
        .root
        .root_fields()
        .iter()
        .map(|field| JsonPathSegment::Field((*field).to_owned()))
        .collect::<Vec<_>>();
    segments.extend(path.segments.clone());

    write_segments(target, &segments, value)
}

fn write_segments(
    target: &mut Value,
    segments: &[JsonPathSegment],
    value: Value,
) -> Result<(), MappingError> {
    if segments.is_empty() {
        *target = value;
        return Ok(());
    }

    match &segments[0] {
        JsonPathSegment::Field(field) => {
            if !target.is_object() {
                *target = Value::Object(Map::new());
            }

            let Some(object) = target.as_object_mut() else {
                return Err(MappingError::InvalidJsonPath {
                    path: field.clone(),
                });
            };
            let entry = object.entry(field.clone()).or_insert(Value::Null);

            write_segments(entry, &segments[1..], value)
        }
        JsonPathSegment::Index(index) => {
            if !target.is_array() {
                *target = Value::Array(Vec::new());
            }

            let Some(array) = target.as_array_mut() else {
                return Err(MappingError::InvalidJsonPath {
                    path: index.to_string(),
                });
            };
            while array.len() <= *index {
                array.push(Value::Null);
            }

            write_segments(&mut array[*index], &segments[1..], value)
        }
    }
}

fn apply_transform(value: Value, transform: Option<&TransformKind>) -> Result<Value, MappingError> {
    let Some(transform) = transform else {
        return Ok(value);
    };

    match transform {
        TransformKind::Identity => Ok(value),
        TransformKind::ToString => Ok(Value::String(to_string_value(&value))),
        TransformKind::ToNumber => to_number(value),
        TransformKind::ToBoolean => to_boolean(value),
        TransformKind::Join => join_values(value),
        TransformKind::Split => split_value(value),
        TransformKind::WrapArray => Ok(match value {
            Value::Array(values) => Value::Array(values),
            other => Value::Array(vec![other]),
        }),
        TransformKind::UnwrapSingleton => unwrap_singleton(value),
    }
}

fn to_string_value(value: &Value) -> String {
    match value {
        Value::Null => "null".to_owned(),
        Value::Bool(boolean) => boolean.to_string(),
        Value::Number(number) => number.to_string(),
        Value::String(text) => text.clone(),
        Value::Array(_) | Value::Object(_) => serde_json::to_string(value).unwrap_or_default(),
    }
}

fn to_number(value: Value) -> Result<Value, MappingError> {
    let number = match value {
        Value::Number(number) => return Ok(Value::Number(number)),
        Value::String(text) => {
            if let Ok(integer) = text.parse::<i64>() {
                Number::from(integer)
            } else {
                Number::from_f64(text.parse::<f64>().map_err(|_| {
                    MappingError::InvalidTransformInput {
                        transform: "to_number",
                        actual: "string",
                    }
                })?)
                .ok_or(MappingError::InvalidTransformInput {
                    transform: "to_number",
                    actual: "string",
                })?
            }
        }
        Value::Bool(boolean) => Number::from(if boolean { 1 } else { 0 }),
        other => {
            return Err(MappingError::InvalidTransformInput {
                transform: "to_number",
                actual: type_name(&other),
            });
        }
    };

    Ok(Value::Number(number))
}

fn to_boolean(value: Value) -> Result<Value, MappingError> {
    let boolean = match value {
        Value::Bool(boolean) => boolean,
        Value::Number(number) => {
            number.as_i64().unwrap_or_default() != 0 || number.as_f64().unwrap_or_default() != 0.0
        }
        Value::String(text) => match text.to_ascii_lowercase().as_str() {
            "true" | "1" | "yes" => true,
            "false" | "0" | "no" => false,
            _ => {
                return Err(MappingError::InvalidTransformInput {
                    transform: "to_boolean",
                    actual: "string",
                });
            }
        },
        other => {
            return Err(MappingError::InvalidTransformInput {
                transform: "to_boolean",
                actual: type_name(&other),
            });
        }
    };

    Ok(Value::Bool(boolean))
}

fn join_values(value: Value) -> Result<Value, MappingError> {
    let Value::Array(values) = value else {
        return Err(MappingError::InvalidTransformInput {
            transform: "join",
            actual: type_name(&value),
        });
    };

    Ok(Value::String(
        values
            .iter()
            .map(to_string_value)
            .collect::<Vec<_>>()
            .join(","),
    ))
}

fn split_value(value: Value) -> Result<Value, MappingError> {
    let Value::String(text) = value else {
        return Err(MappingError::InvalidTransformInput {
            transform: "split",
            actual: type_name(&value),
        });
    };

    Ok(Value::Array(
        text.split(',')
            .map(|part| Value::String(part.trim().to_owned()))
            .collect(),
    ))
}

fn unwrap_singleton(value: Value) -> Result<Value, MappingError> {
    match value {
        Value::Array(mut values) if values.len() == 1 => Ok(values.remove(0)),
        Value::Array(_) => Err(MappingError::InvalidTransformInput {
            transform: "unwrap_singleton",
            actual: "array",
        }),
        other => Err(MappingError::InvalidTransformInput {
            transform: "unwrap_singleton",
            actual: type_name(&other),
        }),
    }
}

fn type_name(value: &Value) -> &'static str {
    match value {
        Value::Null => "null",
        Value::Bool(_) => "boolean",
        Value::Number(_) => "number",
        Value::String(_) => "string",
        Value::Array(_) => "array",
        Value::Object(_) => "object",
    }
}

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

    use crate::{MappingRule, MappingSet, Transform, TransformKind};

    #[test]
    fn applies_input_mapping_into_request_context() {
        let mapping = MappingSet {
            rules: vec![
                MappingRule {
                    source: "$.mcp.email".to_owned(),
                    target: "$.request.body.contact.email".to_owned(),
                    required: true,
                    default_value: None,
                    transform: None,
                    condition: None,
                    notes: None,
                },
                MappingRule {
                    source: "$.mcp.tags".to_owned(),
                    target: "$.request.query.tags".to_owned(),
                    required: false,
                    default_value: None,
                    transform: Some(Transform {
                        kind: TransformKind::Join,
                    }),
                    condition: None,
                    notes: None,
                },
            ],
        };

        let result = mapping
            .apply(&json!({
                "mcp": {
                    "email": "user@example.com",
                    "tags": ["warm", "priority"]
                }
            }))
            .unwrap();

        assert_eq!(
            result["request"]["body"]["contact"]["email"],
            "user@example.com"
        );
        assert_eq!(result["request"]["query"]["tags"], "warm,priority");
    }

    #[test]
    fn applies_output_mapping_into_output_context() {
        let mapping = MappingSet {
            rules: vec![MappingRule {
                source: "$.response.body.lead_id".to_owned(),
                target: "$.output.id".to_owned(),
                required: true,
                default_value: None,
                transform: Some(Transform {
                    kind: TransformKind::ToString,
                }),
                condition: None,
                notes: None,
            }],
        };

        let result = mapping
            .apply(&json!({
                "response": {
                    "body": {
                        "lead_id": 42
                    }
                }
            }))
            .unwrap();

        assert_eq!(result["output"]["id"], "42");
    }

    #[test]
    fn uses_default_value_for_missing_optional_source() {
        let mapping = MappingSet {
            rules: vec![MappingRule {
                source: "$.mcp.region".to_owned(),
                target: "$.request.headers.X-Region".to_owned(),
                required: false,
                default_value: Some(json!("eu-central")),
                transform: None,
                condition: None,
                notes: None,
            }],
        };

        let result = mapping.apply(&json!({ "mcp": {} })).unwrap();

        assert_eq!(result["request"]["headers"]["X-Region"], "eu-central");
    }
}
