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

use serde::{Deserialize, Serialize};

use crate::recipe_find::recipe_find;

/// Specifies how to download the source for a recipe
#[derive(Debug, Deserialize, PartialEq, Serialize)]
#[serde(untagged)]
pub enum SourceRecipe {
    /// Reuse the source directory of another package
    ///
    /// This is useful when a single source repo contains multiple projects which each have their
    /// own recipe to build them.
    SameAs {
        /// Relative path to the package for which to reuse the source dir
        same_as: String,
    },
    /// Path source
    Path {
        /// The path to the source
        path: String,
    },
    /// A git repository source
    Git {
        /// The URL for the git repository, such as https://gitlab.redox-os.org/redox-os/ion.git
        git: String,
        /// The URL for an upstream repository
        upstream: Option<String>,
        /// The optional branch of the git repository to track, such as master. Please specify to
        /// make updates to the rev easier
        branch: Option<String>,
        /// The optional revision of the git repository to use for builds. Please specify for
        /// reproducible builds
        rev: Option<String>,
        /// A list of patch files to apply to the source
        #[serde(default)]
        patches: Vec<String>,
        /// Optional script to run to prepare the source
        script: Option<String>,
    },
    /// A tar file source
    Tar {
        /// The URL of a tar source
        tar: String,
        /// The optional blake3 sum of the tar file. Please specify this to make reproducible
        /// builds more reliable
        blake3: Option<String>,
        /// A list of patch files to apply to the source
        #[serde(default)]
        patches: Vec<String>,
        /// Optional script to run to prepare the source, such as ./autogen.sh
        script: Option<String>,
    },
}

/// Specifies how to build a recipe
#[derive(Debug, Deserialize, PartialEq, Serialize)]
#[serde(tag = "template")]
pub enum BuildKind {
    /// Will build and install using cargo
    #[serde(rename = "cargo")]
    Cargo {
        #[serde(default)]
        package_path: Option<String>,

        #[serde(default)]
        cargoflags: String,
    },
    /// Will build and install using configure and make
    #[serde(rename = "configure")]
    Configure,
    /// Will build and install using custom commands
    #[serde(rename = "custom")]
    Custom { script: String },
}

#[derive(Debug, Deserialize, PartialEq, Serialize)]
pub struct BuildRecipe {
    #[serde(flatten)]
    pub kind: BuildKind,
    #[serde(default)]
    pub dependencies: Vec<String>,
}

#[derive(Debug, Default, Deserialize, PartialEq, Serialize)]
pub struct PackageRecipe {
    #[serde(default)]
    pub dependencies: Vec<String>,
    #[serde(rename = "shared-deps", default)]
    pub shared_deps: Vec<String>,
}

/// Everything required to build a Redox package
#[derive(Debug, Deserialize, PartialEq, Serialize)]
pub struct Recipe {
    /// Specifies how to donload the source for this recipe
    pub source: Option<SourceRecipe>,
    /// Specifies how to build this recipe
    pub build: BuildRecipe,
    /// Specifies how to package this recipe
    #[serde(default)]
    pub package: PackageRecipe,
}

impl Recipe {
    #[inline]
    pub fn dependencies_iter(&self) -> impl Iterator<Item = &String> {
        self.build
            .dependencies
            .iter()
            .chain(self.package.shared_deps.iter())
    }

    /// `[build.dependencies] + [package.shared_deps]`
    #[inline]
    pub fn dependencies(&self) -> Vec<String> {
        self.dependencies_iter().cloned().collect::<Vec<_>>()
    }

    /// `[package.dependencies] + [package.shared_deps]`
    #[inline]
    pub fn runtime_dependencies(&self) -> Vec<String> {
        self.package
            .dependencies
            .iter()
            .chain(self.package.shared_deps.iter())
            .cloned()
            .collect::<Vec<_>>()
    }
}

pub struct CookRecipe {
    pub name: String,
    pub dir: PathBuf,
    pub recipe: Recipe,
}

