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

use crate::types::*;
use crate::AppState;

pub(crate) fn router() -> Router<AppState> {
    Router::new()
        .route(
            "/api/v1/projects/:project_id/nodes/:node_id/evidence",
            post(upload_evidence).get(list_evidence),
        )
        .route(
            "/api/v1/projects/:project_id/nodes/:node_id/evidence/:filename",
            get(get_evidence).delete(delete_evidence),
        )
}

pub(crate) fn guess_mime_type(path: &str) -> String {
    let ext = std::path::Path::new(path)
        .extension()
        .and_then(|e| e.to_str())
        .unwrap_or("")
        .to_lowercase();
    match ext.as_str() {
        "jpg" | "jpeg" => "image/jpeg",
        "png" => "image/png",
        "gif" => "image/gif",
        "webp" => "image/webp",
        "svg" => "image/svg+xml",
        "pdf" => "application/pdf",
        "txt" => "text/plain",
        "html" | "htm" => "text/html",
        "css" => "text/css",
        "js" => "application/javascript",
        "json" => "application/json",
        "xml" => "application/xml",
        "zip" => "application/zip",
        "tar" => "application/x-tar",
        "gz" | "gzip" => "application/gzip",
        "doc" => "application/msword",
        "docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
        "xls" => "application/vnd.ms-excel",
        "xlsx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
        "ppt" => "application/vnd.ms-powerpoint",
        "pptx" => "application/vnd.openxmlformats-officedocument.presentationml.presentation",
        _ => "application/octet-stream",
    }
    .to_string()
}

async fn list_evidence(
    State(state): State<AppState>,
    Path((project_id, node_id)): Path<(String, String)>,
) -> Result<Json<ApiResponse<Vec<EvidenceFile>>>, StatusCode> {
    let pid = crate::resolve_project_id(&state.db, &project_id).await?;
    let nid = crate::parse_uuid(&node_id)?;
    let files = state.db.list_evidence_files(pid, nid).map_err(|err| {
        crate::db_error_status("evidence.list_evidence_files", "evidence_file", err)
    })?;
    Ok(Json(ApiResponse::ok(files)))
}

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

    // Validate node exists
    state
        .db
        .get_node(pid, nid)
        .map_err(|err| crate::db_error_status("evidence.upload.get_node", "node", err))?
        .ok_or(StatusCode::NOT_FOUND)?;

    // Create evidence dir: data/pid/evidence/nid/
    let dir = format!("data/{pid}/evidence/{nid}");
    fs::create_dir_all(&dir).map_err(|err| {
        let app_err = crate::AppError::internal(
            "filesystem.create_dir_failed",
            "evidence.upload.create_dir",
            "Failed to create evidence directory.",
            "创建证据目录失败。",
        )
        .with_entity("evidence_file")
        .with_hint(format!("dir={dir}"))
        .with_source(err.to_string());
        let status = app_err.status;
        app_err.log();
        status
    })?;

    let mut saved: Option<EvidenceFile> = None;
    while let Ok(Some(field)) = multipart.next_field().await {
        if let Some(name) = field.file_name() {
            let name = name.to_string();
            let ext = std::path::Path::new(&name)
                .extension()
                .and_then(|e| e.to_str())
                .unwrap_or("bin");
            let stored_name = format!("{}.{}", Uuid::new_v4(), ext);
            let data = field.bytes().await.map_err(|_| StatusCode::BAD_REQUEST)?;
            let file_path = format!("{}/{}", dir, stored_name);
            let mut f = fs::File::create(&file_path).map_err(|err| {
                let app_err = crate::AppError::internal(
                    "filesystem.create_file_failed",
                    "evidence.upload.create_file",
                    "Failed to create evidence file.",
                    "创建证据文件失败。",
                )
                .with_entity("evidence_file")
                .with_hint(format!("file={file_path}"))
                .with_source(err.to_string());
                let status = app_err.status;
                app_err.log();
                status
            })?;
            f.write_all(&data).map_err(|err| {
                let app_err = crate::AppError::internal(
                    "filesystem.write_file_failed",
                    "evidence.upload.write_file",
                    "Failed to write evidence file.",
                    "写入证据文件失败。",
                )
                .with_entity("evidence_file")
                .with_hint(format!("file={file_path}"))
                .with_source(err.to_string());
                let status = app_err.status;
                app_err.log();
                status
            })?;

            let evidence = EvidenceFile {
                id: Uuid::new_v4(),
                project_id: pid,
                node_id: nid,
                original_name: name.clone(),
                stored_name: stored_name.clone(),
                file_size: data.len() as i64,
                mime_type: guess_mime_type(&name).into(),
                created_at: Utc::now(),
            };
            state.db.insert_evidence_file(&evidence).map_err(|err| {
                crate::db_error_status("evidence.insert_evidence_file", "evidence_file", err)
            })?;
            saved = Some(evidence);
        }
    }

    saved
        .map(|e| (StatusCode::CREATED, Json(ApiResponse::ok(e))))
        .ok_or(StatusCode::BAD_REQUEST)
}

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

    // Reject path traversal
    if filename.contains("..") || filename.starts_with('/') {
        return Err(StatusCode::BAD_REQUEST);
    }

    let file_path = format!("data/{pid}/evidence/{nid}/{filename}");
    let data = fs::read(&file_path).map_err(|_| StatusCode::NOT_FOUND)?;

    let mime = guess_mime_type(&file_path);
    let mut res = axum::response::Response::new(axum::body::Body::from(data));
    res.headers_mut().insert(
        CONTENT_TYPE,
        mime.parse()
            .unwrap_or_else(|_| "application/octet-stream".parse().unwrap()),
    );
    Ok(res)
}

