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

use eyre::Context;

pub struct RustSource {
    client: dagger_sdk::Query,

    exclude: Vec<String>,
}

impl RustSource {
    pub fn new(client: dagger_sdk::Query) -> Self {
        Self {
            client,
            exclude: vec!["node_modules/", ".git/", "target/", ".cuddle/"]
                .into_iter()
                .map(|s| s.to_string())
                .collect(),
        }
    }

    pub fn with_exclude(
        &mut self,
        exclude: impl IntoIterator<Item = impl Into<String>>,
    ) -> &mut Self {
        self.exclude = exclude.into_iter().map(|s| s.into()).collect();

        self
    }

    pub fn append_exclude(
        &mut self,
        exclude: impl IntoIterator<Item = impl Into<String>>,
    ) -> &mut Self {
        self.exclude
            .append(&mut exclude.into_iter().map(|s| s.into()).collect::<Vec<_>>());

        self
    }

    pub async fn get_rust_src<T, I>(
        &self,
        source: Option<T>,
        crate_paths: I,
    ) -> eyre::Result<(dagger_sdk::Directory, dagger_sdk::Directory)>
    where
        T: Into<PathBuf>,
        T: Clone,
        I: IntoIterator,
        I::Item: Into<String>,
    {
        let source_path = match source.clone() {
            Some(s) => s.into(),
            None => PathBuf::from("."),
        };

        let (skeleton_files, _crates) = self
            .get_rust_skeleton_files(&source_path, crate_paths)
            .await?;

        let src = self.get_src(source.clone()).await?;
        let rust_src = self.get_rust_dep_src(source).await?;
        let rust_src = rust_src.with_directory(".", skeleton_files);

        Ok((src, rust_src))
    }

    pub async fn get_src(
        &self,
        source: Option<impl Into<PathBuf>>,
    ) -> eyre::Result<dagger_sdk::Directory> {
        let source = source.map(|s| s.into()).unwrap_or(PathBuf::from("."));

        let directory = self.client.host().directory_opts(
            source.display().to_string(),
            dagger_sdk::HostDirectoryOptsBuilder::default()
                .exclude(self.exclude.iter().map(|s| s.as_str()).collect::<Vec<_>>())
                .build()?,
        );

        Ok(directory)
    }

    pub async fn get_rust_dep_src(
        &self,
        source: Option<impl Into<PathBuf>>,
    ) -> eyre::Result<dagger_sdk::Directory> {
        let source = source.map(|s| s.into()).unwrap_or(PathBuf::from("."));

        let mut excludes = self.exclude.clone();
        excludes.push("**/src".to_string());
        excludes.push("**/tests".to_string());

        let directory = self.client.host().directory_opts(
            source.display().to_string(),
            dagger_sdk::HostDirectoryOptsBuilder::default()
                //.include(vec!["**/Cargo.toml", "**/Cargo.lock"])
                .exclude(excludes.iter().map(|s| s.as_str()).collect::<Vec<_>>())
                .build()?,
        );

        Ok(directory)
    }

    pub async fn get_rust_target_src(
        &self,
        source_path: &Path,
        container: dagger_sdk::Container,
        crate_paths: impl IntoIterator<Item = impl Into<String>>,
    ) -> eyre::Result<dagger_sdk::Directory> {
        let (_skeleton_files, crates) = self
            .get_rust_skeleton_files(source_path, crate_paths)
            .await?;

        let exclude = crates
            .iter()
            .map(|c| format!("**/*{}*", c.replace('-', "_")))
            .collect::<Vec<_>>();

        let mut original_crates = crates.clone();
        original_crates.extend(exclude);

        let exclude = original_crates.iter().map(|c| c.as_str()).collect();

        let incremental_dir = self.client.directory().with_directory_opts(
            ".",
            container.directory("target"),
            dagger_sdk::DirectoryWithDirectoryOpts {
                exclude: Some(exclude),
                include: None,
            },
        );

        Ok(incremental_dir)
    }

    pub async fn get_rust_skeleton_files(
        &self,
        source_path: &Path,
        crate_paths: impl IntoIterator<Item = impl Into<String>>,
    ) -> eyre::Result<(dagger_sdk::Directory, Vec<String>)> {
        let paths = crate_paths
            .into_iter()
            .map(|s| s.into())
            .collect::<Vec<String>>();

        let mut crates = Vec::new();
        for path in paths {
            if path.ends_with("/*") {
                let mut dirs = tokio::fs::read_dir(source_path.join(path.trim_end_matches("/*")))
                    .await
                    .context(format!("failed to find path: {}", path.clone()))?;
                while let Some(entry) = dirs.next_entry().await? {
                    if entry.metadata().await?.is_dir() {
                        crates.push(entry.path());
                    }
                }
            } else {
                crates.push(PathBuf::from(path));
            }
        }

        fn create_skeleton_files(
            directory: dagger_sdk::Directory,
            path: &Path,
        ) -> eyre::Result<dagger_sdk::Directory> {
            let main_content = r#"
        #[allow(dead_code)]
        fn main() { panic!("should never be executed"); }"#;
            let lib_content = r#"
        #[allow(dead_code)]
        fn some() { panic!("should never be executed"); }"#;

            let directory = directory.with_new_file(
                path.join("src").join("main.rs").display().to_string(),
                main_content,
            );
            let directory = directory.with_new_file(
                path.join("src").join("lib.rs").display().to_string(),
                lib_content,
            );

            Ok(directory)
        }

        let mut directory = self.client.directory();
        let mut crate_names = Vec::new();

        for rust_crate in crates.iter() {
            if let Some(file_name) = rust_crate.file_name() {
                crate_names.push(file_name.to_str().unwrap().to_string());
            }
            directory = create_skeleton_files(
                directory,
                rust_crate.strip_prefix(source_path).unwrap_or(rust_crate),
            )?;
        }

        Ok((directory, crate_names))
    }
}
