use axum::{
    extract::{Multipart, Path, Query, State},
    http::StatusCode,
    response::IntoResponse,
    routing::{get, post},
    Json, Router,
};
use chrono::Utc;
use std::fs;
use std::io::Write;
use uuid::Uuid;

use crate::AppState;
use theseus_core::*;

const VALID_VERDICTS: &[&str] = &["pending", "success", "failed", "in_progress"];
const VALID_EVIDENCE: &[&str] = &["confirmed", "unverified", "hypothesis"];

fn is_valid_custom_type(s: &str) -> bool {
    !s.is_empty()
        && s.len() <= 30
        && s.chars()
            .next()
            .map(|c| c.is_ascii_lowercase())
            .unwrap_or(false)
        && s.chars()
            .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
}

pub(crate) fn router() -> Router<AppState> {
    Router::new()
        .route(
            "/api/projects/:project_id/nodes",
            post(create_node).get(list_nodes),
        )
        .route(
            "/api/projects/:project_id/nodes/:node_id",
            get(get_node).put(update_node).delete(delete_node),
        )
        .route(
            "/api/projects/:project_id/nodes/:node_id/upload",
            post(upload_screenshot),
        )
        .route("/api/projects/:project_id/nodes/search", get(search_nodes))
}

async fn create_node(
    State(state): State<AppState>,
    Path(project_id): Path<String>,
    Json(payload): Json<CreateNodeRequest>,
) -> (StatusCode, Json<ApiResponse<Node>>) {
    let pid = match crate::resolve_project_id(&state.db, &project_id).await {
        Ok(p) => p,
        Err(_) => {
            return (
                StatusCode::BAD_REQUEST,
                Json(ApiResponse::err("invalid project_id")),
            )
        }
    };

    // Validate label
    if payload.label.trim().is_empty() {
        return (
            StatusCode::BAD_REQUEST,
            Json(ApiResponse::err("label cannot be empty")),
        );
    }
    // Validate node_type
    if let NodeType::Custom(ref s) = payload.node_type {
        if !is_valid_custom_type(s) {
            return (
                StatusCode::BAD_REQUEST,
                Json(ApiResponse::err(&format!(
                    "invalid node_type '{}', allowed: host, service, vulnerability, credential, finding, note, entry_point, attack_step, impact, objective, or custom [a-z][a-z0-9_]{{0,29}}", s
                ))),
            );
        }
    }
    // Validate properties is Object
    if !payload.properties.is_object() {
        return (
            StatusCode::BAD_REQUEST,
            Json(ApiResponse::err(
                "properties must be a JSON object, not array/string/null/number",
            )),
        );
    }

    // Rule 1: non-entry nodes must have `after` (no isolated nodes)
    let existing = match state.db.list_nodes(pid) {
        Ok(n) => n,
        Err(_) => {
            return (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ApiResponse::err("db error")),
            )
        }
    };
    if !existing.is_empty() && payload.after.is_none() {
        return (
            StatusCode::BAD_REQUEST,
            Json(ApiResponse::err(
                "孤立节点：项目已有节点，新节点必须通过 `after` 指定前驱节点并连线。用法: --after <NODE_ID> --edge-label \"描述\"",
            )),
        );
    }

    // Rule 2: content must use full URLs, not bare paths
    if let Some(content) = payload.properties.get("content").and_then(|v| v.as_str()) {
        for line in content.lines() {
            // Skip lines that already contain a full URL (scheme://host)
            if line.contains("://") {
                continue;
            }
            // Check each whitespace-separated token for bare paths
            for word in line.split_whitespace() {
                if word.starts_with('/')
                    && word.len() > 1
                    && word.chars().nth(1).map_or(false, |c| c.is_alphabetic())
                {
                    return (
                        StatusCode::BAD_REQUEST,
                        Json(ApiResponse::err(&format!(
                            "裸路径检测：content 中包含裸路径 '{}'，必须使用完整绝对 URL（如 http://host{}）",
                            word, word
                        ))),
                    );
                }
            }
        }
    }

    let node = Node {
        id: Uuid::new_v4(),
        project_id: pid,
        label: payload.label.clone(),
        node_type: payload.node_type.clone(),
        position: payload.position.clone(),
        properties: payload.properties.clone(),
        created_at: Utc::now(),
    };

    if state.db.insert_node(&node).is_err() {
        return (
            StatusCode::INTERNAL_SERVER_ERROR,
            Json(ApiResponse::err("db error")),
        );
    }

    // Auto-create edge if `after` is specified
    if let Some(after_raw) = &payload.after {
        let after_id = match crate::resolve_node_id(state.db, pid, after_raw) {
            Ok(id) => id,
            Err(StatusCode::NOT_FOUND) => {
                return (
                    StatusCode::BAD_REQUEST,
                    Json(ApiResponse::err("after: node not found")),
                )
            }
            Err(StatusCode::BAD_REQUEST) => {
                return (
                    StatusCode::BAD_REQUEST,
                    Json(ApiResponse::err("after: ambiguous or too short node ID")),
                )
            }
            Err(e) => return (e, Json(ApiResponse::err("db error"))),
        };
        let edge = Edge {
            id: Uuid::new_v4(),
            project_id: pid,
            source_node_id: after_id,
            target_node_id: node.id,
            label: payload.edge_label.clone(),
            created_at: Utc::now(),
        };
        if state.db.insert_edge(&edge).is_err() {
            return (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ApiResponse::err("db error")),
            );
        }
    }

    let _ = state.tx.send(format!("node_created:{pid}"));
    (StatusCode::CREATED, Json(ApiResponse::ok(node)))
}

