//! Tasks related to zellij CI
use crate::{
    build,
    flags::{self, BuildRelease, CiCmd, Cross, E2e},
    metadata,
};
use anyhow::Context;
use std::{
    ffi::OsString,
    path::{Path, PathBuf},
};
use xshell::{cmd, Shell};

pub fn main(sh: &Shell, flags: flags::Ci) -> anyhow::Result<()> {
    let err_context = "failed to run CI task";

    match flags.subcommand {
        CiCmd::E2e(E2e {
            build: false,
            test: false,
            ..
        }) => Err(anyhow::anyhow!(
            "either '--build' or '--test' must be provided!"
        )),
        CiCmd::E2e(E2e {
            build: true,
            test: true,
            ..
        }) => Err(anyhow::anyhow!(
            "flags '--build' and '--test' are mutually exclusive!"
        )),
        CiCmd::E2e(E2e {
            build: true,
            test: false,
            ..
        }) => e2e_build(sh),
        CiCmd::E2e(E2e {
            build: false,
            test: true,
            args,
        }) => e2e_test(sh, args),
        CiCmd::Cross(Cross { triple, no_web }) => cross_compile(sh, &triple, no_web),
        CiCmd::BuildRelease(BuildRelease { no_web }) => build_release(sh, no_web),
    }
    .context(err_context)
}

fn e2e_build(sh: &Shell) -> anyhow::Result<()> {
    let err_context = "failed to build E2E binary";

    build::build(
        sh,
        flags::Build {
            release: true,
            no_plugins: false,
            plugins_only: true,
            no_web: false,
        },
    )
    .context(err_context)?;

    // Copy plugins to e2e data-dir
    let plugin_dir = crate::asset_dir().join("plugins");
    let project_root = crate::project_root();
    let data_dir = project_root.join("target").join("e2e-data");
    let plugins: Vec<_> = std::fs::read_dir(plugin_dir)
        .context(err_context)?
        .filter_map(|dir_entry| {
            if let Ok(entry) = dir_entry {
                entry
                    .file_name()
                    .to_string_lossy()
                    .ends_with(".wasm")
                    .then_some(entry.path())
            } else {
                None
            }
        })
        .collect();

    sh.remove_path(&data_dir)
        .and_then(|_| sh.create_dir(&data_dir))
        .and_then(|_| sh.create_dir(data_dir.join("plugins")))
        .context(err_context)?;

    for plugin in plugins {
        sh.copy_file(plugin, data_dir.join("plugins"))
            .context(err_context)?;
    }

    let _pd = sh.push_dir(project_root);
    // On Windows, build natively (MSVC).
    // On macOS: use cargo-zigbuild which handles musl cross-compilation for any arch without
    //   needing a separate linker toolchain. The target arch matches the host so the Docker
    //   container runs natively without emulation (arm64 on Apple Silicon, amd64 on Intel).
    //   Install with: cargo install cargo-zigbuild && brew install zig
    // On Linux: cross-compile for musl.
    if cfg!(windows) {
        crate::cargo()
            .and_then(|cargo| {
                cmd!(sh, "{cargo} build --release")
                    .run()
                    .map_err(anyhow::Error::new)
            })
            .context(err_context)
    } else if cfg!(target_os = "macos") {
        let target = if cfg!(target_arch = "aarch64") {
            "aarch64-unknown-linux-musl"
        } else {
            "x86_64-unknown-linux-musl"
        };
        crate::cargo()
            .and_then(|cargo| {
                if which::which("cargo-zigbuild").is_err() {
                    eprintln!("!! 'cargo-zigbuild' wasn't found but is needed on macOS.");
                    eprintln!("!! Please install it with:");
                    eprintln!("!!   cargo install cargo-zigbuild");
                    eprintln!("!!   brew install zig");
                    return Err(anyhow::anyhow!("couldn't find 'cargo-zigbuild'"));
                }
                cmd!(sh, "rustup target add {target}")
                    .run()
                    .map_err(anyhow::Error::new)?;
                cmd!(sh, "{cargo} zigbuild --release --target {target}")
                    .run()
                    .map_err(anyhow::Error::new)
            })
            .context(err_context)
    } else {
        crate::cargo()
            .and_then(|cargo| {
                cmd!(
                    sh,
                    "{cargo} build --release --target x86_64-unknown-linux-musl"
                )
                .run()
                .map_err(anyhow::Error::new)
            })
            .context(err_context)
    }
}

