//! The revenue share's operations (extensions/revenue-share.md §10): `wai contract create`
//! (`contract.create`), `wai contract accept` (`contract.update`), `wai statement create`
//! (`statement.create`) and `wai statement verify` (`statement.verify`), 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` (data to standard output, logs to standard
//! error), `--dry-run` on the three that write (every file through `out.rs`, nothing written),
//! and `--yes` before a file that exists is replaced (asked at a terminal; without one, exit 4
//! with the proposed change and the confirming command). A refusal by the extension's text has
//! its reason as the code (`not_countersigned`, `outside_window`, ...), at exit 1.

use super::envelope::{self as agent, EXIT_FAILED, EXIT_INTERNAL, EXIT_NOT_FOUND, EXIT_PERMISSION, OpError};
#[cfg(feature = "revenue_share")]
use super::envelope::EXIT_USAGE;
use crate::out;
use serde_json::Value;
#[cfg(feature = "revenue_share")]
use serde_json::json;

type R = Result<bool, OpError>;

/// Print what an operation did: its envelope with `--json`, its text otherwise; in a dry run of
/// an operation that writes, with the plan of what it would have written.
#[cfg_attr(not(feature = "revenue_share"), allow(dead_code))]
pub(super) fn done(op: &str, writes: bool, data: Value, text: &str) -> R {
    let plan = (writes && out::dry_run()).then(out::plan);
    if agent::ctx().json {
        agent::print(&agent::envelope_with(op, true, data, plan));
    } else if plan.is_some() {
        println!("{text}");
        println!("dry run: nothing was written. Without --dry-run, wai would:");
        for l in out::plan_lines() {
            println!("  {l}");
        }
    } else {
        println!("{text}");
    }
    Ok(true)
}

/// A file that could not be read or written.
#[cfg_attr(not(feature = "revenue_share"), allow(dead_code))]
pub(super) fn io_fail(path: &str, e: &std::io::Error, hint: &str) -> OpError {
    match e.kind() {
        std::io::ErrorKind::NotFound => OpError::new(EXIT_NOT_FOUND, "not-found", format!("{path}: {e}"), hint),
        std::io::ErrorKind::PermissionDenied => OpError::new(EXIT_PERMISSION, "permission-denied", format!("{path}: {e}"), "the file's owner runs this"),
        _ => OpError::new(EXIT_INTERNAL, "io-error", format!("{path}: {e}"), hint),
    }
}

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

#[cfg_attr(not(feature = "revenue_share"), allow(dead_code))]
pub(super) fn hex(b: &[u8]) -> String {
    b.iter().map(|x| format!("{x:02x}")).collect()
}

/// A signing key by its name in the keystore, or from a key file (32 bytes, or 64 hexadecimal
/// digits): what `--key` takes on every signing operation.
#[cfg(feature = "revenue_share")]
pub(super) fn read_key(arg: &str, what: &str) -> Result<ed25519_dalek::SigningKey, OpError> {
    super::key::signing_key(arg, false).map_err(|e| if e.hint.is_empty() { e.with_hint(what) } else { e })
}

#[cfg(feature = "revenue_share")]
pub(super) fn read_json(path: &str, what: &str) -> Result<Value, OpError> {
    let b = std::fs::read(path).map_err(|e| io_fail(path, &e, what))?;
    serde_json::from_slice(&b).map_err(|e| OpError::usage(format!("{path}: not JSON: {e}"), what))
}

/// Write `bytes` to `file`, asking first when it exists; in a dry run, plan it.
#[cfg(feature = "revenue_share")]
pub(super) fn write(file: &str, bytes: &[u8], what: &str) -> Result<(), OpError> {
    if !out::dry_run() && std::path::Path::new(file).exists() {
        agent::confirm(&format!("{file} exists, and {what} would replace it"), vec![json!({"write": file, "bytes": bytes.len(), "replaces": true})])?;
    }
    out::write(file, bytes).map_err(|e| io_fail(file, &e, "check the directory"))
}

#[cfg(feature = "revenue_share")]
pub(super) fn required<'a>(args: &'a [String], flag: &str, what: &str, usage: &str) -> Result<&'a str, OpError> {
    super::opt(args, flag).ok_or_else(|| OpError::usage(format!("{flag}: {what}"), usage))
}

