//! Schema definitions for kaze.toml configuration.

use indexmap::IndexMap;
use serde::Deserialize;

/// Build phases for extra argument lists.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Phase {
    /// Configure phase.
    Conf,
    /// Build phase.
    Build,
    /// Flash phase.
    Flash,
    /// Run phase.
    Run,
}

/**
 * Top-level configuration file schema.
 *
 * Example `kaze.toml` shape:
 * ```toml
 * [project]
 * name = "demo"
 * board = "native_sim"
 * default_profile = "dev"
 *
 * [build]
 * root = "build"
 *
 * [args]
 * build = ["-DOPT=1"]
 *
 * [profiles.dev]
 * runner = "native"
 * ```
 */
#[derive(Debug, Clone, Deserialize, Default)]
pub struct FileConfig {
    /// Project configuration settings.
    #[serde(default)]
    pub project: ProjectCfg,

    /// Zephyr workspace configuration settings.
    #[serde(default)]
    pub zephyr: ZephyrCfg,

    /// Build output configuration settings.
    #[serde(default)]
    pub build: BuildCfg,

    /// Extra arguments per phase.
    #[serde(default)]
    pub args: PhaseArgsCfg,

    /// Profile-specific configuration overrides.
    #[serde(default)]
    pub profiles: IndexMap<String, ProfileCfg>,
}

impl FileConfig {
    /**
     * Overlays another configuration on top of this one.
     *
     * # Returns
     * Merged configuration with RHS values taking precedence.
     */
    #[must_use]
    pub fn overlay(mut self, rhs: Self) -> Self {
        self.project = self.project.overlay(rhs.project);
        self.zephyr = self.zephyr.overlay(rhs.zephyr);
        self.build = self.build.overlay(rhs.build);
        self.args = self.args.overlay(rhs.args);

        for (name, prof_rhs) in rhs.profiles {
            match self.profiles.get_mut(&name) {
                Some(prof_lhs) => {
                    *prof_lhs = prof_lhs.clone().overlay(prof_rhs);
                }
                None => {
                    self.profiles.insert(name, prof_rhs);
                }
            }
        }

        self
    }
}

/// Project-level configuration values.
#[derive(Debug, Clone, Deserialize, Default)]
pub struct ProjectCfg {
    /// Default board selection.
    pub board: Option<String>,
    /// Default runner selection.
    pub runner: Option<String>,
    /// Default profile name.
    pub default_profile: Option<String>,
    /// Project name override.
    pub name: Option<String>,
}

impl ProjectCfg {
    /**
     * Overlays another project config on top of this one.
     *
     * # Returns
     * Merged configuration with RHS values taking precedence.
     */
    #[must_use]
    pub fn overlay(mut self, rhs: Self) -> Self {
        if rhs.board.is_some() {
            self.board = rhs.board;
        }
        if rhs.runner.is_some() {
            self.runner = rhs.runner;
        }
        if rhs.default_profile.is_some() {
            self.default_profile = rhs.default_profile;
        }
        if rhs.name.is_some() {
            self.name = rhs.name;
        }

        self
    }
}

/// Zephyr workspace configuration values.
#[derive(Debug, Clone, Deserialize, Default)]
pub struct ZephyrCfg {
    /// Explicit Zephyr workspace path.
    pub workspace: Option<String>,
    /// Explicit Zephyr base path.
    pub base: Option<String>,
}

impl ZephyrCfg {
    /**
     * Overlays another Zephyr config on top of this one.
     *
     * # Returns
     * Merged configuration with RHS values taking precedence.
     */
    #[must_use]
    pub fn overlay(mut self, rhs: Self) -> Self {
        if rhs.workspace.is_some() {
            self.workspace = rhs.workspace;
        }
        if rhs.base.is_some() {
            self.base = rhs.base;
        }

        self
    }
}

/// Build output configuration values.
#[derive(Debug, Clone, Deserialize, Default)]
pub struct BuildCfg {
    /// Build output root directory.
    #[serde(default = "default_build_root")]
    pub root: String,

    /// Whether to link `compile_commands.json`.
    #[serde(default)]
    pub link_compile_commands: Option<bool>,
}

/**
 * Returns the default build output directory name.
 *
 * # Returns
 * Default build root name (`"build"`).
 */
fn default_build_root() -> String {
    "build".into()
}

impl BuildCfg {
    /**
     * Overlays another build config on top of this one.
     *
     * # Returns
     * Merged configuration with RHS values taking precedence.
     */
    #[must_use]
    pub fn overlay(mut self, rhs: Self) -> Self {
        if !rhs.root.is_empty() {
            self.root = rhs.root;
        }
        if rhs.link_compile_commands.is_some() {
            self.link_compile_commands = rhs.link_compile_commands;
        }

        self
    }
}

