use std::path::PathBuf;

use crate::{
    build::{BuildProfile, BuildTarget, RustVersion, SlimImage},
    source::RustSource,
};

pub struct LeptosBuild {
    client: dagger_sdk::Query,
    registry: Option<String>,
}

impl LeptosBuild {
    pub fn new(client: dagger_sdk::Query) -> Self {
        Self {
            client,
            registry: None,
        }
    }

    pub async fn build(
        &self,
        source_path: Option<impl Into<PathBuf>>,
        rust_version: impl AsRef<RustVersion>,
        profile: impl AsRef<BuildProfile>,
        crates: &[&str],
        extra_deps: &[&str],
    ) -> eyre::Result<dagger_sdk::Container> {
        let source_path = source_path.map(|s| s.into()).unwrap_or(PathBuf::from("."));
        let rust_version = rust_version.as_ref();
        let profile = profile.as_ref();

        let rust_source = RustSource::new(self.client.clone());

        let (src, dep_src) = rust_source
            .get_rust_src(Some(&source_path), crates.to_vec())
            .await?;

        let mut deps = vec!["apt", "install", "-y"];
        deps.extend(extra_deps);

        let rust_build_image = self
            .client
            .container()
            .from(rust_version.to_string())
            .with_exec(vec!["rustup", "target", "add", "wasm32-unknown-unknown"])
            .with_exec(vec!["apt", "update"])
            .with_exec(deps)
            .with_exec(vec!["wget", "https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-x86_64-unknown-linux-musl.tgz"])
            .with_exec("tar -xvf cargo-binstall-x86_64-unknown-linux-musl.tgz".split_whitespace().collect())
            .with_exec("mv cargo-binstall /usr/local/cargo/bin".split_whitespace().collect())
            .with_exec(vec!["cargo", "binstall", "cargo-leptos", "-y"]);

        let target_cache = self
            .client
            .cache_volume(format!("rust_leptos_{}", profile.to_string()));

        let build_options = vec!["cargo", "leptos", "build", "--release", "-vv"];

        let rust_prebuild = rust_build_image
            .with_workdir("/mnt/src")
            .with_directory("/mnt/src", dep_src)
            .with_exec(build_options)
            .with_mounted_cache("/mnt/src/target/", target_cache);

        let incremental_dir = rust_source
            .get_rust_target_src(&source_path, rust_prebuild.clone(), crates.to_vec())
            .await?;

        let rust_with_src = rust_build_image
            .with_workdir("/mnt/src")
            .with_directory(
                "/usr/local/cargo",
                rust_prebuild.directory("/usr/local/cargo"),
            )
            .with_directory("/mnt/src/target", incremental_dir)
            .with_directory("/mnt/src/", src);

        Ok(rust_with_src)
    }

    pub async fn build_release(
        &self,
        source_path: Option<impl Into<PathBuf>>,
        rust_version: impl AsRef<RustVersion>,
        crates: &[&str],
        extra_deps: &[&str],
        images: impl IntoIterator<Item = SlimImage>,
        bin_name: &str,
    ) -> eyre::Result<Vec<dagger_sdk::Container>> {
        let images = images.into_iter().collect::<Vec<_>>();
        let source_path = source_path.map(|s| s.into());

        let mut containers = Vec::new();
        for container_image in images {
            let container = match &container_image {
                SlimImage::Debian { image, deps, .. } => {
                    let _target = BuildTarget::from_target(&container_image);

                    let build_container = self
                        .build(
                            source_path.clone(),
                            &rust_version,
                            BuildProfile::Release,
                            crates,
                            extra_deps,
                        )
                        .await?;

                    let binary_build = build_container.with_exec(vec![
                        "cargo",
                        "leptos",
                        "build",
                        "--release",
                        "-vv",
                    ]);

                    self.build_debian_image(
                        binary_build,
                        image,
                        BuildTarget::from_target(&container_image),
                        deps.iter()
                            .map(|d| d.as_str())
                            .collect::<Vec<&str>>()
                            .as_slice(),
                        bin_name,
                    )
                    .await?
                }
                SlimImage::Alpine { image, deps, .. } => {
                    let target = BuildTarget::from_target(&container_image);

                    let build_container = self
                        .build(
                            source_path.clone(),
                            &rust_version,
                            BuildProfile::Release,
                            crates,
                            extra_deps,
                        )
                        .await?;

                    let bin = build_container
                        .with_exec(vec![
                            "cargo",
                            "build",
                            "--target",
                            &target.to_string(),
                            "--release",
                            "-p",
                            bin_name,
                        ])
                        .file(format!(
                            "target/{}/release/{}",
                            target.to_string(),
                            bin_name
                        ));

                    self.build_alpine_image(
                        bin,
                        image,
                        BuildTarget::from_target(&container_image),
                        deps.iter()
                            .map(|d| d.as_str())
                            .collect::<Vec<&str>>()
                            .as_slice(),
                        bin_name,
                    )
                    .await?
                }
            };

            containers.push(container);
        }

        Ok(containers)
    }

    async fn build_debian_image(
        &self,
        builder_image: dagger_sdk::Container,
        image: &str,
        target: BuildTarget,
        production_deps: &[&str],
        bin_name: &str,
    ) -> eyre::Result<dagger_sdk::Container> {
        let base_debian = self
            .client
            .container_opts(dagger_sdk::QueryContainerOpts {
                platform: Some(target.into_platform()),
            })
            .from(image);

        let mut packages = vec!["apt", "install", "-y"];
        packages.extend_from_slice(production_deps);
        let base_debian = base_debian
            .with_exec(vec!["apt", "update"])
            .with_exec(packages);

        let final_image = base_debian
            .with_workdir("/mnt/app")
            .with_file(
                format!("/mnt/app/{bin_name}"),
                builder_image.file(format!("/mnt/src/target/release/{bin_name}")),
            )
            .with_directory(
                "/mnt/app/target/site",
                builder_image.directory("/mnt/src/target/site".to_string()),
            )
            .with_file(
                "/mnt/app/Cargo.toml",
                builder_image.file(format!("/mnt/src/crates/{bin_name}/Cargo.toml")),
            )
            .with_env_variable("RUST_LOG", "debug")
            .with_env_variable("APP_ENVIRONMENT", "production")
            .with_env_variable("LEPTOS_OUTPUT_NAME", bin_name)
            .with_env_variable("LEPTOS_SITE_ADDR", "0.0.0.0:8080")
            .with_env_variable("LEPTOS_SITE_ROOT", "site")
            .with_env_variable("LEPTOS_SITE_PKG_DIR", "pkg")
            .with_exposed_port(8080)
            .with_entrypoint(vec![format!("/mnt/app/{bin_name}")]);

        final_image.sync().await?;

        Ok(final_image)
    }

    async fn build_alpine_image(
        &self,
        bin: dagger_sdk::File,
        image: &str,
        target: BuildTarget,
        production_deps: &[&str],
        bin_name: &str,
    ) -> eyre::Result<dagger_sdk::Container> {
        let base_debian = self
            .client
            .container_opts(dagger_sdk::QueryContainerOpts {
                platform: Some(target.into_platform()),
            })
            .from(image);

        let mut packages = vec!["apk", "add"];
        packages.extend_from_slice(production_deps);
        let base_debian = base_debian.with_exec(packages);

        let final_image = base_debian.with_file(format!("/usr/local/bin/{}", bin_name), bin);

        Ok(final_image)
    }
}
