//! Keys, statuses, grants and parameter sets over HTTP (http-delivery §8.4, §8.5).
//!
//! What is served here is jwp-receipts §7's, checked by `key_log` and nowhere else:
//!
//! - an origin's key log at [`KEY_LOG_PATH`] (§7.2). A verifier anchors an origin
//!   (§7.3) only from a log retrieved from that location over HTTPS, answered `200`
//!   with no redirect ([`anchor_retrieved`], `key_log_not_from_origin` otherwise); a log
//!   retrieved through any cache adds revisions, each checked against its parent, and
//!   never moves the anchor ([`hold_cached_revisions`]);
//! - a key status at [`STATUS_PATH`] and an origin's module grants at
//!   `/.well-known/wai-grants/<module digest>.json` (§7.9). The grants file is a locator:
//!   each grant in it is handed to §7.9's checks on its own ([`read_grants_file`]);
//! - a parameter set at `<base>/<sha256>`, named by the digest its pin states. The name
//!   locates it and proves nothing: the bytes are held only as SPEC §3.1's checks hold
//!   them, check 5 being §7.9's ([`check_fetched_set`]), so `pin::verify_bytes_with` and
//!   `int_payload::resolve` decide, never the URL.
//!
//! A module grant is §7.9's form. WAI carries a JCP grant only as a reference by its
//! content hash inside the receipt that names it (`quantum_ops::GrantRef`, as a
//! playlist receipt carries its publisher's grant, §8.6); this binding serves no JCP
//! grant, and a client checks such a reference only as the receipt carrying it binds it.
//!
//! Nothing here performs I/O or reads a clock.

use std::collections::BTreeMap;

use serde_json::{Map, Value};

use super::hex;
use crate::container::{PinRefusal, Prior};
use crate::jwp_delivery::ObjectReceipt;
use crate::key_log::{
    anchor_log, read_log, Anchor, AnchorPath, Anchoring, Confirmation, Item, ModuleGrant, Reason, Revision, SinkKeyLog,
};
use crate::pin::{HeldBytes, PinAuthority, VerifiedRef};

pub use crate::signer_id::{key_log_url, KEY_LOG_PATH};

/// Where an origin serves its key status (§8.4).
pub const STATUS_PATH: &str = "/.well-known/wai-key-status";

/// The most bytes of a key log a client reads (§8.4); a longer one is
/// `key_log_too_large`, whatever its first bytes say. A log is a few revisions of a few
/// keys each, kilobytes long; the cap bounds what one retrieval costs.
///
/// cbindgen:ignore
pub const MAX_KEY_LOG_BYTES: usize = 1 << 20;
/// Where an origin serves its module grants, one file per module digest (§8.4).
pub const GRANTS_PATH: &str = "/.well-known/wai-grants/";
/// `kind` of a module grants file (§8.4).
pub const GRANTS_KIND: &str = "wai-module-grants";

/// The URL of `origin`'s key status.
pub fn status_url(origin: &str) -> String {
    format!("{origin}{STATUS_PATH}")
}

/// The URL of `origin`'s grants file for the module or parameter set `digest`.
pub fn grants_url(origin: &str, digest: &[u8; 32]) -> String {
    format!("{origin}{GRANTS_PATH}{}.json", hex(digest))
}

/// The URL of the parameter set whose digest is `sha256` (64 lowercase hexadecimal
/// digits, as a pin states it) under `base` (§8.5).
pub fn set_url(base: &str, sha256: &str) -> String {
    format!("{}/{sha256}", base.trim_end_matches('/'))
}

/// The headers a verifier sends when it retrieves a key log to anchor its origin (§8.4):
/// it asks every cache on the way to revalidate with the origin.
pub fn anchoring_request_headers() -> Vec<(&'static str, &'static str)> {
    vec![("Cache-Control", "no-cache")]
}

