use serde::{Deserialize, Serialize};
use serde_json::Value;

use crate::{JsonPath, JsonPathRoot, MappingError};

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum MappingTargetContext {
    Unknown,
    Input,
    Output,
    Mixed,
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct MappingCondition {
    pub source: String,
    pub equals: Value,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TransformKind {
    Identity,
    ToString,
    ToNumber,
    ToBoolean,
    Join,
    Split,
    WrapArray,
    UnwrapSingleton,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Transform {
    pub kind: TransformKind,
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct MappingRule {
    pub source: String,
    pub target: String,
    #[serde(default)]
    pub required: bool,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub default_value: Option<Value>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub transform: Option<Transform>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub condition: Option<MappingCondition>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub notes: Option<String>,
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Default)]
pub struct MappingSet {
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub rules: Vec<MappingRule>,
}

impl MappingSet {
    pub fn is_empty(&self) -> bool {
        self.rules.is_empty()
    }

    pub fn len(&self) -> usize {
        self.rules.len()
    }

    pub fn target_context(&self) -> MappingTargetContext {
        let mut detected = MappingTargetContext::Unknown;

        for rule in &self.rules {
            let Ok(path) = JsonPath::parse(&rule.target) else {
                return MappingTargetContext::Mixed;
            };

            let current = match target_root_context(path.root) {
                Some(context) => context,
                None => return MappingTargetContext::Mixed,
            };

            detected = match (detected, current) {
                (MappingTargetContext::Unknown, context) => context,
                (existing, current) if existing == current => existing,
                _ => MappingTargetContext::Mixed,
            };
        }

        detected
    }

    pub fn validate_paths(&self) -> Result<(), MappingError> {
        let context = self.target_context();

        if context == MappingTargetContext::Mixed {
            return Err(MappingError::MixedTargetContext);
        }

        for rule in &self.rules {
            let source = JsonPath::parse(&rule.source)?;
            let target = JsonPath::parse(&rule.target)?;

            validate_source_root(source.root, context)?;
            validate_target_root(target.root, context)?;

            if let Some(condition) = &rule.condition {
                let condition_path = JsonPath::parse(&condition.source)?;
                validate_source_root(condition_path.root, context)?;
            }
        }

        Ok(())
    }
}

fn target_root_context(root: JsonPathRoot) -> Option<MappingTargetContext> {
    match root {
        JsonPathRoot::RequestPath
        | JsonPathRoot::RequestQuery
        | JsonPathRoot::RequestHeaders
        | JsonPathRoot::RequestBody
        | JsonPathRoot::RequestVariables
        | JsonPathRoot::RequestGrpc => Some(MappingTargetContext::Input),
        JsonPathRoot::Output => Some(MappingTargetContext::Output),
        _ => None,
    }
}

fn validate_source_root(
    root: JsonPathRoot,
    context: MappingTargetContext,
) -> Result<(), MappingError> {
    let valid = match context {
        MappingTargetContext::Unknown => true,
        MappingTargetContext::Input => root == JsonPathRoot::Mcp,
        MappingTargetContext::Output => {
            matches!(
                root,
                JsonPathRoot::ResponseBody
                    | JsonPathRoot::ResponseData
                    | JsonPathRoot::ResponseGrpc
            )
        }
        MappingTargetContext::Mixed => false,
    };

    if valid {
        return Ok(());
    }

    Err(MappingError::UnsupportedSourceRoot {
        root: root.as_str().to_owned(),
        context: context_name(context),
    })
}

fn validate_target_root(
    root: JsonPathRoot,
    context: MappingTargetContext,
) -> Result<(), MappingError> {
    let valid = match context {
        MappingTargetContext::Unknown => true,
        MappingTargetContext::Input => matches!(
            root,
            JsonPathRoot::RequestPath
                | JsonPathRoot::RequestQuery
                | JsonPathRoot::RequestHeaders
                | JsonPathRoot::RequestBody
                | JsonPathRoot::RequestVariables
                | JsonPathRoot::RequestGrpc
        ),
        MappingTargetContext::Output => root == JsonPathRoot::Output,
        MappingTargetContext::Mixed => false,
    };

    if valid {
        return Ok(());
    }

    Err(MappingError::UnsupportedTargetRoot {
        root: root.as_str().to_owned(),
        context: context_name(context),
    })
}

fn context_name(context: MappingTargetContext) -> &'static str {
    match context {
        MappingTargetContext::Unknown => "unknown",
        MappingTargetContext::Input => "input",
        MappingTargetContext::Output => "output",
        MappingTargetContext::Mixed => "mixed",
    }
}

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

    use super::{MappingRule, MappingSet, MappingTargetContext, Transform, TransformKind};

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

        assert!(!mapping.is_empty());
        assert_eq!(mapping.len(), 1);
    }

    #[test]
    fn mapping_rule_serializes_jsonpath_fields() {
        let mapping = MappingSet {
            rules: vec![MappingRule {
                source: "$.response.body.id".to_owned(),
                target: "$.output.id".to_owned(),
                required: false,
                default_value: Some(json!("lead_123")),
                transform: None,
                condition: None,
                notes: Some("map identifier".to_owned()),
            }],
        };

        let value = serde_json::to_value(mapping).unwrap();

        assert_eq!(value["rules"][0]["source"], "$.response.body.id");
        assert_eq!(value["rules"][0]["target"], "$.output.id");
        assert_eq!(value["rules"][0]["default_value"], "lead_123");
    }

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

        let yaml = serde_yaml::to_string(&mapping).unwrap();
        let restored: MappingSet = serde_yaml::from_str(&yaml).unwrap();

        assert!(yaml.contains("source: $.mcp.tags"));
        assert_eq!(restored, mapping);
    }

    #[test]
    fn detects_input_context_from_target_roots() {
        let mapping = MappingSet {
            rules: vec![MappingRule {
                source: "$.mcp.email".to_owned(),
                target: "$.request.body.email".to_owned(),
                required: true,
                default_value: None,
                transform: None,
                condition: None,
                notes: None,
            }],
        };

        assert_eq!(mapping.target_context(), MappingTargetContext::Input);
    }

    #[test]
    fn rejects_mixed_target_contexts() {
        let mapping = MappingSet {
            rules: vec![
                MappingRule {
                    source: "$.mcp.email".to_owned(),
                    target: "$.request.body.email".to_owned(),
                    required: true,
                    default_value: None,
                    transform: None,
                    condition: None,
                    notes: None,
                },
                MappingRule {
                    source: "$.response.body.id".to_owned(),
                    target: "$.output.id".to_owned(),
                    required: true,
                    default_value: None,
                    transform: None,
                    condition: None,
                    notes: None,
                },
            ],
        };

        let error = mapping.validate_paths().unwrap_err();

        assert_eq!(error, crate::MappingError::MixedTargetContext);
    }
}
