//! Playlist receipts (http-delivery §8.6, §11.6): a WAI playlist sealed by a
//! `wai.video.manifest` receipt whose `manifest_hash` is the BLAKE3 of the playlist's
//! bytes, served at `mr/<manifest_hash>.json`, and each live revision chained to the
//! one before it.
//!
//! A public WAI playlist is sealed under [`public_class`]: entitlement `public`, no
//! geography, blackout or ad pod, and the playlist's media sequence as the window. The
//! receipt carries the playlist's rendering (`render`, `extras`), so a verifier renders
//! the playlist from the receipt and compares bytes. Every receipt is also served at
//! `mr/<receipt_hash>.json`, so a client that skipped revisions walks back to the head
//! it holds by the links the receipts name ([`LiveChain`]).

use ed25519_dalek::SigningKey;

use super::hls::{parse_media_playlist, Bindings, PlaylistError};
use crate::quantum_ops::GrantRef;
use crate::wai_video_manifest::{ManifestReceipt, SessionClass};

/// Why a playlist and its receipt are refused (http-delivery §13).
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ReceiptError {
    /// The receipt file does not read as one (`playlist_receipt_malformed`).
    Malformed(String),
    /// The file's name is not `<manifest_hash>.json` (`receipt_name_mismatch`, §8.1).
    NameMismatch,
    /// The playlist's bytes are not the ones the receipt renders and hashes
    /// (`playlist_receipt_mismatch`).
    Mismatch,
    /// The receipt does not verify: its signature, or a value it recomputes
    /// (`playlist_receipt_invalid`).
    Invalid,
    /// A live revision that does not chain to the held head: another key, or a walk
    /// that ends before it (`playlist_receipt_unchained`).
    Unchained,
    /// A parent receipt the walk needs is not served (`playlist_receipt_parent_missing`).
    ParentMissing,
    /// The held head is more than the bound's parents back
    /// (`playlist_receipt_walk_exceeded`).
    WalkExceeded,
    /// A receipt lists another cid for a URI or a media sequence number the chain bound
    /// (`playlist_listing_rebound`).
    Rebound,
}

impl ReceiptError {
    pub fn code(&self) -> &'static str {
        match self {
            ReceiptError::Malformed(_) => "playlist_receipt_malformed",
            ReceiptError::NameMismatch => "receipt_name_mismatch",
            ReceiptError::Mismatch => "playlist_receipt_mismatch",
            ReceiptError::Invalid => "playlist_receipt_invalid",
            ReceiptError::Unchained => "playlist_receipt_unchained",
            ReceiptError::ParentMissing => "playlist_receipt_parent_missing",
            ReceiptError::WalkExceeded => "playlist_receipt_walk_exceeded",
            ReceiptError::Rebound => "playlist_listing_rebound",
        }
    }
}

impl std::fmt::Display for ReceiptError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ReceiptError::Malformed(s) => write!(f, "{}: {s}", self.code()),
            other => f.write_str(other.code()),
        }
    }
}

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

/// The session class of a public WAI playlist: `content_id` as given, entitlement
/// `public`, no geography, blackout or ad pod, and the window `media_sequence`.
pub fn public_class(content_id: &str, media_sequence: u64) -> SessionClass {
    SessionClass {
        content_id: content_id.to_string(),
        entitlement: "public".into(),
        geo: String::new(),
        blackouts: Vec::new(),
        ad_pod_id: String::new(),
        window: media_sequence,
    }
}

/// Seal a WAI playlist (its text, in the form this binding renders) under
/// [`public_class`], unmetered, for one generation, with `parent`'s receipt hash as
/// its parent. Refused when the text does not parse or is not in the rendered form
/// (`playlist_malformed`).
pub fn seal_playlist(
    text: &str,
    signer: &SigningKey,
    signer_id: &str,
    content_id: &str,
    grant: GrantRef,
    parent: Option<&ManifestReceipt>,
) -> Result<ManifestReceipt, PlaylistError> {
    let p = parse_media_playlist(text)?;
    if !p.is_rendered_form(text) {
        return Err(PlaylistError::Malformed("the playlist is not in the form this binding renders, so its rendering cannot be sealed".into()));
    }
    let (segments, render, extras) = p.render_inputs();
    let r = ManifestReceipt::seal_with(signer, signer_id, public_class(content_id, render.media_sequence), segments, 1, 0, None, grant,
        parent.map(ManifestReceipt::receipt_hash), render, extras)
        .map_err(|e| PlaylistError::Malformed(e.0))?;
    debug_assert!(r.verify_render(text.as_bytes()));
    Ok(r)
}

