//! `wai verify <path>…`: every receipt index (`*.wai.receipts`, http-delivery §8.2) under
//! each path, checked offline, as `wai http receipts` checks one over HTTP: each delivery
//! item's group receipt read from `rcpt/<link hash>.json` beside it (§8.1), its parents
//! by their links, jwp-receipts §3 steps 1–5 against the object `<cid>.wai` where it is
//! there (`delivered-verified`), or against nothing (`inclusion-verified`); and each claim
//! the index lists. Nothing is fetched from anywhere.
//!
//! A signature proves the bytes were signed by the key the receipt names. Which party holds
//! that key is a key log's to say (http-delivery §8.4); this command names the key.
//!
//! It also checks every conformance result record (`*.wai-conform.json`, written by `wai
//! conform`) under each path: its signature, and that its summary and verdict are what its
//! cases say. With `--conformant`, each record must also admit its implementation
//! (conformance-results §4.1): the run passed, against the corpus of a checkout of the
//! standard (`--root <wai-dir>`, or found from the current directory), named by its digest.
//!
//! And every envelope (`*.wai`) under each path: what a verifier finds about its pins
//! without holding a parameter set (SPEC §3.1, §7), which makes it non-conforming, and the
//! informative report `prior_unpinned` where it pins nothing that a capability it decodes
//! through is registered as decoding against (SPEC §3.1, *Pin by default*), which does not.
//!
//! A fleet report's bundle (`*.wai-fleet.json`, extensions/fleet.md §7) under a path is verified
//! too, in a build with `fleet`: the report recomputed from its enrolments and records.

use super::envelope::OpError;

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

#[cfg(feature = "quickstart")]
pub fn main(args: &[String]) -> Result<bool, OpError> {
    use std::path::{Path, PathBuf};
    let usage = super::usage("receipt.verify");
    let mut i = 0;
    let (mut conformant, mut root) = (false, None);
    while i < args.len() {
        match args[i].as_str() {
            "--conformant" => conformant = true,
            "--root" => {
                root = Some(args.get(i + 1).ok_or_else(|| OpError::usage(format!("wai verify: --root takes the wai/ directory of a checkout ({usage})"), ""))?.as_str());
                i += 1;
            }
            a if a.starts_with("--") => return Err(OpError::usage(format!("wai verify: {a}: not an option ({usage})"), "")),
            _ => {}
        }
        i += 1;
    }
    if root.is_some() && !conformant {
        return Err(OpError::usage(format!("wai verify: --root names the corpus --conformant holds a record to ({usage})"), ""));
    }
    // Admission compares a record's corpus digest with the corpus of a checkout: without
    // one there is nothing to compare it with.
    let root = if conformant { Some(super::conform::wai_root(root)?) } else { None };
    let paths = super::positional(args, &["--root"]);
    if paths.is_empty() {
        return Err(OpError::usage(usage, ""));
    }
    fn walk(p: &Path, out: &mut Vec<PathBuf>, bundles: &mut Vec<PathBuf>, depth: usize) -> Result<(), String> {
        if p.is_file() {
            let n = p.to_string_lossy();
            if n.ends_with(".wai.receipts") || n.ends_with(super::conform::record::SUFFIX) || n.ends_with(".wai") {
                out.push(p.to_path_buf());
            } else if p.to_string_lossy().ends_with(".wai-fleet.json") {
                bundles.push(p.to_path_buf());
            }
            return Ok(());
        }
        if !p.is_dir() {
            return Err(format!("{}: no such file or directory", p.display()));
        }
        if depth > 16 {
            return Ok(());
        }
        let mut entries: Vec<PathBuf> = std::fs::read_dir(p).map_err(|e| format!("{}: {e}", p.display()))?.flatten().map(|e| e.path()).collect();
        entries.sort();
        for e in entries {
            if e.is_dir() && e.file_name().is_some_and(|n| n == "rcpt") {
                continue;
            }
            walk(&e, out, bundles, depth + 1)?;
        }
        Ok(())
    }
    let (mut indexes, mut bundles) = (Vec::new(), Vec::new());
    for p in &paths {
        walk(Path::new(p), &mut indexes, &mut bundles, 0)?;
    }
    if indexes.is_empty() && bundles.is_empty() {
        return Err(OpError::from(format!(
            "no receipt index (*.wai.receipts), conformance result (*.wai-conform.json), fleet bundle (*.wai-fleet.json) or envelope (*.wai) under {}",
            paths.join(", ")
        )));
    }
    let mut all = true;
    let (mut n_ix, mut n_rec, mut n_env) = (0, 0, 0);
    for ix in &indexes {
        println!("{}", ix.display());
        let (ok, lines) = if ix.to_string_lossy().ends_with(super::conform::record::SUFFIX) {
            n_rec += 1;
            let bytes = std::fs::read(ix).map_err(|e| format!("{}: {e}", ix.display()))?;
            match root.as_deref() {
                Some(r) => super::conform::record::admit(&bytes, r),
                None => super::conform::record::check(&bytes),
            }
        } else if ix.to_string_lossy().ends_with(".wai") {
            n_env += 1;
            check_envelope(&std::fs::read(ix).map_err(|e| format!("{}: {e}", ix.display()))?)
        } else {
            n_ix += 1;
            check_index_file(ix)?
        };
        for l in lines {
            println!("  {l}");
        }
        all &= ok;
    }
    for b in &bundles {
        println!("{}", b.display());
        let (ok, lines) = check_bundle_file(b)?;
        for l in lines {
            println!("  {l}");
        }
        all &= ok;
    }
    let m = bundles.len();
    let mut what = Vec::new();
    if n_ix > 0 {
        what.push(format!("{} receipt index{}", n_ix, if n_ix == 1 { "" } else { "es" }));
    }
    if n_rec > 0 {
        what.push(format!("{} conformance result{}", n_rec, if n_rec == 1 { "" } else { "s" }));
    }
    if n_env > 0 {
        what.push(format!("{} envelope{}", n_env, if n_env == 1 { "" } else { "s" }));
    }
    if m > 0 {
        what.push(format!("{m} fleet bundle{}", if m == 1 { "" } else { "s" }));
    }
    println!("wai verify: {} {}", what.join(" and "), if all { "verified" } else { "with a refusal (above)" });
    if let (Some(r), true, true) = (root.as_deref(), all, n_rec > 0) {
        println!("wai verify: every conformance result admits its implementation (conformance-results \u{a7}4.1), against the corpus of {}", r.display());
    }
    Ok(all)
}

