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

use miette::Diagnostic;
use thiserror::Error;

use crate::schema::ImageSpec;

#[derive(Debug, Error, Diagnostic)]
pub enum ResolveError {
    #[error("Failed to read spec file: {path}")]
    #[diagnostic(help("Ensure the file exists and is readable"))]
    ReadFile {
        path: String,
        #[source]
        source: std::io::Error,
    },

    #[error("Failed to parse included spec: {path}")]
    #[diagnostic(help("Check the KDL syntax in the included file"))]
    ParseInclude {
        path: String,
        #[source]
        source: crate::ParseError,
    },

    #[error("Circular include detected: {path}")]
    #[diagnostic(
        help("The include chain forms a cycle. Remove the circular reference."),
        code(spec_parser::circular_include)
    )]
    CircularInclude { path: String },

    #[error("Failed to resolve base spec: {path}")]
    #[diagnostic(help("Ensure the base spec path is correct and the file exists"))]
    ResolveBase {
        path: String,
        #[source]
        source: Box<ResolveError>,
    },
}

/// Resolve all includes and base references in an `ImageSpec`, producing a
/// fully merged spec. The `spec_dir` is the directory containing the root spec
/// file, used to resolve relative paths.
pub fn resolve(spec: ImageSpec, spec_dir: &Path) -> Result<ImageSpec, ResolveError> {
    let mut visited = HashSet::new();
    resolve_inner(spec, spec_dir, &mut visited)
}

fn resolve_inner(
    mut spec: ImageSpec,
    spec_dir: &Path,
    visited: &mut HashSet<PathBuf>,
) -> Result<ImageSpec, ResolveError> {
    // Resolve base spec first (base is the "parent" we inherit from)
    if let Some(base_path) = spec.base.take() {
        let base_abs = resolve_path(spec_dir, &base_path);
        let canonical = base_abs
            .canonicalize()
            .map_err(|e| ResolveError::ReadFile {
                path: base_abs.display().to_string(),
                source: e,
            })?;

        if !visited.insert(canonical.clone()) {
            return Err(ResolveError::CircularInclude {
                path: canonical.display().to_string(),
            });
        }

        let base_content =
            std::fs::read_to_string(&canonical).map_err(|e| ResolveError::ReadFile {
                path: canonical.display().to_string(),
                source: e,
            })?;

        let base_spec = crate::parse(&base_content).map_err(|e| ResolveError::ParseInclude {
            path: canonical.display().to_string(),
            source: e,
        })?;

        let base_dir = canonical.parent().unwrap_or(spec_dir);
        let resolved_base = resolve_inner(base_spec, base_dir, visited).map_err(|e| {
            ResolveError::ResolveBase {
                path: canonical.display().to_string(),
                source: Box::new(e),
            }
        })?;

        spec = merge_base(resolved_base, spec);
    }

    // Resolve includes (siblings that contribute packages/overlays/customizations)
    let includes = std::mem::take(&mut spec.includes);
    for include in includes {
        let inc_abs = resolve_path(spec_dir, &include.path);
        let canonical = inc_abs
            .canonicalize()
            .map_err(|e| ResolveError::ReadFile {
                path: inc_abs.display().to_string(),
                source: e,
            })?;

        if !visited.insert(canonical.clone()) {
            return Err(ResolveError::CircularInclude {
                path: canonical.display().to_string(),
            });
        }

        let inc_content =
            std::fs::read_to_string(&canonical).map_err(|e| ResolveError::ReadFile {
                path: canonical.display().to_string(),
                source: e,
            })?;

        let inc_spec = crate::parse(&inc_content).map_err(|e| ResolveError::ParseInclude {
            path: canonical.display().to_string(),
            source: e,
        })?;

        let inc_dir = canonical.parent().unwrap_or(spec_dir);
        let resolved_inc = resolve_inner(inc_spec, inc_dir, visited)?;

        merge_include(&mut spec, resolved_inc);
    }

    Ok(spec)
}

