use axum::{
    extract::{Path, Query, State},
    http::StatusCode,
    routing::{get, post},
    Json, Router,
};

use crate::types::{ApiResponse, Edge, Node};

use crate::AppState;

pub(crate) fn router() -> Router<AppState> {
    Router::new()
        .route("/api/v1/projects/:project_id/analysis", get(get_analysis))
        .route(
            "/api/v1/projects/:project_id/nodes/check-duplicate",
            post(check_duplicate),
        )
}

#[derive(serde::Deserialize)]
pub(crate) struct AnalysisQuery {
    ai: Option<String>,
}

#[derive(Debug, serde::Deserialize)]
pub struct DuplicateCheckRequest {
    pub label: String,
    pub content: String,
    pub node_type: String,
}

#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct DuplicateCandidate {
    pub node_id: String,
    pub label: String,
    pub similarity: String,
    pub reason: String,
}

async fn get_analysis(
    State(state): State<AppState>,
    Path(project_id): Path<String>,
    Query(query): Query<AnalysisQuery>,
) -> Result<Json<ApiResponse<crate::types::AttackAnalysis>>, StatusCode> {
    let pid = crate::resolve_project_id(&state.db, &project_id).await?;
    let mut analysis = state
        .db
        .analyze(pid)
        .map_err(|err| crate::db_error_status("analysis.analyze", "project", err))?;

    if query.ai.as_deref() == Some("true") {
        if let Some(ref api_key) = state.config.deepseek_api_key {
            let nodes = state
                .db
                .list_nodes(pid)
                .map_err(|err| crate::db_error_status("analysis.ai.list_nodes", "node", err))?;
            let edges = state
                .db
                .list_edges(pid)
                .map_err(|err| crate::db_error_status("analysis.ai.list_edges", "edge", err))?;
            if let Some((suggestions, cross)) = ai_suggest(&state.http_client, api_key, &analysis, &nodes, &edges).await
            {
                analysis.next_steps.extend(suggestions);
                analysis.cross_connections.extend(cross);
            }
        } else {
            analysis.ai_hint = Some("AI suggestions unavailable — no API key configured. Use --api-key, ~/.config/theseus/config.toml, or ./theseus.toml".to_string());
        }
    }

    Ok(Json(ApiResponse::ok(analysis)))
}

async fn check_duplicate(
    State(state): State<AppState>,
    Path(project_id): Path<String>,
    Json(req): Json<DuplicateCheckRequest>,
) -> Result<Json<ApiResponse<Vec<DuplicateCandidate>>>, StatusCode> {
    let api_key = state
        .config
        .deepseek_api_key
        .as_deref()
        .ok_or(StatusCode::BAD_REQUEST)?;
    let pid = crate::resolve_project_id(&state.db, &project_id).await?;
    let nodes = state.db.list_nodes(pid).map_err(|err| {
        crate::db_error_status("analysis.check_duplicate.list_nodes", "node", err)
    })?;
    let candidates = detect_duplicates(&state.http_client, api_key, &req, &nodes)
        .await?;
    Ok(Json(ApiResponse::ok(candidates)))
}