/// §8.6, §11.6: check `playlist` against the receipt file `receipt` named `name` (its
/// file name, when the client fetched it as `mr/<name>`), in order: the file reads as a
/// receipt (`playlist_receipt_malformed`); its name is `<manifest_hash>.json`
/// (`receipt_name_mismatch`); the playlist's BLAKE3 is `manifest_hash` and the receipt's
/// rendering gives the playlist's bytes (`playlist_receipt_mismatch`); the receipt
/// verifies (`playlist_receipt_invalid`).
pub fn check_playlist(playlist: &[u8], receipt: &[u8], name: Option<&str>) -> Result<ManifestReceipt, ReceiptError> {
    let text = std::str::from_utf8(receipt).map_err(|_| ReceiptError::Malformed("not UTF-8".into()))?;
    let r = ManifestReceipt::from_json(text).map_err(ReceiptError::Malformed)?;
    if name.is_some_and(|n| n != r.receipt_file_name()) {
        return Err(ReceiptError::NameMismatch);
    }
    if *blake3::hash(playlist).as_bytes() != r.manifest_hash || r.rendered().as_bytes() != playlist {
        return Err(ReceiptError::Mismatch);
    }
    if !r.verify() {
        return Err(ReceiptError::Invalid);
    }
    Ok(r)
}

/// How many parent receipts a client fetches for one revision, at most (§8.6). A
/// revision further from the held head than this re-anchors the chain when it is ahead
/// of the head, and is refused otherwise (`playlist_receipt_walk_exceeded`). A client
/// MAY hold a lower bound.
/// cbindgen:ignore
pub const WALK_BOUND: usize = 64;

/// What a revision's receipt did to the chain a client holds (§8.6).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Step {
    /// The first receipt held: it becomes the head.
    First,
    /// The receipt is the head itself, fetched again (a reload with no new revision).
    Unchanged,
    /// The receipt chains to the head through `walked` parents fetched by their links
    /// (0: its parent is the head), and becomes the head.
    Chained { walked: usize },
    /// The head is further back than the bound, and the receipt is ahead of it: the
    /// chain re-anchors at the receipt, keeping what it bound. The `walked` parents and
    /// any revision between them and the head are not signed (`reanchored`).
    Reanchored { walked: usize },
}

/// A client's hold on a live playlist's receipts (§8.6, §11.6): the head (the latest
/// receipt that verified and chained) and what every receipt in the chain bound (each
/// URI and media sequence number to its cid, [`Bindings`]).
#[derive(Clone, Debug)]
pub struct LiveChain {
    head: Option<ManifestReceipt>,
    bindings: Bindings,
    bound: usize,
}

impl Default for LiveChain {
    fn default() -> Self {
        LiveChain::new()
    }
}

impl LiveChain {
    /// A chain that holds nothing, walking at most [`WALK_BOUND`] parents.
    pub fn new() -> LiveChain {
        LiveChain::with_bound(WALK_BOUND)
    }

    /// A chain that walks at most `bound` parents for one revision.
    pub fn with_bound(bound: usize) -> LiveChain {
        LiveChain { head: None, bindings: Bindings::default(), bound }
    }

    /// A chain of the playlist at `url` (against which its receipts' lines resolve),
    /// walking at most `bound` parents for one revision.
    pub fn for_playlist(url: &str, bound: usize) -> LiveChain {
        LiveChain { head: None, bindings: Bindings::for_playlist(url), bound }
    }

    /// The receipt held as the head.
    pub fn head(&self) -> Option<&ManifestReceipt> {
        self.head.as_ref()
    }

    /// What the chain's receipts bound.
    pub fn bindings(&self) -> &Bindings {
        &self.bindings
    }

