use anyhow::{Context, Result};
use std::collections::HashMap;
use std::fs;
use std::path::Path;
use toml::Value as TomlValue;

/// Cached snippet, content vars, and list data
#[derive(Default)]
pub struct Caches {
    pub snippets: HashMap<String, Snippet>,                    // key: rel "path/to/file.htmls"
    pub vars:     HashMap<String, HashMap<String, String>>,    // key: rel "path/to/file.toml"
    pub toml_raw: HashMap<String, TomlValue>,                  // key: rel "path/to/file.toml"
    pub lists:    HashMap<String, ListData>,                   // key: rel "path/to/file.toml" (items=[])
    pub list_ptr: HashMap<String, ListData>,                   // key: "<abs_toml_path>::path=<dot.path>"
}

impl Caches {
    pub fn new() -> Self { Self::default() }

    pub fn invalidate_snippet(&mut self, rel_key_htmls: &str) {
        self.snippets.remove(rel_key_htmls);
    }
    pub fn invalidate_content(&mut self, rel_key_toml: &str) {
        self.vars.remove(rel_key_toml);
        self.toml_raw.remove(rel_key_toml);
        self.lists.remove(rel_key_toml);
        // note: list_ptr keys are by abs path + pointer; we intentionally keep them
        // and let next load refresh them as needed.
    }

    pub fn peek_toml(&self, rel_key_toml: &str) -> Option<TomlValue> {
        self.toml_raw.get(rel_key_toml).cloned()
    }
}

/// Simple snippet (HTML only)
#[derive(Clone)]
pub struct Snippet {
    pub html: String,
}

/// A list of map items used for `is="list"`
#[derive(Clone, Default)]
pub struct ListData {
    pub items: Vec<HashMap<String, String>>,
}

pub fn load_snippet(c: &mut Caches, key_htmls_rel: &str, file: &Path) -> Result<Snippet> {
    if let Some(s) = c.snippets.get(key_htmls_rel) {
        return Ok(s.clone());
    }
    let html = fs::read_to_string(file)
        .with_context(|| format!("read snippet {}", file.display()))?;
    let snip = Snippet { html };
    c.snippets.insert(key_htmls_rel.to_string(), snip.clone());
    Ok(snip)
}

pub fn load_vars(c: &mut Caches, key_toml_rel: &str, file: &Path) -> Result<HashMap<String, String>> {
    if let Some(m) = c.vars.get(key_toml_rel) {
        return Ok(m.clone());
    }
    let raw = fs::read_to_string(file)
        .with_context(|| format!("read content {}", file.display()))?;
    let toml: TomlValue = toml::from_str(&raw)
        .with_context(|| format!("parse content {}", file.display()))?;
    let mut flat = HashMap::new();
    flatten_toml("", &toml, &mut flat);
    c.vars.insert(key_toml_rel.to_string(), flat.clone());
    c.toml_raw.insert(key_toml_rel.to_string(), toml);
    Ok(flat)
}

pub fn load_list(c: &mut Caches, key_toml_rel: &str, file: &Path) -> Result<ListData> {
    if let Some(l) = c.lists.get(key_toml_rel) {
        return Ok(l.clone());
    }
    let raw = fs::read_to_string(file)
        .with_context(|| format!("read content {}", file.display()))?;
    let toml: TomlValue = toml::from_str(&raw)
        .with_context(|| format!("parse content {}", file.display()))?;
    let items = extract_items(&toml)
        .with_context(|| format!("expected `items = [ ... ]` in {}", file.display()))?;
    let data = ListData { items };
    c.lists.insert(key_toml_rel.to_string(), data.clone());
    c.toml_raw.insert(key_toml_rel.to_string(), toml);
    Ok(data)
}

pub fn load_list_from_pointer(c: &mut Caches, file: &Path, ptr: Option<&str>) -> Result<ListData> {
    let abs = canonical_string(file);
    let cache_key = match ptr {
        Some(p) if !p.is_empty() => format!("{}::path={}", abs, p),
        _ => format!("{}::path=", abs),
    };

    if let Some(l) = c.list_ptr.get(&cache_key) {
        return Ok(l.clone());
    }

    let raw = fs::read_to_string(file)
        .with_context(|| format!("read content {}", file.display()))?;
    let toml: TomlValue = toml::from_str(&raw)
        .with_context(|| format!("parse content {}", file.display()))?;

    let target = match ptr {
        Some(p) if !p.is_empty() => get_subtree(&toml, p)
            .with_context(|| format!("subtree `{}` not found in {}", p, file.display()))?,
        _ => toml,
    };

    let items = match target {
        TomlValue::Array(arr) => array_to_items(&arr),
        TomlValue::Table(_) => {
            if let Some(items_val) = target.get("items") {
                ensure_array_items(items_val, file)?
            } else {
                table_values_to_items(&target)?
            }
        }
        _ => {
            if let Some(items_val) = target.get("items") {
                ensure_array_items(items_val, file)?
            } else {
                anyhow::bail!("pointer does not resolve to an array or table with items");
            }
        }
    };

    let data = ListData { items };
    c.list_ptr.insert(cache_key, data.clone());
    Ok(data)
}

