//! `wai upscale run` and `wai tonemap run`: an exact derivation of a picture, with the signed record
//! a verifier recomputes, and their `verify` (extensions/exact-upscale.md, extensions/exact-tonemap.md).
//!
//! ```text
//! wai upscale run <in.png> --prior <p.wsr> [--derivation sr2|sr4] -o <out.png>
//!     [--record <out.json> --envelope <in.wai> --key <seed>] [--issued-at <date-time>] [--json]
//! wai upscale verify <record.json> --envelope <in.wai> --prior <p.wsr> [--picture <out.png>]
//! wai tonemap run <in.wai> --headrooms <h,h,...> --out <dir> [--input-class <class>]
//!     [--record <out.json> --key <seed>] [--issued-at <date-time>] [--json]
//! wai tonemap verify <record.json> --envelope <in.wai> [--renditions <dir>]
//! ```
//!
//! Each writes what it derives, and with `--record` signs the record of it. `--json` prints the
//! picture's size, the subpixels, each file with its SHA-256 and the record's link hash.

use super::opt;
use ed25519_dalek::SigningKey;
use serde_json::{json, Value};
use sha2::{Digest, Sha256};

fn write(path: &str, bytes: &[u8]) -> Result<(), String> {
    crate::out::write(path, bytes).map_err(|e| format!("{path}: {e}"))
}

fn read(path: &str) -> Result<Vec<u8>, String> {
    std::fs::read(path).map_err(|e| format!("{path}: {e}"))
}

/// The positional argument: the first that is neither a flag nor a flag's value.
fn path_of<'a>(args: &'a [String], flags: &[&str]) -> Option<&'a str> {
    let mut i = 0;
    while i < args.len() {
        if flags.contains(&args[i].as_str()) {
            i += 2;
        } else if args[i].starts_with('-') {
            i += 1;
        } else {
            return Some(&args[i]);
        }
    }
    None
}

/// `--key`: a key's name in the keystore, 64 hexadecimal digits, or a file holding them.
fn signing_key(args: &[String]) -> Result<SigningKey, String> {
    let s = opt(args, "--key").ok_or("--record needs --key")?;
    crate::dx::key::signing_key(s, true).map_err(|e| e.message)
}

fn issued_at(args: &[String]) -> String {
    opt(args, "--issued-at").map(String::from).unwrap_or_else(|| {
        let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_or(0, |d| d.as_secs());
        wai::claim_receipt::rfc3339_utc(now)
    })
}

/// What a derivation wrote, printed for a person or, with `--json`, as its output's schema.
fn report(args: &[String], width: u64, height: u64, subpixels: u64, files: &[(String, Vec<u8>)], record: Option<[u8; 32]>) {
    let digest = |b: &[u8]| Sha256::digest(b).iter().map(|x| format!("{x:02x}")).collect::<String>();
    let link = record.map(|l| l.iter().map(|x| format!("{x:02x}")).collect::<String>());
    if args.iter().any(|a| a == "--json") {
        let listed: Vec<Value> = files.iter().map(|(n, b)| json!({"file": n, "sha256": digest(b)})).collect();
        println!("{}", json!({"width": width, "height": height, "subpixels": subpixels, "files": listed, "record": link}));
        return;
    }
    for (n, b) in files {
        println!("wrote {n} ({} bytes), SHA-256 {}", b.len(), digest(b));
    }
    println!("{width}×{height}, {subpixels} subpixels");
    if let Some(l) = link {
        println!("record {l}");
    }
}

