//! The opportunity report's operations (extensions/opportunity-report.md §12): `wai opportunity
//! create` (`opportunity.create`), `wai opportunity verify` (`opportunity.verify`) and `wai
//! statement check` (`statement.check`), on files on this machine.
//!
//! They keep the agent rules of [`super::envelope`], as every operation of the catalogue does:
//! the exit codes, the JSON envelope with `--json`, `--dry-run` on the two that write (every file
//! through `out.rs`, nothing written), and `--yes` before a file that exists is replaced. A
//! refusal by the extension's text has its reason as the code (`cue_section_mismatch`,
//! `reported_foreign`, ...), at exit 1.

#[cfg(feature = "opportunity_report")]
use super::envelope as agent;
use super::envelope::{EXIT_FAILED, OpError};
#[cfg(feature = "opportunity_report")]
use super::share::{done, hex, io_fail, number, read_json, read_key, required, write};
#[cfg(feature = "opportunity_report")]
use crate::out;
#[cfg(feature = "opportunity_report")]
use serde_json::{Value, json};

type R = Result<bool, OpError>;

/// A refusal by the extension's text, its reason as the code.
#[cfg_attr(not(feature = "opportunity_report"), allow(dead_code))]
fn refused(what: &str, reason: &'static str, at: &serde_json::Value) -> OpError {
    OpError::new(EXIT_FAILED, reason, format!("{what} is refused: {reason} at {at}"), "extensions/opportunity-report.md §13 names each reason")
}

/// A key of 64 hexadecimal digits.
#[cfg(feature = "opportunity_report")]
fn key_arg(v: &str, flag: &str, usage: &str) -> Result<[u8; 32], OpError> {
    (v.len() == 64 && v.bytes().all(|c| c.is_ascii_hexdigit()))
        .then(|| (0..32).map(|i| u8::from_str_radix(&v[2 * i..2 * i + 2], 16).expect("hex")).collect::<Vec<u8>>())
        .and_then(|b| b.try_into().ok())
        .ok_or_else(|| OpError::usage(format!("{flag}: a key, 64 hexadecimal digits"), usage))
}

/// A report's outcome, for a person: its party, its lines and its totals.
#[cfg(feature = "opportunity_report")]
fn summarise(o: &Value) -> String {
    let d = o["report"].as_str().unwrap_or("");
    let party = o["party"].as_str().unwrap_or("");
    let mut t = format!(
        "opportunity report {}… of the {} {}… for [{}, {}): {} break(s), {} ms, {} slot(s)",
        &d[..16.min(d.len())],
        o["role"].as_str().unwrap_or("?"),
        &party[..16.min(party.len())],
        o["start"],
        o["end"],
        o["totals"]["breaks"],
        o["totals"]["break_ms"],
        o["totals"]["slots"],
    );
    for l in o["lines"].as_array().into_iter().flatten() {
        t.push_str(&format!("\n  split {} {:<9} {} break(s), {} ms, {} slot(s), {} unslotted, share {} ppm", l["split"], l["class"].as_str().unwrap_or(""), l["breaks"], l["break_ms"], l["slots"], l["unslotted"], l["ppm"]));
    }
    t
}

/// A check's outcome, for a person: each line with its figures and flags.
#[cfg(feature = "opportunity_report")]
fn summarise_check(o: &Value) -> String {
    let d = o["check"].as_str().unwrap_or("");
    let r = o["report"].as_str().unwrap_or("");
    let mut t = format!(
        "statement check {}… of report {}… beside a reported statement ({}, {}){}",
        &d[..16.min(d.len())],
        &r[..16.min(r.len())],
        o["form"].as_str().unwrap_or("?"),
        o["basis"].as_str().unwrap_or("?"),
        if o["statement"].is_null() { "" } else { " and the signed statement" },
    );
    for l in o["lines"].as_array().into_iter().flatten() {
        let flags: Vec<&str> = l["flags"].as_array().into_iter().flatten().filter_map(Value::as_str).collect();
        t.push_str(&format!(
            "\n  {:<9} {:<12} {} slot(s), {} impression(s) reported, fill {} ppm{}{}",
            l["scope"].as_str().unwrap_or(""),
            l["key"].as_str().unwrap_or(""),
            l["slots"],
            l["impressions"],
            l["fill_ppm"],
            l["signed"].get("impressions").map(|s| format!(", {s} signed")).unwrap_or_default(),
            if flags.is_empty() { String::new() } else { format!("  [{}]", flags.join(", ")) },
        ));
    }
    t
}

#[cfg(not(feature = "opportunity_report"))]
pub fn opportunity_create(_: &[String]) -> R {
    Err(crate::door::not_built(wai::catalogue::op("opportunity.create").expect("listed")))
}

