//! `wai generate` — make a `wai.generate.spectral` picture natively, with the
//! reference generator (`wai::generate`), and print its SHA-256: the native pin a
//! page or another sink checks its own output against (SPEC §5, *Payload of
//! `wai.generate.spectral`*; §7, *Digests*). A build with the feature `generate`.
//!
//! ```text
//! wai generate --model <json> --prompt <s> --seed <u32> --steps <n> --size WxH -o <out.rgb> [--json]
//! wai generate --model <json> --instr <object> --size WxH -o <out.rgb> [--json]
//! wai generate --model <json> --prompt <s> --seed <u32> --steps <n> --sizes WxH,WxH,... --write-instr <object>
//! ```
//!
//! The first form makes one picture from flags; the second makes one listed size of
//! a form-v0 instruction object, which it reads as a sink does (the model's SHA-256
//! must be the object's `model`, and the size must be listed); the third writes the
//! canonical object for the model and the flags. Every form holds the flags to the
//! bounds of form v0, so whatever it makes, an instruction object can name.
//!
//! The output is RGB8 — rows top to bottom, pixels left to right, R, G, B bytes —
//! `w·h·3` bytes. `--json` prints `{"w","h","bytes","sha256"}`.

use super::instr::{self, Instruction};
use super::opt;
use serde_json::Value;
use wai::generate::{generate, SpectralModel};

const USAGE: &str = "usage:
  wai generate --model <json> --prompt <s> --seed <u32> --steps <n> --size WxH -o <out.rgb> [--json]
  wai generate --model <json> --instr <object> --size WxH -o <out.rgb> [--json]
  wai generate --model <json> --prompt <s> --seed <u32> --steps <n> --sizes WxH,... --write-instr <object>";

/// Read the model file and check it before the reference generator reads it: every
/// field `SpectralModel::from_value` unwraps is present with its type, and every value
/// lies within bounds under which no sum or product of `generate` leaves `i64` at any
/// size form v0 allows. The committed model sits well inside them (a 1024-entry LUT of
/// ±16384, 6 amplitude bits, 8 + 8 components, lexicon values within ±470). A file
/// outside them is refused with a reason rather than panicking or wrapping.
fn load_model(path: &str) -> Result<(Vec<u8>, SpectralModel), String> {
    let bytes = std::fs::read(path).map_err(|e| format!("read {path}: {e}"))?;
    let v: Value = serde_json::from_slice(&bytes).map_err(|e| format!("{path}: not JSON: {e}"))?;
    let within = |k: &str, lo: i64, hi: i64| -> Result<i64, String> {
        v.get(k).and_then(Value::as_i64).filter(|n| (lo..=hi).contains(n))
            .ok_or_else(|| format!("{path}: `{k}` is not an integer from {lo} to {hi}"))
    };
    let lut_size = within("lut_size", 1, 65536)?;
    let lut = v.get("sine_lut").and_then(Value::as_array).ok_or_else(|| format!("{path}: `sine_lut` is not an array"))?;
    if lut.len() as i64 != lut_size {
        return Err(format!("{path}: `sine_lut` has {} entries and `lut_size` is {lut_size}", lut.len()));
    }
    if lut.iter().any(|n| n.as_i64().is_none_or(|n| n.unsigned_abs() > 1 << 15)) {
        return Err(format!("{path}: a `sine_lut` entry is not an integer within ±2^15"));
    }
    let co = v.get("color_offsets").and_then(Value::as_array).ok_or_else(|| format!("{path}: no `color_offsets`"))?;
    if co.len() != 3 || co.iter().any(|n| n.as_i64().is_none_or(|n| n.unsigned_abs() > 1 << 20)) {
        return Err(format!("{path}: `color_offsets` is not three integers within ±2^20"));
    }
    within("freq_max", 1, 4096)?;
    within("amp_bits", 0, 12)?;
    if v.get("lut_bits").is_some() {
        within("lut_bits", 0, 15)?;
    }
    within("base_components", 0, 64)?;
    within("step_components", 0, 64)?;
    if let Some(lex) = v.get("lexicon") {
        let lex = lex.as_object().ok_or_else(|| format!("{path}: `lexicon` is not an object"))?;
        for (word, e) in lex {
            let ok = e.as_array().is_some_and(|a| {
                a.len() <= 10 && a.iter().all(|n| n.as_i64().is_some_and(|n| n.unsigned_abs() <= 1 << 16))
            });
            if !ok {
                return Err(format!("{path}: lexicon entry `{word}` is not at most 10 integers within ±2^16"));
            }
        }
    }
    Ok((bytes, SpectralModel::from_value(&v)))
}

