use compliance_core::error::CoreError;
use compliance_core::traits::pentest_tool::{PentestTool, PentestToolContext, PentestToolResult};
use serde_json::json;
use tracing::info;

/// Tool that discovers and parses OpenAPI/Swagger specification files.
///
/// Returns structured endpoint definitions for the LLM to use when planning
/// further tests. This tool produces data rather than security findings.
pub struct OpenApiParserTool {
    http: reqwest::Client,
}

/// A parsed endpoint from an OpenAPI spec.
#[derive(Debug, Clone)]
struct ParsedEndpoint {
    path: String,
    method: String,
    operation_id: Option<String>,
    summary: Option<String>,
    parameters: Vec<ParsedParameter>,
    request_body_content_type: Option<String>,
    response_codes: Vec<String>,
    security: Vec<String>,
    tags: Vec<String>,
}

/// A parsed parameter from an OpenAPI spec.
#[derive(Debug, Clone)]
struct ParsedParameter {
    name: String,
    location: String,
    required: bool,
    param_type: Option<String>,
    description: Option<String>,
}

impl OpenApiParserTool {
    pub fn new(http: reqwest::Client) -> Self {
        Self { http }
    }

    /// Common paths where OpenAPI/Swagger specs are typically served.
    fn common_spec_paths() -> Vec<&'static str> {
        vec![
            "/openapi.json",
            "/openapi.yaml",
            "/swagger.json",
            "/swagger.yaml",
            "/api-docs",
            "/api-docs.json",
            "/v2/api-docs",
            "/v3/api-docs",
            "/docs/openapi.json",
            "/api/swagger.json",
            "/api/openapi.json",
            "/api/v1/openapi.json",
            "/api/v2/openapi.json",
            "/.well-known/openapi.json",
        ]
    }

    /// Try to fetch a spec from a URL and return the JSON value if successful.
    async fn try_fetch_spec(
        http: &reqwest::Client,
        url: &str,
    ) -> Option<(String, serde_json::Value)> {
        let resp = http.get(url).send().await.ok()?;
        if !resp.status().is_success() {
            return None;
        }

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

        let body = resp.text().await.ok()?;

        // Try JSON first
        if let Ok(val) = serde_json::from_str::<serde_json::Value>(&body) {
            // Verify it looks like an OpenAPI / Swagger spec
            if val.get("openapi").is_some()
                || val.get("swagger").is_some()
                || val.get("paths").is_some()
            {
                return Some((url.to_string(), val));
            }
        }

        // If content type suggests YAML, we can't easily parse without a YAML dep,
        // so just report the URL as found
        if content_type.contains("yaml")
            || body.starts_with("openapi:")
            || body.starts_with("swagger:")
        {
            // Return a minimal JSON indicating YAML was found
            return Some((
                url.to_string(),
                json!({
                    "_note": "YAML spec detected but not parsed. Fetch and convert to JSON.",
                    "_raw_url": url,
                }),
            ));
        }

        None
    }

    /// Parse an OpenAPI 3.x or Swagger 2.x spec into structured endpoints.
    fn parse_spec(spec: &serde_json::Value, _base_url: &str) -> Vec<ParsedEndpoint> {
        let mut endpoints = Vec::new();

        // Determine base path
        let base_path = if let Some(servers) = spec.get("servers").and_then(|v| v.as_array()) {
            servers
                .first()
                .and_then(|s| s.get("url"))
                .and_then(|u| u.as_str())
                .unwrap_or("")
                .to_string()
        } else if let Some(bp) = spec.get("basePath").and_then(|v| v.as_str()) {
            bp.to_string()
        } else {
            String::new()
        };

        let paths = match spec.get("paths").and_then(|v| v.as_object()) {
            Some(p) => p,
            None => return endpoints,
        };

        for (path, path_item) in paths {
            let path_obj = match path_item.as_object() {
                Some(o) => o,
                None => continue,
            };

            // Path-level parameters
            let path_params = path_obj
                .get("parameters")
                .and_then(|v| v.as_array())
                .cloned()
                .unwrap_or_default();

            for method in &["get", "post", "put", "patch", "delete", "head", "options"] {
                let operation = match path_obj.get(*method).and_then(|v| v.as_object()) {
                    Some(o) => o,
                    None => continue,
                };

                let operation_id = operation
                    .get("operationId")
                    .and_then(|v| v.as_str())
                    .map(String::from);

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

                let tags: Vec<String> = operation
                    .get("tags")
                    .and_then(|v| v.as_array())
                    .map(|arr| {
                        arr.iter()
                            .filter_map(|t| t.as_str().map(String::from))
                            .collect()
                    })
                    .unwrap_or_default();

                // Merge path-level and operation-level parameters
                let mut parameters = Vec::new();
                let op_params = operation
                    .get("parameters")
                    .and_then(|v| v.as_array())
                    .cloned()
                    .unwrap_or_default();

                for param_val in path_params.iter().chain(op_params.iter()) {
                    let name = param_val
                        .get("name")
                        .and_then(|v| v.as_str())
                        .unwrap_or_default()
                        .to_string();
                    let location = param_val
                        .get("in")
                        .and_then(|v| v.as_str())
                        .unwrap_or("query")
                        .to_string();
                    let required = param_val
                        .get("required")
                        .and_then(|v| v.as_bool())
                        .unwrap_or(false);

                    // Type from schema or direct type field
                    let param_type = param_val
                        .get("schema")
                        .and_then(|s| s.get("type"))
                        .or_else(|| param_val.get("type"))
                        .and_then(|v| v.as_str())
                        .map(String::from);

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

                    parameters.push(ParsedParameter {
                        name,
                        location,
                        required,
                        param_type,
                        description,
                    });
                }

                // Request body (OpenAPI 3.x)
                let request_body_content_type = operation
                    .get("requestBody")
                    .and_then(|rb| rb.get("content"))
                    .and_then(|c| c.as_object())
                    .and_then(|obj| obj.keys().next().cloned());

                // Response codes
                let response_codes: Vec<String> = operation
                    .get("responses")
                    .and_then(|r| r.as_object())
                    .map(|obj| obj.keys().cloned().collect())
                    .unwrap_or_default();

                // Security requirements
                let security: Vec<String> = operation
                    .get("security")
                    .and_then(|v| v.as_array())
                    .map(|arr| {
                        arr.iter()
                            .filter_map(|s| s.as_object())
                            .flat_map(|obj| obj.keys().cloned())
                            .collect()
                    })
                    .unwrap_or_default();

                endpoints.push(ParsedEndpoint {
                    path: format!("{}{}", base_path, path),
                    method: method.to_uppercase(),
                    operation_id,
                    summary,
                    parameters,
                    request_body_content_type,
                    response_codes,
                    security,
                    tags,
                });
            }
        }

        endpoints
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;

    #[test]
    fn common_spec_paths_not_empty() {
        let paths = OpenApiParserTool::common_spec_paths();
        assert!(paths.len() >= 5);
        assert!(paths.contains(&"/openapi.json"));
        assert!(paths.contains(&"/swagger.json"));
    }

    #[test]
    fn parse_spec_openapi3_basic() {
        let spec = json!({
            "openapi": "3.0.0",
            "info": { "title": "Test API", "version": "1.0" },
            "paths": {
                "/users": {
                    "get": {
                        "operationId": "listUsers",
                        "summary": "List all users",
                        "parameters": [
                            {
                                "name": "limit",
                                "in": "query",
                                "required": false,
                                "schema": { "type": "integer" }
                            }
                        ],
                        "responses": {
                            "200": { "description": "OK" },
                            "401": { "description": "Unauthorized" }
                        },
                        "tags": ["users"]
                    },
                    "post": {
                        "operationId": "createUser",
                        "requestBody": {
                            "content": {
                                "application/json": {}
                            }
                        },
                        "responses": { "201": {} },
                        "security": [{ "bearerAuth": [] }]
                    }
                }
            }
        });

        let endpoints = OpenApiParserTool::parse_spec(&spec, "https://api.example.com");
        assert_eq!(endpoints.len(), 2);

        let get_ep = endpoints.iter().find(|e| e.method == "GET").unwrap();
        assert_eq!(get_ep.path, "/users");
        assert_eq!(get_ep.operation_id.as_deref(), Some("listUsers"));
        assert_eq!(get_ep.summary.as_deref(), Some("List all users"));
        assert_eq!(get_ep.parameters.len(), 1);
        assert_eq!(get_ep.parameters[0].name, "limit");
        assert_eq!(get_ep.parameters[0].location, "query");
        assert!(!get_ep.parameters[0].required);
        assert_eq!(get_ep.parameters[0].param_type.as_deref(), Some("integer"));
        assert_eq!(get_ep.response_codes.len(), 2);
        assert_eq!(get_ep.tags, vec!["users"]);

        let post_ep = endpoints.iter().find(|e| e.method == "POST").unwrap();
        assert_eq!(
            post_ep.request_body_content_type.as_deref(),
            Some("application/json")
        );
        assert_eq!(post_ep.security, vec!["bearerAuth"]);
    }

    #[test]
    fn parse_spec_swagger2_with_base_path() {
        let spec = json!({
            "swagger": "2.0",
            "basePath": "/api/v1",
            "paths": {
                "/items": {
                    "get": {
                        "parameters": [
                            { "name": "id", "in": "path", "required": true, "type": "string" }
                        ],
                        "responses": { "200": {} }
                    }
                }
            }
        });

        let endpoints = OpenApiParserTool::parse_spec(&spec, "https://api.example.com");
        assert_eq!(endpoints.len(), 1);
        assert_eq!(endpoints[0].path, "/api/v1/items");
        assert_eq!(
            endpoints[0].parameters[0].param_type.as_deref(),
            Some("string")
        );
    }

    #[test]
    fn parse_spec_empty_paths() {
        let spec = json!({ "openapi": "3.0.0", "paths": {} });
        let endpoints = OpenApiParserTool::parse_spec(&spec, "https://example.com");
        assert!(endpoints.is_empty());
    }

    #[test]
    fn parse_spec_no_paths_key() {
        let spec = json!({ "openapi": "3.0.0" });
        let endpoints = OpenApiParserTool::parse_spec(&spec, "https://example.com");
        assert!(endpoints.is_empty());
    }

    #[test]
    fn parse_spec_servers_base_url() {
        let spec = json!({
            "openapi": "3.0.0",
            "servers": [{ "url": "/api/v2" }],
            "paths": {
                "/health": {
                    "get": { "responses": { "200": {} } }
                }
            }
        });
        let endpoints = OpenApiParserTool::parse_spec(&spec, "https://example.com");
        assert_eq!(endpoints[0].path, "/api/v2/health");
    }

    #[test]
    fn parse_spec_path_level_parameters_merged() {
        let spec = json!({
            "openapi": "3.0.0",
            "paths": {
                "/items/{id}": {
                    "parameters": [
                        { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }
                    ],
                    "get": {
                        "parameters": [
                            { "name": "fields", "in": "query", "schema": { "type": "string" } }
                        ],
                        "responses": { "200": {} }
                    }
                }
            }
        });
        let endpoints = OpenApiParserTool::parse_spec(&spec, "https://example.com");
        assert_eq!(endpoints[0].parameters.len(), 2);
        assert!(endpoints[0]
            .parameters
            .iter()
            .any(|p| p.name == "id" && p.location == "path"));
        assert!(endpoints[0]
            .parameters
            .iter()
            .any(|p| p.name == "fields" && p.location == "query"));
    }
}