    /// Take `cur`, a revision's receipt already checked against its playlist
    /// ([`check_playlist`]), into the chain. `parent(h)` gives the bytes served at
    /// `mr/<h>.json` beside the playlist, or `None`. Refused, with the hold unchanged:
    ///
    /// - `cur` under another key than the head (`playlist_receipt_unchained`);
    /// - the walk from `cur` back by `parent_receipt_hash` reaches a receipt with no
    ///   parent before the head (`playlist_receipt_unchained`); needs more than the
    ///   bound when `cur` is not ahead of the head (`playlist_receipt_walk_exceeded`;
    ///   ahead, the chain re-anchors at `cur`, [`Step::Reanchored`]); needs a parent
    ///   that is not served (`playlist_receipt_parent_missing`); or meets one that does
    ///   not read
    ///   (`playlist_receipt_malformed`), whose receipt hash is not the link it was
    ///   fetched by (`receipt_name_mismatch`), that does not verify
    ///   (`playlist_receipt_invalid`) or that is under another key
    ///   (`playlist_receipt_unchained`);
    /// - a receipt on the walk, oldest first, or `cur` lists another cid for a URI or
    ///   a media sequence number the chain bound (`playlist_listing_rebound`).
    pub fn advance(&mut self, cur: &ManifestReceipt, mut parent: impl FnMut(&[u8; 32]) -> Option<Vec<u8>>) -> Result<Step, ReceiptError> {
        let Some(head) = &self.head else {
            let mut b = self.bindings.clone();
            bind(&mut b, cur)?;
            self.bindings = b;
            self.head = Some(cur.clone());
            return Ok(Step::First);
        };
        let target = head.receipt_hash();
        if cur.receipt_hash() == target {
            return Ok(Step::Unchanged);
        }
        if cur.signer_pubkey != head.signer_pubkey {
            return Err(ReceiptError::Unchained);
        }
        let mut walked: Vec<ManifestReceipt> = Vec::new();
        let mut link = cur.parent_receipt_hash;
        loop {
            let l = match link {
                None => return Err(ReceiptError::Unchained),
                Some(l) if l == target => break,
                Some(l) => l,
            };
            if walked.len() == self.bound {
                // §8.6: past the bound, a revision ahead of the head (more segments
                // complete) re-anchors the chain, its bindings kept; one that is not
                // ahead is refused, so a stale or older revision never rolls it back.
                if last_segment(cur) <= last_segment(head) {
                    return Err(ReceiptError::WalkExceeded);
                }
                let mut b = self.bindings.clone();
                bind(&mut b, cur)?;
                self.bindings = b;
                self.head = Some(cur.clone());
                return Ok(Step::Reanchored { walked: walked.len() });
            }
            let bytes = parent(&l).ok_or(ReceiptError::ParentMissing)?;
            let text = std::str::from_utf8(&bytes).map_err(|_| ReceiptError::Malformed("not UTF-8".into()))?;
            let r = ManifestReceipt::from_json(text).map_err(ReceiptError::Malformed)?;
            if r.receipt_hash() != l {
                return Err(ReceiptError::NameMismatch);
            }
            if !r.verify() {
                return Err(ReceiptError::Invalid);
            }
            if r.signer_pubkey != cur.signer_pubkey {
                return Err(ReceiptError::Unchained);
            }
            link = r.parent_receipt_hash;
            walked.push(r);
        }
        let mut b = self.bindings.clone();
        for r in walked.iter().rev().chain(std::iter::once(cur)) {
            bind(&mut b, r)?;
        }
        self.bindings = b;
        self.head = Some(cur.clone());
        Ok(Step::Chained { walked: walked.len() })
    }
}

/// The media sequence number after the last complete segment `r` lists: how far its
/// playlist has advanced.
fn last_segment(r: &ManifestReceipt) -> u128 {
    u128::from(r.render.as_ref().map_or(0, |x| x.media_sequence)) + r.segments.len() as u128
}

/// Bind what `r`'s rendering lists.
fn bind(b: &mut Bindings, r: &ManifestReceipt) -> Result<(), ReceiptError> {
    let p = parse_media_playlist(&r.rendered()).map_err(|e| ReceiptError::Malformed(e.to_string()))?;
    b.bind(&p).map_err(|e| match e {
        PlaylistError::Rebound(_) => ReceiptError::Rebound,
        e => ReceiptError::Malformed(e.to_string()),
    })
}

/// A live playlist's receipts in the order a client fetched them (§8.6): the first is
/// held, and each after it is the head again or chains to the head, its parents found
/// among `revisions` by receipt hash. The first failing revision's index and reason.
pub fn check_chain(revisions: &[ManifestReceipt]) -> Result<(), (usize, ReceiptError)> {
    let mut chain = LiveChain::new();
    for (k, r) in revisions.iter().enumerate() {
        chain
            .advance(r, |h| revisions.iter().find(|x| x.receipt_hash() == *h).map(|x| x.to_json().into_bytes()))
            .map_err(|e| (k, e))?;
    }
    Ok(())
}
