//! HTTP delivery (`extensions/http-delivery.md`): an envelope served as one immutable
//! object named by its content hash, its head proved against that hash (`WHP1`), and
//! one `WAI2` component fetched by range and checked against its entry's digest, with
//! the pick equal to whole-envelope selection on every path.
//!
//! - [`head`]: the head of a `WAI1` or `WAI2` (§2, §5.1) and `covered_len`;
//! - [`head_proof`]: the `WHP1` head proof (§6);
//! - [`plan`]: which ranges to request, in which priority class (§7, §10.1);
//! - [`session`]: a sans-IO driver of §7, its response checks (§7.1) and the
//!   whole-object fallback;
//! - [`codecs`]: the `codecs` grammar, the union and the floor (§4);
//! - [`playlist`]: the `#WAI-LAYER` tag (§10.4);
//! - [`abr`]: reference adaptation policies, in integers: an entry order for one
//!   `WAI2` and a layer depth (Appendix F);
//! - [`hls`]: WAI media and multivariant playlists beside a classical HLS
//!   presentation, their adjacency and the session-data line (§11);
//! - [`dash`]: `wai.mpd` on the classical timeline (§11.1);
//! - `playlist_receipt` (with `video_manifest`): playlist receipts, sealed and checked,
//!   and a live revision chain (§8.6, §11.6);
//! - [`origin`]: an origin's responses (§5.3) and byte-range answers;
//! - `receipts` (with `receipt_claims`): receipt files named by link hash, an object's
//!   receipt index, and the checks a client runs on what they locate, an object fetched
//!   in part attested through the cid its head proof established (§8.1–§8.3, §10.5);
//! - `keys` (with `key_discovery`): key logs anchored over HTTP and taken through caches,
//!   statuses, grants files, and parameter sets fetched by their digest and held only as
//!   their pins and jwp-receipts §7.9 hold them (§8.4, §8.5).
//!
//! Nothing here performs I/O: the `wai http` commands and the reference origin do.

pub mod abr;
pub mod codecs;
pub mod dash;
pub mod dash_read;
pub mod head;
pub mod head_proof;
pub mod hls;
#[cfg(feature = "key_discovery")]
pub mod keys;
pub mod origin;
pub mod plan;
pub mod playlist;
#[cfg(feature = "video_manifest")]
pub mod playlist_receipt;
#[cfg(feature = "receipt_claims")]
pub mod receipts;
pub mod session;
pub mod uri;

#[cfg(test)]
mod tests;
#[cfg(test)]
mod tests_abr;
#[cfg(test)]
mod tests_playlists;
#[cfg(all(test, feature = "receipt_claims"))]
mod tests_receipts;

/// The proposed media type of an object (§3). It is not registered.
pub const MEDIA_TYPE: &str = "application/wai";

/// jwp-receipts §2.1's content hash: an object's name (§2). One implementation,
/// [`crate::jwp_hash::content_hash`].
pub use crate::jwp_hash::content_hash;

/// The smallest `envelope_len` a head proof can leave uncovered bytes in: one BLAKE3
/// chunk less the content-hash domain (§5.4). Below it a client fetches whole.
pub(crate) const MIN_RANGE_OBJECT: u64 = 1024 - DOMAIN_LEN;

/// The length of jwp-receipts §2.1's domain prefix, `"moq-jwp:object\x01"`.
pub(crate) const DOMAIN_LEN: u64 = crate::jwp_hash::DOMAIN_OBJECT.len() as u64;

/// 64 lowercase hexadecimal digits.
pub fn hex(bytes: &[u8]) -> String {
    bytes.iter().map(|b| format!("{b:02x}")).collect()
}

/// 32 bytes from 64 lowercase hexadecimal digits, or `None`.
pub fn unhex32(s: &str) -> Option<[u8; 32]> {
    if s.len() != 64 || !s.bytes().all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b)) {
        return None;
    }
    let mut out = [0u8; 32];
    for (i, o) in out.iter_mut().enumerate() {
        *o = u8::from_str_radix(&s[2 * i..2 * i + 2], 16).ok()?;
    }
    Some(out)
}

/// The last path segment of an object's canonical URI, `<cid>.wai` (§5.2).
pub fn object_name(cid: &[u8; 32]) -> String {
    format!("{}.wai", hex(cid))
}

/// The fragment identifiers of an object's URI (§3.1). Unknown keys are ignored, and
/// so is a value that does not parse.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Fragment {
    pub cid: Option<[u8; 32]>,
    pub n: Option<u64>,
    pub h: Option<u64>,
    pub c: Option<String>,
}

/// Split `uri` at its fragment: the URI without it, and the identifiers it carries.
pub fn parse_fragment(uri: &str) -> (String, Fragment) {
    let Some((base, frag)) = uri.split_once('#') else { return (uri.to_string(), Fragment::default()) };
    let mut f = Fragment::default();
    for pair in frag.split('&') {
        let Some((k, v)) = pair.split_once('=') else { continue };
        match k {
            "cid" => f.cid = unhex32(v),
            "n" => f.n = v.parse().ok(),
            "h" => f.h = v.parse().ok(),
            "c" if crate::container::is_component_id(v) => f.c = Some(v.to_string()),
            _ => {}
        }
    }
    (base.to_string(), f)
}

/// An inclusive byte range, as an HTTP `Range` names one (RFC 9110 §14.1.2).
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ByteRange {
    pub first: u64,
    pub last: u64,
}