/// The largest `max-age`, in seconds, a key status may be served with (§8.4): a quarter
/// of the head's `status_max_age_s`, so a status stays fresh at the sink for most of its
/// life after any cache served it.
pub fn status_max_age_s(head: &Revision) -> u64 {
    head.status_max_age_s / 4
}

/// Why a key log, a grants file or a status was not taken (http-delivery §13, and
/// jwp-receipts §7.7 unchanged).
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum KeysRefusal {
    /// §8.4: a log offered for anchoring that was not retrieved from the origin's own
    /// key-log location over HTTPS, answered `200` with no redirect
    /// (`key_log_not_from_origin`).
    NotFromOrigin(&'static str),
    /// §8.4: a grants file not of its form, or for another module than the one it is
    /// fetched for (`grants_file_malformed`).
    GrantsFileMalformed(&'static str),
    /// §8.4: a key log longer than [`MAX_KEY_LOG_BYTES`] (`key_log_too_large`).
    TooLarge,
    /// jwp-receipts §7.3, §7.7: the log's own reason.
    Log(Reason),
}

impl KeysRefusal {
    /// The reason code.
    pub fn code(&self) -> &'static str {
        match self {
            KeysRefusal::NotFromOrigin(_) => "key_log_not_from_origin",
            KeysRefusal::GrantsFileMalformed(_) => "grants_file_malformed",
            KeysRefusal::TooLarge => "key_log_too_large",
            KeysRefusal::Log(r) => r.label(),
        }
    }
}

impl std::fmt::Display for KeysRefusal {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            KeysRefusal::NotFromOrigin(why) | KeysRefusal::GrantsFileMalformed(why) => write!(f, "{}: {why}", self.code()),
            KeysRefusal::Log(r) => f.write_str(r.label()),
            KeysRefusal::TooLarge => f.write_str(self.code()),
        }
    }
}

impl std::error::Error for KeysRefusal {}

/// How the answer to a key-log request reached the verifier: the URL it requested, the
/// URL that answered (another one after a redirect a client library followed), and the
/// status.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Retrieval<'a> {
    pub requested: &'a str,
    pub answered: &'a str,
    pub status: u16,
}

/// §8.4 and jwp-receipts §7.3: anchor `origin` from `body`, a key log retrieved as
/// `retrieval` states at `acquired_at`, the verifier holding `current` (its anchor and
/// revisions) where it has one. Refused (`key_log_not_from_origin`) unless `origin` is an
/// `https` origin, the request was for its key-log location ([`key_log_url`]), and the
/// answer is a `200` from that same URL; a body longer than [`MAX_KEY_LOG_BYTES`] is
/// `key_log_too_large`. Then §7.3's checks and moves, as `key_log`
/// makes them: the log's reason, `key_log_rolled_back` for an older copy, and
/// `key_log_replaced` for a log that neither contains the anchor nor supersedes it.
pub fn anchor_retrieved(
    origin: &str,
    retrieval: &Retrieval<'_>,
    body: &[u8],
    acquired_at: &str,
    current: Option<(&Anchor, &[Revision])>,
) -> Result<Anchor, KeysRefusal> {
    let location = key_log_url(origin);
    if !origin.starts_with("https://") {
        return Err(KeysRefusal::NotFromOrigin("the origin is not an https origin"));
    }
    if retrieval.requested != location {
        return Err(KeysRefusal::NotFromOrigin("the request was not for the origin's key-log location"));
    }
    if retrieval.answered != location {
        return Err(KeysRefusal::NotFromOrigin("the answer came from another URL: a redirect was followed"));
    }
    if retrieval.status != 200 {
        return Err(KeysRefusal::NotFromOrigin("the answer is not a 200"));
    }
    if body.len() > MAX_KEY_LOG_BYTES {
        return Err(KeysRefusal::TooLarge);
    }
    let text = std::str::from_utf8(body).map_err(|_| KeysRefusal::Log(Reason::KeyLogMalformed))?;
    match anchor_log(origin, text, AnchorPath::Fetched { acquired_at: acquired_at.into() }, current) {
        Anchoring::Anchored(a) => Ok(a),
        Anchoring::NotAnchored(r) => Err(KeysRefusal::Log(r)),
        Anchoring::Replaced => Err(KeysRefusal::Log(Reason::KeyLogReplaced)),
    }
}