async fn delete_evidence(
    State(state): State<AppState>,
    Path((project_id, node_id, filename)): Path<(String, String, String)>,
) -> Result<Json<ApiResponse<()>>, StatusCode> {
    let pid = crate::resolve_project_id(&state.db, &project_id).await?;
    let nid = crate::parse_uuid(&node_id)?;

    if filename.contains("..") || filename.starts_with('/') {
        return Err(StatusCode::BAD_REQUEST);
    }

    let file_path = format!("data/{pid}/evidence/{nid}/{filename}");
    fs::remove_file(&file_path).map_err(|err| {
        let app_err = crate::AppError::internal(
            "filesystem.delete_file_failed",
            "evidence.delete.remove_file",
            "Failed to delete evidence file from disk.",
            "删除磁盘上的证据文件失败。",
        )
        .with_entity("evidence_file")
        .with_hint(format!("file={file_path}"))
        .with_source(err.to_string());
        let status = app_err.status;
        app_err.log();
        status
    })?;
    state
        .db
        .delete_evidence_file(pid, nid, &filename)
        .map_err(|err| {
            crate::db_error_status("evidence.delete_evidence_file", "evidence_file", err)
        })?;
    Ok(Json(ApiResponse::ok(())))
}

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

    async fn spawn_app() -> (String, Uuid, Uuid) {
        let db_path =
            std::env::temp_dir().join(format!("theseus-evidence-test-{}.db", Uuid::new_v4()));
        let db = Box::leak(Box::new(crate::db::Database::open(&db_path).unwrap()));
        let project = db
            .create_project(&CreateProjectRequest {
                name: "Evidence".into(),
            })
            .unwrap();
        let node = Node {
            id: Uuid::new_v4(),
            project_id: project.id,
            label: "screen target".into(),
            node_type: NodeType::Service,
            position: None,
            properties: serde_json::json!({}),
            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, node.id)
    }

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

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

        let empty = client
            .post(format!("{base}/api/v1/projects/{pid}/nodes/{nid}/evidence"))
            .multipart(reqwest::multipart::Form::new())
            .send()
            .await
            .unwrap();
        assert_eq!(empty.status(), StatusCode::BAD_REQUEST);

        let part = reqwest::multipart::Part::bytes("hello evidence".as_bytes().to_vec())
            .file_name("proof.txt");
        let upload = client
            .post(format!("{base}/api/v1/projects/{pid}/nodes/{nid}/evidence"))
            .multipart(reqwest::multipart::Form::new().part("file", part))
            .send()
            .await
            .unwrap();
        assert_eq!(upload.status(), StatusCode::CREATED);
        let upload_body = json(upload).await;
        assert_eq!(upload_body["success"], true);
        assert_eq!(upload_body["data"]["original_name"], "proof.txt");
        assert_eq!(upload_body["data"]["mime_type"], "text/plain");
        let stored = upload_body["data"]["stored_name"]
            .as_str()
            .unwrap()
            .to_string();

        let list = client
            .get(format!("{base}/api/v1/projects/{pid}/nodes/{nid}/evidence"))
            .send()
            .await
            .unwrap();
        assert_eq!(list.status(), StatusCode::OK);
        assert_eq!(json(list).await["data"].as_array().unwrap().len(), 1);

        let download = client
            .get(format!(
                "{base}/api/v1/projects/{pid}/nodes/{nid}/evidence/{stored}"
            ))
            .send()
            .await
            .unwrap();
        assert_eq!(download.status(), StatusCode::OK);
        assert_eq!(download.headers()[CONTENT_TYPE], "text/plain");
        assert_eq!(download.text().await.unwrap(), "hello evidence");

        let traversal = client
            .get(format!(
                "{base}/api/v1/projects/{pid}/nodes/{nid}/evidence/..%2Fsecret"
            ))
            .send()
            .await
            .unwrap();
        assert_eq!(traversal.status(), StatusCode::BAD_REQUEST);

        let delete = client
            .delete(format!(
                "{base}/api/v1/projects/{pid}/nodes/{nid}/evidence/{stored}"
            ))
            .send()
            .await
            .unwrap();
        assert_eq!(delete.status(), StatusCode::OK);
        assert_eq!(json(delete).await["success"], true);

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