use anyhow::{Context, Result};
use std::collections::HashMap;
use std::fs;
use std::io::Read;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
use toml::Value;

fn modified_or_epoch(meta: &fs::Metadata) -> SystemTime {
    meta.modified().unwrap_or(UNIX_EPOCH)
}

pub fn build_safe_path(root: &Path, name: &str, ext: &str) -> anyhow::Result<PathBuf> {
    // normalize separators
    let name = name.replace('\\', "/");

    // disallow absolute and traversal
    if name.starts_with('/') || name.split('/').any(|p| p == ".." || p.is_empty()) {
        anyhow::bail!("Invalid path component(s): {name}");
    }

    // canonicalize root only (root must exist)
    let root_real = root.canonicalize()
        .map_err(|_| anyhow::anyhow!("Root does not exist: {}", root.display()))?;

    // join parts under root
    let mut path = root_real.clone();
    for part in name.split('/') {
        if part != "." {
            path.push(part);
        }
    }
    path.set_extension(ext);

    // verify still under root
    if !path.starts_with(&root_real) {
        anyhow::bail!("Path escapes root: {name}");
    }

    Ok(path)
}

/// Write dest only if content differs (size+bytes compare).
pub fn write_if_changed(dest: &Path, bytes: &[u8]) -> Result<()> {
    if let Some(parent) = dest.parent() { fs::create_dir_all(parent)?; }
    if file_has_content(dest, bytes)? { return Ok(()); }
    fs::write(dest, bytes).with_context(|| format!("writing {:?}", dest))
}

fn file_has_content(path: &Path, new_bytes: &[u8]) -> Result<bool> {
    if !path.exists() { return Ok(false); }
    let meta = fs::metadata(path)?;
    if meta.len() != new_bytes.len() as u64 { return Ok(false); }
    let mut f = fs::File::open(path)?;
    let mut buf = Vec::with_capacity(new_bytes.len());
    f.read_to_end(&mut buf)?;
    Ok(buf == new_bytes)
}

/// Copy src→dest if missing or stale (size/mtime).
pub fn copy_if_stale(src: &Path, dest: &Path) -> Result<()> {
    let src_meta = fs::metadata(src)?;
    let (src_len, src_mtime) = (src_meta.len(), modified_or_epoch(&src_meta));
    let should = if dest.exists() {
        let dst_meta = fs::metadata(dest)?;
        dst_meta.len() != src_len || modified_or_epoch(&dst_meta) < src_mtime
    } else { true };

    if should {
        if let Some(parent) = dest.parent() { fs::create_dir_all(parent)?; }
        fs::copy(src, dest).with_context(|| format!("copying {:?} → {:?}", src, dest))?;
        println!("Copied {}", dest.display());
    }
    Ok(())
}

/// Template cache (path → (mtime, contents))
pub struct TemplateCache {
    map: HashMap<PathBuf, (SystemTime, String)>,
}
impl TemplateCache {
    pub fn new() -> Self { Self { map: HashMap::new() } }

    pub fn load(&mut self, path: &Path) -> Result<Option<&str>> {
        if !path.exists() { return Ok(None); }
        let meta = fs::metadata(path)?;
        let mtime = modified_or_epoch(&meta);
        let entry = self.map.get(path).cloned();

        if let Some((cached_mtime, _)) = entry {
            if cached_mtime >= mtime {
                return Ok(self.map.get(path).map(|(_, s)| s.as_str()));
            }
        }
        let txt = fs::read_to_string(path).with_context(|| format!("reading {:?}", path))?;
        self.map.insert(path.to_path_buf(), (mtime, txt));
        Ok(self.map.get(path).map(|(_, s)| s.as_str()))
    }
}

/// TOML cache (path → (mtime, Value))
pub struct TomlCache {
    map: HashMap<PathBuf, (SystemTime, Value)>,
}
impl TomlCache {
    pub fn new() -> Self { Self { map: HashMap::new() } }

    pub fn load(&mut self, path: &Path) -> Result<Option<&Value>> {
        if !path.exists() { return Ok(None); }
        let meta = fs::metadata(path)?;
        let mtime = modified_or_epoch(&meta);
        let entry = self.map.get(path).cloned();

        if let Some((cached_mtime, _)) = entry {
            if cached_mtime >= mtime {
                return Ok(self.map.get(path).map(|(_, v)| v));
            }
        }
        let s = fs::read_to_string(path).with_context(|| format!("reading {:?}", path))?;
        let v: Value = toml::from_str(&s).with_context(|| format!("parsing TOML {:?}", path))?;
        self.map.insert(path.to_path_buf(), (mtime, v));
        Ok(self.map.get(path).map(|(_, v)| v))
    }
}