/// Native release build: builds plugins, generates protobufs, and runs
/// `cargo build --release` without cross-compilation.
fn build_release(sh: &Shell, no_web: bool) -> anyhow::Result<()> {
    let err_context = "failed to perform native release build";

    // Build plugins and generate protobufs
    build::build(
        sh,
        flags::Build {
            release: true,
            no_plugins: false,
            plugins_only: true,
            no_web,
        },
    )
    .context(err_context)?;

    // Build the main binary natively
    let _pd = sh.push_dir(crate::project_root());
    crate::cargo()
        .and_then(|cargo| {
            if no_web {
                match metadata::get_no_web_features(sh, ".")
                    .context("Failed to check web features for build-release")?
                {
                    Some(features) => {
                        let mut cmd = cmd!(
                            sh,
                            "{cargo} build --verbose --release --no-default-features"
                        );
                        if !features.is_empty() {
                            cmd = cmd.arg("--features").arg(features);
                        }
                        cmd.run().map_err(anyhow::Error::new)
                    },
                    None => cmd!(sh, "{cargo} build --verbose --release")
                        .run()
                        .map_err(anyhow::Error::new),
                }
            } else {
                cmd!(sh, "{cargo} build --verbose --release")
                    .run()
                    .map_err(anyhow::Error::new)
            }
        })
        .context(err_context)
}

fn e2e_test(sh: &Shell, args: Vec<OsString>) -> anyhow::Result<()> {
    let err_context = "failed to run E2E tests";

    e2e_build(sh).context(err_context)?;

    let _pd = sh.push_dir(crate::project_root());

    // set --no-default-features so the test binary gets built with the plugins from assets/plugins that just got built
    crate::cargo()
        .and_then(|cargo| {
            // e2e tests
            cmd!(
                sh,
                "{cargo} test --no-default-features -- --ignored --nocapture --test-threads 1"
            )
            .args(args.clone())
            .run()
            .map_err(anyhow::Error::new)?;

            // plugin system tests are run here because they're medium-slow
            let _pd = sh.push_dir(Path::new("zellij-server"));
            println!();
            let msg = ">> Testing Plugin System".to_string();
            crate::status(&msg);
            println!("{}", msg);

            cmd!(sh, "{cargo} test -- --ignored --nocapture --test-threads 1")
                .args(args.clone())
                .run()
                .with_context(|| "Failed to run tests for the Plugin System".to_string())?;
            Ok(())
        })
        .context(err_context)
}

fn cross_compile(sh: &Shell, target: &OsString, no_web: bool) -> anyhow::Result<()> {
    let err_context = || format!("failed to cross-compile for {target:?}");

    crate::cargo()
        .and_then(|cargo| {
            cmd!(sh, "{cargo} install mandown").run()?;
            Ok(cargo)
        })
        .and_then(|cargo| {
            cmd!(sh, "{cargo} install cross")
                .run()
                .map_err(anyhow::Error::new)
        })
        .with_context(err_context)?;

    build::build(
        sh,
        flags::Build {
            release: true,
            no_plugins: false,
            plugins_only: true,
            no_web,
        },
    )
    .and_then(|_| build::manpage(sh))
    .with_context(err_context)?;

    cross()
        .and_then(|cross| {
            if no_web {
                match metadata::get_no_web_features(sh, ".")
                    .context("Failed to check web features for cross compilation")?
                {
                    Some(features) => {
                        let mut cmd = cmd!(sh, "{cross} build --verbose --release --target {target} --no-default-features");
                        if !features.is_empty() {
                            cmd = cmd.arg("--features").arg(features);
                        }
                        cmd.run().map_err(anyhow::Error::new)
                    },
                    None => {
                        // Main crate doesn't have web_server_capability, build normally
                        cmd!(sh, "{cross} build --verbose --release --target {target}")
                            .run()
                            .map_err(anyhow::Error::new)
                    },
                }
            } else {
                cmd!(sh, "{cross} build --verbose --release --target {target}")
                    .run()
                    .map_err(anyhow::Error::new)
            }
        })
        .with_context(err_context)
}

fn cross() -> anyhow::Result<PathBuf> {
    match which::which("cross") {
        Ok(path) => Ok(path),
        Err(e) => {
            eprintln!("!! 'cross' wasn't found but is needed for this build step.");
            eprintln!("!! Please install it with: `cargo install cross`");
            Err(e).context("couldn't find 'cross' executable")
        },
    }
}
