//! Output formatting: JSON (crypto-spec v3.0 compatible), CSV, and summary.

use std::collections::HashMap;
use std::io::Write;
use std::path::Path;

use serde::Serialize;

use crate::detect::{verification_code, ScanResult};

/// Crypto-spec v3.0 compatible output.
#[derive(Debug, Serialize)]
pub struct CryptoOutput {
    pub crypto_spec_version: f64,
    pub file_collection_verification_code: String,
    pub package_name: String,
    pub files_scanned: usize,
    pub total_matches: usize,
    pub crypto_evidence: HashMap<String, FileEvidence>,
}

#[derive(Debug, Serialize)]
pub struct FileEvidence {
    pub file_paths: Vec<String>,
    pub is_source_code: bool,
    pub hits: Vec<HitOutput>,
}

#[derive(Debug, Serialize)]
pub struct HitOutput {
    pub matched_text: String,
    pub evidence_type: String,
    pub detection_method: String,
    pub line_number: usize,
    pub file_index_begin: usize,
    pub file_index_end: usize,
    pub line_index_begin: usize,
    pub line_index_end: usize,
    pub line_text: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub encryption_library: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub encryption_api_usage: Option<String>,
    pub line_text_before_1: String,
    pub line_text_before_2: String,
    pub line_text_before_3: String,
    pub line_text_after_1: String,
    pub line_text_after_2: String,
    pub line_text_after_3: String,
}

impl CryptoOutput {
    /// Convert a ScanResult into crypto-spec v3.0 JSON output.
    pub fn from_scan(result: &ScanResult) -> Self {
        let mut evidence: HashMap<String, FileEvidence> = HashMap::new();

        for file in &result.file_results {
            let hits: Vec<HitOutput> = file
                .matches
                .iter()
                .map(|m| HitOutput {
                    matched_text: m.inner.matched_text.clone(),
                    evidence_type: m.inner.evidence_type.clone(),
                    detection_method: m.detection_method.clone(),
                    line_number: m.inner.line_number,
                    file_index_begin: m.inner.file_index_begin,
                    file_index_end: m.inner.file_index_end,
                    line_index_begin: m.inner.line_index_begin,
                    line_index_end: m.inner.line_index_end,
                    line_text: m.inner.line_text.clone(),
                    encryption_library: m.inner.encryption_library.clone(),
                    encryption_api_usage: m.inner.encryption_api_usage.clone(),
                    line_text_before_1: m.inner.context_before[2].clone(),
                    line_text_before_2: m.inner.context_before[1].clone(),
                    line_text_before_3: m.inner.context_before[0].clone(),
                    line_text_after_1: m.inner.context_after[0].clone(),
                    line_text_after_2: m.inner.context_after[1].clone(),
                    line_text_after_3: m.inner.context_after[2].clone(),
                })
                .collect();

            let entry = evidence.entry(file.sha1.clone()).or_insert_with(|| {
                FileEvidence {
                    file_paths: Vec::new(),
                    is_source_code: file.is_source_code,
                    hits: Vec::new(),
                }
            });
            entry.file_paths.push(file.path.clone());
            entry.hits.extend(hits);
        }

        let verif = verification_code(&result.file_results);

        Self {
            crypto_spec_version: 3.0,
            file_collection_verification_code: verif,
            package_name: result.package_name.clone(),
            files_scanned: result.files_scanned,
            total_matches: result.total_matches,
            crypto_evidence: evidence,
        }
    }

    /// Write as JSON to a file.
    pub fn write_json(&self, path: &Path, pretty: bool) -> std::io::Result<()> {
        let json = if pretty {
            serde_json::to_string_pretty(self)?
        } else {
            serde_json::to_string(self)?
        };
        std::fs::write(path, json)
    }

    /// Write as CSV to a file.
    pub fn write_csv(&self, path: &Path) -> std::io::Result<()> {
        let mut f = std::fs::File::create(path)?;
        writeln!(
            f,
            "File Paths,Evidence Type,Detection Method,Match,Line Number,Source Matched,Index Begin,Index End"
        )?;

        for (_, evidence) in &self.crypto_evidence {
            let paths = evidence.file_paths.join("; ");
            for hit in &evidence.hits {
                writeln!(
                    f,
                    "\"{}\",\"{}\",\"{}\",\"{}\",{},\"{}\",{},{}",
                    paths,
                    hit.evidence_type,
                    hit.detection_method,
                    hit.matched_text.replace('"', "\"\""),
                    hit.line_number,
                    hit.line_text.replace('"', "\"\""),
                    hit.file_index_begin,
                    hit.file_index_end,
                )?;
            }
        }

        Ok(())
    }
}
