// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2026 Pierre Gronau, ndaal in Cologne

//! CSAF document CRUD API handlers with HATEOAS links.

use http::{Response, StatusCode};
use serde::Deserialize;
use serde_json::json;

use crate::app_state::AppState;
use crate::hateoas::{api_root_links, collection_links, csaf_links, self_link};
use crate::router::{
    Body, json_response, parse_json_body, parse_query, path_param, problem_response,
};

/// Default number of items per page.
const DEFAULT_PER_PAGE: usize = 20;

/// Maximum allowed items per page.
const MAX_PER_PAGE: usize = 100;

/// Query parameters for paginated collection endpoints.
#[derive(Debug, Deserialize)]
struct PaginationQuery {
    /// Page number (1-based).
    #[serde(default = "default_page")]
    page: String,

    /// Items per page.
    #[serde(default = "default_per_page")]
    per_page: String,
}

/// Return the default page number as a string.
fn default_page() -> String {
    "1".to_owned()
}

/// Return the default per-page count as a string.
fn default_per_page() -> String {
    DEFAULT_PER_PAGE.to_string()
}

/// Parse and clamp pagination parameters.
fn parse_pagination(query: &PaginationQuery) -> (usize, usize) {
    let page = query.page.parse::<usize>().unwrap_or(1).max(1);
    let per_page = query
        .per_page
        .parse::<usize>()
        .unwrap_or(DEFAULT_PER_PAGE)
        .clamp(1, MAX_PER_PAGE);
    (page, per_page)
}

/// `GET /api/v1` -- API root with HATEOAS discovery links.
pub async fn api_root(
    _state: AppState,
    _parts: http::request::Parts,
    _params: Vec<(String, String)>,
) -> Response<Body> {
    let body = json!({
        "data": {
            "name": "CSAF API",
            "version": env!("CARGO_PKG_VERSION"),
        },
        "_links": api_root_links(),
    });
    json_response(StatusCode::OK, &body)
}

/// `GET /api/v1/csaf` -- List CSAF documents with pagination and HATEOAS links.
pub async fn list_csaf(
    state: AppState,
    parts: http::request::Parts,
    _params: Vec<(String, String)>,
) -> Response<Body> {
    let query = match parse_query::<PaginationQuery>(&parts.uri) {
        Ok(q) => q,
        Err(e) => {
            return problem_response(
                StatusCode::BAD_REQUEST,
                "https://ndaal.eu/csaf/errors/bad-request",
                "Bad Request",
                &e,
            );
        },
    };

    let (page, per_page) = parse_pagination(&query);
    let offset = (page - 1) * per_page;

    let total = match state.csaf_storage().count_documents() {
        Ok(c) => c,
        Err(e) => {
            tracing::error!("Failed to count documents: {e}");
            return problem_response(
                StatusCode::INTERNAL_SERVER_ERROR,
                "https://ndaal.eu/csaf/errors/storage",
                "Storage Error",
                "Failed to count documents",
            );
        },
    };

    let items = match state.csaf_storage().list_meta(per_page, offset) {
        Ok(list) => list,
        Err(e) => {
            tracing::error!("Failed to list documents: {e}");
            return problem_response(
                StatusCode::INTERNAL_SERVER_ERROR,
                "https://ndaal.eu/csaf/errors/storage",
                "Storage Error",
                "Failed to list documents",
            );
        },
    };

    let items_json: Vec<serde_json::Value> = items
        .iter()
        .map(|meta| {
            let mut val = serde_json::to_value(meta).unwrap_or_default();
            val["_links"] = csaf_links(&meta.tracking_id);
            val
        })
        .collect();

    let body = json!({
        "data": items_json,
        "total": total,
        "page": page,
        "per_page": per_page,
        "_links": collection_links("/api/v1/csaf", page, per_page, total),
    });

    json_response(StatusCode::OK, &body)
}