#[cfg(feature = "revenue_share")]
pub(super) fn number(args: &[String], flag: &str, usage: &str) -> Result<u64, OpError> {
    let v = required(args, flag, "microseconds since 1970", usage)?;
    v.parse().map_err(|_| OpError::usage(format!("{flag} {v}: microseconds since 1970, an integer"), usage))
}

/// What a person reads of a contract: its digest, channel, period and who has countersigned.
#[cfg(feature = "revenue_share")]
fn contract_json(c: &wai::revenue_share::Contract, file: &str) -> Value {
    json!({
        "digest": hex(&c.digest()), "operator": hex(&c.operator), "channel": String::from_utf8_lossy(&c.channel), "start": c.start, "end": c.end,
        "currency": String::from_utf8_lossy(&c.currency), "rule": wai::revenue_share::rule::name(c.rule),
        "owners": c.owners.iter().zip(&c.countersigs).map(|(k, s)| json!({"owner": hex(k), "countersigned": *s != [0; 64]})).collect::<Vec<_>>(),
        "in_force": c.check_countersigs().is_ok(), "contract": hex(&c.to_bytes()), "file": file,
    })
}

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

/// `wai contract create <terms.json> --key <file> [--out F]`: the operator's contract from its
/// terms, every countersignature zero (§3.3).
#[cfg(feature = "revenue_share")]
pub fn contract_create(args: &[String]) -> R {
    use wai::revenue_share::{Contract, terms};
    let usage = super::usage("contract.create");
    let with_value = ["--key", "--out"];
    let pos = agent::check_args(args, &with_value, &with_value, 1, &usage)?;
    let path = *pos.first().ok_or_else(|| OpError::usage("the terms: a JSON file", &usage))?;
    let key = read_key(required(args, "--key", "the operator key's file", &usage)?, "the operator key's file (32 bytes, or 64 hexadecimal digits)")?;
    let t = read_json(path, "the contract's terms")?;
    let c = terms(&t).map_err(|e| OpError::usage(format!("{path}: {e}"), &usage))?.offer(&key);
    let bytes = c.to_bytes();
    // Its own rules, before anything is written.
    Contract::read(&bytes).map_err(|r| OpError::new(EXIT_USAGE, r, format!("the contract is refused: {r}"), "extensions/revenue-share.md §3.4 states the rules"))?;
    let file = super::opt(args, "--out").map(str::to_string).unwrap_or_else(|| format!("{}.wrc", &hex(&c.digest())[..16]));
    write(&file, &bytes, "the contract")?;
    let text = format!(
        "contract {}… of {} for [{}, {}): {} owner(s) to countersign with `wai contract accept`{}",
        &hex(&c.digest())[..16],
        String::from_utf8_lossy(&c.channel),
        c.start,
        c.end,
        c.owners.len(),
        if out::dry_run() { String::new() } else { format!("; written to {file}") }
    );
    done("contract.create", true, contract_json(&c, &file), &text)
}

#[cfg(not(feature = "revenue_share"))]
pub fn contract_update(_: &[String]) -> R {
    Err(crate::door::not_built(wai::catalogue::op("contract.update").expect("listed")))
}

/// `wai contract accept <contract> --key <file> [--out F]`: an owner's countersignature added,
/// after the contract's rules and the operator's signature are checked (§3.3, §3.4).
#[cfg(feature = "revenue_share")]
pub fn contract_update(args: &[String]) -> R {
    use wai::revenue_share::Contract;
    let usage = super::usage("contract.update");
    let with_value = ["--key", "--out"];
    let pos = agent::check_args(args, &with_value, &with_value, 1, &usage)?;
    let path = *pos.first().ok_or_else(|| OpError::usage("the contract's file", &usage))?;
    let key = read_key(required(args, "--key", "the owner key's file", &usage)?, "the owner key's file (32 bytes, or 64 hexadecimal digits)")?;
    let b = std::fs::read(path).map_err(|e| io_fail(path, &e, "the contract's file"))?;
    let c = Contract::read(&b).map_err(|r| refused("the contract", r, &json!(["contract"])))?;
    c.check_operator().map_err(|(r, at)| refused("the contract", r, &at))?;
    let c = c.countersign(&key).ok_or_else(|| {
        OpError::new(EXIT_FAILED, "contract_mismatch", "the key is not one of the contract's owners", "give the key of an owner the contract names")
    })?;
    let file = super::opt(args, "--out").unwrap_or(path).to_string();
    write(&file, &c.to_bytes(), "the countersigned contract")?;
    let signed = c.countersigs.iter().filter(|s| **s != [0; 64]).count();
    let text = format!(
        "contract {}… countersigned by {}…: {signed} of {} owner(s){}{}",
        &hex(&c.digest())[..16],
        &hex(&key.verifying_key().to_bytes())[..16],
        c.owners.len(),
        if c.check_countersigs().is_ok() { ", in force" } else { "" },
        if out::dry_run() { String::new() } else { format!("; written to {file}") }
    );
    done("contract.update", true, contract_json(&c, &file), &text)
}

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