/* ----------------------- helpers ----------------------- */

fn canonical_string(p: &Path) -> String {
    std::fs::canonicalize(p).unwrap_or_else(|_| p.to_path_buf()).to_string_lossy().to_string()
}

fn flatten_toml(prefix: &str, v: &TomlValue, out: &mut HashMap<String, String>) {
    match v {
        TomlValue::String(s) => {
            let key = prefix.trim_matches('.');
            if !key.is_empty() {
                out.insert(key.to_string(), s.clone());
            }
        }
        TomlValue::Integer(i) => {
            let key = prefix.trim_matches('.');
            if !key.is_empty() {
                out.insert(key.to_string(), i.to_string());
            }
        }
        TomlValue::Float(f) => {
            let key = prefix.trim_matches('.');
            if !key.is_empty() {
                out.insert(key.to_string(), f.to_string());
            }
        }
        TomlValue::Boolean(b) => {
            let key = prefix.trim_matches('.');
            if !key.is_empty() {
                out.insert(key.to_string(), b.to_string());
            }
        }
        TomlValue::Datetime(dt) => {
            let key = prefix.trim_matches('.');
            if !key.is_empty() {
                out.insert(key.to_string(), dt.to_string());
            }
        }
        TomlValue::Array(arr) => {
            for (idx, elem) in arr.iter().enumerate() {
                let pre = if prefix.is_empty() { idx.to_string() } else { format!("{}.{}", prefix, idx) };
                flatten_toml(&pre, elem, out);
            }
        }
        TomlValue::Table(tbl) => {
            for (k, val) in tbl {
                let pre = if prefix.is_empty() { k.clone() } else { format!("{}.{}", prefix, k) };
                flatten_toml(&pre, val, out);
            }
        }
    }
}

pub fn get_subtree(root: &TomlValue, path: &str) -> Result<TomlValue> {
    let mut current = root;
    for seg in path.split('.') {
        match current {
            TomlValue::Table(t) => {
                current = t.get(seg).ok_or_else(|| anyhow::anyhow!("segment `{}` missing", seg))?;
            }
            _ => return Err(anyhow::anyhow!("segment `{}` not under a table", seg)),
        }
    }
    Ok(current.clone())
}

fn extract_items(v: &TomlValue) -> Result<Vec<HashMap<String, String>>> {
    let items_val = v.get("items").ok_or_else(|| anyhow::anyhow!("missing `items`"))?;
    ensure_array_items(items_val, Path::new("<inline>"))
}

fn ensure_array_items(items_val: &TomlValue, _file: &Path) -> Result<Vec<HashMap<String, String>>> {
    match items_val {
        TomlValue::Array(arr) => Ok(array_to_items(arr)),
        _ => Err(anyhow::anyhow!("`items` must be an array of tables")),
    }
}

fn array_to_items(arr: &[TomlValue]) -> Vec<HashMap<String, String>> {
    let mut out = Vec::with_capacity(arr.len());
    for el in arr {
        out.push(value_to_flat_map(el));
    }
    out
}

fn table_values_to_items(tbl: &TomlValue) -> Result<Vec<HashMap<String, String>>> {
    if let TomlValue::Table(map) = tbl {
        let mut out = Vec::new();
        for (_k, v) in map {
            out.push(value_to_flat_map(v));
        }
        Ok(out)
    } else {
        Err(anyhow::anyhow!("expected table for table_values_to_items"))
    }
}

fn value_to_flat_map(v: &TomlValue) -> HashMap<String, String> {
    let mut out = HashMap::new();
    match v {
        TomlValue::Table(_) => flatten_toml("", v, &mut out),
        TomlValue::Array(_) => flatten_toml("", v, &mut out),
        TomlValue::String(s) => { out.insert("value".into(), s.clone()); }
        TomlValue::Integer(i) => { out.insert("value".into(), i.to_string()); }
        TomlValue::Float(f) => { out.insert("value".into(), f.to_string()); }
        TomlValue::Boolean(b) => { out.insert("value".into(), b.to_string()); }
        TomlValue::Datetime(dt) => { out.insert("value".into(), dt.to_string()); }
    }
    out
}