/// `POST /api/v1/csaf` -- Create a new CSAF document.
pub async fn create_csaf(
    state: AppState,
    parts: http::request::Parts,
    _params: Vec<(String, String)>,
) -> Response<Body> {
    let doc: csaf_models::csaf_document::CsafDocument = match parse_json_body(&parts) {
        Ok(d) => d,
        Err(e) => {
            return problem_response(
                StatusCode::BAD_REQUEST,
                "https://ndaal.eu/csaf/errors/invalid-json",
                "Invalid JSON",
                &e,
            );
        },
    };

    // Validate the document.
    let validation_errors = csaf_core::validation::validate(&doc);
    let hard_errors: Vec<_> = validation_errors
        .iter()
        .filter(|e| e.severity == csaf_core::validation::Severity::Error)
        .collect();

    if !hard_errors.is_empty() {
        let details: Vec<serde_json::Value> = hard_errors
            .iter()
            .map(|e| json!({"path": e.path, "message": e.message}))
            .collect();
        let body = json!({
            "type": "https://ndaal.eu/csaf/errors/validation",
            "title": "Validation Failed",
            "status": 422,
            "detail": format!("{} validation error(s) found", details.len()),
            "errors": details,
        });
        return json_response(StatusCode::UNPROCESSABLE_ENTITY, &body);
    }

    // Check for duplicates.
    let tracking_id = doc.tracking_id().to_owned();
    match state.csaf_storage().document_exists(&tracking_id) {
        Ok(true) => {
            return problem_response(
                StatusCode::CONFLICT,
                "https://ndaal.eu/csaf/errors/duplicate",
                "Duplicate Document",
                &format!("Document '{tracking_id}' already exists"),
            );
        },
        Err(e) => {
            tracing::error!("Failed to check document existence: {e}");
            return problem_response(
                StatusCode::INTERNAL_SERVER_ERROR,
                "https://ndaal.eu/csaf/errors/storage",
                "Storage Error",
                "Failed to check document existence",
            );
        },
        Ok(false) => {},
    }

    // Store the document.
    if let Err(e) = state.csaf_storage().put_document(&doc) {
        tracing::error!("Failed to store document: {e}");
        return problem_response(
            StatusCode::INTERNAL_SERVER_ERROR,
            "https://ndaal.eu/csaf/errors/storage",
            "Storage Error",
            "Failed to store document",
        );
    }

    // Record audit log entry.
    let audit_result = state
        .db_pool()
        .with_conn(|conn| csaf_models::audit_log::record(conn, "create", &tracking_id, None, None));
    if let Err(e) = audit_result {
        tracing::warn!("Failed to record audit log: {e}");
    }

    let body = json!({
        "tracking_id": tracking_id,
        "message": "Document created successfully",
        "_links": csaf_links(&tracking_id),
    });

    Response::builder()
        .status(StatusCode::CREATED)
        .header("content-type", "application/json; charset=utf-8")
        .header("location", format!("/api/v1/csaf/{tracking_id}"))
        .body(Body::from(serde_json::to_string(&body).unwrap_or_default()))
        .unwrap_or_default()
}

/// `GET /api/v1/csaf/{id}` -- Get a single CSAF document with HATEOAS links.
pub async fn get_csaf(
    state: AppState,
    _parts: http::request::Parts,
    params: Vec<(String, String)>,
) -> Response<Body> {
    let tracking_id = match path_param(&params, "id") {
        Some(id) => id,
        None => {
            return problem_response(
                StatusCode::BAD_REQUEST,
                "https://ndaal.eu/csaf/errors/bad-request",
                "Bad Request",
                "Missing document ID",
            );
        },
    };

    match state.csaf_storage().get_document_json(&tracking_id) {
        Ok(Some(doc_json)) => {
            let body = json!({
                "document": doc_json,
                "_links": csaf_links(&tracking_id),
            });
            json_response(StatusCode::OK, &body)
        },
        Ok(None) => problem_response(
            StatusCode::NOT_FOUND,
            "https://ndaal.eu/csaf/errors/not-found",
            "Not Found",
            &format!("Document '{tracking_id}' not found"),
        ),
        Err(e) => {
            tracing::error!("Failed to get document: {e}");
            problem_response(
                StatusCode::INTERNAL_SERVER_ERROR,
                "https://ndaal.eu/csaf/errors/storage",
                "Storage Error",
                "Failed to retrieve document",
            )
        },
    }
}

