//! `wai hello`: WAI's four steps on a picture made here, each said as it runs.
//!
//! 1. **Name.** A picture is packed into a `WAI1` envelope whose manifest names the
//!    capability it needs, `wai.image.png` (PNG, of the mandatory floor).
//! 2. **Dispatch.** The envelope is read back and dispatched by SPEC §4 against the
//!    capabilities this binary decodes.
//! 3. **Rebuild.** The capability runs, and the picture it rebuilds is compared with the
//!    one packed, byte for byte.
//! 4. **Receipt.** The object's content hash goes into a group receipt signed with an
//!    Ed25519 key made for this run (jwp-receipts §2.4), written as http-delivery §8 lays
//!    receipts out beside an object: `<cid>.wai`, its index `<cid>.wai.receipts` and the
//!    group's file `rcpt/<link hash>.json`. Then they are checked as `wai verify` checks
//!    them.
//!
//! The key's secret is never written: the receipt proves the bytes were signed by the key
//! it names, and a later `wai verify` checks that again, offline.

use super::envelope::OpError;

#[cfg(not(feature = "quickstart"))]
pub fn main(_: &[String]) -> Result<bool, OpError> {
    Err(OpError::from("`wai hello` needs a build with the feature `quickstart`"))
}

#[cfg(feature = "quickstart")]
pub fn main(args: &[String]) -> Result<bool, OpError> {
    use super::hex;
    use crate::out;
    use std::path::PathBuf;
    use wai::codecs::image;
    use wai::http_binding::receipts::{IndexItem, RECEIPT_DIR, group_file, index_file};
    use wai::jwp_delivery::{GroupBuilder, ObjectLabel, WaiReceiptProperty};
    use wai::{CAP_IMAGE_PNG, Conditioning, Manifest, ModelRequirement, Wai};

    let usage = super::usage("toolchain.test");
    if let Some(a) = args.iter().find(|a| a.starts_with("--")) {
        return Err(OpError::usage(format!("wai hello: {a}: not an option ({usage})"), ""));
    }
    let pos = super::positional(args, &[]);
    if pos.len() > 1 {
        return Err(OpError::usage(usage, ""));
    }
    let dir = PathBuf::from(pos.first().copied().unwrap_or("wai-hello"));
    // Only a directory that is empty, or one an earlier `wai hello` wrote, is written to; an
    // earlier run's files are replaced only once that is confirmed.
    let mut earlier = Vec::new();
    if dir.exists() {
        let theirs = std::fs::read_dir(&dir).map_err(|e| format!("{}: {e}", dir.display()))?.flatten().any(|e| {
            let n = e.file_name().to_string_lossy().into_owned();
            !(n == "rcpt" || n.ends_with(".wai") || n.ends_with(".wai.receipts"))
        });
        if theirs {
            return Err(OpError::from(format!("{}: holds files `wai hello` did not write; name another directory", dir.display())));
        }
        // An earlier run's object, index and receipt files, and nothing else.
        for e in std::fs::read_dir(&dir).map_err(|e| e.to_string())?.flatten() {
            let p = e.path();
            if e.file_name() == "rcpt" {
                for r in std::fs::read_dir(&p).map_err(|e| e.to_string())?.flatten() {
                    if r.file_name().to_string_lossy().ends_with(".json") {
                        earlier.push(r.path());
                    }
                }
            } else {
                earlier.push(p);
            }
        }
    }
    if !earlier.is_empty() {
        super::envelope::confirm(
            &format!("{} holds an earlier `wai hello`'s {} files, which this run replaces", dir.display(), earlier.len()),
            earlier.iter().map(|p| serde_json::json!({"remove": p.display().to_string()})).collect(),
        )?;
        for p in &earlier {
            let _ = out::remove_file(p);
        }
    }
    out::create_dir_all(dir.join(RECEIPT_DIR)).map_err(|e| format!("{}: {e}", dir.display()))?;
    println!("wai hello: WAI's four steps, on this machine\n");

    // 1. Name.
    let (w, h) = (64u32, 40u32);
    let mut rgb = Vec::with_capacity((w * h * 3) as usize);
    for y in 0..h {
        for x in 0..w {
            let band = ((x / 8 + y / 8) % 2) as u8;
            rgb.extend_from_slice(&[(x * 4) as u8, (y * 6) as u8, 96 + 64 * band]);
        }
    }
    let png = image::png_encode(&rgb, h, w).map_err(|e| format!("{e:?}"))?;
    let manifest = Manifest::new(crate::media_for(CAP_IMAGE_PNG), "replicate", ModelRequirement::new(CAP_IMAGE_PNG), Conditioning::new("png"));
    let envelope = Wai::new(manifest, png).to_bytes().map_err(|e| e.to_string())?;
    let cid = wai::jwp_hash::content_hash(&envelope);
    let object = dir.join(format!("{}.wai", hex(&cid)));
    out::write(&object, &envelope).map_err(|e| e.to_string())?;
    println!("1 Name      a {w}×{h} picture, packed in an envelope that names the capability it needs:");
    println!("            {CAP_IMAGE_PNG} (PNG, one of the mandatory floor every sink implements)");
    println!("            {} ({} bytes)\n", object.display(), envelope.len());

    // 2. Dispatch.
    let read = Wai::from_bytes(&envelope).map_err(|e| e.to_string())?;
    let native = crate::native_capabilities();
    let d = read.dispatch_verified(|c| native.contains(&c), wai::container::no_parameter_sets()).map_err(|e| format!("inert at this sink: {e:?}"))?;
    println!("2 Dispatch  this sink decodes {} natively;", native.join(", "));
    println!("            the envelope names {}, so that capability runs (SPEC §4). One it does not", d.capability);
    println!("            hold would run the envelope's fallback, or be reported inert, never guessed\n");

    // 3. Rebuild.
    let (back, bh, bw) = image::png_decode(&read.payload).map_err(|e| format!("{e:?}"))?;
    let exact = (bw, bh) == (w, h) && back == rgb;
    println!("3 Rebuild   {bw}×{bh} pixels rebuilt: {}\n", if exact { "byte for byte the picture packed in step 1" } else { "NOT the picture packed" });
    if !exact {
        return Ok(false);
    }

    // 4. Receipt.
    let mut seed = [0u8; 32];
    getrandom::fill(&mut seed).map_err(|e| format!("no random key: {e}"))?;
    let key = ed25519_dalek::SigningKey::from_bytes(&seed);
    seed.fill(0);
    let pk = key.verifying_key().to_bytes();
    let (start, end) = (rfc3339(0), rfc3339(1));
    let mut b = GroupBuilder::default();
    b.add(cid, 0, "wai hello", Some(ObjectLabel::Unmetered));
    let (group, objects) = b.finish(&key, &wai::signer_id::did_key(&pk), 0, 0, &start, &end, None, true).ok_or("the group would not seal")?;
    let link = group.link_hash();
    out::write(dir.join(format!("{RECEIPT_DIR}{}.json", hex(&link))), group_file(&group)?).map_err(|e| e.to_string())?;
    let item = IndexItem {
        property: WaiReceiptProperty { content_hash: cid, joules_micro: 0, acquisition: String::new(), acquisition_evidence: Vec::new(), group_receipt: link },
        object_receipt: objects[0].clone(),
    };
    let index = dir.join(format!("{}.wai.receipts", hex(&cid)));
    out::write(&index, index_file(&cid, &[item], &[])?).map_err(|e| e.to_string())?;
    println!("4 Receipt   the object's BLAKE3 content hash, in a group receipt signed with a key made for");
    println!("            this run (Ed25519 {}…; its secret is not kept). Energy is stated as not", &hex(&pk)[..16]);
    println!("            measured: this run reads no meter");
    if out::dry_run() {
        println!("            (a dry run: the receipt is not written, so it is not checked here)");
        return Ok(true);
    }
    let (ok, lines) = super::verify::check_index_file(&index)?;
    for l in lines {
        println!("            {l}");
    }
    println!();
    println!("Next:");
    println!("  wai verify {}                       check the receipts again, offline", dir.display());
    println!("  wai create my-room --template video-room   a video room in one page");
    println!("  cd my-room && wai dev                      run it here, and open it in two tabs");
    Ok(ok)
}

/// Now plus `plus_s` seconds, as RFC 3339 in UTC, to the second.
#[cfg(feature = "quickstart")]
fn rfc3339(plus_s: u64) -> String {
    let t = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0) + plus_s;
    let (days, secs) = ((t / 86_400) as i64, t % 86_400);
    // Civil from days (H. Hinnant's algorithm).
    let z = days + 719_468;
    let era = z.div_euclid(146_097);
    let doe = z - era * 146_097;
    let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
    let mp = (5 * doy + 2) / 153;
    let d = doy - (153 * mp + 2) / 5 + 1;
    let m = if mp < 10 { mp + 3 } else { mp - 9 };
    let y = yoe + era * 400 + if m <= 2 { 1 } else { 0 };
    format!("{y:04}-{m:02}-{d:02}T{:02}:{:02}:{:02}Z", secs / 3600, (secs / 60) % 60, secs % 60)
}

#[cfg(all(test, feature = "quickstart"))]
mod tests {
    #[test]
    fn rfc3339_is_a_utc_time_of_day() {
        let t = super::rfc3339(0);
        assert_eq!(t.len(), 20);
        assert!(t.ends_with('Z') && &t[4..5] == "-" && &t[10..11] == "T");
    }
}