impl CookRecipe {
    pub fn new(name: String) -> Result<Self, String> {
        //TODO: sanitize recipe name?
        let dir = recipe_find(&name, Path::new("recipes"))?;
        if dir.is_none() {
            return Err(format!("failed to find recipe directory '{}'", name));
        }
        let dir = dir.unwrap();
        let file = dir.join("recipe.toml");
        if !file.is_file() {
            return Err(format!("failed to find recipe file '{}'", file.display()));
        }

        let toml = fs::read_to_string(&file).map_err(|err| {
            format!(
                "failed to read recipe file '{}': {}\n{:#?}",
                file.display(),
                err,
                err
            )
        })?;

        let recipe: Recipe = toml::from_str(&toml).map_err(|err| {
            format!(
                "failed to parse recipe file '{}': {}\n{:#?}",
                file.display(),
                err,
                err
            )
        })?;

        Ok(Self { name, dir, recipe })
    }

    //TODO: make this more efficient, smarter, and not return duplicates
    pub fn new_recursive(
        names: &[String],
        recursion: usize,
        runtime_deps_only: bool,
    ) -> Result<Vec<Self>, String> {
        if recursion == 0 {
            return Err(format!(
                "recursion limit while processing build dependencies: {:#?}",
                names
            ));
        }

        let mut recipes = Vec::new();
        for name in names {
            let recipe = Self::new(name.clone())?;
            let all_deps = recipe.recipe.dependencies();
            let runtime_deps = recipe.recipe.runtime_dependencies();

            let dependencies = Self::new_recursive(
                if runtime_deps_only {
                    &runtime_deps
                } else {
                    &all_deps
                },
                recursion - 1,
                runtime_deps_only,
            )
            .map_err(|err| format!("{}: failed on loading build dependencies:\n{}", name, err))?;

            for dependency in dependencies {
                recipes.push(dependency);
            }

            recipes.push(recipe);
        }

        Ok(recipes)
    }
}

#[cfg(test)]
mod tests {
    #[test]
    fn git_cargo_recipe() {
        use crate::recipe::{BuildKind, BuildRecipe, PackageRecipe, Recipe, SourceRecipe};

        let recipe: Recipe = toml::from_str(
            r#"
            [source]
            git = "https://gitlab.redox-os.org/redox-os/acid.git"
            branch = "master"
            rev = "06344744d3d55a5ac9a62a6059cb363d40699bbc"

            [build]
            template = "cargo"
        "#,
        )
        .unwrap();

        assert_eq!(
            recipe,
            Recipe {
                source: Some(SourceRecipe::Git {
                    git: "https://gitlab.redox-os.org/redox-os/acid.git".to_string(),
                    upstream: None,
                    branch: Some("master".to_string()),
                    rev: Some("06344744d3d55a5ac9a62a6059cb363d40699bbc".to_string()),
                    patches: Vec::new(),
                    script: None,
                }),
                build: BuildRecipe {
                    kind: BuildKind::Cargo {
                        package_path: None,
                        cargoflags: String::new(),
                    },
                    dependencies: Vec::new(),
                },
                package: PackageRecipe {
                    dependencies: Vec::new(),
                    shared_deps: Vec::new(),
                },
            }
        );
    }

    #[test]
    fn tar_custom_recipe() {
        use crate::recipe::{BuildKind, BuildRecipe, PackageRecipe, Recipe, SourceRecipe};

        let recipe: Recipe = toml::from_str(
            r#"
            [source]
            tar = "http://downloads.xiph.org/releases/ogg/libogg-1.3.3.tar.xz"
            blake3 = "8220c0e4082fa26c07b10bfe31f641d2e33ebe1d1bb0b20221b7016bc8b78a3a"

            [build]
            template = "custom"
            script = "make"
        "#,
        )
        .unwrap();

        assert_eq!(
            recipe,
            Recipe {
                source: Some(SourceRecipe::Tar {
                    tar: "http://downloads.xiph.org/releases/ogg/libogg-1.3.3.tar.xz".to_string(),
                    blake3: Some(
                        "8220c0e4082fa26c07b10bfe31f641d2e33ebe1d1bb0b20221b7016bc8b78a3a"
                            .to_string()
                    ),
                    patches: Vec::new(),
                    script: None,
                }),
                build: BuildRecipe {
                    kind: BuildKind::Custom {
                        script: "make".to_string()
                    },
                    dependencies: Vec::new(),
                },
                package: PackageRecipe {
                    dependencies: Vec::new(),
                    shared_deps: Vec::new(),
                },
            }
        );
    }
}