/// `PUT /api/v1/csaf/{id}` -- Update an existing CSAF document.
pub async fn update_csaf(
    state: AppState,
    parts: http::request::Parts,
    params: Vec<(String, String)>,
) -> Response<Body> {
    let tracking_id = match path_param(&params, "id") {
        Some(id) => id,
        None => {
            return problem_response(
                StatusCode::BAD_REQUEST,
                "https://ndaal.eu/csaf/errors/bad-request",
                "Bad Request",
                "Missing document ID",
            );
        },
    };

    let doc: csaf_models::csaf_document::CsafDocument = match parse_json_body(&parts) {
        Ok(d) => d,
        Err(e) => {
            return problem_response(
                StatusCode::BAD_REQUEST,
                "https://ndaal.eu/csaf/errors/invalid-json",
                "Invalid JSON",
                &e,
            );
        },
    };

    // Ensure path ID matches document tracking ID.
    if doc.tracking_id() != tracking_id {
        return problem_response(
            StatusCode::BAD_REQUEST,
            "https://ndaal.eu/csaf/errors/id-mismatch",
            "ID Mismatch",
            "URL tracking ID does not match document tracking ID",
        );
    }

    // Validate the document.
    let validation_errors = csaf_core::validation::validate(&doc);
    let hard_errors: Vec<_> = validation_errors
        .iter()
        .filter(|e| e.severity == csaf_core::validation::Severity::Error)
        .collect();

    if !hard_errors.is_empty() {
        let details: Vec<serde_json::Value> = hard_errors
            .iter()
            .map(|e| json!({"path": e.path, "message": e.message}))
            .collect();
        let body = json!({
            "type": "https://ndaal.eu/csaf/errors/validation",
            "title": "Validation Failed",
            "status": 422,
            "detail": format!("{} validation error(s) found", details.len()),
            "errors": details,
        });
        return json_response(StatusCode::UNPROCESSABLE_ENTITY, &body);
    }

    // Check document exists.
    match state.csaf_storage().document_exists(&tracking_id) {
        Ok(false) => {
            return problem_response(
                StatusCode::NOT_FOUND,
                "https://ndaal.eu/csaf/errors/not-found",
                "Not Found",
                &format!("Document '{tracking_id}' not found"),
            );
        },
        Err(e) => {
            tracing::error!("Failed to check document existence: {e}");
            return problem_response(
                StatusCode::INTERNAL_SERVER_ERROR,
                "https://ndaal.eu/csaf/errors/storage",
                "Storage Error",
                "Failed to check document existence",
            );
        },
        Ok(true) => {},
    }

    // Update the document.
    if let Err(e) = state.csaf_storage().put_document(&doc) {
        tracing::error!("Failed to update document: {e}");
        return problem_response(
            StatusCode::INTERNAL_SERVER_ERROR,
            "https://ndaal.eu/csaf/errors/storage",
            "Storage Error",
            "Failed to update document",
        );
    }

    // Record audit log entry.
    let audit_result = state
        .db_pool()
        .with_conn(|conn| csaf_models::audit_log::record(conn, "update", &tracking_id, None, None));
    if let Err(e) = audit_result {
        tracing::warn!("Failed to record audit log: {e}");
    }

    let body = json!({
        "tracking_id": tracking_id,
        "message": "Document updated successfully",
        "_links": csaf_links(&tracking_id),
    });

    json_response(StatusCode::OK, &body)
}

