//! JSON layout models for `uv audit`.

use serde::Serialize;

#[derive(Debug, Serialize)]
pub(crate) struct Report {
    schema: Schema,
    summary: Summary,
    vulnerabilities: Vec<Vulnerability>,
    adverse_statuses: Vec<AdverseStatus>,
}

impl Report {
    pub(crate) fn from_findings(
        n_packages: usize,
        vulnerabilities: &[&uv_audit::Vulnerability],
        statuses: &[&uv_audit::ProjectStatus],
    ) -> Self {
        let mut vulnerabilities = vulnerabilities
            .iter()
            .copied()
            .map(Vulnerability::from)
            .collect::<Vec<_>>();
        vulnerabilities.sort_by(|first, second| {
            first
                .dependency
                .name
                .cmp(&second.dependency.name)
                .then_with(|| first.dependency.version.cmp(&second.dependency.version))
                .then_with(|| first.display_id.cmp(&second.display_id))
        });

        let mut adverse_statuses = statuses
            .iter()
            .copied()
            .map(AdverseStatus::from)
            .collect::<Vec<_>>();
        adverse_statuses.sort_by(|first, second| {
            first
                .name
                .cmp(&second.name)
                .then_with(|| first.status.cmp(&second.status))
        });

        Self {
            schema: Schema::default(),
            summary: Summary {
                audited_packages: n_packages,
                vulnerabilities: vulnerabilities.len(),
                adverse_statuses: adverse_statuses.len(),
            },
            vulnerabilities,
            adverse_statuses,
        }
    }
}

#[derive(Debug, Serialize, Default)]
struct Schema {
    version: SchemaVersion,
}

#[derive(Debug, Serialize, Default)]
#[serde(rename_all = "snake_case")]
enum SchemaVersion {
    #[default]
    Preview,
}

#[derive(Debug, Serialize)]
struct Summary {
    audited_packages: usize,
    vulnerabilities: usize,
    adverse_statuses: usize,
}

#[derive(Debug, Serialize)]
struct Dependency {
    name: String,
    version: String,
}

impl From<&uv_audit::Dependency> for Dependency {
    fn from(dependency: &uv_audit::Dependency) -> Self {
        Self {
            name: dependency.name().to_string(),
            version: dependency.version().to_string(),
        }
    }
}

#[derive(Debug, Serialize)]
struct Vulnerability {
    dependency: Dependency,
    id: String,
    display_id: String,
    aliases: Vec<String>,
    summary: Option<String>,
    description: Option<String>,
    link: Option<String>,
    fix_versions: Vec<String>,
    published: Option<String>,
    modified: Option<String>,
}

impl From<&uv_audit::Vulnerability> for Vulnerability {
    fn from(vulnerability: &uv_audit::Vulnerability) -> Self {
        Self {
            dependency: Dependency::from(&vulnerability.dependency),
            id: vulnerability.id.as_str().to_string(),
            display_id: vulnerability.best_id().as_str().to_string(),
            aliases: vulnerability
                .aliases
                .iter()
                .map(|id| id.as_str().to_string())
                .collect(),
            summary: vulnerability.summary.clone(),
            description: vulnerability.description.clone(),
            link: vulnerability
                .link
                .as_ref()
                .map(|link| link.as_str().to_string()),
            fix_versions: vulnerability
                .fix_versions
                .iter()
                .map(std::string::ToString::to_string)
                .collect(),
            published: vulnerability
                .published
                .as_ref()
                .map(std::string::ToString::to_string),
            modified: vulnerability
                .modified
                .as_ref()
                .map(std::string::ToString::to_string),
        }
    }
}

#[derive(Debug, Serialize)]
struct AdverseStatus {
    name: String,
    status: String,
    reason: Option<String>,
}

impl From<&uv_audit::ProjectStatus> for AdverseStatus {
    fn from(status: &uv_audit::ProjectStatus) -> Self {
        Self {
            name: status.name.to_string(),
            status: status.status.to_string(),
            reason: status.reason.clone(),
        }
    }
}