/// Merge a base (parent) spec with the child. The child's values take
/// precedence; the base provides defaults.
fn merge_base(mut base: ImageSpec, child: ImageSpec) -> ImageSpec {
    // Metadata comes from the child
    base.metadata = child.metadata;

    // distro: child overrides
    if child.distro.is_some() {
        base.distro = child.distro;
    }

    // build_host: child overrides
    if child.build_host.is_some() {
        base.build_host = child.build_host;
    }

    // repositories: merge publishers from child into base (child publishers appended)
    for pub_entry in child.repositories.publishers {
        if !base
            .repositories
            .publishers
            .iter()
            .any(|p| p.name == pub_entry.name)
        {
            base.repositories.publishers.push(pub_entry);
        }
    }

    // repositories: merge apt_mirrors from child into base (dedup by URL)
    for mirror in child.repositories.apt_mirrors {
        if !base
            .repositories
            .apt_mirrors
            .iter()
            .any(|m| m.url == mirror.url)
        {
            base.repositories.apt_mirrors.push(mirror);
        }
    }

    // incorporation: child overrides
    if child.incorporation.is_some() {
        base.incorporation = child.incorporation;
    }

    // variants: merge
    if let Some(child_variants) = child.variants {
        if let Some(ref mut base_variants) = base.variants {
            for var in child_variants.vars {
                if let Some(existing) = base_variants.vars.iter_mut().find(|v| v.name == var.name) {
                    existing.value = var.value;
                } else {
                    base_variants.vars.push(var);
                }
            }
        } else {
            base.variants = Some(child_variants);
        }
    }

    // certificates: merge
    if let Some(child_certs) = child.certificates {
        if let Some(ref mut base_certs) = base.certificates {
            base_certs.ca.extend(child_certs.ca);
        } else {
            base.certificates = Some(child_certs);
        }
    }

    // packages, customizations, overlays: child appended after base
    base.packages.extend(child.packages);
    base.customizations.extend(child.customizations);
    base.overlays.extend(child.overlays);

    // includes: already resolved, don't carry forward
    base.includes = Vec::new();

    // targets: child's targets replace base entirely
    if !child.targets.is_empty() {
        base.targets = child.targets;
    }

    // builder: child's builder replaces base entirely
    if child.builder.is_some() {
        base.builder = child.builder;
    }

    base
}

/// Merge an included spec into the current spec. Includes contribute
/// repositories, variants, certificates, packages, customizations, and
/// overlays — but not metadata, distro, targets, or builder.
fn merge_include(spec: &mut ImageSpec, included: ImageSpec) {
    // Merge publishers (dedup by name)
    for pub_entry in included.repositories.publishers {
        if !spec
            .repositories
            .publishers
            .iter()
            .any(|p| p.name == pub_entry.name)
        {
            spec.repositories.publishers.push(pub_entry);
        }
    }

    // Merge apt_mirrors (dedup by URL)
    for mirror in included.repositories.apt_mirrors {
        if !spec
            .repositories
            .apt_mirrors
            .iter()
            .any(|m| m.url == mirror.url)
        {
            spec.repositories.apt_mirrors.push(mirror);
        }
    }

    // Merge variants
    if let Some(inc_variants) = included.variants {
        if let Some(ref mut spec_variants) = spec.variants {
            for var in inc_variants.vars {
                if let Some(existing) = spec_variants.vars.iter_mut().find(|v| v.name == var.name) {
                    existing.value = var.value;
                } else {
                    spec_variants.vars.push(var);
                }
            }
        } else {
            spec.variants = Some(inc_variants);
        }
    }

    // Merge certificates
    if let Some(inc_certs) = included.certificates {
        if let Some(ref mut spec_certs) = spec.certificates {
            spec_certs.ca.extend(inc_certs.ca);
        } else {
            spec.certificates = Some(inc_certs);
        }
    }

    // Merge incorporation (included overrides only if spec doesn't have one)
    if spec.incorporation.is_none() && included.incorporation.is_some() {
        spec.incorporation = included.incorporation;
    }

    spec.packages.extend(included.packages);
    spec.customizations.extend(included.customizations);
    spec.overlays.extend(included.overlays);
}

