// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2026 Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
//
// Build command generator for nimiser.
// Generates Nim compilation commands and shell scripts from manifest settings.
// Produces a build.sh script and a nim.cfg configuration file.

use crate::abi::CompilationTarget;
use crate::manifest::Manifest;

/// A complete Nim compilation command with all flags.
#[derive(Debug, Clone)]
pub struct NimBuildCommand {
    /// The nim compiler subcommand ("c", "cpp", or "js").
    pub backend: String,
    /// GC strategy flag: "--gc:arc", "--gc:orc", or "--gc:none".
    pub gc_flag: String,
    /// Optimisation flag: "-d:release", "-d:danger", or empty.
    pub opt_flag: String,
    /// Size optimisation: "--opt:size" or "--opt:speed".
    pub opt_level_flag: String,
    /// Additional user-specified flags from the manifest.
    pub extra_flags: Vec<String>,
    /// The source file to compile (library .nim file).
    pub source_file: String,
    /// Output binary/library name.
    pub output_name: String,
    /// Whether to compile as a shared library (--app:lib).
    pub as_library: bool,
}

impl NimBuildCommand {
    /// Render the full `nim` command line as a string.
    pub fn render(&self) -> String {
        let mut parts: Vec<String> = vec!["nim".into(), self.backend.clone()];

        // GC strategy
        parts.push(self.gc_flag.clone());

        // Optimisation
        if !self.opt_flag.is_empty() {
            parts.push(self.opt_flag.clone());
        }
        parts.push(self.opt_level_flag.clone());

        // Library mode
        if self.as_library {
            parts.push("--app:lib".into());
            parts.push("--noMain".into());
        }

        // Extra flags
        for flag in &self.extra_flags {
            parts.push(flag.clone());
        }

        // Output name
        parts.push(format!("-o:{}", self.output_name));

        // Source file
        parts.push(self.source_file.clone());

        parts.join(" ")
    }
}

/// Build a NimBuildCommand from the manifest settings.
pub fn build_command_from_manifest(
    manifest: &Manifest,
    source_file: &str,
    release: bool,
) -> NimBuildCommand {
    let backend = manifest.nim.backend.to_string();

    let gc_flag = format!("--gc:{}", manifest.nim.gc);

    let opt_flag = if release {
        "-d:release".to_string()
    } else {
        String::new()
    };

    let opt_level_flag = match manifest.nim.opt_level.as_str() {
        "size" => "--opt:size".into(),
        "speed" => "--opt:speed".into(),
        "none" => "--opt:none".into(),
        _ => "--opt:speed".into(),
    };

    // Determine output name based on backend and OS conventions.
    let output_name = match manifest.nim.backend {
        CompilationTarget::C | CompilationTarget::Cpp => {
            format!("lib{}.so", manifest.project.name.replace('-', "_"))
        }
        CompilationTarget::Js => {
            format!("{}.js", manifest.project.name.replace('-', "_"))
        }
    };

    NimBuildCommand {
        backend,
        gc_flag,
        opt_flag,
        opt_level_flag,
        extra_flags: manifest.nim.flags.clone(),
        source_file: source_file.to_string(),
        output_name,
        as_library: matches!(
            manifest.nim.backend,
            CompilationTarget::C | CompilationTarget::Cpp
        ),
    }
}

/// Generate a nim.cfg configuration file for the project.
/// This file is placed alongside the generated .nim source for reproducible builds.
pub fn generate_nim_cfg(manifest: &Manifest) -> String {
    let mut lines: Vec<String> = Vec::new();

    lines.push("# SPDX-License-Identifier: MPL-2.0".into());
    lines.push(format!(
        "# nim.cfg for {} — generated by nimiser",
        manifest.project.name
    ));
    lines.push("# DO NOT EDIT — regenerate with `nimiser generate`.".into());
    lines.push(String::new());

    // GC strategy
    lines.push(format!("gc = \"{}\"", manifest.nim.gc));

    // Optimisation
    lines.push(format!("opt = \"{}\"", manifest.nim.opt_level));

    // Backend-specific settings
    match manifest.nim.backend {
        CompilationTarget::C | CompilationTarget::Cpp => {
            lines.push("app = \"lib\"".into());
            lines.push("noMain".into());
        }
        CompilationTarget::Js => {
            // No special library flags for JS.
        }
    }

    // Extra flags
    for flag in &manifest.nim.flags {
        // Strip leading -- or - for nim.cfg format
        let cleaned = flag
            .strip_prefix("--")
            .or_else(|| flag.strip_prefix("-"))
            .unwrap_or(flag);
        lines.push(cleaned.to_string());
    }

    lines.push(String::new());
    lines.join("\n")
}

