// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html

use std::env;

use anyhow::Result;
use ninja_gen::action::BuildAction;
use ninja_gen::build::BuildProfile;
use ninja_gen::build::FilesHandle;
use ninja_gen::cargo::CargoBuild;
use ninja_gen::cargo::CargoClippy;
use ninja_gen::cargo::CargoFormat;
use ninja_gen::cargo::CargoTest;
use ninja_gen::cargo::RustOutput;
use ninja_gen::git::SyncSubmodule;
use ninja_gen::glob;
use ninja_gen::hash::simple_hash;
use ninja_gen::input::BuildInput;
use ninja_gen::inputs;
use ninja_gen::Build;

use crate::platform::overriden_rust_target_triple;

pub fn build_rust(build: &mut Build) -> Result<()> {
    prepare_translations(build)?;
    build_proto_descriptors_and_interfaces(build)?;
    build_rsbridge(build)
}

fn prepare_translations(build: &mut Build) -> Result<()> {
    let offline_build = env::var("OFFLINE_BUILD").is_ok();

    // ensure repos are checked out
    build.add_action(
        "ftl:repo:core",
        SyncSubmodule {
            path: "ftl/core-repo",
            offline_build,
        },
    )?;
    build.add_action(
        "ftl:repo:qt",
        SyncSubmodule {
            path: "ftl/qt-repo",
            offline_build,
        },
    )?;
    // build anki_i18n and spit out strings.json
    build.add_action(
        "rslib:i18n",
        CargoBuild {
            inputs: inputs![
                glob!["rslib/i18n/**"],
                glob!["ftl/{core,core-repo,qt,qt-repo}/**"],
                ":ftl:repo",
            ],
            outputs: &[
                RustOutput::Data("py", "pylib/anki/_fluent.py"),
                RustOutput::Data("ts", "ts/lib/generated/ftl.ts"),
            ],
            target: None,
            extra_args: "-p anki_i18n",
            release_override: None,
        },
    )?;

    build.add_action(
        "ftl:bin",
        CargoBuild {
            inputs: inputs![glob!["ftl/**"],],
            outputs: &[RustOutput::Binary("ftl")],
            target: None,
            extra_args: "-p ftl",
            release_override: None,
        },
    )?;

    // These don't use :group notation, as it doesn't make sense to invoke multiple
    // commands as a group.
    build.add_action(
        "ftl-sync",
        FtlCommand {
            args: "sync",
            deps: inputs![":ftl:repo", glob!["ftl/**"]],
        },
    )?;

    build.add_action(
        "ftl-deprecate",
        FtlCommand {
            args: "deprecate --ftl-roots ftl/core ftl/qt --source-roots pylib qt rslib ts --json-roots ftl/usage",
            deps: inputs!["ftl/core", "ftl/qt", "pylib", "qt", "rslib", "ts"],
        },
    )?;

    Ok(())
}

struct FtlCommand {
    args: &'static str,
    deps: BuildInput,
}

impl BuildAction for FtlCommand {
    fn command(&self) -> &str {
        "$ftl_bin $args"
    }

    fn files(&mut self, build: &mut impl FilesHandle) {
        build.add_inputs("", &self.deps);
        build.add_inputs("ftl_bin", inputs![":ftl:bin"]);
        build.add_variable("args", self.args);
        build.add_output_stamp(format!("ftl/stamp.{}", simple_hash(self.args)));
    }
}

fn build_proto_descriptors_and_interfaces(build: &mut Build) -> Result<()> {
    let outputs = vec![
        RustOutput::Data("descriptors.bin", "rslib/proto/descriptors.bin"),
        RustOutput::Data("py", "pylib/anki/_backend_generated.py"),
        RustOutput::Data("ts", "ts/lib/generated/backend.ts"),
    ];
    build.add_action(
        "rslib:proto",
        CargoBuild {
            inputs: inputs![glob!["{proto,rslib/proto}/**"], ":protoc_binary",],
            outputs: &outputs,
            target: None,
            extra_args: "-p anki_proto",
            release_override: None,
        },
    )?;
    Ok(())
}

