use axum::extract::{Extension, Query};
use axum::http::{header, StatusCode};
use axum::response::IntoResponse;
use axum::Json;
use mongodb::bson::doc;

use super::dto::*;
use compliance_core::models::SbomEntry;
use compliance_core::tenant_ctx::TenantCtx;

const COPYLEFT_LICENSES: &[&str] = &[
    "GPL-2.0",
    "GPL-2.0-only",
    "GPL-2.0-or-later",
    "GPL-3.0",
    "GPL-3.0-only",
    "GPL-3.0-or-later",
    "AGPL-3.0",
    "AGPL-3.0-only",
    "AGPL-3.0-or-later",
    "LGPL-2.1",
    "LGPL-2.1-only",
    "LGPL-2.1-or-later",
    "LGPL-3.0",
    "LGPL-3.0-only",
    "LGPL-3.0-or-later",
    "MPL-2.0",
];

#[tracing::instrument(skip_all)]
pub async fn sbom_filters(
    Extension(agent): AgentExt,
    tenant: TenantCtx,
) -> Result<Json<serde_json::Value>, StatusCode> {
    let db = tenant_db(&agent, &tenant).await?;
    let db = &db;

    let managers: Vec<String> = db
        .sbom_entries()
        .distinct("package_manager", doc! {})
        .await
        .unwrap_or_default()
        .into_iter()
        .filter_map(|v| v.as_str().map(|s| s.to_string()))
        .filter(|s| !s.is_empty() && s != "unknown" && s != "file")
        .collect();

    let licenses: Vec<String> = db
        .sbom_entries()
        .distinct("license", doc! {})
        .await
        .unwrap_or_default()
        .into_iter()
        .filter_map(|v| v.as_str().map(|s| s.to_string()))
        .filter(|s| !s.is_empty())
        .collect();

    Ok(Json(serde_json::json!({
        "package_managers": managers,
        "licenses": licenses,
    })))
}

#[tracing::instrument(skip_all, fields(repo_id = ?filter.repo_id, package_manager = ?filter.package_manager))]
pub async fn list_sbom(
    Extension(agent): AgentExt,
    tenant: TenantCtx,
    Query(filter): Query<SbomFilter>,
) -> ApiResult<Vec<SbomEntry>> {
    let db = tenant_db(&agent, &tenant).await?;
    let db = &db;
    let mut query = doc! {};

    if let Some(repo_id) = &filter.repo_id {
        query.insert("repo_id", repo_id);
    }
    if let Some(pm) = &filter.package_manager {
        query.insert("package_manager", pm);
    }
    if let Some(q) = &filter.q {
        if !q.is_empty() {
            query.insert("name", doc! { "$regex": q, "$options": "i" });
        }
    }
    if let Some(has_vulns) = filter.has_vulns {
        if has_vulns {
            query.insert("known_vulnerabilities", doc! { "$exists": true, "$ne": [] });
        } else {
            query.insert("known_vulnerabilities", doc! { "$size": 0 });
        }
    }
    if let Some(license) = &filter.license {
        query.insert("license", license);
    }

    let skip = (filter.page.saturating_sub(1)) * filter.limit as u64;
    let total = db
        .sbom_entries()
        .count_documents(query.clone())
        .await
        .unwrap_or(0);

    let entries = match db
        .sbom_entries()
        .find(query)
        .sort(doc! { "name": 1 })
        .skip(skip)
        .limit(filter.limit)
        .await
    {
        Ok(cursor) => collect_cursor_async(cursor).await,
        Err(e) => {
            tracing::warn!("Failed to fetch SBOM entries: {e}");
            Vec::new()
        }
    };

    Ok(Json(ApiResponse {
        data: entries,
        total: Some(total),
        page: Some(filter.page),
    }))
}