/// One envelope's pins, as a verifier holding no parameter set reads them (SPEC §3.1, §7):
/// whether it conforms on that count — a static finding makes it non-conforming — and a line
/// for each thing found, the informative `prior_unpinned` (SPEC §3.1, *Pin by default*)
/// among them, which does not.
#[cfg(feature = "quickstart")]
pub(crate) fn check_envelope(bytes: &[u8]) -> (bool, Vec<String>) {
    use wai::container::{Wai, WaiMulti};
    let pin = |p: Option<&wai::container::Prior>| p.map(|p| format!("pinned {}", wai::pin::describe_pin(p))).unwrap_or_else(|| "no pin".into());
    match bytes.get(..4) {
        Some(b"WAI1") => match Wai::from_bytes(bytes) {
            Err(e) => (false, vec![format!("FAIL not an envelope that parses: {e}")]),
            Ok(w) => {
                let mut lines = Vec::new();
                let ok = match w.check_pin() {
                    Ok(()) => {
                        lines.push(format!("ok   WAI1 {}, {}: no static finding on its pin (SPEC \u{a7}3.1, \u{a7}7)", w.capability(), pin(w.prior())));
                        true
                    }
                    Err(f) => {
                        lines.push(format!("FAIL WAI1 {}: {f}", w.capability()));
                        false
                    }
                };
                lines.extend(w.check_unpinned().map(|f| format!("note {f}")));
                (ok, lines)
            }
        },
        Some(b"WAI2") => match WaiMulti::from_bytes(bytes) {
            Err(e) => (false, vec![format!("FAIL not an envelope that parses: {e}")]),
            Ok(m) => {
                let mut lines = Vec::new();
                let ok = match m.check_pins() {
                    Ok(()) => {
                        lines.push(format!("ok   WAI2, {} entries: no static finding on their pins (SPEC \u{a7}3.1, \u{a7}7)", m.manifest.renditions.len()));
                        true
                    }
                    Err((i, f)) => {
                        lines.push(format!("FAIL WAI2 entry [{i}] {}: {f}", m.manifest.renditions[i].capability));
                        false
                    }
                };
                lines.extend(m.check_unpinned().into_iter().map(|(i, f)| format!("note [{i}] {f}")));
                (ok, lines)
            }
        },
        _ => (false, vec!["FAIL not an envelope: it begins with neither WAI1 nor WAI2".into()]),
    }
}

