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

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

use crate::AppState;

type ApiResult<T> = Result<Json<ApiResponse<T>>, (StatusCode, Json<ApiResponse<T>>)>;

fn server_err<T: serde::Serialize>() -> (StatusCode, Json<ApiResponse<T>>) {
    crate::error_response(crate::AppError::internal(
        "report.generation_failed",
        "report.generate",
        "Report generation failed.",
        "报告生成失败。",
    ))
}

pub(crate) fn router() -> Router<AppState> {
    Router::new().route("/api/v1/projects/:project_id/report", get(get_report))
}

#[derive(Debug, serde::Serialize)]
pub struct ReportOutput {
    pub markdown: String,
    pub generated_at: String,
}

async fn get_report(
    State(state): State<AppState>,
    Path(project_id): Path<String>,
) -> ApiResult<ReportOutput> {
    let api_key = state.config.deepseek_api_key.as_deref().ok_or_else(|| {
        (
            StatusCode::BAD_REQUEST,
            Json(ApiResponse::err("AI features require API key. Configure via --api-key, ~/.config/theseus/config.toml, or ./theseus.toml")),
        )
    })?;
    let pid = crate::resolve_project_id(&state.db, &project_id)
        .await
        .map_err(|s| (s, Json(ApiResponse::err("project not found"))))?;
    let nodes = state.db.list_nodes(pid).map_err(|_| server_err())?;
    let edges = state.db.list_edges(pid).map_err(|_| server_err())?;
    let analysis = state.db.analyze(pid).map_err(|_| server_err())?;
    let timeline = state.db.project_timeline(pid).map_err(|_| server_err())?;
    let context = build_report_context(&nodes, &edges, &analysis, &timeline);
    let system_prompt = "\
你是资深渗透测试报告撰写专家。请基于 Theseus 攻击路径图谱生成一份 Markdown 渗透测试报告。

报告必须包含以下章节：
1. 执行摘要 (Executive Summary)
2. 攻击路径分析 (Attack Path Analysis)
3. 漏洞发现详情 (Vulnerability Findings - one section per vuln node)
4. 时间线 (Timeline)
5. 统计附录 (Statistics)

要求：基于节点内容、证据状态、判定状态和攻击路径写作；不要编造图谱中没有的事实；漏洞详情应引用具体节点标签和内容细节。";
    let markdown = crate::deepseek_chat(&state.http_client, api_key, system_prompt, &context, 0.2, 6000, false)
        .await
        .map_err(|_| server_err())?;

    Ok(Json(ApiResponse::ok(ReportOutput {
        markdown,
        generated_at: Utc::now().to_rfc3339(),
    })))
}