#[tracing::instrument(skip_all)]
pub async fn export_sbom(
    Extension(agent): AgentExt,
    tenant: TenantCtx,
    Query(params): Query<SbomExportParams>,
) -> Result<impl IntoResponse, StatusCode> {
    let db = tenant_db(&agent, &tenant).await?;
    let db = &db;
    let entries: Vec<SbomEntry> = match db
        .sbom_entries()
        .find(doc! { "repo_id": &params.repo_id })
        .await
    {
        Ok(cursor) => collect_cursor_async(cursor).await,
        Err(e) => {
            tracing::warn!("Failed to fetch SBOM entries for export: {e}");
            Vec::new()
        }
    };

    let body = if params.format == "spdx" {
        // SPDX 2.3 format
        let packages: Vec<serde_json::Value> = entries
            .iter()
            .enumerate()
            .map(|(i, e)| {
                serde_json::json!({
                    "SPDXID": format!("SPDXRef-Package-{i}"),
                    "name": e.name,
                    "versionInfo": e.version,
                    "downloadLocation": "NOASSERTION",
                    "licenseConcluded": e.license.as_deref().unwrap_or("NOASSERTION"),
                    "externalRefs": e.purl.as_ref().map(|p| vec![serde_json::json!({
                        "referenceCategory": "PACKAGE-MANAGER",
                        "referenceType": "purl",
                        "referenceLocator": p,
                    })]).unwrap_or_default(),
                })
            })
            .collect();

        serde_json::json!({
            "spdxVersion": "SPDX-2.3",
            "dataLicense": "CC0-1.0",
            "SPDXID": "SPDXRef-DOCUMENT",
            "name": format!("sbom-{}", params.repo_id),
            "documentNamespace": format!("https://compliance-scanner/sbom/{}", params.repo_id),
            "packages": packages,
        })
    } else {
        // CycloneDX 1.5 format
        let components: Vec<serde_json::Value> = entries
            .iter()
            .map(|e| {
                let mut comp = serde_json::json!({
                    "type": "library",
                    "name": e.name,
                    "version": e.version,
                    "group": e.package_manager,
                });
                if let Some(purl) = &e.purl {
                    comp["purl"] = serde_json::Value::String(purl.clone());
                }
                if let Some(license) = &e.license {
                    comp["licenses"] = serde_json::json!([{ "license": { "id": license } }]);
                }
                if !e.known_vulnerabilities.is_empty() {
                    comp["vulnerabilities"] = serde_json::json!(
                        e.known_vulnerabilities.iter().map(|v| serde_json::json!({
                            "id": v.id,
                            "source": { "name": v.source },
                            "ratings": v.severity.as_ref().map(|s| vec![serde_json::json!({"severity": s})]).unwrap_or_default(),
                        })).collect::<Vec<_>>()
                    );
                }
                comp
            })
            .collect();

        serde_json::json!({
            "bomFormat": "CycloneDX",
            "specVersion": "1.5",
            "version": 1,
            "metadata": {
                "component": {
                    "type": "application",
                    "name": format!("repo-{}", params.repo_id),
                }
            },
            "components": components,
        })
    };

    let json_str =
        serde_json::to_string_pretty(&body).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
    let filename = if params.format == "spdx" {
        format!("sbom-{}-spdx.json", params.repo_id)
    } else {
        format!("sbom-{}-cyclonedx.json", params.repo_id)
    };

    let disposition = format!("attachment; filename=\"{filename}\"");
    Ok((
        [
            (
                header::CONTENT_TYPE,
                header::HeaderValue::from_static("application/json"),
            ),
            (
                header::CONTENT_DISPOSITION,
                header::HeaderValue::from_str(&disposition)
                    .unwrap_or_else(|_| header::HeaderValue::from_static("attachment")),
            ),
        ],
        json_str,
    ))
}

