//! Schema fetching — spawns the configured backend's IR-export command
//! and parses the KDL it writes to stdout.
//!
//! Backends:
//!   - FastAPI: `python -m mizan_fastapi.ir <module>`
//!   - Django:  `python manage.py export_mizan_ir`
//!   - Rust:    `cargo run --bin <bin>` (consumer-side binary that
//!              force-links its `#[derive(Mizan)]` types and
//!              `#[mizan::client]` functions, then calls
//!              `mizan_core::build_ir()`).
//!
//! The Rust source supports an optional `[source.rust.pydantic]`
//! pre-step that invokes decoru on a Pydantic module to author the
//! Rust types before the cargo bin runs — the "Pydantic + Rust"
//! canonical DX.

use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};

use anyhow::{anyhow, Context, Result};
use serde_json::json;

use crate::config::{Config, DjangoSource, FastapiSource, PydanticPreStep, RustSource, ScriptSource};
use crate::ir::{parse_ir, MizanIR};


/// Embedded decoru bridge script — piped to `python -` at codegen time
/// when `[source.rust.pydantic]` is set. The script imports decoru,
/// walks the named module's BaseModel subclasses, and writes a Rust
/// file. See `scripts/run_decoru.py` for the full body.
const DECORU_BRIDGE_SCRIPT: &str = include_str!("../scripts/run_decoru.py");


pub fn fetch_schema(config: &Config, config_dir: &Path) -> Result<MizanIR> {
    let raw = if let Some(rs) = &config.source.rust {
        if let Some(py) = &rs.pydantic {
            run_pydantic_prestep(py, config_dir)
                .context("running [source.rust.pydantic] pre-step")?;
        }
        run_rust(rs, config_dir)?
    } else if let Some(fa) = &config.source.fastapi {
        run_fastapi(fa, config_dir)?
    } else if let Some(dj) = &config.source.django {
        run_django(dj, config_dir)?
    } else if let Some(sc) = &config.source.script {
        run_script(sc, config_dir)?
    } else {
        return Err(anyhow!(
            "config.source must declare one of [source.rust], [source.fastapi], [source.django], or [source.script]"
        ));
    };

    parse_ir(&raw).context("parsing Mizan IR from backend KDL output")
}


fn run_fastapi(src: &FastapiSource, config_dir: &Path) -> Result<String> {
    let cwd = match &src.cwd {
        Some(rel) => config_dir.join(rel),
        None => config_dir.to_path_buf(),
    };

    let (program, mut args) = resolve_command(&src.command, &src.python);
    args.extend([
        "-m".to_string(),
        "mizan_fastapi.ir".to_string(),
        src.module.clone(),
    ]);

    run_subprocess(&program, &args, &cwd, &src.env, "FastAPI IR export")
}


fn run_script(src: &ScriptSource, config_dir: &Path) -> Result<String> {
    let cwd = match &src.cwd {
        Some(rel) => config_dir.join(rel),
        None => config_dir.to_path_buf(),
    };
    let (program, args) = src.command.split_first().ok_or_else(|| {
        anyhow!("[source.script]: command must be non-empty")
    })?;
    run_subprocess(program, args, &cwd, &src.env, "script IR export")
}


fn run_django(src: &DjangoSource, config_dir: &Path) -> Result<String> {
    let manage_path = config_dir.join(&src.manage_path);
    let manage_dir = manage_path
        .parent()
        .ok_or_else(|| anyhow!("django manage_path has no parent: {}", manage_path.display()))?
        .to_path_buf();

    let (program, mut args) = resolve_command(&src.command, &src.python);

    if src.command.is_some() {
        args.push("manage.py".to_string());
    } else {
        args.push(manage_path.to_string_lossy().into_owned());
    }
    args.push("export_mizan_ir".to_string());

    run_subprocess(&program, &args, &manage_dir, &src.env, "Django IR export")
}


