use std::{collections::BTreeMap, time::Duration};

use crank_core::{SoapTarget, SoapVersion};
use crank_mapping::JsonPath;
use reqwest::{
    Client,
    header::{HeaderMap, HeaderName, HeaderValue},
};
use serde_json::{Value, json};

use crate::{SoapAdapterError, SoapRequest, SoapResponse, xml};

#[derive(Clone, Debug)]
pub struct SoapAdapter {
    client: Client,
}

impl Default for SoapAdapter {
    fn default() -> Self {
        Self::new()
    }
}

impl SoapAdapter {
    pub fn new() -> Self {
        Self {
            client: Client::new(),
        }
    }

    pub async fn execute(
        &self,
        target: &SoapTarget,
        request: &SoapRequest,
    ) -> Result<SoapResponse, SoapAdapterError> {
        let endpoint = target
            .endpoint_override
            .as_deref()
            .ok_or(SoapAdapterError::MissingEndpoint)?;
        let headers = build_headers(target, request)?;
        let envelope_headers = resolve_envelope_headers(target, request)?;
        let envelope = xml::build_envelope(
            &target.operation_name,
            target.metadata.namespaces.first().map(String::as_str),
            &request.body,
            envelope_namespace(target.soap_version),
            target.binding_style,
            &envelope_headers,
        );
        let response = self
            .client
            .post(endpoint)
            .headers(headers)
            .body(envelope)
            .timeout(Duration::from_millis(request.timeout_ms))
            .send()
            .await?;
        let status = response.status();
        let headers = normalize_headers(response.headers());
        let body_text = response.text().await?;
        let body = xml::decode_envelope(&body_text)?;

        if !status.is_success() {
            return Err(SoapAdapterError::UnexpectedStatus {
                status: status.as_u16(),
                body,
            });
        }

        Ok(SoapResponse {
            status_code: status.as_u16(),
            headers,
            body,
        })
    }
}

fn resolve_envelope_headers(
    target: &SoapTarget,
    request: &SoapRequest,
) -> Result<Vec<xml::SoapEnvelopeHeader>, SoapAdapterError> {
    let context = json!({
        "request": {
            "body": request.body.clone(),
        },
        "mcp": request.body.clone(),
    });
    let mut rendered = Vec::new();

    for header in &target.headers {
        let Some(value) = resolve_header_value(header, &context, &request.body)? else {
            if header.required {
                return Err(SoapAdapterError::MissingRequiredHeader {
                    name: header.name.clone(),
                });
            }
            continue;
        };

        rendered.push(xml::SoapEnvelopeHeader {
            name: header.name.clone(),
            namespace_uri: header.namespace_uri.clone(),
            value,
        });
    }

    Ok(rendered)
}

fn resolve_header_value(
    header: &crank_core::SoapHeaderConfig,
    context: &Value,
    body: &Value,
) -> Result<Option<Value>, SoapAdapterError> {
    if let Some(path) = header.value_path.as_deref() {
        let path = JsonPath::parse(path).map_err(|_| SoapAdapterError::InvalidHeaderValuePath {
            path: path.to_owned(),
        })?;
        return Ok(path.read(context).cloned());
    }

    Ok(match body {
        Value::Object(map) => map.get(&header.name).cloned(),
        _ => None,
    })
}

fn build_headers(
    target: &SoapTarget,
    request: &SoapRequest,
) -> Result<HeaderMap, SoapAdapterError> {
    let mut headers = HeaderMap::new();
    let content_type = match target.soap_version {
        SoapVersion::Soap11 => "text/xml; charset=utf-8".to_owned(),
        SoapVersion::Soap12 => match target.soap_action.as_deref() {
            Some(action) => format!(r#"application/soap+xml; charset=utf-8; action="{action}""#),
            None => "application/soap+xml; charset=utf-8".to_owned(),
        },
    };
    headers.insert(
        reqwest::header::CONTENT_TYPE,
        HeaderValue::from_str(&content_type).map_err(|_| SoapAdapterError::MissingEndpoint)?,
    );
    headers.insert(
        reqwest::header::ACCEPT,
        HeaderValue::from_static("application/soap+xml, text/xml, application/xml"),
    );

    if matches!(target.soap_version, SoapVersion::Soap11) {
        if let Some(action) = target.soap_action.as_deref() {
            headers.insert(
                HeaderName::from_static("soapaction"),
                HeaderValue::from_str(action).map_err(|_| SoapAdapterError::MissingEndpoint)?,
            );
        }
    }

    for (name, value) in &request.headers {
        let header_name =
            HeaderName::try_from(name.as_str()).map_err(|_| SoapAdapterError::MissingEndpoint)?;
        let header_value =
            HeaderValue::try_from(value).map_err(|_| SoapAdapterError::MissingEndpoint)?;
        headers.insert(header_name, header_value);
    }

    Ok(headers)
}

fn envelope_namespace(version: SoapVersion) -> &'static str {
    match version {
        SoapVersion::Soap11 => "http://schemas.xmlsoap.org/soap/envelope/",
        SoapVersion::Soap12 => "http://www.w3.org/2003/05/soap-envelope",
    }
}