async fn detect_duplicates(
    client: &reqwest::Client,
    api_key: &str,
    req: &DuplicateCheckRequest,
    nodes: &[Node],
) -> Result<Vec<DuplicateCandidate>, StatusCode> {
    let mut ctx = String::new();
    ctx.push_str("## New node\n\n");
    ctx.push_str(&format!(
        "Label: {}\nType: {}\nContent:\n```markdown\n{}\n```\n\n",
        req.label, req.node_type, req.content
    ));
    ctx.push_str("## Existing nodes\n\n");
    for node in nodes {
        let content = node
            .properties
            .get("content")
            .and_then(|v| v.as_str())
            .unwrap_or("");
        ctx.push_str(&format!(
            "- id: {}\n  label: {}\n  type: {}\n  content:\n```markdown\n{}\n```\n",
            node.id, node.label, node.node_type, content
        ));
    }

    let system_prompt = "Given this new node and existing nodes in the project, find semantically similar nodes. Return a JSON object with a \"duplicates\" key containing an array of {label, similarity, reason}. Only report similarity >= medium.";
    let content = crate::deepseek_chat(client, api_key, system_prompt, &ctx, 0.2, 2000, true)
        .await?;
    let parsed = parse_json_object(&content).ok_or(StatusCode::INTERNAL_SERVER_ERROR)?;
    let arr = parsed.get("duplicates").and_then(|v| v.as_array()).ok_or(StatusCode::INTERNAL_SERVER_ERROR)?;
    let mut out = Vec::new();
    for item in arr {
        let label = item
            .get("label")
            .and_then(|v| v.as_str())
            .unwrap_or_default()
            .trim();
        if label.is_empty() {
            continue;
        }
        if let Some(node) = nodes
            .iter()
            .find(|n| n.label == label)
            .or_else(|| nodes.iter().find(|n| n.label.eq_ignore_ascii_case(label)))
        {
            out.push(DuplicateCandidate {
                node_id: node.id.to_string(),
                label: node.label.clone(),
                similarity: item
                    .get("similarity")
                    .and_then(|v| v.as_str())
                    .unwrap_or("medium")
                    .to_string(),
                reason: item
                    .get("reason")
                    .and_then(|v| v.as_str())
                    .unwrap_or("")
                    .to_string(),
            });
        }
    }
    Ok(out)
}