fn build_rsbridge(build: &mut Build) -> Result<()> {
    let features = if cfg!(target_os = "linux") {
        "rustls"
    } else {
        "native-tls"
    };
    build.add_action(
        "pylib:rsbridge",
        CargoBuild {
            inputs: inputs![
                glob!["{pylib/rsbridge/**,rslib/**}"],
                // declare a dependency on i18n/proto so they get built first, allowing
                // things depending on them to build faster, and ensuring
                // changes to the ftl files trigger a rebuild
                ":rslib:i18n",
                ":rslib:proto",
                // when env vars change the build hash gets updated
                "$builddir/env",
                "$builddir/buildhash",
                // building on Windows requires python3.lib
                if cfg!(windows) {
                    inputs![":extract:python"]
                } else {
                    inputs![]
                }
            ],
            outputs: &[RustOutput::DynamicLib("rsbridge")],
            target: overriden_rust_target_triple(),
            extra_args: &format!("-p rsbridge --features {features}"),
            release_override: None,
        },
    )
}

pub fn check_rust(build: &mut Build) -> Result<()> {
    let inputs = inputs![
        glob!("{rslib/**,pylib/rsbridge/**,ftl/**,build/**,tools/workspace-hack/**}"),
        "Cargo.lock",
        "Cargo.toml",
        "rust-toolchain.toml",
    ];
    build.add_action(
        "check:format:rust",
        CargoFormat {
            inputs: inputs.clone(),
            check_only: true,
            working_dir: Some("cargo/format"),
        },
    )?;
    build.add_action(
        "format:rust",
        CargoFormat {
            inputs: inputs.clone(),
            check_only: false,
            working_dir: Some("cargo/format"),
        },
    )?;

    let inputs = inputs![
        inputs,
        // defer tests until build has completed; ensure re-run on changes
        ":pylib:rsbridge"
    ];

    build.add_action(
        "check:clippy",
        CargoClippy {
            inputs: inputs.clone(),
        },
    )?;
    build.add_action("check:rust_test", CargoTest { inputs })?;

    Ok(())
}

pub fn check_minilints(build: &mut Build) -> Result<()> {
    struct RunMinilints {
        pub deps: BuildInput,
        pub fix: bool,
    }

    impl BuildAction for RunMinilints {
        fn command(&self) -> &str {
            "$minilints_bin $fix $stamp"
        }

        fn bypass_runner(&self) -> bool {
            true
        }

        fn files(&mut self, build: &mut impl FilesHandle) {
            build.add_inputs("minilints_bin", inputs![":build:minilints"]);
            build.add_inputs("", &self.deps);
            build.add_variable("fix", if self.fix { "fix" } else { "check" });
            build.add_output_stamp(format!("tests/minilints.{}", self.fix));
        }

        fn on_first_instance(&self, build: &mut Build) -> Result<()> {
            build.add_action(
                "build:minilints",
                CargoBuild {
                    inputs: inputs![glob!("tools/minilints/**/*")],
                    outputs: &[RustOutput::Binary("minilints")],
                    target: None,
                    extra_args: "-p minilints",
                    release_override: Some(BuildProfile::Debug),
                },
            )
        }
    }

    let files = inputs![
        glob![
            "**/*.{py,rs,ts,svelte,mjs}",
            "{node_modules,qt/bundle/PyOxidizer,ts/.svelte-kit}/**"
        ],
        "Cargo.lock"
    ];

    build.add_action(
        "check:minilints",
        RunMinilints {
            deps: files.clone(),
            fix: false,
        },
    )?;
    build.add_action(
        "fix:minilints",
        RunMinilints {
            deps: files,
            fix: true,
        },
    )?;
    Ok(())
}