fn resolve_command(
    explicit: &Option<Vec<String>>,
    python_override: &Option<String>,
) -> (String, Vec<String>) {
    if let Some(cmd) = explicit {
        let (head, tail) = cmd.split_first().expect("command must be non-empty");
        return (head.clone(), tail.to_vec());
    }
    let python = python_override.as_deref().unwrap_or("python");
    (python.to_string(), Vec::new())
}


fn run_subprocess(
    program: &str,
    args: &[String],
    cwd: &Path,
    env: &std::collections::BTreeMap<String, String>,
    label: &str,
) -> Result<String> {
    let mut cmd = Command::new(program);
    cmd.args(args).current_dir(cwd);
    for (k, v) in env {
        cmd.env(k, v);
    }

    let output = cmd
        .output()
        .with_context(|| format!("spawning {label} ({program})"))?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        let stdout = String::from_utf8_lossy(&output.stdout);
        return Err(anyhow!(
            "{label} failed (exit {:?})\n--- stderr ---\n{stderr}\n--- stdout ---\n{stdout}",
            output.status.code(),
        ));
    }

    let stdout = String::from_utf8(output.stdout)
        .with_context(|| format!("{label}: non-UTF-8 stdout"))?;
    Ok(stdout)
}


fn run_rust(src: &RustSource, config_dir: &Path) -> Result<String> {
    let manifest = config_dir.join(
        src.manifest_path
            .clone()
            .unwrap_or_else(|| PathBuf::from("Cargo.toml")),
    );

    let mut args: Vec<String> = vec![
        "run".to_string(),
        "--quiet".to_string(),
        "--manifest-path".to_string(),
        manifest.to_string_lossy().into_owned(),
        "--bin".to_string(),
        src.bin.clone(),
    ];
    if src.release {
        args.push("--release".to_string());
    }
    if !src.features.is_empty() {
        args.push("--features".to_string());
        args.push(src.features.join(","));
    }

    run_subprocess("cargo", &args, config_dir, &src.env, "Rust IR export")
}


fn run_pydantic_prestep(src: &PydanticPreStep, config_dir: &Path) -> Result<()> {
    let cwd = match &src.cwd {
        Some(rel) => config_dir.join(rel),
        None => config_dir.to_path_buf(),
    };

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

    let payload = json!({
        "module": &src.module,
        "output": output_abs.to_string_lossy(),
        "derives": &src.derives,
        "header": &src.header,
    })
    .to_string();

    let (program, mut args) = resolve_command(&src.command, &src.python);
    // `python -` reads the script body from stdin; the JSON payload is
    // passed as argv[1] (which lands on sys.argv[1] inside the script).
    args.push("-".to_string());
    args.push(payload);

    let mut cmd = Command::new(&program);
    cmd.args(&args).current_dir(&cwd);
    for (k, v) in &src.env {
        cmd.env(k, v);
    }
    cmd.stdin(Stdio::piped());
    cmd.stdout(Stdio::inherit());
    cmd.stderr(Stdio::inherit());

    let mut child = cmd
        .spawn()
        .with_context(|| format!("spawning decoru bridge ({program})"))?;

    {
        let stdin = child
            .stdin
            .as_mut()
            .ok_or_else(|| anyhow!("failed to acquire decoru bridge stdin"))?;
        stdin
            .write_all(DECORU_BRIDGE_SCRIPT.as_bytes())
            .context("piping decoru bridge script to python")?;
    }

    let status = child
        .wait()
        .context("waiting for decoru bridge to complete")?;
    if !status.success() {
        return Err(anyhow!(
            "[source.rust.pydantic]: decoru bridge exited with status {:?}",
            status.code()
        ));
    }

    Ok(())
}


/// Library helper: parse a KDL IR from a string.
pub fn parse_ir_from_str(source: &str) -> Result<MizanIR> {
    parse_ir(source)
}


/// Library helper: resolve a path relative to the config directory.
pub fn resolve_path(config_dir: &Path, p: impl Into<PathBuf>) -> PathBuf {
    let p = p.into();
    if p.is_absolute() {
        p
    } else {
        config_dir.join(p)
    }
}