fn build_report_context(
    nodes: &[Node],
    edges: &[Edge],
    analysis: &AttackAnalysis,
    timeline: &[TimelineItem],
) -> String {
    let mut ctx = String::new();
    ctx.push_str("# Theseus Project Context\n\n");

    ctx.push_str("## All nodes\n\n");
    for node in nodes {
        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!(
            "### {}\n- id: {}\n- type: {}\n- verdict: {}\n- evidence: {}\n- created_at: {}\n\n```markdown\n{}\n```\n\n",
            node.label, node.id, node.node_type, verdict, evidence, node.created_at, content
        ));
    }

    ctx.push_str("## Edges\n\n");
    for edge in edges {
        let source = nodes
            .iter()
            .find(|n| n.id == edge.source_node_id)
            .map(|n| n.label.as_str())
            .unwrap_or("?");
        let target = nodes
            .iter()
            .find(|n| n.id == edge.target_node_id)
            .map(|n| n.label.as_str())
            .unwrap_or("?");
        ctx.push_str(&format!(
            "- {} -> {}: {}\n",
            source,
            target,
            edge.label.as_deref().unwrap_or("")
        ));
    }
    ctx.push('\n');

    ctx.push_str("## Attack paths\n\n");
    for (index, path) in analysis.attack_paths.iter().enumerate() {
        let labels: Vec<&str> = path.nodes.iter().map(|n| n.label.as_str()).collect();
        ctx.push_str(&format!("- Path {}: {}\n", index + 1, labels.join(" -> ")));
        if !path.edge_labels.is_empty() {
            ctx.push_str(&format!("  Methods: {}\n", path.edge_labels.join(" -> ")));
        }
    }
    ctx.push('\n');

    ctx.push_str("## Timeline order\n\n");
    for item in timeline {
        ctx.push_str(&format!(
            "- {} [{}] {}\n",
            item.created_at, item.node_type, item.node_label
        ));
    }
    ctx.push('\n');

    ctx.push_str("## Statistics summary\n\n");
    ctx.push_str(&format!("- total_nodes: {}\n", nodes.len()));
    ctx.push_str(&format!("- total_edges: {}\n", edges.len()));
    ctx.push_str(&format!(
        "- attack_paths: {}\n",
        analysis.attack_paths.len()
    ));
    ctx.push_str(&format!(
        "- unexploited_vulns: {}\n",
        analysis.unexploited_vulns.len()
    ));
    ctx.push_str(&format!("- dead_ends: {}\n", analysis.dead_ends.len()));
    ctx.push_str(&format!(
        "- isolated_nodes: {}\n",
        analysis.isolated_nodes.len()
    ));

    let mut by_type = std::collections::BTreeMap::new();
    let mut by_verdict = std::collections::BTreeMap::new();
    let mut by_evidence = std::collections::BTreeMap::new();
    for node in nodes {
        *by_type.entry(node.node_type.to_string()).or_insert(0usize) += 1;
        let verdict = node
            .properties
            .get("verdict")
            .and_then(|v| v.as_str())
            .unwrap_or("pending");
        let evidence = node
            .properties
            .get("evidence")
            .and_then(|v| v.as_str())
            .unwrap_or("unverified");
        *by_verdict.entry(verdict.to_string()).or_insert(0usize) += 1;
        *by_evidence.entry(evidence.to_string()).or_insert(0usize) += 1;
    }
    ctx.push_str(&format!("- nodes_by_type: {:?}\n", by_type));
    ctx.push_str(&format!("- nodes_by_verdict: {:?}\n", by_verdict));
    ctx.push_str(&format!("- nodes_by_evidence: {:?}\n", by_evidence));

    ctx
}

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

    async fn spawn_app() -> Uuid {
        let db_path =
            std::env::temp_dir().join(format!("theseus-report-context-test-{}.db", Uuid::new_v4()));
        let db = crate::db::Database::open(&db_path).unwrap();
        let project = db
            .create_project(&crate::types::CreateProjectRequest {
                name: "Report".into(),
            })
            .unwrap();
        project.id
    }

    async fn spawn_http_app() -> (String, Uuid) {
        let db_path =
            std::env::temp_dir().join(format!("theseus-report-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: "Report".into(),
            })
            .unwrap();
        let node = Node {
            id: Uuid::new_v4(),
            project_id: project.id,
            label: "Finding".into(),
            node_type: crate::types::NodeType::Vulnerability,
            position: None,
            properties: serde_json::json!({"content":"impact","verdict":"success","evidence":"confirmed"}),
            created_at: Utc::now(),
        };
        db.insert_node(&node).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 report_requires_api_key_and_uses_json_error_format() {
        let (base, pid) = spawn_http_app().await;
        let client = reqwest::Client::new();

        let report = client
            .get(format!("{base}/api/v1/projects/{pid}/report"))
            .send()
            .await
            .unwrap();
        assert_eq!(report.status(), StatusCode::BAD_REQUEST);
        let body = json(report).await;
        assert_eq!(body["success"], false);
        assert!(body["error"].as_str().unwrap().contains("API key"));

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

    #[tokio::test]
    async fn report_context_contains_nodes_edges_timeline_and_stats() {
        let project_id = spawn_app().await;
        let node = Node {
            id: Uuid::new_v4(),
            project_id,
            label: "SQLi".into(),
            node_type: crate::types::NodeType::Vulnerability,
            position: None,
            properties: serde_json::json!({"content":"id=1","verdict":"success","evidence":"confirmed"}),
            created_at: Utc::now(),
        };
        let analysis = AttackAnalysis {
            attack_paths: vec![],
            unexploited_vulns: vec![],
            dead_ends: vec![],
            next_steps: vec![],
            cross_connections: vec![],
            isolated_nodes: vec![],
            health_checks: vec![],
            check_summary: None,
            ai_hint: None,
        };
        let timeline = vec![crate::types::TimelineItem {
            node_id: node.id,
            project_id,
            node_label: node.label.clone(),
            node_type: node.node_type.to_string(),
            created_at: node.created_at,
            properties: node.properties.clone(),
            incoming: vec![],
            outgoing: vec![],
        }];
        let context = build_report_context(&[node], &[], &analysis, &timeline);
        assert!(context.contains("# Theseus Project Context"));
        assert!(context.contains("SQLi"));
        assert!(context.contains("total_nodes: 1"));
        assert!(context.contains("Timeline order"));
    }
}