impl ByteRange {
    /// The range [first, first + len), or `None` when `len` is 0 or the range would
    /// pass the last byte offset a `u64` names.
    pub fn from_len(first: u64, len: u64) -> Option<Self> {
        let last = first.checked_add(len.checked_sub(1)?)?;
        Some(ByteRange { first, last })
    }
    /// The `Range` header value, `bytes=first-last`.
    pub fn header(&self) -> String {
        format!("bytes={}-{}", self.first, self.last)
    }
    /// The number of bytes (saturating at `u64::MAX` for the whole of `u64`).
    pub fn len(&self) -> u64 {
        (self.last - self.first).saturating_add(1)
    }
    /// Never empty: a range holds at least one byte.
    pub fn is_empty(&self) -> bool {
        false
    }
    /// The end, exclusive (saturating at `u64::MAX`).
    pub fn end(&self) -> u64 {
        self.last.saturating_add(1)
    }
}

/// Why a client refused bytes, or fetched a whole object instead of ranges
/// (http-delivery §13). [`RangeRefusal::code`] is the reason.
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RangeRefusal {
    /// §5.1: the envelope's length is not its fields' sum.
    ObjectLengthMismatch { expected: u64, got: u64 },
    /// §7 step 3: the head does not parse.
    HeadMalformed(String),
    /// §6.3 check 1.
    HeadProofMalformed(&'static str),
    /// §6.3 check 2.
    HeadProofMismatch,
    /// §7.1: a status other than `206`, or a `200` that is not the whole object.
    HttpStatusUnexpected(u16),
    /// §7.1: a content coding other than none.
    ContentCodingPresent(String),
    /// §7.1: `Content-Range` names another range than the one requested.
    ContentRangeMismatch { want: ByteRange, got: String },
    /// §7.1: a complete length, or a `200` body length, other than the object's.
    CompleteLengthMismatch { want: u64, got: u64 },
    /// §7 step 5: a component the sink uses states no `sha256`. A reason to fetch the
    /// whole object, never a skip.
    ComponentDigestAbsent { index: usize },
    /// §7 step 6: a component's bytes do not match its entry's `sha256`. A reason to
    /// fetch the whole object, where SPEC §7.1's selection skips the entry.
    ComponentDigestMismatch { index: usize },
    /// §7 step 7: the whole envelope does not hash to the cid.
    ObjectDigestMismatch,
    /// §4: a `codecs` value, or a head against one.
    Codecs(codecs::CodecsError),
    /// SPEC §7.1 step 5: no entry is picked; each entry with its reason, unchanged.
    Selection(Vec<(usize, crate::container::Skip)>),
    /// A base-class request was never answered (§10.1: a deadline never applies to a
    /// base).
    BaseUnanswered,
    /// An object listed as a layer (§10.4) is not one: not a `WAI1` that carries
    /// `refines` or names a refinement-only capability.
    NotALayer,
    /// A `#WAI-LAYER` line that does not have §10.4's form.
    LayerTagMalformed(&'static str),
}

impl RangeRefusal {
    /// The reason code (http-delivery §13; `inert` for a selection that picks nothing,
    /// whose per-entry reasons are SPEC §7.1's).
    pub fn code(&self) -> &'static str {
        match self {
            Self::ObjectLengthMismatch { .. } => "object_length_mismatch",
            Self::HeadMalformed(_) => "head_malformed",
            Self::HeadProofMalformed(_) => "head_proof_malformed",
            Self::HeadProofMismatch => "head_proof_mismatch",
            Self::HttpStatusUnexpected(_) => "http_status_unexpected",
            Self::ContentCodingPresent(_) => "content_coding_present",
            Self::ContentRangeMismatch { .. } => "content_range_mismatch",
            Self::CompleteLengthMismatch { .. } => "complete_length_mismatch",
            Self::ComponentDigestAbsent { .. } => "component_digest_absent",
            Self::ComponentDigestMismatch { .. } => "component_digest_mismatch",
            Self::ObjectDigestMismatch => "object_digest_mismatch",
            Self::Codecs(e) => e.code(),
            Self::Selection(_) => "inert",
            Self::BaseUnanswered => "base_unanswered",
            Self::NotALayer => "not_a_layer",
            Self::LayerTagMalformed(_) => "layer_tag_malformed",
        }
    }
}

impl std::fmt::Display for RangeRefusal {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::ObjectLengthMismatch { expected, got } => write!(f, "object_length_mismatch: the fields give {expected} bytes, the object has {got}"),
            Self::HeadMalformed(why) => write!(f, "head_malformed: {why}"),
            Self::HeadProofMalformed(why) => write!(f, "head_proof_malformed: {why}"),
            Self::LayerTagMalformed(why) => write!(f, "layer_tag_malformed: {why}"),
            Self::HttpStatusUnexpected(s) => write!(f, "http_status_unexpected: {s}"),
            Self::ContentCodingPresent(c) => write!(f, "content_coding_present: {c}"),
            Self::ContentRangeMismatch { want, got } => write!(f, "content_range_mismatch: asked {}, got {got:?}", want.header()),
            Self::CompleteLengthMismatch { want, got } => write!(f, "complete_length_mismatch: {got}, the object has {want}"),
            Self::ComponentDigestAbsent { index } => write!(f, "component_digest_absent: entry {index}"),
            Self::ComponentDigestMismatch { index } => write!(f, "component_digest_mismatch: entry {index}"),
            Self::Codecs(e) => e.fmt(f),
            Self::Selection(skips) => {
                f.write_str("inert:")?;
                for (i, s) in skips {
                    write!(f, " {i}={}", s.code())?;
                }
                Ok(())
            }
            other => f.write_str(other.code()),
        }
    }
}

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