async fn ai_suggest(
    client: &reqwest::Client,
    api_key: &str,
    analysis: &crate::types::AttackAnalysis,
    nodes: &[Node],
    graph_edges: &[Edge],
) -> Option<(Vec<crate::types::NextStep>, Vec<crate::types::CrossConnection>)> {
    let mut ctx = String::new();
    let node_by_id: std::collections::HashMap<String, &Node> =
        nodes.iter().map(|n| (n.id.to_string(), n)).collect();

    if !analysis.attack_paths.is_empty() {
        ctx.push_str("## 攻击路径\n\n");
        for (i, path) in analysis.attack_paths.iter().enumerate() {
            let labels: Vec<&str> = path.nodes.iter().map(|n| n.label.as_str()).collect();
            let edges: Vec<&str> = path.edge_labels.iter().map(|s| s.as_str()).collect();
            ctx.push_str(&format!("路径{}: {}\n", i + 1, labels.join(" → ")));
            if !edges.is_empty() {
                ctx.push_str(&format!("  (方法: {})\n", edges.join(" → ")));
            }
            for path_node in &path.nodes {
                if let Some(node) = node_by_id.get(&path_node.id) {
                    let evidence = node
                        .properties
                        .get("evidence")
                        .and_then(|v| v.as_str())
                        .unwrap_or("unverified");
                    let verdict = node
                        .properties
                        .get("verdict")
                        .and_then(|v| v.as_str())
                        .unwrap_or("pending");
                    let content = node
                        .properties
                        .get("content")
                        .and_then(|v| v.as_str())
                        .unwrap_or("");
                    ctx.push_str(&format!(
                        "  - {} (类型: {}, verdict: {}, evidence: {})\n",
                        node.label, node.node_type, verdict, evidence
                    ));
                    if !content.is_empty() {
                        ctx.push_str(&format!("    内容:\n```\n{}\n```\n", content));
                    }
                }
            }
        }
        ctx.push('\n');
    }

    let mut connected_ids = std::collections::HashSet::new();
    for edge in graph_edges {
        connected_ids.insert(edge.source_node_id.to_string());
        connected_ids.insert(edge.target_node_id.to_string());
    }
    if !connected_ids.is_empty() {
        ctx.push_str("## 所有节点概览\n\n");
        for node in nodes
            .iter()
            .filter(|n| connected_ids.contains(&n.id.to_string()))
        {
            let evidence = node
                .properties
                .get("evidence")
                .and_then(|v| v.as_str())
                .unwrap_or("unverified");
            let verdict = node
                .properties
                .get("verdict")
                .and_then(|v| v.as_str())
                .unwrap_or("pending");
            let content = node
                .properties
                .get("content")
                .and_then(|v| v.as_str())
                .unwrap_or("");
            let preview: String = content.chars().take(200).collect();
            let truncated = if content.chars().count() > 200 {
                "...(已截断)"
            } else {
                ""
            };
            ctx.push_str(&format!(
                "- **{}** (类型: {}, verdict: {}, evidence: {})\n  内容: {}{}\n",
                node.label, node.node_type, verdict, evidence, preview, truncated
            ));
        }
        ctx.push('\n');
    }

    if !analysis.dead_ends.is_empty() {
        ctx.push_str("## 已失败的尝试（不要重复建议）\n\n");
        for node in &analysis.dead_ends {
            let evidence = node
                .properties
                .get("evidence")
                .and_then(|v| v.as_str())
                .unwrap_or("-");
            let verdict = node
                .properties
                .get("verdict")
                .and_then(|v| v.as_str())
                .unwrap_or("failed");
            let content_preview: String = node.content.chars().take(200).collect();
            ctx.push_str(&format!(
                "- **{}** (类型: {}, verdict: {}, evidence: {})\n",
                node.label, node.node_type, verdict, evidence
            ));
            if !content_preview.is_empty() {
                ctx.push_str(&format!("  内容: {}\n", content_preview));
            }
        }
        ctx.push('\n');
    }

    if !analysis.cross_connections.is_empty() {
        ctx.push_str("## 已发现的跨分支关联\n\n");
        for cc in analysis.cross_connections.iter().take(15) {
            ctx.push_str(&format!(
                "- {} ↔ {}: {}\n",
                cc.from_label, cc.to_label, cc.reason
            ));
        }
        ctx.push('\n');
    }

    ctx.push_str("## 待利用漏洞（需要给出具体建议）\n\n");
    for node in &analysis.unexploited_vulns {
        let evidence = node
            .properties
            .get("evidence")
            .and_then(|v| v.as_str())
            .unwrap_or("-");
        let verdict = node
            .properties
            .get("verdict")
            .and_then(|v| v.as_str())
            .unwrap_or("pending");
        let content_preview: String = node.content.chars().take(2000).collect();
        let truncated = if node.content.chars().count() > 2000 {
            "…(已截断)"
        } else {
            ""
        };

        ctx.push_str(&format!(
            "### {} (类型: {}, verdict: {}, evidence: {})\n",
            node.label, node.node_type, verdict, evidence
        ));
        if !content_preview.is_empty() {
            ctx.push_str(&format!(
                "详细记录:\n```\n{}{}\n```\n",
                content_preview, truncated
            ));
        }
        ctx.push('\n');
    }

    let system_prompt = "\
你是一个资深渗透测试专家，擅长基于证据的攻击路径分析和漏洞利用规划。

请根据以下渗透测试图谱数据，完成两项任务：

## 任务一：为每个「待利用漏洞」给出具体建议
- 必须给出具体工具名称，以及可执行的示例命令、请求、或 payload
- 必须基于节点内容中的具体细节提出建议，例如引用「/gateway/router 端点返回...」这类实际记录
- 按真实可利用性排序：confirmed + 有细节的节点优先；unverified + 描述模糊的节点先建议验证
- 不要重复「已失败尝试」中的方向
- 每条建议必须包含：节点标签、具体工具/命令、为什么建议这样做

## 任务二：发现跨分支关联
分析整个图谱中所有节点（不论是否已有连线），找出可能存在关系但尚未连接的节点对。例如：
- 某节点发现的凭据/密钥/Token，可能可用于另一个节点（即使它们不在同一条攻击路径上）
- 某节点的信息（端口、服务、漏洞细节）对利用另一个节点有帮助
- 两个节点涉及相同的技术栈、服务、或目标，可以协同利用

## 输出格式

严格按以下格式输出，不要包含任何其他解释性文字：

### 任务一：漏洞利用建议

[逐条列出每个待利用漏洞的具体建议。每条建议必须包含：节点标签、具体工具/命令、为什么建议这样做]

### 任务二：跨分支关联

[列出发现的潜在关联，格式：节点A ↔ 节点B: 关联原因]";

    let content = crate::deepseek_chat(client, api_key, system_prompt, &ctx, 0.3, 4000, false)
        .await
        .ok()?;

    let mut sections = content.split("### 任务二：跨分支关联");
    let suggestions_section = sections.next().unwrap_or_default();
    let cross_section = sections.next().unwrap_or_default();

    let suggestions: Vec<crate::types::NextStep> = suggestions_section
        .split("\n\n")
        .filter(|para| {
            let trimmed = para.trim();
            if trimmed.is_empty() {
                return false;
            }
            if trimmed.starts_with('#') || trimmed.starts_with("任务") {
                return false;
            }
            // Skip horizontal rules: ---, ***, ___
            let rule: String = trimmed
                .chars()
                .filter(|c| *c == '-' || *c == '*' || *c == '_')
                .collect();
            if rule.len() >= 3 && rule.len() == trimmed.len() {
                return false;
            }
            true
        })
        .filter_map(|para| {
            let lines: Vec<&str> = para.lines().collect();
            if lines.is_empty() {
                return None;
            }
            let combined = para.trim();

            // Extract label: first bold (**label**) or text before first colon
            let label = if let Some(bold) = combined.split("**").nth(1) {
                bold.split(|c: char| c == '*' || c == '（' || c == '(')
                    .next()
                    .unwrap_or(bold)
                    .trim()
            } else if combined.contains('：') {
                combined.split('：').next().unwrap_or(combined).trim()
            } else if combined.contains(':') {
                combined.split(':').next().unwrap_or(combined).trim()
            } else {
                lines[0]
                    .trim_start_matches(|c: char| c == '-' || c == '*' || c == ' ')
                    .trim()
            };

            Some(crate::types::NextStep {
                node_id: String::new(),
                label: label.to_string(),
                node_type: String::new(),
                suggestion: combined.to_string(),
                from_path: Vec::new(),
                evidence: String::new(),
                chain_status: String::new(),
            })
        })
        .collect();

    let cross_connections: Vec<crate::types::CrossConnection> = cross_section
        .lines()
        .filter(|line| line.contains('↔') || line.contains("<->"))
        .filter_map(|line| {
            let parts: Vec<&str> = if line.contains('↔') {
                line.split('↔').collect()
            } else {
                line.split("<->").collect()
            };
            if parts.len() < 2 {
                return None;
            }
            let left = parts[0]
                .trim()
                .trim_start_matches(|c: char| c == '[' || c == '-' || c == '*' || c == ' ')
                .trim_end_matches(|c: char| c == ']' || c == ' ');
            let right_parts: Vec<&str> = parts[1].splitn(2, ':').collect();
            let right = right_parts[0]
                .trim()
                .trim_start_matches(|c: char| c == '[' || c == ' ')
                .trim_end_matches(|c: char| c == ']' || c == ' ');
            let reason = right_parts
                .get(1)
                .map(|s| s.trim())
                .unwrap_or("可能存在未发现的关联");
            Some(crate::types::CrossConnection {
                from_id: String::new(),
                from_label: left.to_string(),
                to_id: String::new(),
                to_label: right.to_string(),
                reason: reason.to_string(),
            })
        })
        .collect();

    Some((suggestions, cross_connections))
}