/// §8.4: take the revisions of a key log retrieved other than to anchor — through any
/// cache, a mirror, or the origin without the anchoring rules. The log is read as §7.3
/// reads one (its form, rules 1–3 for `origin`, its first `seq` 0, rules 4–6 between
/// consecutive revisions; that rule's reason). One whose latest revision is a trusted
/// revision older than `anchor` — the anchor's held ancestors — is an older copy
/// (`key_log_rolled_back`) and adds nothing. Otherwise every revision of it the
/// verifier does not hold is added to `held`, and §7.4's head walk decides what each is
/// worth; the anchor never moves. Returns how many were added. `anchor` must be for
/// `origin` (`origin_not_anchored`).
pub fn hold_cached_revisions(origin: &str, body: &[u8], anchor: &Anchor, held: &mut Vec<Revision>) -> Result<usize, KeysRefusal> {
    if anchor.origin != origin {
        return Err(KeysRefusal::Log(Reason::OriginNotAnchored));
    }
    if body.len() > MAX_KEY_LOG_BYTES {
        return Err(KeysRefusal::TooLarge);
    }
    let text = std::str::from_utf8(body).map_err(|_| KeysRefusal::Log(Reason::KeyLogMalformed))?;
    let revs = read_log(origin, text).map_err(KeysRefusal::Log)?;
    let by: BTreeMap<[u8; 32], &Revision> =
        held.iter().filter(|r| r.check_held(origin).is_ok()).map(|r| (r.revision_hash(), r)).collect();
    let mut trusted = vec![anchor.revision];
    while let Some(p) = by.get(trusted.last().expect("non-empty")).and_then(|r| r.prev) {
        if !by.contains_key(&p) || trusted.contains(&p) {
            break;
        }
        trusted.push(p);
    }
    let latest = revs.last().expect("a log is non-empty").revision_hash();
    if latest != anchor.revision && trusted.contains(&latest) {
        return Err(KeysRefusal::Log(Reason::KeyLogRolledBack));
    }
    let mut have: Vec<[u8; 32]> = held.iter().map(Revision::revision_hash).collect();
    let mut added = 0;
    for r in revs {
        let h = r.revision_hash();
        if !have.contains(&h) {
            have.push(h);
            held.push(r);
            added += 1;
        }
    }
    Ok(added)
}

/// §8.4: read the grants file fetched for `module`: JSON as jwp-receipts §3.1 step 1
/// reads it, with exactly the members `kind` (`wai-module-grants`), `module` (64 lowercase
/// hexadecimal digits, the digest it is fetched for) and `grants` (an array), or
/// `grants_file_malformed`. Returns each grant's text, to be checked on its own by §7.9
/// (`key_log::module_check`, `key_log::SinkKeyLog::with_grants`): its canonical form, or,
/// for one outside the canonical subset, its JSON as read, which check 1 refuses.
pub fn read_grants_file(module: &[u8; 32], bytes: &[u8]) -> Result<Vec<String>, KeysRefusal> {
    let bad = KeysRefusal::GrantsFileMalformed;
    let text = std::str::from_utf8(bytes).map_err(|_| bad("not UTF-8"))?;
    let v = crate::claim_receipt::parse_strict(text).map_err(|_| bad("not JSON as a receipt is read"))?;
    let m = v.as_object().ok_or(bad("not an object"))?;
    const MEMBERS: [&str; 3] = ["kind", "module", "grants"];
    if m.len() != MEMBERS.len() || MEMBERS.iter().any(|k| !m.contains_key(*k)) {
        return Err(bad("the members are not kind, module and grants"));
    }
    if m["kind"].as_str() != Some(GRANTS_KIND) {
        return Err(bad("kind is not wai-module-grants"));
    }
    if m["module"].as_str().and_then(super::unhex32) != Some(*module) {
        return Err(bad("module is not the digest the file is fetched for"));
    }
    let grants = m["grants"].as_array().ok_or(bad("grants is not an array"))?;
    Ok(grants.iter().map(|g| crate::jcs::jcs(g).unwrap_or_else(|_| g.to_string())).collect())
}

