use std::collections::BTreeMap;

use roxmltree::Document;

use crate::{SoapAdapterError, SoapOperationSummary, SoapPortSummary, SoapServiceSummary};

#[derive(Clone, Debug)]
struct BindingSummary {
    soap_version: String,
    operations: Vec<SoapOperationSummary>,
}

pub fn inspect_wsdl(payload: &[u8]) -> Result<Vec<SoapServiceSummary>, SoapAdapterError> {
    let text = std::str::from_utf8(payload).map_err(|_| SoapAdapterError::EmptyBody)?;
    let document = Document::parse(text)?;
    let bindings = collect_bindings(&document);

    let services = document
        .descendants()
        .filter(|node| node.is_element() && node.tag_name().name() == "service")
        .map(|service| {
            let service_name = service.attribute("name").unwrap_or_default().to_owned();
            let ports = service
                .children()
                .filter(|node| node.is_element() && node.tag_name().name() == "port")
                .map(|port| {
                    let port_name = port.attribute("name").unwrap_or_default().to_owned();
                    let binding_name = local_name(port.attribute("binding").unwrap_or_default());
                    let endpoint = port
                        .descendants()
                        .find(|node| node.is_element() && node.tag_name().name() == "address")
                        .and_then(|node| node.attribute("location"))
                        .map(ToOwned::to_owned);
                    let binding = bindings
                        .get(&binding_name)
                        .cloned()
                        .unwrap_or(BindingSummary {
                            soap_version: "soap_11".to_owned(),
                            operations: Vec::new(),
                        });

                    SoapPortSummary {
                        port_name,
                        binding_name,
                        endpoint,
                        soap_version: binding.soap_version,
                        operations: binding.operations,
                    }
                })
                .collect();

            SoapServiceSummary {
                service_name,
                ports,
            }
        })
        .collect();

    Ok(services)
}

fn collect_bindings(document: &Document<'_>) -> BTreeMap<String, BindingSummary> {
    document
        .descendants()
        .filter(|node| node.is_element() && node.tag_name().name() == "binding")
        .map(|binding| {
            let name = binding.attribute("name").unwrap_or_default().to_owned();
            let soap_binding = binding
                .children()
                .find(|node| node.is_element() && node.tag_name().name() == "binding");
            let soap_namespace = soap_binding
                .and_then(|node| node.tag_name().namespace())
                .unwrap_or("http://schemas.xmlsoap.org/wsdl/soap/");
            let soap_version = if soap_namespace.contains("soap12") {
                "soap_12"
            } else {
                "soap_11"
            }
            .to_owned();
            let binding_style = soap_binding
                .and_then(|node| node.attribute("style"))
                .map(|value| match value {
                    "rpc" => "rpc_literal",
                    _ => "document_literal",
                })
                .unwrap_or("document_literal")
                .to_owned();
            let operations = binding
                .children()
                .filter(|node| node.is_element() && node.tag_name().name() == "operation")
                .map(|operation| SoapOperationSummary {
                    operation_name: operation.attribute("name").unwrap_or_default().to_owned(),
                    soap_action: operation
                        .children()
                        .find(|node| node.is_element() && node.tag_name().name() == "operation")
                        .and_then(|node| node.attribute("soapAction"))
                        .map(ToOwned::to_owned),
                    binding_style: operation
                        .children()
                        .find(|node| node.is_element() && node.tag_name().name() == "operation")
                        .and_then(|node| node.attribute("style"))
                        .map(|value| match value {
                            "rpc" => "rpc_literal",
                            _ => "document_literal",
                        })
                        .unwrap_or(binding_style.as_str())
                        .to_owned(),
                })
                .collect();

            (
                name,
                BindingSummary {
                    soap_version,
                    operations,
                },
            )
        })
        .collect()
}

fn local_name(value: &str) -> String {
    value.rsplit(':').next().unwrap_or(value).to_owned()
}

#[cfg(test)]
mod tests {
    use super::inspect_wsdl;

    #[test]
    fn parses_services_ports_and_operations_from_wsdl() {
        let services = inspect_wsdl(
            br#"<?xml version="1.0"?>
            <definitions xmlns="http://schemas.xmlsoap.org/wsdl/"
                xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
                xmlns:tns="urn:crm"
                targetNamespace="urn:crm">
              <binding name="LeadBinding" type="tns:LeadPortType">
                <soap:binding transport="http://schemas.xmlsoap.org/soap/http" style="document"/>
                <operation name="CreateLead">
                  <soap:operation soapAction="urn:createLead"/>
                </operation>
              </binding>
              <service name="LeadService">
                <port name="LeadPort" binding="tns:LeadBinding">
                  <soap:address location="https://soap.example.com/lead"/>
                </port>
              </service>
            </definitions>"#,
        )
        .unwrap();

        assert_eq!(services[0].service_name, "LeadService");
        assert_eq!(services[0].ports[0].port_name, "LeadPort");
        assert_eq!(services[0].ports[0].soap_version, "soap_11");
        assert_eq!(
            services[0].ports[0].operations[0].operation_name,
            "CreateLead"
        );
        assert_eq!(
            services[0].ports[0].operations[0].soap_action.as_deref(),
            Some("urn:createLead")
        );
    }
}
