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::AppState;
use theseus_core::*;

pub(crate) fn router() -> Router<AppState> {
    Router::new()
        .route(
            "/api/projects/:project_id/nodes/:node_id/evidence",
            post(upload_evidence).get(list_evidence),
        )
        .route(
            "/api/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(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
    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(|_| StatusCode::INTERNAL_SERVER_ERROR)?
        .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(|_| StatusCode::INTERNAL_SERVER_ERROR)?;

    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(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
            f.write_all(&data)
                .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;

            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(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
            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(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
    state
        .db
        .delete_evidence_file(pid, nid, &filename)
        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
    Ok(Json(ApiResponse::ok(())))
}