/// `wai opportunity create <inputs.json> --party <key> --start µs --end µs --key <file> [--out F]`:
/// the report of a party and a period from a document without its report (§8.1), signed by the
/// issuer key and written with its document.
#[cfg(feature = "opportunity_report")]
pub fn opportunity_create(args: &[String]) -> R {
    use wai::opportunity_report as opp;
    let usage = super::usage("opportunity.create");
    let with_value = ["--party", "--start", "--end", "--key", "--out"];
    let pos = agent::check_args(args, &with_value, &with_value, 1, &usage)?;
    let path = *pos.first().ok_or_else(|| OpError::usage("the inputs: a report's verification document without its report", &usage))?;
    let party = key_arg(required(args, "--party", "the party's key: 64 hexadecimal digits", &usage)?, "--party", &usage)?;
    let (start, end) = (number(args, "--start", &usage)?, number(args, "--end", &usage)?);
    let key = read_key(required(args, "--key", "the issuer key's file", &usage)?, "the issuer key's file (32 bytes, or 64 hexadecimal digits)")?;
    let inputs = read_json(path, "the report's inputs")?;
    let (r, doc) = opp::create(&inputs, &key, party, start, end).map_err(|(reason, at)| refused("the report", reason, &at))?;
    let o = opp::outcome_json(&opp::verify(&doc));
    let file = super::opt(args, "--out").map(str::to_string).unwrap_or_else(|| format!("{}.wai-opportunity.json", &hex(&r.digest())[..16]));
    write(&file, (serde_json::to_string_pretty(&doc).expect("JSON") + "\n").as_bytes(), "the report's document")?;
    let text = format!("{}{}", summarise(&o), if out::dry_run() { String::new() } else { format!("\nwritten to {file}; check it anywhere with `wai opportunity verify {file}`") });
    done("opportunity.create", true, json!({"file": file, "report": hex(&r.to_bytes()), "outcome": o}), &text)
}

#[cfg(not(feature = "opportunity_report"))]
pub fn opportunity_verify(_: &[String]) -> R {
    Err(crate::door::not_built(wai::catalogue::op("opportunity.verify").expect("listed")))
}

/// `wai opportunity verify <document.json>`: a report's (§8.1) or a check's (§11.1) document,
/// verified; a refusal is exit 1 with its reason as the code.
#[cfg(feature = "opportunity_report")]
pub fn opportunity_verify(args: &[String]) -> R {
    use wai::opportunity_report as opp;
    let usage = super::usage("opportunity.verify");
    let pos = agent::check_args(args, &[], &[], 1, &usage)?;
    let path = *pos.first().ok_or_else(|| OpError::usage("the verification document", &usage))?;
    let doc = read_json(path, "the verification document")?;
    if opp::is_check_document(&doc) {
        let a = opp::verify_check(&doc).map_err(|(r, at)| refused("the check", r, &at))?;
        let o = opp::check_json(&a);
        let text = summarise_check(&o);
        return done("opportunity.verify", false, o, &text);
    }
    let a = opp::verify(&doc).map_err(|(r, at)| refused("the report", r, &at))?;
    let o = opp::accepted_json(&a);
    let text = summarise(&o);
    done("opportunity.verify", false, o, &text)
}

#[cfg(not(feature = "opportunity_report"))]
pub fn statement_check(_: &[String]) -> R {
    Err(crate::door::not_built(wai::catalogue::op("statement.check").expect("listed")))
}

/// A bound in millionths.
#[cfg(feature = "opportunity_report")]
fn ppm(args: &[String], flag: &str, default: u32, usage: &str) -> Result<u32, OpError> {
    match super::opt(args, flag) {
        None => Ok(default),
        Some(v) => v.parse().map_err(|_| OpError::usage(format!("{flag} {v}: millionths, an integer"), usage)),
    }
}

/// `wai statement check <report.json> <reported> [--statement <document.json>] [--floor ppm]
/// [--ceiling ppm] --key <file> [--out F]`: the check of a reported statement (§9; CSV for a file
/// named `.csv`) against a report's document and, when given, a signed statement's (§10), signed
/// by the issuer key and written with its document.
#[cfg(feature = "opportunity_report")]
pub fn statement_check(args: &[String]) -> R {
    use wai::opportunity_report as opp;
    let usage = super::usage("statement.check");
    let with_value = ["--statement", "--floor", "--ceiling", "--key", "--out"];
    let pos = agent::check_args(args, &with_value, &with_value, 2, &usage)?;
    let (report_path, reported_path) = match pos.as_slice() {
        [a, b] => (*a, *b),
        _ => return Err(OpError::usage("the report's document and the reported statement", &usage)),
    };
    let key = read_key(required(args, "--key", "the issuer key's file", &usage)?, "the issuer key's file (32 bytes, or 64 hexadecimal digits)")?;
    let (floor, ceiling) = (ppm(args, "--floor", opp::FLOOR_PPM, &usage)?, ppm(args, "--ceiling", opp::CEILING_PPM, &usage)?);
    let report = read_json(report_path, "the report's verification document")?;
    let raw = std::fs::read(reported_path).map_err(|e| io_fail(reported_path, &e, "the reported statement"))?;
    let text = String::from_utf8(raw).map_err(|_| OpError::usage(format!("{reported_path}: not UTF-8 text"), &usage))?;
    let form = if reported_path.ends_with(".csv") { "csv" } else { "json" };
    let statement = match super::opt(args, "--statement") {
        Some(p) => read_json(p, "the signed statement's verification document")?,
        None => Value::Null,
    };
    let inputs = json!({"report": report, "reported": {"form": form, "text": text}, "statement": statement});
    let (c, doc) = opp::create_check(&inputs, &key, floor, ceiling).map_err(|(reason, at)| refused("the check", reason, &at))?;
    let o = opp::check_outcome_json(&opp::verify_check(&doc));
    let file = super::opt(args, "--out").map(str::to_string).unwrap_or_else(|| format!("{}.wai-check.json", &hex(&c.digest())[..16]));
    write(&file, (serde_json::to_string_pretty(&doc).expect("JSON") + "\n").as_bytes(), "the check's document")?;
    let text = format!("{}{}", summarise_check(&o), if out::dry_run() { String::new() } else { format!("\nwritten to {file}; check it anywhere with `wai opportunity verify {file}`") });
    done("statement.check", true, json!({"file": file, "check": hex(&c.to_bytes()), "outcome": o}), &text)
}