fn parse_json_object(content: &str) -> Option<serde_json::Value> {
    if let Ok(value) = serde_json::from_str::<serde_json::Value>(content.trim()) {
        if value.is_object() {
            return Some(value);
        }
    }
    let without_fence = content
        .trim()
        .trim_start_matches("```json")
        .trim_start_matches("```")
        .trim_end_matches("```")
        .trim();
    if let Ok(value) = serde_json::from_str::<serde_json::Value>(without_fence) {
        if value.is_object() {
            return Some(value);
        }
    }
    let start = content.find('{')?;
    let end = content.rfind('}')?;
    serde_json::from_str::<serde_json::Value>(&content[start..=end]).ok()
}

#[cfg(test)]
mod tests {
    use super::*;
    use axum::middleware;
    use chrono::Utc;
    use tokio::sync::broadcast;
    use uuid::Uuid;

    async fn spawn_app() -> (String, Uuid) {
        let db_path =
            std::env::temp_dir().join(format!("theseus-analysis-test-{}.db", Uuid::new_v4()));
        let db = Box::leak(Box::new(crate::db::Database::open(&db_path).unwrap()));
        let project = db
            .create_project(&crate::types::CreateProjectRequest {
                name: "Analysis".into(),
            })
            .unwrap();
        let entry = Node {
            id: Uuid::new_v4(),
            project_id: project.id,
            label: "Entry".into(),
            node_type: crate::types::NodeType::EntryPoint,
            position: None,
            properties: serde_json::json!({"verdict":"pending"}),
            created_at: Utc::now(),
        };
        let vuln = Node {
            id: Uuid::new_v4(),
            project_id: project.id,
            label: "SQLi".into(),
            node_type: crate::types::NodeType::Vulnerability,
            position: None,
            properties: serde_json::json!({"verdict":"success","evidence":"confirmed","content":"id parameter"}),
            created_at: Utc::now(),
        };
        db.insert_node(&entry).unwrap();
        db.insert_node(&vuln).unwrap();
        db.insert_edge(&Edge {
            id: Uuid::new_v4(),
            project_id: project.id,
            source_node_id: entry.id,
            target_node_id: vuln.id,
            label: Some("finds".into()),
            created_at: Utc::now(),
        })
        .unwrap();
        let (tx, _) = broadcast::channel(16);
        let state = crate::AppState {
            db,
            tx,
            config: crate::Config {
                api_key: None,
                deepseek_api_key: None,
                cors_origins: None,
                providers: std::collections::HashMap::new(),
                agent_workers: Vec::new(),
                default_provider: None,
            },
            api_key: None,
            http_client: reqwest::Client::new(),
                provider_registry: None,
        };
        let app = router()
            .layer(middleware::from_fn(crate::json_error_middleware))
            .with_state(state);
        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
        let addr = listener.local_addr().unwrap();
        tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });
        (format!("http://{addr}"), project.id)
    }

    async fn json(resp: reqwest::Response) -> serde_json::Value {
        resp.json::<serde_json::Value>().await.unwrap()
    }

    #[tokio::test]
    async fn analysis_returns_graph_summary_ai_hint_and_duplicate_error() {
        let (base, pid) = spawn_app().await;
        let client = reqwest::Client::new();

        let analysis = client
            .get(format!("{base}/api/v1/projects/{pid}/analysis?ai=true"))
            .send()
            .await
            .unwrap();
        assert_eq!(analysis.status(), StatusCode::OK);
        let body = json(analysis).await;
        assert_eq!(body["success"], true);
        assert!(body["data"]["attack_paths"].is_array());
        assert!(body["data"]["unexploited_vulns"].is_array());
        assert!(body["data"]["health_checks"].is_array());
        assert!(body["data"]["ai_hint"]
            .as_str()
            .unwrap()
            .contains("no API key"));

        let missing = client
            .get(format!("{base}/api/v1/projects/abcdef/analysis"))
            .send()
            .await
            .unwrap();
        assert_eq!(missing.status(), StatusCode::NOT_FOUND);

        let duplicate = client
            .post(format!("{base}/api/v1/projects/{pid}/nodes/check-duplicate"))
            .json(&serde_json::json!({"label":"SQLi","content":"same","node_type":"vulnerability"}))
            .send()
            .await
            .unwrap();
        assert_eq!(duplicate.status(), StatusCode::BAD_REQUEST);
        assert_eq!(json(duplicate).await["success"], false);
    }
}
