//! The delivery-decision record (extensions/route-record.md §6.3): why a delivery of a job's output
//! took the route it took.
//!
//! A delivery is routed as a job is, from its own request, price book (of version 2) and policy,
//! by [`super::route`]: the candidates are the routes that could carry it. Its record restates
//! every route with the figures its price book stated, every limit that removed one, the order and
//! the decision, the route taken, and for every other survivor the key that ranked the route taken
//! above it ([`passed`]); and it names its inputs by digest: the delivery's three documents and, as
//! its `parent`, the job's route record. Signed by the job's router, it is chained: each decision
//! after a job's first names the one before it by `prev`. [`check`] holds a job's decisions, in
//! order, to their documents, to §4 over them, to their parent and to their chain (§7.2 step 10).

use std::cmp::Ordering;
use std::collections::BTreeSet;

use ed25519_dalek::{Signer, SigningKey};
use serde_json::{json, Value};

use super::{
    at, by_key, candidate_form, canonical, capability, check_routed, currency, digest, exactly, hex, int, operation, output_commitment, quantities, read_document, route, tok, unhex, Candidate,
    PriceBook, Policy, Reason, Refusal, Request, Routed, DECIDED_BY, VERSION_2,
};

pub const KIND: &str = "wai-route-delivery-decision";
pub const VERSION: u64 = 1;
/// The domain a record is signed under (§6.3).
pub const SIGN_DOMAIN: &[u8] = b"wai:route-delivery-decision\x01";

/// The members of a record (§6.3).
pub const MEMBERS: [&str; 23] = [
    "kind", "version", "job", "parent", "seq", "prev", "objects", "delivery", "operation", "capability", "fallback", "quantities", "request", "policy", "pricebook", "candidates",
    "order", "decision", "route", "passed", "decided", "signer", "sig",
];

/// The keys a survivor may be passed over by: those of a key list (§4.4).
pub const PASSED_KEYS: [&str; 6] = ["targets", "price", "ttfb", "energy", "quality", "endpoint"];

/// A delivery's texts in a verification document (§7.1): its three documents and its decision.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Texts<'a> {
    pub request: &'a str,
    pub pricebook: &'a str,
    pub policy: &'a str,
    pub decision: &'a str,
}

