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

use anyhow::{Context, Result, bail};
use indexmap::IndexMap;
use serde::Deserialize;

use crate::registry::{Dep, Registry};

pub const OVERLAY_FILENAME: &str = "rustelo-deps-overlay.toml";

#[derive(Deserialize, Debug, Default)]
pub struct Overlay {
    /// Parent layer this overlay extends, as a path to the directory containing
    /// the parent `rustelo-deps-overlay.toml`, relative to THIS overlay's own
    /// directory. Resolution walks the chain parent→child; feature additions and
    /// extras accumulate down it under the same additive-only rule a single
    /// overlay obeys over the registry. Absent = a one-link chain (the historical
    /// single-overlay behavior, byte-for-byte).
    #[serde(default)]
    pub extends: Option<String>,
    /// Feature additions to registry deps (or to extras introduced upstream in
    /// the chain). Cannot override version or path.
    #[serde(default)]
    pub overlay: IndexMap<String, FeatureOverlay>,
    /// Deps the layer needs but that are not in the registry (impl-specific
    /// feature crates, sibling-project paths, the layer's own crates). Paths are
    /// rustelo-root-relative, like registry path-deps, and rebased per target.
    #[serde(default)]
    pub extra: IndexMap<String, Dep>,
}

#[derive(Deserialize, Debug, Default)]
pub struct FeatureOverlay {
    #[serde(default)]
    pub features: Vec<String>,
}

/// One resolved overlay in a chain, paired with the canonical directory its file
/// lives in. The directory is needed only to resolve the next `extends` hop;
/// rendering rebases extra paths via the rustelo root, not this directory.
#[derive(Debug)]
pub struct Layer {
    pub overlay: Overlay,
    pub dir: PathBuf,
}

/// Loads `{target_dir}/rustelo-deps-overlay.toml` if it exists. Absent file is
/// not an error — most layers do not need an overlay.
pub fn load(target_dir: &Path) -> Result<Option<Overlay>> {
    let path = target_dir.join(OVERLAY_FILENAME);
    if !path.exists() {
        return Ok(None);
    }
    Ok(Some(load_file(&path)?))
}

/// Loads an overlay from an explicit file path (used by the `--overlay` flag).
pub fn load_file(path: &Path) -> Result<Overlay> {
    let text =
        fs::read_to_string(path).with_context(|| format!("reading overlay {}", path.display()))?;
    toml::from_str(&text).with_context(|| format!("parsing overlay {}", path.display()))
}

/// Resolves the overlay chain for a target, ordered parent→child (the target's
/// own overlay is last, so its feature additions win the rendering order). Each
/// overlay's `extends` names the directory of its parent layer, relative to the
/// overlay's own location. A target with no overlay file yields an empty chain
/// (equivalent to "no overlay"); an `extends` that points at a directory with no
/// overlay file, or that forms a cycle, is an error.
pub fn load_chain(target_dir: &Path) -> Result<Vec<Layer>> {
    let mut chain: Vec<Layer> = Vec::new();
    let mut seen: HashSet<PathBuf> = HashSet::new();
    let mut dir = target_dir.to_path_buf();

    loop {
        let canon = dir
            .canonicalize()
            .with_context(|| format!("canonicalize layer dir {}", dir.display()))?;
        if !seen.insert(canon.clone()) {
            bail!(
                "overlay `extends` cycle detected: {} is reached twice",
                canon.display()
            );
        }

        let overlay = match load(&canon)? {
            Some(o) => o,
            None => {
                if chain.is_empty() {
                    // Target itself has no overlay → no chain at all.
                    return Ok(Vec::new());
                }
                bail!(
                    "{} is referenced via `extends` but has no {}",
                    canon.display(),
                    OVERLAY_FILENAME
                );
            }
        };

        let next = overlay
            .extends
            .as_ref()
            .map(|rel| canon.join(rel));
        chain.push(Layer {
            overlay,
            dir: canon,
        });
        match next {
            Some(parent) => dir = parent,
            None => break,
        }
    }

    chain.reverse();
    Ok(chain)
}

