use std::collections::BTreeMap;
use std::fs;
use std::path::{Path, PathBuf};

use serde::{Deserialize, Serialize};
use serde_json::Value;

use crate::query_db::QueryDbResult;

#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ArtifactFileSummary {
    pub path: String,
    pub size_bytes: Option<u64>,
    pub modified_unix_secs: Option<u64>,
}

#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct QueryAuditArtifactSummary {
    pub status: Option<String>,
    pub events_path: Option<String>,
    pub events_count: Option<u64>,
    pub events_sha256: Option<String>,
    pub writer_version: Option<u64>,
}

#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ArtifactManifestSummary {
    pub files: BTreeMap<String, ArtifactFileSummary>,
    pub query_audit: Option<QueryAuditArtifactSummary>,
    pub cir_counts_available: bool,
    pub validation_events_indexed: bool,
}

impl ArtifactManifestSummary {
    pub fn artifact_paths(&self) -> BTreeMap<String, String> {
        self.files
            .iter()
            .map(|(name, item)| (name.clone(), item.path.clone()))
            .collect()
    }
}

pub fn build_artifact_manifest(
    run_dir: &Path,
    query_audit: Option<&Value>,
) -> QueryDbResult<ArtifactManifestSummary> {
    let mut manifest = ArtifactManifestSummary {
        query_audit: query_audit.map(query_audit_summary),
        cir_counts_available: false,
        validation_events_indexed: false,
        ..ArtifactManifestSummary::default()
    };
    for name in [
        "report.json",
        "input.cir",
        "result.cir",
        "result.ccr",
        "stage-timing.json",
        "run-summary.json",
    ] {
        let path = run_dir.join(name);
        if path.exists() {
            manifest
                .files
                .insert(name.to_string(), file_summary(&path)?);
        }
    }
    if let Some(events_path) = manifest
        .query_audit
        .as_ref()
        .and_then(|query_audit| query_audit.events_path.as_ref())
    {
        let path = sidecar_path(run_dir, events_path);
        if path.exists() {
            manifest
                .files
                .insert("validationEvents".to_string(), file_summary(&path)?);
        }
    }
    Ok(manifest)
}

fn file_summary(path: &Path) -> QueryDbResult<ArtifactFileSummary> {
    let metadata = fs::metadata(path)?;
    let modified_unix_secs = metadata
        .modified()
        .ok()
        .and_then(|time| time.duration_since(std::time::UNIX_EPOCH).ok())
        .map(|duration| duration.as_secs());
    Ok(ArtifactFileSummary {
        path: path.display().to_string(),
        size_bytes: Some(metadata.len()),
        modified_unix_secs,
    })
}

fn query_audit_summary(value: &Value) -> QueryAuditArtifactSummary {
    QueryAuditArtifactSummary {
        status: json_str(value, &["status"]).map(str::to_string),
        events_path: json_str(value, &["eventsPath"])
            .or_else(|| json_str(value, &["events_path"]))
            .map(str::to_string),
        events_count: json_u64(value, &["eventsCount"])
            .or_else(|| json_u64(value, &["events_count"])),
        events_sha256: json_str(value, &["eventsSha256"])
            .or_else(|| json_str(value, &["events_sha256"]))
            .map(str::to_string),
        writer_version: json_u64(value, &["writerVersion"])
            .or_else(|| json_u64(value, &["writer_version"])),
    }
}

pub fn sidecar_path(run_dir: &Path, path: &str) -> PathBuf {
    let path = PathBuf::from(path);
    if path.is_absolute() {
        path
    } else {
        run_dir.join(path)
    }
}

fn json_str<'a>(value: &'a Value, path: &[&str]) -> Option<&'a str> {
    let mut current = value;
    for key in path {
        current = current.get(*key)?;
    }
    current.as_str()
}

fn json_u64(value: &Value, path: &[&str]) -> Option<u64> {
    let mut current = value;
    for key in path {
        current = current.get(*key)?;
    }
    current.as_u64()
}