impl<'a> Texts<'a> {
    /// An object of exactly `request`, `pricebook`, `policy` and `decision`, each a string.
    pub fn from_value(v: &'a Value) -> Option<Texts<'a>> {
        let o = exactly(v, &["request", "pricebook", "policy", "decision"])?;
        Some(Texts { request: o["request"].as_str()?, pricebook: o["pricebook"].as_str()?, policy: o["policy"].as_str()?, decision: o["decision"].as_str()? })
    }
}

/// What a verification reads of a decision that holds (§7.3).
#[derive(Clone, Debug, PartialEq)]
pub struct Read {
    pub seq: u64,
    pub delivery: String,
    pub digest: [u8; 32],
    /// The route the delivery took, or `None` when no route survived.
    pub route: Option<String>,
    pub decision: Value,
    /// Each later survivor's endpoint, with the key it was passed over by.
    pub passed: Vec<(String, &'static str)>,
}

/// For each survivor after the first, in the order, its endpoint and the first key of the key list
/// on which it and the first differ (§6.3 `passed`).
pub fn passed(r: &Request, routed: &Routed) -> Vec<(String, &'static str)> {
    let Some((&first, rest)) = routed.order.split_first() else { return Vec::new() };
    let a: &Candidate = &routed.candidates[first];
    rest.iter()
        .map(|&i| {
            let b = &routed.candidates[i];
            let k = routed.key_list.iter().find(|k| by_key(r, k, a, b) != Ordering::Equal).expect("endpoints differ");
            (b.entry.endpoint.clone(), PASSED_KEYS.iter().copied().find(|x| x == k).expect("a key of the key list"))
        })
        .collect()
}

/// What a route record's served attempt delivered (§6.3 *its parent*): the count and the
/// commitment of its outputs, and its `end`. `None` for a record of version 1, which states no
/// output, and for one no hop served.
pub fn delivered_by(record: &Value) -> Option<(u64, String, u64)> {
    let a = record["attempts"].as_array()?.last().filter(|a| a["outcome"] == "served")?;
    let end = a["end"].as_u64()?;
    if let Some(d) = a.get("delivered") {
        return Some((d["count"].as_u64()?, d["root"].as_str()?.to_string(), end));
    }
    let hashes = a.get("outputs")?.as_array()?.iter().map(|x| unhex::<32>(x.as_str()?)).collect::<Option<Vec<_>>>()?;
    Some((hashes.len() as u64, hex(&output_commitment(&hashes)), end))
}

/// The bytes a record is signed over (§6.3): the domain, then the canonical form of the record
/// without `sig`.
pub fn signing_bytes(record: &Value) -> Vec<u8> {
    let mut r = record.clone();
    if let Some(o) = r.as_object_mut() {
        o.remove("sig");
    }
    let mut m = SIGN_DOMAIN.to_vec();
    m.extend_from_slice(canonical(&r).as_bytes());
    m
}

/// Sign a record body with the router's `key`: sets `signer` and `sig`.
pub fn sign(mut body: Value, key: &SigningKey) -> Value {
    let o = body.as_object_mut().expect("a record is an object");
    o.insert("signer".into(), json!(hex(&key.verifying_key().to_bytes())));
    o.insert("sig".into(), json!(""));
    let sig = key.sign(&signing_bytes(&body));
    body["sig"] = json!(hex(&sig.to_bytes()));
    body
}

/// Sign a record again over whatever it states, keeping its `signer`: for the corpus's controls.
#[doc(hidden)]
pub fn resign(mut body: Value, key: &SigningKey) -> Value {
    body["sig"] = json!("");
    let sig = key.sign(&signing_bytes(&body));
    body["sig"] = json!(hex(&sig.to_bytes()));
    body
}

/// The decision of a delivery of the output of the job whose route record is `parent` (§6.3):
/// routed from the delivery's three documents, the `seq`-th of the job's, after the decision whose
/// digest is `prev`, decided at `decided`, signed by `key`. `parent` is a record whose served hop
/// delivered (version 2 or 3); [`check`] holds the rest.
#[allow(clippy::too_many_arguments)]
pub fn seal(key: &SigningKey, parent: &Value, seq: u64, prev: Option<[u8; 32]>, r: &Request, b: &PriceBook, p: &Policy, decided: u64) -> Value {
    let routed = route(r, b, p);
    let order = routed.order_endpoints();
    let (count, root, _) = delivered_by(parent).expect("a record whose served hop delivered");
    let body = json!({
        "kind": KIND,
        "version": VERSION,
        "job": parent["job"],
        "parent": hex(&digest(parent)),
        "seq": seq,
        "prev": prev.map(|d| hex(&d)),
        "objects": {"count": count, "root": root},
        "delivery": r.job,
        "operation": r.operation,
        "capability": r.capability,
        "fallback": r.fallback,
        "quantities": r.doc["quantities"],
        "request": hex(&r.digest),
        "policy": {"name": p.name, "version": p.version, "digest": hex(&p.digest)},
        "pricebook": {"digest": hex(&b.digest), "currency": b.currency, "issued": b.issued},
        "candidates": routed.candidates.iter().map(Candidate::to_value).collect::<Vec<_>>(),
        "order": order,
        "decision": routed.decision(),
        "route": order.first(),
        "passed": passed_json(&passed(r, &routed)),
        "decided": decided,
    });
    sign(body, key)
}

fn passed_json(p: &[(String, &'static str)]) -> Value {
    Value::Array(p.iter().map(|(e, k)| json!({"endpoint": e, "key": k})).collect())
}

fn hexn(v: &Value, n: usize) -> bool {
    v.as_str().is_some_and(|s| s.len() == n && s.bytes().all(|c| c.is_ascii_digit() || (b'a'..=b'f').contains(&c)))
}

/// A record of §6.3's members, each of its own form. Relations between members are §7.2 step 10's.
pub fn form(d: &Value) -> bool {
    let Some(o) = exactly(d, &MEMBERS) else { return false };
    let tok_or_null = |v: &Value| v.is_null() || tok(v).is_some();
    o["version"] == json!(VERSION)
        && tok(&o["job"]).is_some()
        && hexn(&o["parent"], 64)
        && int(&o["seq"]).is_some_and(|n| n >= 1)
        && (o["prev"].is_null() || hexn(&o["prev"], 64))
        && exactly(&o["objects"], &["count", "root"]).is_some_and(|x| int(&x["count"]).is_some_and(|n| n >= 1) && hexn(&x["root"], 64))
        && tok(&o["delivery"]).is_some()
        && o["operation"].as_str().is_some_and(operation)
        && o["capability"].as_str().is_some_and(capability)
        && (o["fallback"].is_null() || o["fallback"].as_str().is_some_and(capability))
        && quantities(&o["quantities"]).is_some()
        && hexn(&o["request"], 64)
        && exactly(&o["policy"], &["name", "version", "digest"]).is_some_and(|p| tok(&p["name"]).is_some() && int(&p["version"]).is_some_and(|v| v >= 1) && hexn(&p["digest"], 64))
        && exactly(&o["pricebook"], &["digest", "currency", "issued"])
            .is_some_and(|p| hexn(&p["digest"], 64) && p["currency"].as_str().is_some_and(currency) && int(&p["issued"]).is_some())
        && o["candidates"].as_array().is_some_and(|c| c.len() <= 1024 && c.iter().all(|c| candidate_form(c, VERSION_2)))
        && o["order"].as_array().is_some_and(|x| x.len() <= 1024 && x.iter().all(|e| tok(e).is_some()))
        && exactly(&o["decision"], &["decided_by", "runner_up"]).is_some_and(|d| d["decided_by"].as_str().is_some_and(|x| DECIDED_BY.contains(&x)) && tok_or_null(&d["runner_up"]))
        && tok_or_null(&o["route"])
        && o["passed"].as_array().is_some_and(|ps| {
            ps.len() <= 1023 && ps.iter().all(|x| exactly(x, &["endpoint", "key"]).is_some_and(|x| tok(&x["endpoint"]).is_some() && x["key"].as_str().is_some_and(|k| PASSED_KEYS.contains(&k))))
        })
        && int(&o["decided"]).is_some()
        && hexn(&o["signer"], 64)
        && hexn(&o["sig"], 128)
}

/// §7.2 step 10: a job's decisions, from the first and in order, each read, its signature checked
/// and held to its documents, to §4 over them, to its parent `rec` and to its chain.
pub(super) fn check(rec: &Value, deliveries: &[Texts<'_>]) -> Result<Vec<Read>, Refusal> {
    let parent = delivered_by(rec);
    let parent_digest = hex(&digest(rec));
    let mut read: Vec<Read> = Vec::new();
    let mut before: Option<(String, u64)> = None;
    let mut seen = BTreeSet::new();
    for (i, t) in deliveries.iter().enumerate() {
        let place = |m: &[Value]| {
            let mut a = vec![json!("deliveries"), json!(i)];
            a.extend_from_slice(m);
            at(&a)
        };
        let member = |m: &str| place(&[json!("decision"), json!(m)]);
        let r = Request::read(t.request).map_err(|e| Refusal::new(e, place(&[json!("request")])))?;
        let b = PriceBook::read(t.pricebook).map_err(|e| Refusal::new(e, place(&[json!("pricebook")])))?;
        let p = Policy::read(t.policy).map_err(|e| Refusal::new(e, place(&[json!("policy")])))?;
        let d = read_document(t.decision).ok_or_else(|| Refusal::new(Reason::Malformed, place(&[json!("decision")])))?;
        if d.get("kind").and_then(Value::as_str) != Some(KIND) {
            return Err(Refusal::new(Reason::Malformed, place(&[json!("decision")])));
        }
        if d.get("version").and_then(int).is_some_and(|v| v != VERSION) {
            return Err(Refusal::new(Reason::UnknownVersion, member("version")));
        }
        if !form(&d) {
            return Err(Refusal::new(Reason::DeliveryInvalid, place(&[json!("decision")])));
        }
        let signer: [u8; 32] = unhex(d["signer"].as_str().expect("a key")).expect("a key");
        let sig: [u8; 64] = unhex(d["sig"].as_str().expect("a signature")).expect("a signature");
        crate::sig::verify_strict(&signer, &signing_bytes(&d), &sig).map_err(|e| {
            let reason = match e {
                crate::sig::SigError::BadKey => Reason::BadKey,
                crate::sig::SigError::BadSignature => Reason::BadSignature,
            };
            Refusal::new(reason, member("sig"))
        })?;
        // Its documents.
        for (m, want) in [
            ("delivery", json!(r.job)),
            ("operation", json!(r.operation)),
            ("capability", json!(r.capability)),
            ("fallback", json!(r.fallback)),
            ("quantities", r.doc["quantities"].clone()),
            ("request", json!(hex(&r.digest))),
        ] {
            if d[m] != want {
                return Err(Refusal::new(Reason::RequestMismatch, member(m)));
            }
        }
        if d["policy"] != json!({"name": p.name, "version": p.version, "digest": hex(&p.digest)}) {
            return Err(Refusal::new(Reason::PolicyMismatch, member("policy")));
        }
        if b.version != VERSION_2 || d["pricebook"] != json!({"digest": hex(&b.digest), "currency": b.currency, "issued": b.issued}) {
            return Err(Refusal::new(Reason::PricebookMismatch, member("pricebook")));
        }
        // §4 over them, then the route.
        let routed = route(&r, &b, &p);
        check_routed(&d, &routed, &place(&[json!("decision")]))?;
        let order = routed.order_endpoints();
        if d["route"] != json!(order.first()) {
            return Err(Refusal::new(Reason::DeliveryRouteMismatch, member("route")));
        }
        let why = passed(&r, &routed);
        if d["passed"] != passed_json(&why) {
            return Err(Refusal::new(Reason::DeliveryRouteMismatch, member("passed")));
        }
        // Its parent.
        for m in ["signer", "job"] {
            if d[m] != rec[m] {
                return Err(Refusal::new(Reason::DeliveryParentMismatch, member(m)));
            }
        }
        if d["parent"] != json!(parent_digest) {
            return Err(Refusal::new(Reason::DeliveryParentMismatch, member("parent")));
        }
        let Some((count, root, served_end)) = &parent else { return Err(Refusal::new(Reason::DeliveryParentMismatch, member("objects"))) };
        if d["objects"] != json!({"count": count, "root": root}) {
            return Err(Refusal::new(Reason::DeliveryParentMismatch, member("objects")));
        }
        // Its chain.
        if d["seq"] != json!(i + 1) {
            return Err(Refusal::new(Reason::DeliveryChainBroken, member("seq")));
        }
        if d["prev"] != json!(before.as_ref().map(|(h, _)| h)) {
            return Err(Refusal::new(Reason::DeliveryChainBroken, member("prev")));
        }
        let decided = d["decided"].as_u64().expect("a time");
        if decided < *served_end || before.as_ref().is_some_and(|(_, t)| decided < *t) {
            return Err(Refusal::new(Reason::DeliveryChainBroken, member("decided")));
        }
        if !seen.insert(r.job.clone()) {
            return Err(Refusal::new(Reason::DeliveryChainBroken, member("delivery")));
        }
        let dg = digest(&d);
        before = Some((hex(&dg), decided));
        read.push(Read { seq: i as u64 + 1, delivery: r.job.clone(), digest: dg, route: order.first().cloned(), decision: routed.decision(), passed: why });
    }
    Ok(read)
}

/// A read decision's outcome (§7.3).
pub fn read_json(r: &Read) -> Value {
    json!({"seq": r.seq, "delivery": r.delivery, "digest": hex(&r.digest), "route": r.route, "decision": r.decision, "passed": passed_json(&r.passed)})
}
