// Copyright 2026 TiKV Project Authors. Licensed under Apache-2.0.

//! `materialize-config` subcommand.
//!
//! Reads a TiKV TOML config file, applies defaults, compatibility adjustments,
//! and full validation, then writes the fully-materialized config to an output
//! file.

use std::{
    error::Error,
    io::{Error as IoError, ErrorKind},
    path::{Path, PathBuf},
};

use clap::Args;
use tikv::config::{TikvConfig, write_config};

/// Default output filename when `--output` is not specified.
fn default_output_path() -> PathBuf {
    let hash = option_env!("TIKV_BUILD_GIT_HASH").unwrap_or("unknown");
    PathBuf::from(format!("tikv-{}.toml", hash))
}

#[derive(Args)]
pub struct MaterializeConfigArgs {
    /// Path to the input TiKV TOML config file.
    ///
    /// The config is fully validated, so referenced files such as TLS
    /// certificates must exist.
    #[clap(long, default_value = "tikv.toml")]
    pub config: PathBuf,

    /// Path to write the materialized config.
    ///
    /// Defaults to `tikv-<git-hash>.toml` in the current directory.
    /// Parent directories are created automatically if they do not exist.
    #[clap(long)]
    pub output: Option<PathBuf>,
}

fn ensure_no_unrecognized_config(unrecognized_keys: &[String]) -> Result<(), IoError> {
    if unrecognized_keys.is_empty() {
        return Ok(());
    }

    Err(IoError::new(
        ErrorKind::InvalidInput,
        format!(
            "unknown configuration options: {}",
            unrecognized_keys.join(", ")
        ),
    ))
}

/// Load, adjust, validate, and serialize a [`TikvConfig`] from `config_path`.
///
/// Returns the pretty-printed TOML string of the fully-materialized config.
fn materialize(config_path: &Path) -> Result<String, Box<dyn Error>> {
    let mut unrecognized_keys = Vec::new();
    let mut cfg = TikvConfig::from_file(config_path, Some(&mut unrecognized_keys))?;
    ensure_no_unrecognized_config(&unrecognized_keys)?;
    cfg.logger_compatible_adjust();
    cfg.compatible_adjust();
    cfg.adjust_cpu_limited_worker_counts();
    cfg.validate()?;
    let toml = toml::to_string_pretty(&cfg)?;
    Ok(toml)
}

/// Write `content` to `output_path`, creating parent directories as needed.
fn write_output(output_path: &Path, content: &str) -> Result<(), Box<dyn Error>> {
    if let Some(parent) = output_path.parent() {
        if !parent.as_os_str().is_empty() {
            std::fs::create_dir_all(parent)?;
        }
    }
    write_config(output_path, content.as_bytes())?;
    Ok(())
}

pub fn execute_materialize_config(args: MaterializeConfigArgs) {
    let output = args.output.unwrap_or_else(default_output_path);

    let content = materialize(&args.config).unwrap_or_else(|e| {
        eprintln!(
            "cse-ctl materialize-config: failed to materialize {:?}: {}",
            args.config, e
        );
        std::process::exit(1);
    });

    write_output(&output, &content).unwrap_or_else(|e| {
        eprintln!(
            "cse-ctl materialize-config: failed to write output {:?}: {}",
            output, e
        );
        std::process::exit(1);
    });

    println!("Materialized config written to {:?}", output);
}

#[cfg(test)]
mod tests {
    use std::fs;

    use clap::Parser;
    use tempfile::TempDir;
    use tikv::config::UNIFIED_READPOOL_MIN_CONCURRENCY;
    use tikv_util::sys::SysQuota;

    use super::*;

    fn new_test_config(tmp: &TempDir) -> TikvConfig {
        let data_dir = tmp.path().join("data");
        fs::create_dir_all(&data_dir).unwrap();

        let mut cfg = TikvConfig::default();
        cfg.storage.data_dir = data_dir.display().to_string();
        cfg
    }

    fn write_test_config(tmp: &TempDir, cfg: &TikvConfig) -> PathBuf {
        let config_path = tmp.path().join("tikv.toml");
        fs::write(&config_path, toml::to_string(cfg).unwrap()).unwrap();
        config_path
    }

    #[test]
    fn test_default_output_path_contains_hash_or_unknown() {
        let p = default_output_path();
        let name = p.to_str().unwrap();
        assert!(
            name.starts_with("tikv-"),
            "default output should start with 'tikv-', got: {name}"
        );
        assert!(
            name.ends_with(".toml"),
            "default output should end with '.toml', got: {name}"
        );
    }

    #[test]
    fn test_cli_defaults_for_materialize_config() {
        let cli = crate::Cli::parse_from(["cse-ctl", "materialize-config"]);
        match cli.command {
            crate::Commands::MaterializeConfig(args) => {
                assert_eq!(args.config, PathBuf::from("tikv.toml"));
                assert_eq!(args.output, None);
            }
            _ => panic!("expected materialize-config command"),
        }
    }

    #[test]
    fn test_materialize_config_to_temp_file() {
        let tmp = TempDir::new().unwrap();
        let mut cfg = new_test_config(&tmp);
        let read_pool_limit = std::cmp::max(
            UNIFIED_READPOOL_MIN_CONCURRENCY,
            SysQuota::cpu_cores_quota() as usize,
        );
        cfg.readpool.unified.max_thread_count = read_pool_limit + 1;
        let config_path = write_test_config(&tmp, &cfg);
        let output_path = tmp.path().join("out").join("materialized.toml");

        let content = materialize(&config_path).expect("materialize should succeed");
        write_output(&output_path, &content).expect("write_output should succeed");

        assert!(output_path.exists(), "output file should be created");
        let written = fs::read_to_string(&output_path).unwrap();
        assert_eq!(written, content);
        let reparsed: TikvConfig =
            toml::from_str(&written).expect("materialized TOML must be valid");
        assert_eq!(reparsed.readpool.unified.max_thread_count, read_pool_limit);
    }

    #[test]
    fn test_write_output_creates_parent_dirs() {
        let tmp = TempDir::new().unwrap();
        let nested = tmp.path().join("a").join("b").join("c").join("out.toml");
        write_output(&nested, "hello").unwrap();
        assert_eq!(fs::read_to_string(&nested).unwrap(), "hello");
    }

    #[test]
    fn test_materialize_rejects_unknown_keys() {
        let tmp = TempDir::new().unwrap();
        let cfg = new_test_config(&tmp);
        let config_path = tmp.path().join("tikv.toml");
        let mut content = toml::to_string(&cfg).unwrap();
        content.push_str("\n[definitely-unknown-section]\nvalue = true\n");
        fs::write(&config_path, content).unwrap();

        let err = materialize(&config_path).unwrap_err().to_string();
        assert!(
            err.contains("definitely-unknown-section"),
            "unexpected error: {err}"
        );
    }
}
