use crank_core::SoapBindingStyle;
use roxmltree::{Document, Node};
use serde_json::{Map, Value};

use crate::SoapAdapterError;

#[derive(Clone, Debug, PartialEq)]
pub struct SoapEnvelopeHeader {
    pub name: String,
    pub namespace_uri: Option<String>,
    pub value: Value,
}

pub fn build_envelope(
    operation_name: &str,
    namespace: Option<&str>,
    body: &Value,
    envelope_namespace: &str,
    binding_style: SoapBindingStyle,
    headers: &[SoapEnvelopeHeader],
) -> String {
    let mut xml = String::new();
    xml.push_str(&format!(
        r#"<soap:Envelope xmlns:soap="{envelope_namespace}">"#
    ));
    if !headers.is_empty() {
        xml.push_str("<soap:Header>");
        for header in headers {
            append_header(&mut xml, header);
        }
        xml.push_str("</soap:Header>");
    }
    xml.push_str("<soap:Body>");
    match namespace {
        Some(namespace) => xml.push_str(&format!(r#"<m:{operation_name}" xmlns:m="{namespace}">"#)),
        None => xml.push_str(&format!("<{operation_name}>")),
    }
    let child_prefix = match binding_style {
        SoapBindingStyle::DocumentLiteral => None,
        SoapBindingStyle::RpcLiteral => namespace.map(|_| "m"),
    };
    append_value_children(&mut xml, body, child_prefix);
    match namespace {
        Some(_) => xml.push_str(&format!("</m:{operation_name}>")),
        None => xml.push_str(&format!("</{operation_name}>")),
    }
    xml.push_str("</soap:Body></soap:Envelope>");
    xml
}

pub fn decode_envelope(payload: &str) -> Result<Value, SoapAdapterError> {
    let document = Document::parse(payload)?;
    let body = document
        .descendants()
        .find(|node| node.is_element() && node.tag_name().name() == "Body")
        .ok_or(SoapAdapterError::MissingBody)?;

    let body_child = body
        .children()
        .find(|node| node.is_element())
        .ok_or(SoapAdapterError::EmptyBody)?;

    if body_child.tag_name().name() == "Fault" {
        return Err(parse_fault(body_child));
    }

    Ok(node_to_json(body_child, true))
}

fn parse_fault(fault: Node<'_, '_>) -> SoapAdapterError {
    let code = find_nested_text(fault, &["Code", "Value"])
        .or_else(|| find_nested_text(fault, &["faultcode"]));
    let message = find_nested_text(fault, &["Reason", "Text"])
        .or_else(|| find_nested_text(fault, &["faultstring"]))
        .unwrap_or_else(|| "SOAP fault".to_owned());
    let detail = fault
        .children()
        .find(|node| node.is_element() && node.tag_name().name() == "Detail")
        .map(|node| node_to_json(node, false));

    SoapAdapterError::SoapFault {
        code,
        message,
        detail,
    }
}

fn append_header(xml: &mut String, header: &SoapEnvelopeHeader) {
    match header.namespace_uri.as_deref() {
        Some(namespace_uri) => {
            xml.push_str(&format!(
                r#"<h:{} xmlns:h="{}">"#,
                header.name, namespace_uri
            ));
            append_header_value(xml, &header.value);
            xml.push_str(&format!("</h:{}>", header.name));
        }
        None => {
            xml.push('<');
            xml.push_str(&header.name);
            xml.push('>');
            append_header_value(xml, &header.value);
            xml.push_str("</");
            xml.push_str(&header.name);
            xml.push('>');
        }
    }
}

fn append_header_value(xml: &mut String, value: &Value) {
    match value {
        Value::Null => {}
        Value::Bool(value) => xml.push_str(if *value { "true" } else { "false" }),
        Value::Number(value) => xml.push_str(&value.to_string()),
        Value::String(value) => xml.push_str(&escape_xml(value)),
        Value::Array(_) | Value::Object(_) => append_value_children(xml, value, None),
    }
}

fn find_nested_text(node: Node<'_, '_>, path: &[&str]) -> Option<String> {
    let mut current = node;
    for segment in path {
        current = current
            .children()
            .find(|child| child.is_element() && child.tag_name().name() == *segment)?;
    }

    current
        .text()
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .map(ToOwned::to_owned)
}

fn append_value_children(xml: &mut String, value: &Value, namespace_prefix: Option<&str>) {
    match value {
        Value::Object(object) => {
            for (key, value) in object {
                append_named_value(xml, key, value, namespace_prefix);
            }
        }
        other => append_named_value(xml, "value", other, namespace_prefix),
    }
}

fn append_named_value(xml: &mut String, name: &str, value: &Value, namespace_prefix: Option<&str>) {
    match value {
        Value::Array(items) => {
            for item in items {
                append_named_value(xml, name, item, namespace_prefix);
            }
        }
        Value::Object(object) => {
            push_start_tag(xml, name, namespace_prefix);
            for (child_name, child_value) in object {
                append_named_value(xml, child_name, child_value, namespace_prefix);
            }
            push_end_tag(xml, name, namespace_prefix);
        }
        Value::Null => {
            push_empty_tag(xml, name, namespace_prefix);
        }
        Value::Bool(value) => {
            push_start_tag(xml, name, namespace_prefix);
            xml.push_str(if *value { "true" } else { "false" });
            push_end_tag(xml, name, namespace_prefix);
        }
        Value::Number(value) => {
            push_start_tag(xml, name, namespace_prefix);
            xml.push_str(&value.to_string());
            push_end_tag(xml, name, namespace_prefix);
        }
        Value::String(value) => {
            push_start_tag(xml, name, namespace_prefix);
            xml.push_str(&escape_xml(value));
            push_end_tag(xml, name, namespace_prefix);
        }
    }
}

fn push_start_tag(xml: &mut String, name: &str, namespace_prefix: Option<&str>) {
    xml.push('<');
    if let Some(prefix) = namespace_prefix {
        xml.push_str(prefix);
        xml.push(':');
    }
    xml.push_str(name);
    xml.push('>');
}

fn push_end_tag(xml: &mut String, name: &str, namespace_prefix: Option<&str>) {
    xml.push_str("</");
    if let Some(prefix) = namespace_prefix {
        xml.push_str(prefix);
        xml.push(':');
    }
    xml.push_str(name);
    xml.push('>');
}

fn push_empty_tag(xml: &mut String, name: &str, namespace_prefix: Option<&str>) {
    xml.push('<');
    if let Some(prefix) = namespace_prefix {
        xml.push_str(prefix);
        xml.push(':');
    }
    xml.push_str(name);
    xml.push_str("/>");
}

fn escape_xml(value: &str) -> String {
    value
        .replace('&', "&amp;")
        .replace('<', "&lt;")
        .replace('>', "&gt;")
        .replace('"', "&quot;")
        .replace('\'', "&apos;")
}

fn node_to_json(node: Node<'_, '_>, _unwrap_root: bool) -> Value {
    let children = node
        .children()
        .filter(|child| child.is_element())
        .collect::<Vec<_>>();

    if children.is_empty() {
        return node
            .text()
            .map(str::trim)
            .filter(|value| !value.is_empty())
            .map(|value| Value::String(value.to_owned()))
            .unwrap_or(Value::Null);
    }

    let mut object = Map::new();
    for child in children {
        let key = child.tag_name().name().to_owned();
        let value = node_to_json(child, false);
        match object.get_mut(&key) {
            Some(existing) => match existing {
                Value::Array(array) => array.push(value),
                other => {
                    let previous = other.take();
                    *other = Value::Array(vec![previous, value]);
                }
            },
            None => {
                object.insert(key, value);
            }
        }
    }

    Value::Object(object)
}

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

    use super::{SoapEnvelopeHeader, build_envelope, decode_envelope};
    use crank_core::SoapBindingStyle;

    #[test]
    fn builds_document_literal_envelope() {
        let xml = build_envelope(
            "CreateLead",
            Some("urn:crm"),
            &json!({"email":"user@example.com","source":"mcp"}),
            "http://schemas.xmlsoap.org/soap/envelope/",
            SoapBindingStyle::DocumentLiteral,
            &[],
        );

        assert!(xml.contains("<soap:Envelope"));
        assert!(xml.contains("<m:CreateLead"));
        assert!(xml.contains(r#"xmlns:m="urn:crm""#));
        assert!(xml.contains("<email>user@example.com</email>"));
    }

    #[test]
    fn decodes_wrapped_response_body() {
        let value = decode_envelope(
            r#"<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
                <soap:Body>
                    <CreateLeadResponse>
                        <id>lead_123</id>
                        <status>created</status>
                    </CreateLeadResponse>
                </soap:Body>
            </soap:Envelope>"#,
        )
        .unwrap();

        assert_eq!(value["id"], "lead_123");
        assert_eq!(value["status"], "created");
    }

    #[test]
    fn builds_rpc_literal_envelope_with_headers() {
        let xml = build_envelope(
            "CreateLead",
            Some("urn:crm"),
            &json!({"email":"user@example.com"}),
            "http://schemas.xmlsoap.org/soap/envelope/",
            SoapBindingStyle::RpcLiteral,
            &[SoapEnvelopeHeader {
                name: "CorrelationId".to_owned(),
                namespace_uri: Some("urn:headers".to_owned()),
                value: Value::String("corr-123".to_owned()),
            }],
        );

        assert!(xml.contains("<soap:Header>"));
        assert!(
            xml.contains(r#"<h:CorrelationId xmlns:h="urn:headers">corr-123</h:CorrelationId>"#)
        );
        assert!(xml.contains("<m:email>user@example.com</m:email>"));
    }
}