/// A grants file (§8.4) for `module`, listing `grants`, in JCS.
pub fn grants_file(module: &[u8; 32], grants: &[ModuleGrant]) -> Vec<u8> {
    let mut m = Map::new();
    m.insert("kind".into(), GRANTS_KIND.into());
    m.insert("module".into(), hex(module).into());
    let gs: Vec<Value> = grants
        .iter()
        .map(|g| crate::claim_receipt::parse_strict(&g.to_json()).expect("a grant's canonical form reads"))
        .collect();
    m.insert("grants".into(), Value::Array(gs));
    crate::jcs::jcs(&Value::Object(m)).expect("a grants file is inside the canonical subset").into_bytes()
}

/// §8.5: hold a parameter set fetched by the digest `prior` states, as SPEC §3.1's
/// checks hold it over the bytes `served` (`pin::verify_bytes_with`, reading the forms
/// SPEC registers), check 5 run by `sink` where the sink holds a key log: jwp-receipts
/// §7.9 with the statuses and grants it fetched as §8.4 states. The URL the bytes came
/// from counts for nothing: bytes whose digest is not the pin's are not held
/// (`prior_not_held`), and a set §7.9 refuses is not held (`prior_revoked`,
/// `prior_not_authorised`). A set that is not held leaves its capability unsupported for
/// the envelope, and dispatch continues (SPEC §4, §7.1).
pub fn check_fetched_set(
    capability: &str,
    prior: &Prior,
    served: &HeldBytes<'_>,
    sink: Option<&SinkKeyLog>,
) -> Result<VerifiedRef, PinRefusal> {
    crate::pin::verify_bytes_with(
        capability,
        prior,
        std::slice::from_ref(served),
        crate::pin::READABLE_FORMS,
        sink.map(|s| s as &dyn PinAuthority),
    )
}

/// The files a set listing names, each with the digest its URL is formed from (§8.5):
/// a set in the form `wai.prior.set` is fetched as its listing, by the pin's digest, then
/// each file by its own digest. The listing's own reading (`set_malformed`).
pub fn set_files(listing: &[u8]) -> Result<Vec<(String, [u8; 32])>, PinRefusal> {
    crate::pin::parse_listing(listing)
}

/// jwp-receipts §7.4 for the group of a delivery item accepted from an index (§8.2),
/// with its object receipt: its signer confirmed against the revisions and anchors the
/// verifier holds — anchored over HTTP as §8.4 states, or pinned. Reported beside the
/// item's verdict, never folded into it. `None` where the group or the object receipt
/// does not verify, which an accepted item's always do.
pub fn confirm_delivery(
    group: &crate::jwp_delivery::GroupReceipt,
    object: &ObjectReceipt,
    revisions: &[Revision],
    anchors: &[Anchor],
) -> Option<Confirmation> {
    let item = Item::from_group(group, &[object]).ok()?;
    Some(crate::key_log::confirm(&item, revisions, anchors, None))
}

/// jwp-receipts §7.4 for a claim accepted from an index (§8.2), reported beside its
/// verdict.
pub fn confirm_claim(claim: &crate::claim_receipt::ClaimReceipt, revisions: &[Revision], anchors: &[Anchor]) -> Confirmation {
    crate::key_log::confirm(&Item::from_claim(claim), revisions, anchors, None)
}
