//! Tool: project_curl — perform HTTP requests (GET/POST/PUT/DELETE)

use agent::{ToolContext, ToolDefinition, ToolError, ToolParam, ToolSchema};
use std::collections::HashMap;

/// Maximum response body size: 1 MB.
const MAX_BODY_BYTES: usize = 1 << 20;

/// Perform an HTTP request and return the response body and metadata.
/// Supports GET, POST, PUT, DELETE methods. Useful for fetching web pages,
/// calling external APIs, or downloading resources.
pub async fn curl_exec(
    _ctx: ToolContext,
    args: serde_json::Value,
) -> Result<serde_json::Value, ToolError> {
    let url = args
        .get("url")
        .and_then(|v| v.as_str())
        .ok_or_else(|| ToolError::ExecutionError("url is required".into()))?;

    let method = args
        .get("method")
        .and_then(|v| v.as_str())
        .unwrap_or("GET")
        .to_uppercase();

    let body = args.get("body").and_then(|v| v.as_str()).map(String::from);

    let headers: Vec<(String, String)> = args
        .get("headers")
        .and_then(|v| v.as_object())
        .map(|obj| {
            obj.iter()
                .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
                .collect()
        })
        .unwrap_or_default();

    let timeout_secs = args
        .get("timeout")
        .and_then(|v| v.as_u64())
        .unwrap_or(30)
        .min(120);

    let client = reqwest::Client::builder()
        .timeout(std::time::Duration::from_secs(timeout_secs))
        .build()
        .map_err(|e| ToolError::ExecutionError(format!("Failed to build HTTP client: {}", e)))?;

    let mut request = match method.as_str() {
        "GET" => client.get(url),
        "POST" => client.post(url),
        "PUT" => client.put(url),
        "DELETE" => client.delete(url),
        "PATCH" => client.patch(url),
        "HEAD" => client.head(url),
        _ => {
            return Err(ToolError::ExecutionError(format!(
                "Unsupported HTTP method: {}. Use GET, POST, PUT, DELETE, PATCH, or HEAD.",
                method
            )))
        }
    };

    for (key, value) in &headers {
        request = request.header(key, value);
    }

    // Set default Content-Type for POST/PUT/PATCH if not provided and body exists
    if body.is_some() && !headers.iter().any(|(k, _)| k.to_lowercase() == "content-type") {
        request = request.header("Content-Type", "application/json");
    }

    if let Some(ref b) = body {
        request = request.body(b.clone());
    }

    let response = request
        .send()
        .await
        .map_err(|e| ToolError::ExecutionError(format!("HTTP request failed: {}", e)))?;

    let status = response.status().as_u16();
    let status_text = response.status().canonical_reason().unwrap_or("");

    let response_headers: std::collections::HashMap<String, String> = response
        .headers()
        .iter()
        .map(|(k, v)| {
            (
                k.to_string(),
                v.to_str().unwrap_or("<binary>").to_string(),
            )
        })
        .collect();

    let content_type = response
        .headers()
        .get("content-type")
        .and_then(|v| v.to_str().ok())
        .unwrap_or("")
        .to_string();

    let is_text = content_type.starts_with("text/")
        || content_type.contains("json")
        || content_type.contains("xml")
        || content_type.contains("javascript");

    let body_bytes = response
        .bytes()
        .await
        .map_err(|e| ToolError::ExecutionError(format!("Failed to read response body: {}", e)))?;

    let body_len = body_bytes.len();
    let truncated = body_len > MAX_BODY_BYTES;
    let body_text = if truncated {
        String::from("[Response truncated — exceeds 1 MB limit]")
    } else if is_text {
        String::from_utf8_lossy(&body_bytes).to_string()
    } else {
        format!(
            "[Binary body, {} bytes, Content-Type: {}]",
            body_len, content_type
        )
    };

    Ok(serde_json::json!({
        "url": url,
        "method": method,
        "status": status,
        "status_text": status_text,
        "headers": response_headers,
        "body": body_text,
        "truncated": truncated,
        "size_bytes": body_len,
    }))
}

// ─── tool definition ─────────────────────────────────────────────────────────

pub fn tool_definition() -> ToolDefinition {
    let mut p = HashMap::new();
    p.insert("url".into(), ToolParam {
        name: "url".into(), param_type: "string".into(),
        description: Some("Full URL to request (required).".into()),
        required: true, properties: None, items: None,
    });
    p.insert("method".into(), ToolParam {
        name: "method".into(), param_type: "string".into(),
        description: Some("HTTP method: GET (default), POST, PUT, DELETE, PATCH, HEAD.".into()),
        required: false, properties: None, items: None,
    });
    p.insert("body".into(), ToolParam {
        name: "body".into(), param_type: "string".into(),
        description: Some("Request body. Defaults to 'application/json' Content-Type if provided. Optional.".into()),
        required: false, properties: None, items: None,
    });
    p.insert("headers".into(), ToolParam {
        name: "headers".into(), param_type: "object".into(),
        description: Some("HTTP headers as key-value pairs. Optional.".into()),
        required: false, properties: None, items: None,
    });
    p.insert("timeout".into(), ToolParam {
        name: "timeout".into(), param_type: "integer".into(),
        description: Some("Request timeout in seconds (default 30, max 120). Optional.".into()),
        required: false, properties: None, items: None,
    });
    ToolDefinition::new("project_curl")
        .description(
            "Perform an HTTP request to any URL. Supports GET, POST, PUT, DELETE, PATCH, HEAD. \
             Returns status code, headers, and response body. \
             Response body is truncated at 1 MB. Binary responses are described as text metadata. \
             Useful for fetching web pages, calling APIs, or downloading resources.",
        )
        .parameters(ToolSchema {
            schema_type: "object".into(),
            properties: Some(p),
            required: Some(vec!["url".into()]),
        })
}