/// One fleet report bundle (extensions/fleet.md §7): whether it verified, and a line for its
/// report and for each node, room and scope of energy.
#[cfg(all(feature = "quickstart", feature = "fleet"))]
pub(crate) fn check_bundle_file(path: &std::path::Path) -> Result<(bool, Vec<String>), String> {
    let text = std::fs::read_to_string(path).map_err(|e| format!("{}: {e}", path.display()))?;
    let bundle: serde_json::Value = match serde_json::from_str(&text) {
        Ok(v) => v,
        Err(e) => return Ok((false, vec![format!("FAIL the bundle is not JSON: {e}")])),
    };
    let o = wai::fleet::outcome_json(&wai::fleet::verify(&bundle));
    if o["outcome"] != "accept" {
        return Ok((false, vec![format!("FAIL fleet report: {} at {}", o["reason"].as_str().unwrap_or("?"), o["at"])]));
    }
    let short = |v: &serde_json::Value| v.as_str().map(|s| s[..16.min(s.len())].to_string()).unwrap_or_default();
    let cov = |e: &serde_json::Value| format!("sessions {}, hops {}", e["sessions"]["coverage"].as_str().unwrap_or("?"), e["hops"]["coverage"].as_str().unwrap_or("?"));
    let mut lines = vec![format!(
        "ok   fleet report {}… of organisation {}…, [{}, {}): {} record(s), every line recomputed",
        short(&o["digest"]),
        short(&o["organisation"]),
        o["start"],
        o["end"],
        o["records"]
    )];
    for n in o["nodes"].as_array().into_iter().flatten() {
        lines.push(format!("ok   node {} ({}): {} session(s), {} link(s); energy {}", n["label"].as_str().unwrap_or(""), n["role"].as_str().unwrap_or(""), n["sessions"], n["links"], cov(&n["energy"])));
    }
    for e in o["enrolments"].as_array().into_iter().flatten().filter(|e| e["silent"] == true) {
        lines.push(format!("ok   node {} ({}): silent, no record in the period", e["label"].as_str().unwrap_or(""), e["role"].as_str().unwrap_or("")));
    }
    for r in o["rooms"].as_array().into_iter().flatten() {
        lines.push(format!("ok   room {}…: {} hop record(s); energy {}", short(&r["room"]), r["hops"], cov(&r["energy"])));
    }
    lines.push(format!("ok   fleet energy {} (a partial total is not the fleet's energy)", cov(&o["totals"]["energy"])));
    Ok((true, lines))
}

#[cfg(all(feature = "quickstart", not(feature = "fleet")))]
pub(crate) fn check_bundle_file(path: &std::path::Path) -> Result<(bool, Vec<String>), String> {
    Ok((false, vec![format!("FAIL {}: a fleet bundle is verified by a build with the feature `fleet`", path.display())]))
}

/// One receipt index, checked: whether every item and claim it lists held, and a line for
/// each.
#[cfg(feature = "quickstart")]
pub(crate) fn check_index_file(index: &std::path::Path) -> Result<(bool, Vec<String>), String> {
    use super::hex;
    use wai::http_binding::receipts::{Limits, ObjectHeld, RECEIPT_DIR, check_index};
    use wai::jwp_delivery::ReceiptChains;
    use wai::jwp_hash::EstablishedHash;

    let name = index.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default();
    let cid_hex = name.strip_suffix(".wai.receipts").unwrap_or("");
    let cid: [u8; 32] = match (0..32).map(|i| cid_hex.get(2 * i..2 * i + 2).and_then(|h| u8::from_str_radix(h, 16).ok())).collect::<Option<Vec<u8>>>() {
        Some(v) if cid_hex.len() == 64 => v.try_into().expect("32 bytes"),
        _ => return Ok((false, vec![format!("FAIL {name}: an index is named <cid>.wai.receipts, the cid 64 hexadecimal digits")])),
    };
    let dir = index.parent().unwrap_or(std::path::Path::new("."));
    let bytes = std::fs::read(index).map_err(|e| format!("{}: {e}", index.display()))?;
    let object = dir.join(format!("{cid_hex}.wai"));
    let held = match std::fs::read(&object) {
        Ok(b) => ObjectHeld::Object(EstablishedHash::of_envelope(&b)),
        Err(_) => ObjectHeld::Nothing,
    };
    let mut fetch = |h: &[u8; 32], limit: usize| -> Option<Vec<u8>> {
        let mut b = std::fs::read(dir.join(format!("{RECEIPT_DIR}{}.json", hex(h)))).ok()?;
        b.truncate(limit);
        Some(b)
    };
    let mut chains = ReceiptChains::new();
    let mut lines = Vec::new();
    let mut ok = true;
    let short = |b: &[u8]| hex(b)[..16].to_string();
    match check_index(&cid, &bytes, &held, &mut fetch, &mut chains, 0, &Limits::default(), None) {
        Err(e) => {
            ok = false;
            lines.push(format!("FAIL the index: {} ({e})", e.code()));
        }
        Ok(r) => {
            if r.delivery.is_empty() && r.claims.is_empty() {
                ok = false;
                lines.push("FAIL the index lists nothing".into());
            }
            for d in &r.delivery {
                match d {
                    Ok(v) => lines.push(format!(
                        "ok   {}: group {} signed by {}… ({}), {}{}",
                        v.evidence.label(),
                        short(&v.group.link_hash()),
                        short(&v.group.signer_pubkey),
                        v.group.signer_id,
                        v.admission.label(),
                        if matches!(held, ObjectHeld::Nothing) { "; the object is not here, so its delivery is not shown" } else { "" }
                    )),
                    Err(e) => {
                        ok = false;
                        lines.push(format!("FAIL delivery: {} ({e})", e.code()));
                    }
                }
            }
            for c in &r.claims {
                match c {
                    Ok(v) => lines.push(format!("ok   claim {}: attests {}", v.claim.class().label(), v.attests.label())),
                    Err(e) => {
                        ok = false;
                        lines.push(format!("FAIL claim: {} ({e})", e.code()));
                    }
                }
            }
        }
    }
    Ok((ok, lines))
}