/// A statement's outcome, for a person: its party, its lines and its total.
#[cfg(feature = "revenue_share")]
fn summarise(o: &Value) -> String {
    let d = o["statement"].as_str().unwrap_or("");
    let party = o["party"].as_str().unwrap_or("");
    let mut t = format!(
        "statement {}… of the {} {}… for [{}, {}): {} line(s), total {} millionths of a minor unit of {}",
        &d[..16.min(d.len())],
        o["role"].as_str().unwrap_or("?"),
        &party[..16.min(party.len())],
        o["start"],
        o["end"],
        o["lines"].as_array().map_or(0, Vec::len),
        o["total"],
        o["currency"].as_str().unwrap_or("?"),
    );
    for l in o["lines"].as_array().into_iter().flatten() {
        t.push_str(&format!("\n  split {} {:<9} {} impression(s), revenue {}, share {} ppm, amount {}", l["split"], l["class"].as_str().unwrap_or(""), l["impressions"], l["revenue"], l["ppm"], l["amount"]));
    }
    t
}

/// `wai statement create <inputs.json> --party <key> --start µs --end µs --key <file> [--out F]`:
/// the statement of a party and a period, computed from a verification document without its
/// statement (§8.1), signed by the operator and written with its document.
#[cfg(feature = "revenue_share")]
pub fn statement_create(args: &[String]) -> R {
    let usage = super::usage("statement.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 verification document without its statement", &usage))?;
    let party_hex = required(args, "--party", "the party's key: 64 hexadecimal digits", &usage)?;
    let party: [u8; 32] = (party_hex.len() == 64 && party_hex.bytes().all(|c| c.is_ascii_hexdigit()))
        .then(|| (0..32).map(|i| u8::from_str_radix(&party_hex[2 * i..2 * i + 2], 16).expect("hex")).collect::<Vec<u8>>())
        .and_then(|b| b.try_into().ok())
        .ok_or_else(|| OpError::usage("--party: the party's key, 64 hexadecimal digits", &usage))?;
    let (start, end) = (number(args, "--start", &usage)?, number(args, "--end", &usage)?);
    let key = read_key(required(args, "--key", "the operator key's file", &usage)?, "the operator key's file (32 bytes, or 64 hexadecimal digits)")?;
    let inputs = read_json(path, "the statement's inputs")?;
    let (s, doc) = wai::revenue_share::create(&inputs, &key, party, start, end).map_err(|(r, at)| refused("the statement", r, &at))?;
    let o = wai::revenue_share::outcome_json(&wai::revenue_share::verify(&doc));
    let file = super::opt(args, "--out").map(str::to_string).unwrap_or_else(|| format!("{}.wai-share.json", &hex(&s.digest())[..16]));
    let text_out = serde_json::to_string_pretty(&doc).expect("JSON") + "\n";
    write(&file, text_out.as_bytes(), "the statement's document")?;
    let text = format!("{}{}", summarise(&o), if out::dry_run() { String::new() } else { format!("\nwritten to {file}; check it anywhere with `wai statement verify {file}`") });
    done("statement.create", true, json!({"file": file, "statement": hex(&s.to_bytes()), "outcome": o}), &text)
}

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

/// `wai statement verify <document.json>`: a verification document's outcome (§8.3); a refusal is
/// exit 1 with its reason as the code.
#[cfg(feature = "revenue_share")]
pub fn statement_verify(args: &[String]) -> R {
    let usage = super::usage("statement.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")?;
    match wai::revenue_share::verify(&doc) {
        Ok(a) => {
            let o = wai::revenue_share::accepted_json(&a);
            let text = summarise(&o);
            done("statement.verify", false, o, &text)
        }
        Err((r, at)) => Err(refused("the statement", r, &at)),
    }
}