/// Validates an ordered overlay chain against the registry. The additive-only
/// rule recurses down the chain: a `[overlay.X]` may target a registry dep or an
/// extra introduced by an *upstream* layer; a `[extra.X]` may not collide with a
/// registry dep nor redefine an upstream extra (add features via `[overlay.X]`
/// instead).
pub fn validate_chain(layers: &[Layer], registry: &Registry) -> Result<()> {
    let mut upstream_extras: HashSet<String> = HashSet::new();

    for layer in layers {
        let o = &layer.overlay;
        for name in o.overlay.keys() {
            if !registry.deps.contains_key(name) && !upstream_extras.contains(name) {
                bail!(
                    "overlay references unknown dep `{name}` at {} — add it to registry/Cargo.toml, introduce it via an upstream [extra], or declare it in [extra]",
                    layer.dir.display()
                );
            }
        }
        for (name, dep) in &o.extra {
            if registry.deps.contains_key(name) {
                bail!(
                    "[extra.{name}] at {} conflicts with a registry dep — use [overlay.{name}] for feature additions",
                    layer.dir.display()
                );
            }
            if upstream_extras.contains(name) {
                bail!(
                    "[extra.{name}] at {} redefines an extra from an upstream layer — use [overlay.{name}] to add features instead",
                    layer.dir.display()
                );
            }
            dep.validate(name)?;
            upstream_extras.insert(name.clone());
        }
    }
    Ok(())
}

/// Merges overlay feature additions onto a dep. Order preserved, duplicates
/// dropped.
pub fn apply_features(dep: &Dep, fo: Option<&FeatureOverlay>) -> Dep {
    let mut merged = dep.clone();
    if let Some(fo) = fo {
        for f in &fo.features {
            if !merged.features.contains(f) {
                merged.features.push(f.clone());
            }
        }
    }
    merged
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::registry::Registry;

    fn registry_with(names: &[&str]) -> Registry {
        let mut deps = IndexMap::new();
        for n in names {
            deps.insert(
                (*n).to_string(),
                Dep {
                    version: Some("1.0".into()),
                    rebase: true,
                    ..Dep::default()
                },
            );
        }
        Registry { deps }
    }

    fn layer(toml_src: &str, dir: &str) -> Layer {
        Layer {
            overlay: toml::from_str(toml_src).expect("overlay parses"),
            dir: PathBuf::from(dir),
        }
    }

    #[test]
    fn overlay_may_feature_an_upstream_extra() {
        let reg = registry_with(&["rustelo_server"]);
        let l2 = layer("[extra.content-graph]\nversion = \"0.1\"\n", "/l2");
        let l3 = layer("[overlay.content-graph]\nfeatures = [\"ssr\"]\n", "/l3");
        validate_chain(&[l2, l3], &reg).expect("upstream extra is a valid overlay target");
    }

    #[test]
    fn overlay_on_unknown_dep_is_rejected() {
        let reg = registry_with(&["rustelo_server"]);
        let l3 = layer("[overlay.nope]\nfeatures = [\"x\"]\n", "/l3");
        assert!(validate_chain(&[l3], &reg).is_err());
    }

    #[test]
    fn redefining_an_upstream_extra_is_rejected() {
        let reg = registry_with(&[]);
        let l2 = layer("[extra.shared]\npath = \"../a/shared\"\n", "/l2");
        let l3 = layer("[extra.shared]\npath = \"../b/shared\"\n", "/l3");
        assert!(validate_chain(&[l2, l3], &reg).is_err());
    }

    #[test]
    fn extra_colliding_with_registry_is_rejected() {
        let reg = registry_with(&["rustelo_server"]);
        let l3 = layer("[extra.rustelo_server]\nversion = \"9.9\"\n", "/l3");
        assert!(validate_chain(&[l3], &reg).is_err());
    }
}