#[derive(Debug, serde::Deserialize)]
struct NodeFilterParams {
    node_type: Option<String>,
    verdict: Option<String>,
    evidence: Option<String>,
}

async fn list_nodes(
    State(state): State<AppState>,
    Path(project_id): Path<String>,
    Query(params): Query<NodeFilterParams>,
) -> Result<Json<ApiResponse<Vec<Node>>>, StatusCode> {
    let pid = crate::resolve_project_id(&state.db, &project_id).await?;
    let nodes = state
        .db
        .list_nodes(pid)
        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;

    let filtered: Vec<Node> = nodes
        .into_iter()
        .filter(|node| {
            if let Some(ref t) = params.node_type {
                if node.node_type.to_string() != *t {
                    return false;
                }
            }
            if let Some(ref v) = params.verdict {
                if node
                    .properties
                    .get("verdict")
                    .and_then(|v| v.as_str())
                    .unwrap_or("")
                    != v.as_str()
                {
                    return false;
                }
            }
            if let Some(ref e) = params.evidence {
                if node
                    .properties
                    .get("evidence")
                    .and_then(|e| e.as_str())
                    .unwrap_or("")
                    != e.as_str()
                {
                    return false;
                }
            }
            true
        })
        .collect();

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

async fn get_node(
    State(state): State<AppState>,
    Path((project_id, node_id)): Path<(String, String)>,
) -> Result<Json<ApiResponse<Node>>, StatusCode> {
    let pid = crate::resolve_project_id(&state.db, &project_id).await?;
    let nid = crate::parse_uuid(&node_id)?;
    let node = state
        .db
        .get_node(pid, nid)
        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
        .ok_or(StatusCode::NOT_FOUND)?;
    Ok(Json(ApiResponse::ok(node)))
}

async fn update_node(
    State(state): State<AppState>,
    Path((project_id, node_id)): Path<(String, String)>,
    Json(payload): Json<UpdateNodeRequest>,
) -> (StatusCode, Json<ApiResponse<Node>>) {
    let pid = match crate::resolve_project_id(&state.db, &project_id).await {
        Ok(p) => p,
        Err(_) => {
            return (
                StatusCode::BAD_REQUEST,
                Json(ApiResponse::err("invalid project_id")),
            )
        }
    };
    let nid = match crate::parse_uuid(&node_id) {
        Ok(id) => id,
        Err(s) => return (s, Json(ApiResponse::err("invalid node_id"))),
    };

    // Validate label if provided
    if let Some(ref label) = payload.label {
        if label.trim().is_empty() {
            return (
                StatusCode::BAD_REQUEST,
                Json(ApiResponse::err("label cannot be empty")),
            );
        }
    }
    // Validate node_type if provided
    if let Some(NodeType::Custom(ref s)) = payload.node_type {
        if !is_valid_custom_type(s) {
            return (
                StatusCode::BAD_REQUEST,
                Json(ApiResponse::err(&format!(
                    "invalid node_type '{}', allowed: host, service, vulnerability, credential, finding, note, entry_point, attack_step, impact, objective, or custom [a-z][a-z0-9_]{{0,29}}", s
                ))),
            );
        }
    }
    // Validate properties is Object if provided
    if let Some(ref props) = payload.properties {
        if !props.is_object() {
            return (
                StatusCode::BAD_REQUEST,
                Json(ApiResponse::err(
                    "properties must be a JSON object, not array/string/null/number",
                )),
            );
        }
    }
    // Validate verdict if provided
    if let Some(ref v) = payload.set_verdict {
        if !VALID_VERDICTS.contains(&v.as_str()) {
            return (
                StatusCode::BAD_REQUEST,
                Json(ApiResponse::err(&format!(
                    "invalid verdict '{}', allowed: {}",
                    v,
                    VALID_VERDICTS.join(", ")
                ))),
            );
        }
    }
    // Validate evidence if provided
    if let Some(ref e) = payload.set_evidence {
        if !VALID_EVIDENCE.contains(&e.as_str()) {
            return (
                StatusCode::BAD_REQUEST,
                Json(ApiResponse::err(&format!(
                    "invalid evidence '{}', allowed: {}",
                    e,
                    VALID_EVIDENCE.join(", ")
                ))),
            );
        }
    }

    let node = state.db.update_node(pid, nid, &payload);
    match node {
        Ok(Some(n)) => {
            let _ = state.tx.send(format!("node_updated:{pid}"));
            (StatusCode::OK, Json(ApiResponse::ok(n)))
        }
        Ok(None) => (
            StatusCode::NOT_FOUND,
            Json(ApiResponse::err("node not found")),
        ),
        Err(_) => (
            StatusCode::INTERNAL_SERVER_ERROR,
            Json(ApiResponse::err("db error")),
        ),
    }
}

async fn delete_node(
    State(state): State<AppState>,
    Path((project_id, node_id)): Path<(String, String)>,
    Query(params): Query<std::collections::HashMap<String, String>>,
) -> (StatusCode, Json<ApiResponse<()>>) {
    let pid = match crate::resolve_project_id(&state.db, &project_id).await {
        Ok(p) => p,
        Err(_) => {
            return (
                StatusCode::BAD_REQUEST,
                Json(ApiResponse::err("invalid project_id")),
            )
        }
    };
    let nid = match crate::parse_uuid(&node_id) {
        Ok(id) => id,
        Err(s) => return (s, Json(ApiResponse::err("invalid node_id"))),
    };

    let edge_count = match state.db.count_edges_for_node(pid, nid) {
        Ok(c) => c,
        Err(_) => {
            return (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ApiResponse::err("db error")),
            )
        }
    };

    let force = params.get("force").map(|v| v.as_str()) == Some("true");
    if edge_count > 0 && !force {
        return (
            StatusCode::CONFLICT,
            Json(ApiResponse::err(&format!(
                "Cannot delete node: {} edges connected. Delete edges first or use ?force=true",
                edge_count
            ))),
        );
    }

    match state.db.delete_node(pid, nid) {
        Ok(true) => {
            let _ = state.tx.send(format!("node_deleted:{pid}"));
            (StatusCode::OK, Json(ApiResponse::ok(())))
        }
        Ok(false) => (
            StatusCode::NOT_FOUND,
            Json(ApiResponse::err("node not found")),
        ),
        Err(_) => (
            StatusCode::INTERNAL_SERVER_ERROR,
            Json(ApiResponse::err("db error")),
        ),
    }
}