/// Generate a build.sh script that compiles the library.
pub fn generate_build_script(manifest: &Manifest, nim_source: &str) -> String {
    let cmd = build_command_from_manifest(manifest, nim_source, true);

    let mut lines: Vec<String> = Vec::new();
    lines.push("#!/usr/bin/env bash".into());
    lines.push("# SPDX-License-Identifier: MPL-2.0".into());
    lines.push(format!(
        "# Build script for {} — generated by nimiser",
        manifest.project.name
    ));
    lines.push("# DO NOT EDIT — regenerate with `nimiser generate`.".into());
    lines.push("set -euo pipefail".into());
    lines.push(String::new());

    lines.push("echo \"Building {} with Nim...\"".into());
    lines.push(cmd.render());
    lines.push(String::new());
    lines.push(format!("echo \"Built: {}\"", cmd.output_name));

    lines.join("\n")
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::abi::{CompilationTarget, GcStrategy};
    use crate::manifest::{Manifest, NimConfig, ProjectConfig};

    /// Helper: create a minimal manifest for testing.
    fn test_manifest(backend: CompilationTarget, gc: GcStrategy) -> Manifest {
        Manifest {
            project: ProjectConfig {
                name: "testlib".into(),
                version: "1.0.0".into(),
                description: "A test library".into(),
                author: String::new(),
            },
            functions: vec![],
            nim: NimConfig {
                backend,
                gc,
                opt_level: "speed".into(),
                flags: vec![],
                out_dir: "generated/nim".into(),
            },
            workload: None,
            data: None,
            options: None,
        }
    }

    #[test]
    fn test_build_command_c_arc() {
        let m = test_manifest(CompilationTarget::C, GcStrategy::Arc);
        let cmd = build_command_from_manifest(&m, "testlib.nim", true);
        let rendered = cmd.render();
        assert!(rendered.contains("nim c"));
        assert!(rendered.contains("--gc:arc"));
        assert!(rendered.contains("-d:release"));
        assert!(rendered.contains("--opt:speed"));
        assert!(rendered.contains("--app:lib"));
        assert!(rendered.contains("-o:libtestlib.so"));
        assert!(rendered.contains("testlib.nim"));
    }

    #[test]
    fn test_build_command_js_orc() {
        let m = test_manifest(CompilationTarget::Js, GcStrategy::Orc);
        let cmd = build_command_from_manifest(&m, "testlib.nim", false);
        let rendered = cmd.render();
        assert!(rendered.contains("nim js"));
        assert!(rendered.contains("--gc:orc"));
        assert!(!rendered.contains("-d:release")); // Not a release build.
        assert!(!rendered.contains("--app:lib")); // JS doesn't use --app:lib.
        assert!(rendered.contains("-o:testlib.js"));
    }

    #[test]
    fn test_build_command_cpp_none_gc() {
        let m = test_manifest(CompilationTarget::Cpp, GcStrategy::None);
        let cmd = build_command_from_manifest(&m, "lib.nim", true);
        let rendered = cmd.render();
        assert!(rendered.contains("nim cpp"));
        assert!(rendered.contains("--gc:none"));
    }

    #[test]
    fn test_generate_nim_cfg() {
        let m = test_manifest(CompilationTarget::C, GcStrategy::Arc);
        let cfg = generate_nim_cfg(&m);
        assert!(cfg.contains("gc = \"arc\""));
        assert!(cfg.contains("opt = \"speed\""));
        assert!(cfg.contains("app = \"lib\""));
        assert!(cfg.contains("noMain"));
    }

    #[test]
    fn test_generate_build_script() {
        let m = test_manifest(CompilationTarget::C, GcStrategy::Orc);
        let script = generate_build_script(&m, "mylib.nim");
        assert!(script.contains("#!/usr/bin/env bash"));
        assert!(script.contains("set -euo pipefail"));
        assert!(script.contains("nim c"));
        assert!(script.contains("--gc:orc"));
    }
}