#[cfg(feature = "exact_upscale")]
pub fn upscale(args: &[String]) -> Result<(), String> {
    use wai::exact_upscale as ex;
    const FLAGS: [&str; 7] = ["--prior", "--derivation", "-o", "--record", "--envelope", "--key", "--issued-at"];
    let input = path_of(args, &FLAGS).ok_or("wai upscale run <in.png> --prior <p.wsr> -o <out.png>")?;
    let prior = read(opt(args, "--prior").ok_or("--prior is the WSR1 prior")?)?;
    let out = opt(args, "-o").ok_or("-o is the picture")?;
    let derivation = match opt(args, "--derivation").unwrap_or("sr2") {
        "sr2" => wai::int_sr::SR2,
        "sr4" => wai::int_sr::SR4,
        other => return Err(format!("--derivation {other}: sr2 or sr4")),
    };
    let (w, h, rgb) = wai::png_still::decode(&read(input)?).map_err(|e| format!("{input}: {e}"))?;
    let made = ex::make(&rgb, w, h, derivation, &prior, &issued_at(args))?;
    let mut files = vec![(out.to_string(), made.picture.clone())];
    let mut link = None;
    if let Some(path) = opt(args, "--record") {
        let envelope = opt(args, "--envelope").ok_or("--record needs --envelope: the record names the input's envelope, which a verifier needs")?;
        let record = ex::sign(made.body.clone(), &signing_key(args)?);
        let text = wai::jcs::jcs(&record).map_err(|e| format!("the record's canonical form: {e:?}"))?;
        files.push((path.to_string(), text.into_bytes()));
        files.push((envelope.to_string(), made.envelope.clone()));
        link = Some(ex::link_hash(&record));
    }
    for (n, b) in &files {
        write(n, b)?;
    }
    report(args, made.width, made.height, made.width * made.height * 3, &files, link);
    Ok(())
}

/// A verification's outcome, printed as JSON; whether it accepted.
fn outcome(refused: Option<(String, Value)>, link: Option<[u8; 32]>, subpixels: u64) -> bool {
    let hex = |b: [u8; 32]| b.iter().map(|x| format!("{x:02x}")).collect::<String>();
    let accepted = refused.is_none();
    let v = match refused {
        Some((reason, at)) => json!({"outcome": "refuse", "reason": reason, "at": at}),
        None => json!({"outcome": "accept", "record": link.map(hex), "subpixels": subpixels}),
    };
    println!("{}", serde_json::to_string_pretty(&v).expect("JSON"));
    accepted
}

/// `wai upscale verify`: exact-upscale §7 under `require-proof`, and the picture held to the output.
#[cfg(feature = "exact_upscale")]
pub fn upscale_verify(args: &[String]) -> Result<bool, String> {
    use wai::exact_upscale as ex;
    let path = path_of(args, &["--envelope", "--prior", "--picture"]).ok_or("wai upscale verify <record.json> --envelope <in.wai> --prior <p.wsr>")?;
    let text = String::from_utf8_lossy(&read(path)?).into_owned();
    let envelope = read(opt(args, "--envelope").ok_or("--envelope is the WAI1 of the input")?)?;
    let prior = read(opt(args, "--prior").ok_or("--prior is the WSR1 prior")?)?;
    let refuse = |reason: &str, at: &str| Ok(outcome(Some((reason.to_string(), json!([at]))), None, 0));
    let Ok(env) = wai::container::Wai::from_bytes(&envelope) else { return refuse("input_unreadable", "envelope") };
    if env.capability() != wai::codecs::CAP_IMAGE_PNG {
        return refuse("input_unreadable", "envelope");
    }
    let Ok((_, _, rgb)) = wai::png_still::decode(&env.payload) else { return refuse("input_unreadable", "envelope") };
    let hash = wai::jwp_hash::content_hash(&envelope);
    let case = ex::Case {
        upscales: vec![text.clone()],
        objects: vec![envelope],
        decoded: vec![(hash, rgb)],
        priors: vec![prior],
        policy: ex::Policy { require_proof: true, ..Default::default() },
        ..Default::default()
    };
    if let ex::Outcome::Refuse { at, reason } = ex::verify(&case) {
        return Ok(outcome(Some((reason.code().to_string(), at.to_json())), None, 0));
    }
    let u = ex::read_upscale(&text).map_err(|r| format!("{path}: {}", r.code()))?;
    let (w, h) = u.output_size();
    if let Some(picture) = opt(args, "--picture") {
        let held = wai::png_still::decode(&read(picture)?).ok().is_some_and(|(pw, ph, s)| (u64::from(pw), u64::from(ph)) == (w, h) && Sha256::digest(&s).as_slice() == u.output);
        if !held {
            return refuse("picture_mismatch", "picture");
        }
    }
    Ok(outcome(None, Some(u.link), w * h * 3))
}