/// Profile-specific configuration overrides.
#[derive(Debug, Clone, Deserialize, Default)]
pub struct ProfileCfg {
    /// Profile board selection.
    pub board: Option<String>,
    /// Profile runner selection.
    pub runner: Option<String>,

    /// Phase-specific args for this profile.
    #[serde(default)]
    pub args: PhaseArgsCfg,
}

impl ProfileCfg {
    /**
     * Overlays another profile config on top of this one.
     *
     * # Returns
     * Merged configuration with RHS values taking precedence.
     */
    #[must_use]
    pub fn overlay(mut self, rhs: Self) -> Self {
        if rhs.board.is_some() {
            self.board = rhs.board;
        }
        if rhs.runner.is_some() {
            self.runner = rhs.runner;
        }
        self.args = self.args.overlay(rhs.args);

        self
    }
}

/// Extra arguments per build phase.
#[derive(Debug, Clone, Deserialize, Default)]
pub struct PhaseArgsCfg {
    /// Configure phase arguments.
    #[serde(default)]
    pub conf: ArgList,

    /// Build phase arguments.
    #[serde(default)]
    pub build: ArgList,

    /// Flash phase arguments.
    #[serde(default)]
    pub flash: ArgList,

    /// Run phase arguments.
    #[serde(default)]
    pub run: ArgList,
}

impl PhaseArgsCfg {
    /**
     * Overlays another phase-args config on top of this one.
     *
     * # Returns
     * Merged configuration with RHS values taking precedence.
     */
    #[must_use]
    pub fn overlay(mut self, rhs: Self) -> Self {
        if !rhs.conf.0.is_empty() {
            self.conf = rhs.conf;
        }
        if !rhs.build.0.is_empty() {
            self.build = rhs.build;
        }
        if !rhs.flash.0.is_empty() {
            self.flash = rhs.flash;
        }
        if !rhs.run.0.is_empty() {
            self.run = rhs.run;
        }

        self
    }
}

/**
 * Argument list allowing single strings or arrays in TOML.
 *
 * Examples:
 * ```toml
 * [args]
 * conf = "-DCONF=1"
 * build = ["-DOPT=1", "-DDBG=1"]
 * ```
 */