fn resolve_path(base_dir: &Path, relative: &str) -> PathBuf {
    let path = Path::new(relative);
    if path.is_absolute() {
        path.to_path_buf()
    } else {
        base_dir.join(path)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs;
    use tempfile::TempDir;

    #[test]
    fn test_resolve_with_include() {
        let tmp = TempDir::new().unwrap();

        let included_kdl = r#"
            metadata name="included" version="0.0.1"
            repositories {
                publisher name="extra" origin="http://extra.example.com"
            }
            packages {
                package "extra/pkg"
            }
            overlays {
                ensure-dir "/extra/dir" owner="root" group="root" mode="755"
            }
        "#;
        fs::write(tmp.path().join("included.kdl"), included_kdl).unwrap();

        let root_kdl = r#"
            metadata name="root" version="1.0.0"
            repositories {
                publisher name="main" origin="http://main.example.com"
            }
            include "included.kdl"
            packages {
                package "main/pkg"
            }
        "#;

        let spec = crate::parse(root_kdl).unwrap();
        let resolved = resolve(spec, tmp.path()).unwrap();

        assert_eq!(resolved.metadata.name, "root");
        assert_eq!(resolved.repositories.publishers.len(), 2);
        assert!(resolved
            .repositories
            .publishers
            .iter()
            .any(|p| p.name == "main"));
        assert!(resolved
            .repositories
            .publishers
            .iter()
            .any(|p| p.name == "extra"));
        assert_eq!(resolved.packages.len(), 2);
        assert_eq!(resolved.overlays.len(), 1);
    }

    #[test]
    fn test_resolve_with_base() {
        let tmp = TempDir::new().unwrap();

        let base_kdl = r#"
            metadata name="base" version="0.0.1"
            repositories {
                publisher name="core" origin="http://core.example.com"
            }
            packages {
                package "base/pkg"
            }
        "#;
        fs::write(tmp.path().join("base.kdl"), base_kdl).unwrap();

        let child_kdl = r#"
            metadata name="child" version="1.0.0"
            base "base.kdl"
            repositories {
                publisher name="extra" origin="http://extra.example.com"
            }
            packages {
                package "child/pkg"
            }
            target "vm" kind="qcow2" {
                disk-size "10G"
            }
        "#;

        let spec = crate::parse(child_kdl).unwrap();
        let resolved = resolve(spec, tmp.path()).unwrap();

        assert_eq!(resolved.metadata.name, "child");
        assert_eq!(resolved.repositories.publishers.len(), 2);
        assert_eq!(resolved.packages.len(), 2);
        assert_eq!(resolved.packages[0].packages[0].name, "base/pkg");
        assert_eq!(resolved.packages[1].packages[0].name, "child/pkg");
        assert_eq!(resolved.targets.len(), 1);
    }

    #[test]
    fn test_merge_base_with_apt_mirrors() {
        let tmp = TempDir::new().unwrap();

        let base_kdl = r#"
            metadata name="base" version="0.0.1"
            distro "ubuntu-22.04"
            repositories {
                apt-mirror "http://archive.ubuntu.com/ubuntu" suite="jammy" components="main universe"
            }
            packages {
                package "base-pkg"
            }
        "#;
        fs::write(tmp.path().join("base.kdl"), base_kdl).unwrap();

        let child_kdl = r#"
            metadata name="child" version="1.0.0"
            base "base.kdl"
            repositories {
                apt-mirror "http://ppa.launchpad.net/extra" suite="jammy" components="main"
            }
            packages {
                package "child-pkg"
            }
        "#;

        let spec = crate::parse(child_kdl).unwrap();
        let resolved = resolve(spec, tmp.path()).unwrap();

        assert_eq!(resolved.distro, Some("ubuntu-22.04".to_string()));
        assert_eq!(resolved.repositories.apt_mirrors.len(), 2);
        assert_eq!(
            resolved.repositories.apt_mirrors[0].url,
            "http://archive.ubuntu.com/ubuntu"
        );
        assert_eq!(
            resolved.repositories.apt_mirrors[1].url,
            "http://ppa.launchpad.net/extra"
        );
    }

    #[test]
    fn test_merge_base_distro_child_overrides() {
        let tmp = TempDir::new().unwrap();

        let base_kdl = r#"
            metadata name="base" version="0.0.1"
            repositories {}
        "#;
        fs::write(tmp.path().join("base.kdl"), base_kdl).unwrap();

        let child_kdl = r#"
            metadata name="child" version="1.0.0"
            base "base.kdl"
            distro "ubuntu-22.04"
            repositories {}
        "#;

        let spec = crate::parse(child_kdl).unwrap();
        let resolved = resolve(spec, tmp.path()).unwrap();

        assert_eq!(resolved.distro, Some("ubuntu-22.04".to_string()));
    }

    #[test]
    fn test_circular_include_detected() {
        let tmp = TempDir::new().unwrap();

        let a_kdl = r#"
            metadata name="a" version="0.0.1"
            repositories {}
            include "b.kdl"
        "#;
        let b_kdl = r#"
            metadata name="b" version="0.0.1"
            repositories {}
            include "a.kdl"
        "#;
        fs::write(tmp.path().join("a.kdl"), a_kdl).unwrap();
        fs::write(tmp.path().join("b.kdl"), b_kdl).unwrap();

        let spec = crate::parse(a_kdl).unwrap();
        let result = resolve(spec, tmp.path());
        assert!(result.is_err());
        let err = result.unwrap_err();
        assert!(matches!(err, ResolveError::CircularInclude { .. }));
    }
}