/// `wai tonemap verify`: exact-tonemap §8 under `require-proof`, and each rendition held to its picture.
#[cfg(feature = "exact_tonemap")]
pub fn tonemap_verify(args: &[String]) -> Result<bool, String> {
    use wai::exact_tonemap as tm;
    let path = path_of(args, &["--envelope", "--renditions"]).ok_or("wai tonemap verify <record.json> --envelope <in.wai>")?;
    let text = String::from_utf8_lossy(&read(path)?).into_owned();
    let mut held = tm::Held::default();
    held.hold_envelope(read(opt(args, "--envelope").ok_or("--envelope is the WAI2 the record names")?)?);
    if let tm::Outcome::Refuse { at, reason } = tm::verify(std::slice::from_ref(&text), &held, tm::Policy::RequireProof) {
        return Ok(outcome(Some((reason.code().to_string(), Value::Array(at))), None, 0));
    }
    let r = tm::read_record(&text).map_err(|e| format!("{path}: {}", e.code()))?;
    if let Some(dir) = opt(args, "--renditions") {
        for (k, p) in r.pictures.iter().enumerate() {
            let held = std::fs::read(format!("{dir}/rendition-h{}.rgb16", p.headroom)).is_ok_and(|b| tm::digest(&b) == p.output);
            if !held {
                return Ok(outcome(Some(("rendition_mismatch".to_string(), json!(["renditions", k]))), None, 0));
            }
        }
    }
    Ok(outcome(None, Some(r.link), r.pictures.len() as u64 * r.width * r.height * 3))
}

#[cfg(feature = "exact_tonemap")]
pub fn tonemap(args: &[String]) -> Result<(), String> {
    use wai::exact_tonemap as tm;
    const FLAGS: [&str; 6] = ["--headrooms", "--out", "--input-class", "--record", "--key", "--issued-at"];
    let input = path_of(args, &FLAGS).ok_or("wai tonemap run <in.wai> --headrooms <h,h,...> --out <dir>")?;
    let dir = opt(args, "--out").ok_or("--out is the directory the renditions are written in")?;
    let headrooms = opt(args, "--headrooms").ok_or("--headrooms are the headrooms, in 1/10000 of a stop")?;
    let headrooms: Vec<u64> = headrooms.split(',').map(|h| h.parse().map_err(|_| format!("--headrooms: {h} is not an integer"))).collect::<Result<_, _>>()?;
    let made = tm::make(&read(input)?, &headrooms, opt(args, "--input-class").unwrap_or("captured"), &issued_at(args))?;
    let mut files: Vec<(String, Vec<u8>)> = made.renditions.iter().map(|(h, b)| (format!("{dir}/rendition-h{h}.rgb16"), b.clone())).collect();
    let mut link = None;
    if let Some(path) = opt(args, "--record") {
        let record = tm::sign(made.body.clone(), &signing_key(args)?);
        let text = wai::jcs::jcs(&record).map_err(|e| format!("the record's canonical form: {e:?}"))?;
        files.push((path.to_string(), text.into_bytes()));
        link = Some(tm::link_hash(&record));
    }
    crate::out::create_dir_all(dir).map_err(|e| format!("{dir}: {e}"))?;
    for (n, b) in &files {
        write(n, b)?;
    }
    report(args, made.width, made.height, made.renditions.len() as u64 * made.width * made.height * 3, &files, link);
    Ok(())
}