fn num<T: std::str::FromStr>(args: &[String], flag: &str) -> Result<T, String> {
    let s = opt(args, flag).ok_or_else(|| format!("{flag} is required\n{USAGE}"))?;
    s.parse().map_err(|_| format!("{flag} `{s}` is not a number in range"))
}

/// The flags' `(prompt, seed, steps)`.
fn flags(args: &[String]) -> Result<(String, u32, u32), String> {
    let prompt = opt(args, "--prompt").ok_or_else(|| format!("--prompt is required\n{USAGE}"))?.to_string();
    Ok((prompt, num::<u32>(args, "--seed")?, num::<u32>(args, "--steps")?))
}

fn write_and_report(rgb: &[u8], w: u32, h: u32, out: &str, json: bool) -> Result<(), String> {
    crate::out::write(out, rgb).map_err(|e| format!("write {out}: {e}"))?;
    let sha = wai::pin::sha256_hex(rgb);
    if json {
        println!("{}", serde_json::json!({ "w": w, "h": h, "bytes": rgb.len(), "sha256": sha }));
    } else {
        println!("generated → {out}  ({w}x{h} RGB8, {} B)  sha256 {sha}", rgb.len());
    }
    Ok(())
}

pub fn main(args: &[String]) -> Result<(), String> {
    if args.is_empty() || args.iter().any(|a| a == "-h" || a == "--help") {
        return Err(USAGE.into());
    }
    let model_path = opt(args, "--model").ok_or_else(|| format!("--model is required\n{USAGE}"))?;
    let json = args.iter().any(|a| a == "--json");

    if let Some(dest) = opt(args, "--write-instr") {
        let (prompt, seed, steps) = flags(args)?;
        let list = opt(args, "--sizes").ok_or_else(|| format!("--sizes is required with --write-instr\n{USAGE}"))?;
        let sizes = list.split(',').map(instr::parse_size).collect::<Result<Vec<_>, _>>()?;
        let (bytes, _) = load_model(model_path)?;
        let i = Instruction { model_sha256: wai::pin::sha256_hex(&bytes), prompt, seed, steps, sizes };
        i.check().map_err(|e| e.to_string())?;
        let object = i.canonical();
        // What is written is what a sink reads.
        debug_assert_eq!(Instruction::parse(&object).as_ref(), Ok(&i));
        crate::out::write(dest, &object).map_err(|e| format!("write {dest}: {e}"))?;
        println!("instruction object → {dest}  ({} B)", object.len());
        return Ok(());
    }

    let out = opt(args, "-o").ok_or_else(|| format!("-o is required\n{USAGE}"))?;
    let (w, h) = instr::parse_size(opt(args, "--size").ok_or_else(|| format!("--size is required\n{USAGE}"))?)?;
    let (bytes, model) = load_model(model_path)?;

    let (prompt, seed, steps) = if let Some(path) = opt(args, "--instr") {
        let object = std::fs::read(path).map_err(|e| format!("read {path}: {e}"))?;
        let i = Instruction::parse(&object).map_err(|e| format!("{path}: refused: {e}"))?;
        let held = wai::pin::sha256_hex(&bytes);
        if held != i.model_sha256 {
            return Err(format!("{path}: refused: the object's model is sha256:{}, and {model_path} is sha256:{held}",
                               i.model_sha256));
        }
        if !i.lists(w, h) {
            return Err(format!("{path}: refused: {w}x{h} is not a listed size; a sink makes only listed sizes"));
        }
        (i.prompt, i.seed, i.steps)
    } else {
        let (prompt, seed, steps) = flags(args)?;
        // The same bounds as an instruction object, so every pin this writes is one
        // an object can name.
        Instruction { model_sha256: wai::pin::sha256_hex(&bytes), prompt: prompt.clone(), seed, steps, sizes: vec![(w, h)] }
            .check().map_err(|e| e.to_string())?;
        (prompt, seed, steps)
    };

    let rgb = generate(&model, &prompt, seed as u64, steps as usize, h as usize, w as usize);
    write_and_report(&rgb, w, h, out, json)
}