#[derive(Debug, Clone, Deserialize, Default)]
pub struct ArgList(#[serde(default)] pub Vec<ArgAtom>);

/// Argument list element.
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
pub enum ArgAtom {
    /// Single argument entry.
    One(String),
    /// Multiple argument entries.
    Many(Vec<String>),
}

impl ArgList {
    /**
     * Flattens the argument list into a vector.
     *
     * # Returns
     * Flattened arguments.
     */
    #[must_use]
    pub fn to_vec(&self) -> Vec<String> {
        let mut out = Vec::new();
        for a in &self.0 {
            match *a {
                ArgAtom::One(ref s) => out.push(s.clone()),
                ArgAtom::Many(ref v) => out.extend(v.iter().cloned()),
            }
        }

        out
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn arglist_to_vec_flattens() {
        let list = ArgList(vec![
            ArgAtom::One("a".to_owned()),
            ArgAtom::Many(vec!["b".to_owned(), "c".to_owned()]),
        ]);
        assert_eq!(list.to_vec(), vec!["a", "b", "c"]);
    }

    #[test]
    fn arglist_deserialize_single_and_list() {
        let toml = r#"
[args]
conf = ["-DCONF=1"]
build = [["-DOPT=1", "-DDBG=1"]]
"#;
        let cfg: FileConfig = toml::from_str(toml).expect("parse ok");
        assert_eq!(cfg.args.conf.to_vec(), vec!["-DCONF=1"]);
        assert_eq!(cfg.args.build.to_vec(), vec!["-DOPT=1", "-DDBG=1"]);
    }

    #[test]
    fn project_cfg_overlay_prefers_rhs() {
        let lhs = ProjectCfg {
            board: Some("b1".into()),
            runner: Some("r1".into()),
            default_profile: Some("p1".into()),
            name: Some("n1".into()),
        };
        let rhs = ProjectCfg {
            board: Some("b2".into()),
            runner: None,
            default_profile: Some("p2".into()),
            name: None,
        };
        let merged = lhs.overlay(rhs);
        assert_eq!(merged.board, Some("b2".into()));
        assert_eq!(merged.runner, Some("r1".into()));
        assert_eq!(merged.default_profile, Some("p2".into()));
        assert_eq!(merged.name, Some("n1".into()));
    }

    #[test]
    fn zephyr_cfg_overlay_prefers_rhs() {
        let lhs = ZephyrCfg {
            workspace: Some("ws1".into()),
            base: Some("zb1".into()),
        };
        let rhs = ZephyrCfg {
            workspace: None,
            base: Some("zb2".into()),
        };
        let merged = lhs.overlay(rhs);
        assert_eq!(merged.workspace, Some("ws1".into()));
        assert_eq!(merged.base, Some("zb2".into()));
    }

    #[test]
    fn build_cfg_overlay_respects_root_and_flags() {
        let lhs = BuildCfg {
            root: "build".into(),
            link_compile_commands: Some(false),
        };
        let rhs = BuildCfg {
            root: String::new(),
            link_compile_commands: Some(true),
        };
        let merged = lhs.overlay(rhs);
        assert_eq!(merged.root, "build");
        assert_eq!(merged.link_compile_commands, Some(true));
    }

    #[test]
    fn phase_args_overlay_only_overrides_when_non_empty() {
        let lhs = PhaseArgsCfg {
            conf: ArgList(vec![ArgAtom::One("c1".into())]),
            build: ArgList(vec![ArgAtom::One("b1".into())]),
            flash: ArgList(vec![ArgAtom::One("f1".into())]),
            run: ArgList(vec![ArgAtom::One("r1".into())]),
        };
        let rhs = PhaseArgsCfg {
            conf: ArgList::default(),
            build: ArgList(vec![ArgAtom::One("b2".into())]),
            flash: ArgList::default(),
            run: ArgList::default(),
        };
        let merged = lhs.overlay(rhs);
        assert_eq!(merged.conf.to_vec(), vec!["c1"]);
        assert_eq!(merged.build.to_vec(), vec!["b2"]);
        assert_eq!(merged.flash.to_vec(), vec!["f1"]);
        assert_eq!(merged.run.to_vec(), vec!["r1"]);
    }

    #[test]
    fn profile_cfg_overlay_merges_args() {
        let lhs = ProfileCfg {
            board: Some("b1".into()),
            runner: None,
            args: PhaseArgsCfg {
                build: ArgList(vec![ArgAtom::One("b1".into())]),
                ..PhaseArgsCfg::default()
            },
        };
        let rhs = ProfileCfg {
            board: None,
            runner: Some("r1".into()),
            args: PhaseArgsCfg {
                build: ArgList(vec![ArgAtom::One("b2".into())]),
                ..PhaseArgsCfg::default()
            },
        };
        let merged = lhs.overlay(rhs);
        assert_eq!(merged.board, Some("b1".into()));
        assert_eq!(merged.runner, Some("r1".into()));
        assert_eq!(merged.args.build.to_vec(), vec!["b2"]);
    }

    #[test]
    fn file_config_overlay_merges_profiles() {
        let mut lhs = FileConfig::default();
        lhs.project.board = Some("b1".into());
        lhs.profiles.insert(
            "dev".into(),
            ProfileCfg {
                board: Some("b2".into()),
                runner: None,
                args: PhaseArgsCfg::default(),
            },
        );

        let mut rhs = FileConfig::default();
        rhs.project.runner = Some("r1".into());
        rhs.profiles.insert(
            "dev".into(),
            ProfileCfg {
                board: None,
                runner: Some("r2".into()),
                args: PhaseArgsCfg::default(),
            },
        );
        rhs.profiles.insert(
            "prod".into(),
            ProfileCfg {
                board: Some("b3".into()),
                runner: Some("r3".into()),
                args: PhaseArgsCfg::default(),
            },
        );

        let merged = lhs.overlay(rhs);
        assert_eq!(merged.project.board, Some("b1".into()));
        assert_eq!(merged.project.runner, Some("r1".into()));
        assert_eq!(
            merged
                .profiles
                .get("dev")
                .expect("dev profile present")
                .board,
            Some("b2".into())
        );
        assert_eq!(
            merged
                .profiles
                .get("dev")
                .expect("dev profile present")
                .runner,
            Some("r2".into())
        );
        assert!(merged.profiles.get("prod").is_some());
    }

    #[test]
    fn build_cfg_overlay_empty_root_keeps_lhs() {
        let lhs = BuildCfg {
            root: "build".into(),
            link_compile_commands: None,
        };
        let rhs = BuildCfg {
            root: String::new(),
            link_compile_commands: None,
        };
        let merged = lhs.overlay(rhs);
        assert_eq!(merged.root, "build");
        assert_eq!(merged.link_compile_commands, None);
    }

    #[test]
    fn file_config_overlay_replaces_profiles_when_missing_lhs() {
        let lhs = FileConfig::default();
        let mut rhs = FileConfig::default();
        rhs.profiles.insert("dev".into(), ProfileCfg::default());

        let merged = lhs.overlay(rhs);
        assert!(merged.profiles.contains_key("dev"));
    }

    #[test]
    fn arglist_deserialize_many_entry() {
        let toml = r#"
[args]
conf = [["-DCONF=1", "-DDBG=1"]]
"#;
        let cfg: FileConfig = toml::from_str(toml).expect("parse ok");
        assert_eq!(cfg.args.conf.to_vec(), vec!["-DCONF=1", "-DDBG=1"]);
    }
}