// ── Screenshot upload ──

async fn upload_screenshot(
    State(state): State<AppState>,
    Path((project_id, node_id)): Path<(String, String)>,
    mut multipart: Multipart,
) -> Result<impl IntoResponse, StatusCode> {
    let pid = crate::resolve_project_id(&state.db, &project_id).await?;
    let nid = crate::parse_uuid(&node_id)?;

    // Ensure node exists
    state
        .db
        .get_node(pid, nid)
        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
        .ok_or(StatusCode::NOT_FOUND)?;

    // Create screenshots dir
    let dir = format!("screenshots/{pid}");
    fs::create_dir_all(&dir).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;

    let mut filename = String::new();
    while let Ok(Some(field)) = multipart.next_field().await {
        if let Some(name) = field.file_name() {
            filename = format!("{dir}/{nid}_{}", name);
            let data = field.bytes().await.map_err(|_| StatusCode::BAD_REQUEST)?;
            let mut f =
                fs::File::create(&filename).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
            f.write_all(&data)
                .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
        }
    }

    if filename.is_empty() {
        return Err(StatusCode::BAD_REQUEST);
    }

    Ok((
        StatusCode::CREATED,
        Json(ApiResponse::ok(serde_json::json!({
            "filename": filename
        }))),
    ))
}

async fn search_nodes(
    State(state): State<AppState>,
    Path(project_id): Path<String>,
    Query(params): Query<std::collections::HashMap<String, String>>,
) -> (StatusCode, Json<ApiResponse<Vec<Node>>>) {
    let pid = match crate::resolve_project_id(&state.db, &project_id).await {
        Ok(p) => p,
        Err(_) => {
            return (
                StatusCode::BAD_REQUEST,
                Json(ApiResponse::err("invalid project_id")),
            )
        }
    };
    let query = params.get("q").cloned().unwrap_or_default();
    if query.trim().is_empty() {
        return (
            StatusCode::BAD_REQUEST,
            Json(ApiResponse::err("search query cannot be empty")),
        );
    }
    let nodes = match state.db.search_nodes(pid, &query) {
        Ok(n) => n,
        Err(_) => {
            return (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ApiResponse::err("db error")),
            )
        }
    };
    (StatusCode::OK, Json(ApiResponse::ok(nodes)))
}