/// `DELETE /api/v1/csaf/{id}` -- Delete a CSAF document.
pub async fn delete_csaf(
    state: AppState,
    _parts: http::request::Parts,
    params: Vec<(String, String)>,
) -> Response<Body> {
    let tracking_id = match path_param(&params, "id") {
        Some(id) => id,
        None => {
            return problem_response(
                StatusCode::BAD_REQUEST,
                "https://ndaal.eu/csaf/errors/bad-request",
                "Bad Request",
                "Missing document ID",
            );
        },
    };

    match state.csaf_storage().delete_document(&tracking_id) {
        Ok(true) => {
            // Record audit log entry.
            let audit_result = state.db_pool().with_conn(|conn| {
                csaf_models::audit_log::record(conn, "delete", &tracking_id, None, None)
            });
            if let Err(e) = audit_result {
                tracing::warn!("Failed to record audit log: {e}");
            }

            Response::builder()
                .status(StatusCode::NO_CONTENT)
                .body(Body::default())
                .unwrap_or_default()
        },
        Ok(false) => problem_response(
            StatusCode::NOT_FOUND,
            "https://ndaal.eu/csaf/errors/not-found",
            "Not Found",
            &format!("Document '{tracking_id}' not found"),
        ),
        Err(e) => {
            tracing::error!("Failed to delete document: {e}");
            problem_response(
                StatusCode::INTERNAL_SERVER_ERROR,
                "https://ndaal.eu/csaf/errors/storage",
                "Storage Error",
                "Failed to delete document",
            )
        },
    }
}

/// `GET /api/v1/csaf/{id}/validate` -- Validate a stored CSAF document.
pub async fn validate_csaf(
    state: AppState,
    _parts: http::request::Parts,
    params: Vec<(String, String)>,
) -> Response<Body> {
    let tracking_id = match path_param(&params, "id") {
        Some(id) => id,
        None => {
            return problem_response(
                StatusCode::BAD_REQUEST,
                "https://ndaal.eu/csaf/errors/bad-request",
                "Bad Request",
                "Missing document ID",
            );
        },
    };

    let doc = match state.csaf_storage().get_document(&tracking_id) {
        Ok(Some(d)) => d,
        Ok(None) => {
            return problem_response(
                StatusCode::NOT_FOUND,
                "https://ndaal.eu/csaf/errors/not-found",
                "Not Found",
                &format!("Document '{tracking_id}' not found"),
            );
        },
        Err(e) => {
            tracing::error!("Failed to get document for validation: {e}");
            return problem_response(
                StatusCode::INTERNAL_SERVER_ERROR,
                "https://ndaal.eu/csaf/errors/storage",
                "Storage Error",
                "Failed to retrieve document",
            );
        },
    };

    let errors = csaf_core::validation::validate(&doc);
    let is_valid = errors
        .iter()
        .all(|e| e.severity != csaf_core::validation::Severity::Error);

    let findings: Vec<serde_json::Value> = errors
        .iter()
        .map(|e| {
            json!({
                "path": e.path,
                "severity": match e.severity {
                    csaf_core::validation::Severity::Error => "error",
                    csaf_core::validation::Severity::Warning => "warning",
                },
                "message": e.message,
            })
        })
        .collect();

    let body = json!({
        "tracking_id": tracking_id,
        "valid": is_valid,
        "error_count": errors.iter().filter(|e| e.severity == csaf_core::validation::Severity::Error).count(),
        "warning_count": errors.iter().filter(|e| e.severity == csaf_core::validation::Severity::Warning).count(),
        "findings": findings,
        "_links": json!({
            "self": self_link(&format!("/api/v1/csaf/{tracking_id}/validate")),
            "document": self_link(&format!("/api/v1/csaf/{tracking_id}")),
        }),
    });

    json_response(StatusCode::OK, &body)
}
