//! `mizan-generate` — Rust codegen binary.
//!
//! Replaces the Node-based `protocol/mizan-generate/generator/cli.mjs`.
//! Reads `mizan.toml`, spawns the configured backend to fetch the IR, and
//! dispatches each `--target` to its `CodegenTarget` impl. Per-target file
//! emission writes under the configured `output` directory.

use std::fs;
use std::path::{Path, PathBuf};

use anyhow::{Context, Result};
use clap::Parser;

use mizan_codegen::{config, emit, fetch};


#[derive(Parser, Debug)]
#[command(
    name = "mizan-generate",
    about = "Mizan code generator — consumes Mizan IR; emits typed clients.",
)]
struct Cli {
    /// Path to the codegen config file.
    #[arg(short, long, default_value = "mizan.toml")]
    config: PathBuf,

    /// Output directory (overrides `output` in config).
    #[arg(short, long)]
    output: Option<PathBuf>,

    /// Comma-separated list of targets (overrides `targets` in config).
    #[arg(short, long)]
    target: Option<String>,

    /// Read the IR from a JSON file instead of spawning the backend's
    /// schema-export command. The fixture path used by integration tests.
    #[arg(long)]
    from_json: Option<PathBuf>,
}


fn main() -> Result<()> {
    let cli = Cli::parse();

    let config_exists = cli.config.exists();
    let mut config: config::Config = if config_exists {
        let config_text = fs::read_to_string(&cli.config)
            .with_context(|| format!("reading config: {}", cli.config.display()))?;
        toml::from_str(&config_text)
            .with_context(|| format!("parsing TOML: {}", cli.config.display()))?
    } else if cli.from_json.is_some() {
        // --from-json bypasses the fetcher, so a missing config is fine —
        // CLI flags supply output + targets.
        config::Config {
            project_id: None,
            output: PathBuf::from("."),
            targets: vec![],
            source: Default::default(),
            rust_kernel: None,
            rust_crate_name: None,
        }
    } else {
        return Err(anyhow::anyhow!(
            "config not found: {} (pass --from-json to skip fetch)",
            cli.config.display(),
        ));
    };

    if let Some(o) = cli.output {
        config.output = o;
    }
    if let Some(t) = cli.target {
        config.targets = t.split(',').map(|s| s.trim().to_string()).collect();
    }

    let config_dir = if config_exists {
        resolve_config_dir(&cli.config)?
    } else {
        std::env::current_dir()?
    };

    let ir = if let Some(json_path) = &cli.from_json {
        let abs = if json_path.is_absolute() {
            json_path.clone()
        } else {
            config_dir.join(json_path)
        };
        eprintln!("[mizan] Reading IR from {}", abs.display());
        let raw = fs::read_to_string(&abs)
            .with_context(|| format!("read {}", abs.display()))?;
        fetch::parse_ir_from_str(&raw)?
    } else {
        eprintln!("[mizan] Fetching schema...");
        fetch::fetch_schema(&config, &config_dir)?
    };

    eprintln!(
        "[mizan] Loaded {} function(s), {} context group(s), {} type(s)",
        ir.functions.len(),
        ir.contexts.len(),
        ir.types.len(),
    );

    // Stage 1 is the framework-agnostic foundation that react/vue/svelte
    // import from. Auto-include it whenever any consumer of `./index`
    // (the Stage 1 re-export root) is in the target set.
    let needs_stage1 = config.targets.iter()
        .any(|t| matches!(t.as_str(), "react" | "vue" | "svelte"));
    if needs_stage1 && !config.targets.iter().any(|t| t == "stage1") {
        config.targets.insert(0, "stage1".to_string());
    }
    // Channels schema piggybacks on the main schema (x-mizan-channels);
    // auto-include the channels emit when react is the target and the
    // schema actually carries channels.
    if config.targets.iter().any(|t| t == "react")
        && !ir.channels.is_empty()
        && !config.targets.iter().any(|t| t == "channels")
    {
        config.targets.push("channels".to_string());
    }

    eprintln!("[mizan] Targets: {}", config.targets.join(", "));

    let output_dir = if config.output.is_absolute() {
        config.output.clone()
    } else {
        config_dir.join(&config.output)
    };

    for target_name in &config.targets {
        let Some(target) = emit::target_by_name(target_name) else {
            eprintln!("[mizan] WARN: target '{target_name}' has no emitter yet (Phase 2 scaffold)");
            continue;
        };
        let files = target.emit(&ir, &config);
        for file in files {
            let path = output_dir.join(&file.rel_path);
            write_output(&path, &file.content)?;
            eprintln!("[mizan] {} -> {}", target.name(), file.rel_path.display());
        }
    }

    eprintln!("[mizan] Generation complete.");
    Ok(())
}


fn resolve_config_dir(config_path: &Path) -> Result<PathBuf> {
    let abs = fs::canonicalize(config_path)
        .with_context(|| format!("canonicalize {}", config_path.display()))?;
    Ok(abs
        .parent()
        .map(|p| p.to_path_buf())
        .unwrap_or_else(|| PathBuf::from(".")))
}


fn write_output(path: &Path, content: &str) -> Result<()> {
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent)
            .with_context(|| format!("mkdir {}", parent.display()))?;
    }
    fs::write(path, content).with_context(|| format!("write {}", path.display()))
}