#[tracing::instrument(skip_all)]
pub async fn license_summary(
    Extension(agent): AgentExt,
    tenant: TenantCtx,
    Query(params): Query<SbomFilter>,
) -> ApiResult<Vec<LicenseSummary>> {
    let db = tenant_db(&agent, &tenant).await?;
    let db = &db;
    let mut query = doc! {};
    if let Some(repo_id) = &params.repo_id {
        query.insert("repo_id", repo_id);
    }

    let entries: Vec<SbomEntry> = match db.sbom_entries().find(query).await {
        Ok(cursor) => collect_cursor_async(cursor).await,
        Err(e) => {
            tracing::warn!("Failed to fetch SBOM entries for license summary: {e}");
            Vec::new()
        }
    };

    let mut license_map: std::collections::HashMap<String, Vec<String>> =
        std::collections::HashMap::new();
    for entry in &entries {
        let lic = entry.license.as_deref().unwrap_or("Unknown").to_string();
        license_map.entry(lic).or_default().push(entry.name.clone());
    }

    let mut summaries: Vec<LicenseSummary> = license_map
        .into_iter()
        .map(|(license, packages)| {
            let is_copyleft = COPYLEFT_LICENSES
                .iter()
                .any(|c| license.to_uppercase().contains(&c.to_uppercase()));
            LicenseSummary {
                license,
                count: packages.len() as u64,
                is_copyleft,
                packages,
            }
        })
        .collect();
    summaries.sort_by_key(|s| std::cmp::Reverse(s.count));

    Ok(Json(ApiResponse {
        data: summaries,
        total: None,
        page: None,
    }))
}

#[tracing::instrument(skip_all)]
pub async fn sbom_diff(
    Extension(agent): AgentExt,
    tenant: TenantCtx,
    Query(params): Query<SbomDiffParams>,
) -> ApiResult<SbomDiffResult> {
    let db = tenant_db(&agent, &tenant).await?;
    let db = &db;

    let entries_a: Vec<SbomEntry> = match db
        .sbom_entries()
        .find(doc! { "repo_id": &params.repo_a })
        .await
    {
        Ok(cursor) => collect_cursor_async(cursor).await,
        Err(e) => {
            tracing::warn!("Failed to fetch SBOM entries for repo_a: {e}");
            Vec::new()
        }
    };

    let entries_b: Vec<SbomEntry> = match db
        .sbom_entries()
        .find(doc! { "repo_id": &params.repo_b })
        .await
    {
        Ok(cursor) => collect_cursor_async(cursor).await,
        Err(e) => {
            tracing::warn!("Failed to fetch SBOM entries for repo_b: {e}");
            Vec::new()
        }
    };

    // Build maps by (name, package_manager) -> version
    let map_a: std::collections::HashMap<(String, String), String> = entries_a
        .iter()
        .map(|e| {
            (
                (e.name.clone(), e.package_manager.clone()),
                e.version.clone(),
            )
        })
        .collect();
    let map_b: std::collections::HashMap<(String, String), String> = entries_b
        .iter()
        .map(|e| {
            (
                (e.name.clone(), e.package_manager.clone()),
                e.version.clone(),
            )
        })
        .collect();

    let mut only_in_a = Vec::new();
    let mut version_changed = Vec::new();
    let mut common_count: u64 = 0;

    for (key, ver_a) in &map_a {
        match map_b.get(key) {
            None => only_in_a.push(SbomDiffEntry {
                name: key.0.clone(),
                version: ver_a.clone(),
                package_manager: key.1.clone(),
            }),
            Some(ver_b) if ver_a != ver_b => {
                version_changed.push(SbomVersionDiff {
                    name: key.0.clone(),
                    package_manager: key.1.clone(),
                    version_a: ver_a.clone(),
                    version_b: ver_b.clone(),
                });
            }
            Some(_) => common_count += 1,
        }
    }

    let only_in_b: Vec<SbomDiffEntry> = map_b
        .iter()
        .filter(|(key, _)| !map_a.contains_key(key))
        .map(|(key, ver)| SbomDiffEntry {
            name: key.0.clone(),
            version: ver.clone(),
            package_manager: key.1.clone(),
        })
        .collect();

    Ok(Json(ApiResponse {
        data: SbomDiffResult {
            only_in_a,
            only_in_b,
            version_changed,
            common_count,
        },
        total: None,
        page: None,
    }))
}
