use std::fs;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};

use anyhow::{Context, Result};

use super::{PackDigest, PackEntry, PackRef, PackVersion};
pub struct PackCache {
    root: PathBuf,
}

impl PackCache {
    pub fn new(root: PathBuf) -> Self {
        let root = root.canonicalize().unwrap_or(root);
        Self { root }
    }

    pub fn root(&self) -> &Path {
        &self.root
    }

    pub fn store(&self, entry: &PackEntry, source: &Path, digest: &PackDigest) -> Result<PathBuf> {
        let dest_dir = self.dir_for(&entry.reference);
        fs::create_dir_all(&dest_dir)
            .with_context(|| format!("failed to create cache dir {}", dest_dir.display()))?;
        let dest_path = dest_dir.join("pack.gtpack");
        if dest_path.exists() {
            if digest.matches_file(&dest_path)? {
                return Ok(dest_path);
            }
            fs::remove_file(&dest_path)
                .with_context(|| format!("failed to remove stale cache {}", dest_path.display()))?;
        }

        if same_path(source, &dest_path) {
            return Ok(dest_path);
        }

        copy_atomic(source, &dest_path)?;
        Ok(dest_path)
    }

    fn dir_for(&self, reference: &PackRef) -> PathBuf {
        match &reference.version {
            PackVersion::Semver(version) => self
                .root
                .join(super::sanitize_segment(&reference.name))
                .join(version.to_string()),
            PackVersion::Digest(digest) => self
                .root
                .join(super::sanitize_segment(&reference.name))
                .join(digest.cache_label()),
        }
    }
}

fn copy_atomic(source: &Path, dest: &Path) -> Result<()> {
    let tmp = dest.with_extension(format!(
        "tmp-{}",
        SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .map(|dur| dur.as_nanos())
            .unwrap_or(0)
    ));
    fs::copy(source, &tmp)
        .with_context(|| format!("failed to copy {} -> {}", source.display(), dest.display()))?;
    fs::rename(&tmp, dest)
        .with_context(|| format!("failed to place cached pack at {}", dest.display()))?;
    Ok(())
}

fn same_path(a: &Path, b: &Path) -> bool {
    if let (Ok(a), Ok(b)) = (a.canonicalize(), b.canonicalize()) {
        a == b
    } else {
        a == b
    }
}