fn normalize_headers(headers: &HeaderMap) -> BTreeMap<String, String> {
    headers
        .iter()
        .filter_map(|(name, value)| {
            value
                .to_str()
                .ok()
                .map(|value| (name.as_str().to_owned(), value.to_owned()))
        })
        .collect()
}

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

    use axum::{Router, body::Bytes, routing::post};
    use crank_core::{SoapBindingStyle, SoapOperationMetadata, SoapTarget, SoapVersion, Target};
    use serde_json::json;
    use tokio::net::TcpListener;

    use crate::{SoapAdapter, SoapRequest};

    async fn spawn_server() -> String {
        async fn handler(body: Bytes) -> String {
            let text = String::from_utf8_lossy(&body);
            assert!(text.contains("<email>user@example.com</email>"));
            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>"#
                .to_owned()
        }

        let app = Router::new().route("/", post(handler));
        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
        let address = listener.local_addr().unwrap();

        tokio::spawn(async move {
            axum::serve(listener, app).await.unwrap();
        });

        format!("http://{}", address)
    }

    fn test_target(endpoint: String) -> SoapTarget {
        match Target::Soap(SoapTarget {
            wsdl_ref: "sample_wsdl".into(),
            service_name: "LeadService".to_owned(),
            port_name: "LeadPort".to_owned(),
            operation_name: "CreateLead".to_owned(),
            endpoint_override: Some(endpoint),
            soap_version: SoapVersion::Soap11,
            soap_action: Some("urn:createLead".to_owned()),
            binding_style: SoapBindingStyle::DocumentLiteral,
            headers: Vec::new(),
            fault_contract: None,
            metadata: SoapOperationMetadata {
                input_part_names: vec!["CreateLeadRequest".to_owned()],
                output_part_names: vec!["CreateLeadResponse".to_owned()],
                namespaces: vec!["urn:crm".to_owned()],
            },
        }) {
            Target::Soap(target) => target,
            _ => unreachable!(),
        }
    }

    #[tokio::test]
    async fn executes_soap_request_response() {
        let endpoint = spawn_server().await;
        let adapter = SoapAdapter::new();
        let response = adapter
            .execute(
                &test_target(endpoint),
                &SoapRequest {
                    headers: BTreeMap::new(),
                    body: json!({ "email": "user@example.com" }),
                    timeout_ms: 1_000,
                },
            )
            .await
            .unwrap();

        assert_eq!(response.status_code, 200);
        assert_eq!(response.body["id"], "lead_123");
    }

    #[tokio::test]
    async fn renders_soap_headers_and_rpc_literal_children() {
        async fn handler(body: Bytes) -> String {
            let text = String::from_utf8_lossy(&body);
            assert!(text.contains("<soap:Header>"));
            assert!(
                text.contains(
                    r#"<h:CorrelationId xmlns:h="urn:headers">corr-123</h:CorrelationId>"#
                )
            );
            assert!(text.contains("<m:CreateLead"));
            assert!(text.contains("<m:email>user@example.com</m:email>"));
            r#"<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
                <soap:Body>
                    <CreateLeadResponse>
                        <id>lead_rpc</id>
                    </CreateLeadResponse>
                </soap:Body>
            </soap:Envelope>"#
                .to_owned()
        }

        let app = Router::new().route("/", post(handler));
        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
        let address = listener.local_addr().unwrap();
        tokio::spawn(async move {
            axum::serve(listener, app).await.unwrap();
        });

        let target = SoapTarget {
            binding_style: SoapBindingStyle::RpcLiteral,
            headers: vec![crank_core::SoapHeaderConfig {
                name: "CorrelationId".to_owned(),
                namespace_uri: Some("urn:headers".to_owned()),
                required: true,
                value_path: Some("$.request.body.correlation_id".to_owned()),
            }],
            ..test_target(format!("http://{}", address))
        };

        let adapter = SoapAdapter::new();
        let response = adapter
            .execute(
                &target,
                &SoapRequest {
                    headers: BTreeMap::new(),
                    body: json!({
                        "email": "user@example.com",
                        "correlation_id": "corr-123"
                    }),
                    timeout_ms: 1_000,
                },
            )
            .await
            .unwrap();

        assert_eq!(response.body["id"], "lead_rpc");
    }
}