impl PentestTool for OpenApiParserTool {
    fn name(&self) -> &str {
        "openapi_parser"
    }

    fn description(&self) -> &str {
        "Discovers and parses OpenAPI/Swagger specifications. Tries common spec paths and \
         returns structured endpoint definitions including parameters, methods, and security \
         requirements. Use this to discover all API endpoints before testing."
    }

    fn input_schema(&self) -> serde_json::Value {
        json!({
            "type": "object",
            "properties": {
                "base_url": {
                    "type": "string",
                    "description": "Base URL of the API to discover specs from"
                },
                "spec_url": {
                    "type": "string",
                    "description": "Optional explicit URL of the OpenAPI/Swagger spec file"
                }
            },
            "required": ["base_url"]
        })
    }

    fn execute<'a>(
        &'a self,
        input: serde_json::Value,
        _context: &'a PentestToolContext,
    ) -> std::pin::Pin<
        Box<dyn std::future::Future<Output = Result<PentestToolResult, CoreError>> + Send + 'a>,
    > {
        Box::pin(async move {
            let base_url = input
                .get("base_url")
                .and_then(|v| v.as_str())
                .ok_or_else(|| {
                    CoreError::Dast("Missing required 'base_url' parameter".to_string())
                })?;

            let explicit_spec_url = input.get("spec_url").and_then(|v| v.as_str());

            let base_url_trimmed = base_url.trim_end_matches('/');

            // If an explicit spec URL is provided, try it first
            let mut spec_result: Option<(String, serde_json::Value)> = None;
            if let Some(spec_url) = explicit_spec_url {
                spec_result = Self::try_fetch_spec(&self.http, spec_url).await;
            }

            // If no explicit URL or it failed, try common paths
            if spec_result.is_none() {
                for path in Self::common_spec_paths() {
                    let url = format!("{base_url_trimmed}{path}");
                    if let Some(result) = Self::try_fetch_spec(&self.http, &url).await {
                        spec_result = Some(result);
                        break;
                    }
                }
            }

            match spec_result {
                Some((spec_url, spec)) => {
                    let spec_version = spec
                        .get("openapi")
                        .or_else(|| spec.get("swagger"))
                        .and_then(|v| v.as_str())
                        .unwrap_or("unknown");

                    let api_title = spec
                        .get("info")
                        .and_then(|i| i.get("title"))
                        .and_then(|t| t.as_str())
                        .unwrap_or("Unknown API");

                    let api_version = spec
                        .get("info")
                        .and_then(|i| i.get("version"))
                        .and_then(|v| v.as_str())
                        .unwrap_or("unknown");

                    let endpoints = Self::parse_spec(&spec, base_url_trimmed);

                    let endpoint_data: Vec<serde_json::Value> = endpoints
                        .iter()
                        .map(|ep| {
                            let params: Vec<serde_json::Value> = ep
                                .parameters
                                .iter()
                                .map(|p| {
                                    json!({
                                        "name": p.name,
                                        "in": p.location,
                                        "required": p.required,
                                        "type": p.param_type,
                                        "description": p.description,
                                    })
                                })
                                .collect();

                            json!({
                                "path": ep.path,
                                "method": ep.method,
                                "operation_id": ep.operation_id,
                                "summary": ep.summary,
                                "parameters": params,
                                "request_body_content_type": ep.request_body_content_type,
                                "response_codes": ep.response_codes,
                                "security": ep.security,
                                "tags": ep.tags,
                            })
                        })
                        .collect();

                    let endpoint_count = endpoints.len();
                    info!(
                        spec_url = %spec_url,
                        spec_version,
                        api_title,
                        endpoints = endpoint_count,
                        "OpenAPI spec parsed"
                    );

                    Ok(PentestToolResult {
                        summary: format!(
                            "Found OpenAPI spec ({spec_version}) at {spec_url}. \
                         API: {api_title} v{api_version}. \
                         Parsed {endpoint_count} endpoints."
                        ),
                        findings: Vec::new(), // This tool produces data, not findings
                        data: json!({
                            "spec_url": spec_url,
                            "spec_version": spec_version,
                            "api_title": api_title,
                            "api_version": api_version,
                            "endpoint_count": endpoint_count,
                            "endpoints": endpoint_data,
                            "security_schemes": spec.get("components")
                                .and_then(|c| c.get("securitySchemes"))
                                .or_else(|| spec.get("securityDefinitions")),
                        }),
                    })
                }
                None => {
                    info!(base_url, "No OpenAPI spec found");

                    Ok(PentestToolResult {
                        summary: format!(
                            "No OpenAPI/Swagger specification found for {base_url}. \
                         Tried {} common paths.",
                            Self::common_spec_paths().len()
                        ),
                        findings: Vec::new(),
                        data: json!({
                            "spec_found": false,
                            "paths_tried": Self::common_spec_paths(),
                        }),
                    })
                }
            }
        })
    }
}
