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

use crank_core::GraphqlTarget;
use crank_mapping::JsonPath;
use reqwest::{
    Client,
    header::{CONTENT_TYPE, HeaderMap, HeaderName, HeaderValue},
};
use serde_json::{Map, Value, json};

use crate::{GraphqlAdapterError, GraphqlRequest, GraphqlResponse};

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

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

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

    pub async fn execute(
        &self,
        target: &GraphqlTarget,
        request: &GraphqlRequest,
    ) -> Result<GraphqlResponse, GraphqlAdapterError> {
        let endpoint = reqwest::Url::parse(&target.endpoint).map_err(|_| {
            GraphqlAdapterError::InvalidEndpoint {
                url: target.endpoint.clone(),
            }
        })?;
        let headers = build_headers(request)?;
        let variables = normalize_variables(request.variables.clone())?;
        let payload = json!({
            "query": target.query_template,
            "operationName": target.operation_name,
            "variables": variables
        });

        let response = self
            .client
            .post(endpoint)
            .headers(headers)
            .timeout(Duration::from_millis(request.timeout_ms))
            .json(&payload)
            .send()
            .await?;
        let status = response.status();
        let headers = normalize_headers(response.headers());
        let body = decode_body(response).await?;

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

        if let Some(errors) = body.get("errors").cloned() {
            return Err(GraphqlAdapterError::OperationErrors {
                data: body.get("data").cloned(),
                errors,
            });
        }

        let response_path = JsonPath::parse(&target.response_path).map_err(|_| {
            GraphqlAdapterError::InvalidResponsePath {
                path: target.response_path.clone(),
            }
        })?;
        let response_context = json!({
            "response": {
                "body": body
            }
        });
        let data = response_path
            .read(&response_context)
            .cloned()
            .ok_or_else(|| GraphqlAdapterError::ResponsePathNotFound {
                path: target.response_path.clone(),
            })?;
        let body = response_context["response"]["body"].clone();

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

fn build_headers(request: &GraphqlRequest) -> Result<HeaderMap, GraphqlAdapterError> {
    let mut headers = HeaderMap::new();
    headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));

    for (name, value) in &request.headers {
        let header_name = HeaderName::try_from(name.as_str()).map_err(|_| {
            GraphqlAdapterError::InvalidHeaderName {
                header: name.clone(),
            }
        })?;
        let header_value = HeaderValue::try_from(value.as_str()).map_err(|_| {
            GraphqlAdapterError::InvalidHeaderValue {
                header: name.clone(),
            }
        })?;

        headers.insert(header_name, header_value);
    }

    Ok(headers)
}

fn normalize_variables(variables: Option<Value>) -> Result<Value, GraphqlAdapterError> {
    match variables {
        None | Some(Value::Null) => Ok(Value::Object(Map::new())),
        Some(Value::Object(values)) => Ok(Value::Object(values)),
        Some(_) => Err(GraphqlAdapterError::InvalidVariablesShape),
    }
}

async fn decode_body(response: reqwest::Response) -> Result<Value, GraphqlAdapterError> {
    let bytes = response.bytes().await?;

    if bytes.is_empty() {
        return Ok(Value::Null);
    }

    match serde_json::from_slice::<Value>(&bytes) {
        Ok(value) => Ok(value),
        Err(_) => Ok(Value::String(
            String::from_utf8_lossy(&bytes).trim().to_owned(),
        )),
    }
}

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::{Json, Router, routing::post};
    use crank_core::{GraphqlOperationType, GraphqlTarget};
    use serde_json::{Value, json};
    use tokio::net::TcpListener;

    use crate::{GraphqlAdapter, GraphqlAdapterError, GraphqlRequest};

    #[tokio::test]
    async fn executes_graphql_request_and_extracts_response_path() {
        let endpoint = spawn_graphql_server().await;
        let adapter = GraphqlAdapter::new();
        let target = GraphqlTarget {
            endpoint,
            operation_type: GraphqlOperationType::Mutation,
            operation_name: "CreateLead".to_owned(),
            query_template:
                "mutation CreateLead($email: String!) { createLead(email: $email) { id status } }"
                    .to_owned(),
            response_path: "$.response.body.data.createLead".to_owned(),
        };
        let request = GraphqlRequest {
            headers: BTreeMap::from([("x-trace-id".to_owned(), "trace-123".to_owned())]),
            variables: Some(json!({ "email": "user@example.com" })),
            timeout_ms: 1_000,
        };

        let response = adapter.execute(&target, &request).await.unwrap();

        assert_eq!(response.status_code, 200);
        assert_eq!(
            response.data,
            json!({ "id": "lead_123", "status": "created" })
        );
    }

    #[tokio::test]
    async fn returns_operation_errors_from_graphql_body() {
        let endpoint = spawn_graphql_server().await;
        let adapter = GraphqlAdapter::new();
        let target = GraphqlTarget {
            endpoint,
            operation_type: GraphqlOperationType::Mutation,
            operation_name: "CreateLead".to_owned(),
            query_template:
                "mutation CreateLead($email: String!) { createLead(email: $email) { id status } }"
                    .to_owned(),
            response_path: "$.response.body.data.createLead".to_owned(),
        };
        let request = GraphqlRequest {
            headers: BTreeMap::new(),
            variables: Some(json!({ "email": "fail@example.com" })),
            timeout_ms: 1_000,
        };

        let error = adapter.execute(&target, &request).await.unwrap_err();

        assert!(matches!(
            error,
            GraphqlAdapterError::OperationErrors {
                errors: Value::Array(_),
                ..
            }
        ));
    }

    async fn spawn_graphql_server() -> String {
        let app = Router::new().route("/", post(graphql_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)
    }

    async fn graphql_handler(Json(payload): Json<Value>) -> Json<Value> {
        let email = payload
            .get("variables")
            .and_then(Value::as_object)
            .and_then(|variables| variables.get("email"))
            .and_then(Value::as_str)
            .unwrap_or_default();

        if email == "fail@example.com" {
            return Json(json!({
                "data": { "createLead": null },
                "errors": [{ "message": "lead creation failed" }]
            }));
        }

        Json(json!({
            "data": {
                "createLead": {
                    "id": "lead_123",
                    "status": "created"
                }
            }
        }))
    }
}
