//! `wai http hls|mpd|seal-playlist|check-playlist` — WAI playlists beside a classical
//! HLS presentation (`extensions/http-delivery.md` §8.6, §11):
//!
//!   wai http hls --objects <list.json> --out <dir> [--master wai.m3u8]
//!       write each variant's WAI media playlist, aligned with its classical media
//!       playlist, and the WAI multivariant playlist; print the union and the floor
//!   wai http mpd --objects <list.json> --out <wai.mpd> [--classical-mpd <mpd>]
//!                [--period-id ID] [--classical-set ID] [--set-id 1]
//!       write wai.mpd on the classical timeline, one Representation per variant, a gap
//!       left out: the classical MPD's period, timescale, t and d (its first period and
//!       the first adaptation set with a timeline, or those named), checked adjacent to
//!       it; with no classical MPD, the classical HLS timeline at timescale 1000 from 0
//!   wai http seal-playlist <wai media playlist> --key <name | seed hex | file>
//!                          [--signer-id ID] [--content-id ID] [--parent <mr.json>]
//!                          [--grant <grant.json>] [--out <dir>]
//!       seal a WAI playlist in its rendered form, under the publisher's grant (a JSON
//!       object of grant_hash, capability, joule_ceiling_micro and funds_ceiling, each
//!       ceiling a number up to 2^53 - 1 or null for none; with no --grant, a grant of
//!       no ceilings); write <dir>/mr/<manifest_hash>.json
//!       and <dir>/mr/<receipt_hash>.json (dir: the playlist's own by default) and
//!       print its path and receipt hash
//!   wai http check-playlist <wai media playlist> --receipt <mr/….json>
//!                           [--parent <mr/….json>] [--classical <m3u8>]
//!       check the playlist against its receipt (and its chain to the receipt held as
//!       --parent, walking any revisions between by their links from the receipt's
//!       directory, and its adjacency to the classical playlist); print what held, or
//!       the reason it did not
//!
//! The objects list, its paths relative to its own directory:
//!
//!   { "uri_prefix": "objects/", "end_list": true,
//!     "variants": [ { "uri": "wai-720.m3u8", "classical": "classical-720.m3u8",
//!                     "resolution": [1280, 720], "frame_rate_milli": 25000,
//!                     "segments": [ { "object": "a.wai", "layers": ["l1.wai"], "gap": false,
//!                                     "parts": [ { "object": "p.wai", "alias": "live/720/1200.0.wai",
//!                                                  "independent": true, "layers": [] } ] } ],
//!                     "open": { "parts": [ … ], "preload_hint": "live/720/1201.2.wai" } } ] }
//!
//! A segment's and a part's duration is the classical playlist's (a part's own
//! `duration_ms` where the classical segment lists no parts); `end_list` defaults to
//! the classical playlist's.

use std::path::{Path, PathBuf};

use serde_json::Value;

use wai::http_binding::codecs::{self, SeqObject};
use wai::http_binding::dash::{wai_mpd, DashTimeline, WaiRepresentation};
use wai::http_binding::hls::{
    bandwidths, parse_media_playlist, wai_media_playlist, wai_multivariant, MediaPlaylist, VariantLine, WaiLayer, WaiOpen,
    WaiPart, WaiSegment,
};
use wai::http_binding::hex;

fn opt<'a>(args: &'a [String], flag: &str) -> Option<&'a str> {
    args.iter().position(|a| a == flag).and_then(|i| args.get(i + 1).map(String::as_str))
}

fn read(p: &Path) -> Result<Vec<u8>, String> {
    std::fs::read(p).map_err(|e| format!("{}: {e}", p.display()))
}

fn write(p: &Path, b: &[u8]) -> Result<(), String> {
    if let Some(d) = p.parent() {
        crate::out::create_dir_all(d).map_err(|e| format!("{}: {e}", d.display()))?;
    }
    crate::out::write(p, b).map_err(|e| format!("{}: {e}", p.display()))
}

/// One variant of an objects list, read: its classical playlist and the objects that
/// cover it, with every envelope's bytes (for the union and the floor).
struct Variant {
    uri: String,
    classical: MediaPlaylist,
    resolution: Option<(u32, u32)>,
    frame_rate_milli: Option<u32>,
    segments: Vec<WaiSegment>,
    open: Option<WaiOpen>,
    envelopes: Vec<Vec<u8>>,
}

struct List {
    uri_prefix: String,
    end_list: Option<bool>,
    variants: Vec<Variant>,
}

fn strs(v: &Value) -> Vec<String> {
    v.as_array().into_iter().flatten().filter_map(|x| x.as_str().map(String::from)).collect()
}

fn layers(dir: &Path, v: &Value, envelopes: &mut Vec<Vec<u8>>) -> Result<Vec<WaiLayer>, String> {
    strs(v).iter().map(|p| {
        let env = read(&dir.join(p))?;
        let l = WaiLayer::of(&env).map_err(|e| format!("{p}: {e}"))?;
        envelopes.push(env);
        Ok(l)
    }).collect()
}

fn parts(dir: &Path, v: &Value, classical_ms: &[u64], envelopes: &mut Vec<Vec<u8>>) -> Result<Vec<WaiPart>, String> {
    v.as_array().into_iter().flatten().enumerate().map(|(j, p)| {
        let path = p["object"].as_str().ok_or("a part names its object")?;
        let alias = p["alias"].as_str().ok_or("a part names its alias")?;
        let ms = classical_ms.get(j).copied().or(p["duration_ms"].as_u64())
            .ok_or(format!("{path}: no classical part {j} and no duration_ms"))?;
        let env = read(&dir.join(path))?;
        let mut part = WaiPart::of(&env, alias, ms, p["independent"].as_bool() == Some(true)).map_err(|e| format!("{path}: {e}"))?;
        envelopes.push(env);
        part.layers = layers(dir, &p["layers"], envelopes)?;
        Ok(part)
    }).collect()
}

fn load(path: &Path) -> Result<List, String> {
    let dir = path.parent().unwrap_or(Path::new("."));
    let v: Value = serde_json::from_slice(&read(path)?).map_err(|e| format!("{}: {e}", path.display()))?;
    let mut variants = Vec::new();
    for x in v["variants"].as_array().ok_or("the list has variants")? {
        let cpath = x["classical"].as_str().ok_or("a variant names its classical playlist")?;
        let ctext = String::from_utf8(read(&dir.join(cpath))?).map_err(|_| format!("{cpath}: not UTF-8"))?;
        let classical = parse_media_playlist(&ctext).map_err(|e| format!("{cpath}: {e}"))?;
        let mut envelopes = Vec::new();
        let mut segments = Vec::new();
        for (i, s) in x["segments"].as_array().ok_or("a variant has segments")?.iter().enumerate() {
            let c = classical.segments.get(i).ok_or(format!("{cpath}: {} segments, and the list has more", classical.segments.len()))?;
            let p = s["object"].as_str().ok_or("a segment names its object")?;
            let env = read(&dir.join(p))?;
            let mut seg = WaiSegment::of(&env, c.duration_ms).map_err(|e| format!("{p}: {e}"))?;
            seg.gap = s["gap"].as_bool() == Some(true);
            if !seg.gap {
                envelopes.push(env);
            }
            seg.layers = layers(dir, &s["layers"], &mut envelopes)?;
            let cms: Vec<u64> = c.extra.parts.iter().map(|q| q.duration_ms).collect();
            seg.parts = parts(dir, &s["parts"], &cms, &mut envelopes)?;
            segments.push(seg);
        }
        let open = match &x["open"] {
            Value::Null => None,
            o => {
                let cms: Vec<u64> = classical.open.as_ref().map(|c| c.parts.iter().map(|q| q.duration_ms).collect()).unwrap_or_default();
                Some(WaiOpen { parts: parts(dir, &o["parts"], &cms, &mut envelopes)?, preload_hint: o["preload_hint"].as_str().map(String::from) })
            }
        };
        let resolution = x["resolution"].as_array().and_then(|r| Some((u32::try_from(r.first()?.as_u64()?).ok()?, u32::try_from(r.get(1)?.as_u64()?).ok()?)));
        variants.push(Variant {
            uri: x["uri"].as_str().ok_or("a variant names its URI")?.to_string(),
            classical,
            resolution,
            frame_rate_milli: x["frame_rate_milli"].as_u64().and_then(|f| u32::try_from(f).ok()),
            segments,
            open,
            envelopes,
        });
    }
    Ok(List { uri_prefix: v["uri_prefix"].as_str().unwrap_or("objects/").to_string(), end_list: v["end_list"].as_bool(), variants })
}

/// The union and floor of a variant's objects (§4.2): its segments, layers and parts.
fn union_floor(envs: &[Vec<u8>]) -> Result<(codecs::Element, codecs::Element), String> {
    let objs: Vec<SeqObject> = envs.iter().map(|e| SeqObject::from_envelope(e).map_err(|e| e.to_string())).collect::<Result<_, _>>()?;
    Ok((codecs::union_of(&objs).map_err(|e| e.to_string())?, codecs::floor_of(&objs).map_err(|e| e.to_string())?))
}

/// The peak and average of a variant (§11.1): each segment that is not a gap with its
/// layer objects, or its parts when it has no segment yet.
fn rates(v: &Variant) -> Result<(u64, u64), String> {
    let mut objs: Vec<(u64, u64)> = v.segments.iter().filter(|s| !s.gap)
        .map(|s| (s.n + s.layers.iter().map(|l| l.len).sum::<u64>(), s.duration_ms)).collect();
    if objs.is_empty() {
        objs = v.open.iter().flat_map(|o| o.parts.iter().map(|p| (p.n, p.duration_ms))).collect();
    }
    bandwidths(&objs).ok_or(format!("{}: no object with a duration to compute a bandwidth from", v.uri))
}

pub fn hls(args: &[String]) -> Result<(), String> {
    let list = load(Path::new(opt(args, "--objects").ok_or("hls needs --objects <list.json>")?))?;
    let out = PathBuf::from(opt(args, "--out").ok_or("hls needs --out <dir>")?);
    let mut lines = Vec::new();
    for v in &list.variants {
        let end = list.end_list.unwrap_or(v.classical.end_list);
        let (_, _, _, text) = wai_media_playlist(&v.classical, &v.segments, v.open.as_ref(), &list.uri_prefix, end).map_err(|e| format!("{}: {e}", v.uri))?;
        write(&out.join(&v.uri), text.as_bytes())?;
        let (union, floor) = union_floor(&v.envelopes)?;
        let (peak, avg) = rates(v)?;
        println!("{}\tunion {}\tfloor {}\tbandwidth {peak}\taverage {avg}", out.join(&v.uri).display(), codecs::render(&union), codecs::render(&floor));
        lines.push(VariantLine { uri: v.uri.clone(), union, floor, peak_bps: peak, avg_bps: avg, resolution: v.resolution, frame_rate_milli: v.frame_rate_milli });
    }
    let master = out.join(opt(args, "--master").unwrap_or("wai.m3u8"));
    write(&master, wai_multivariant(&lines).map_err(|e| e.to_string())?.as_bytes())?;
    println!("{}", master.display());
    Ok(())
}

pub fn mpd(args: &[String]) -> Result<(), String> {
    use wai::http_binding::dash_read::check_dash_adjacent;
    let list = load(Path::new(opt(args, "--objects").ok_or("mpd needs --objects <list.json>")?))?;
    let out = PathBuf::from(opt(args, "--out").ok_or("mpd needs --out <wai.mpd>")?);
    let set_id: u32 = opt(args, "--set-id").unwrap_or("1").parse().map_err(|_| "--set-id is an unsigned integer")?;
    let first = list.variants.first().ok_or("the list has no variant")?;
    // §11.1, §11.2: the classical MPD's period, timescale, t and d; without one, the
    // classical HLS timeline at timescale 1000 from 0.
    let classical_mpd = opt(args, "--classical-mpd").map(|p| String::from_utf8(read(Path::new(p))?).map_err(|_| format!("{p}: not UTF-8"))).transpose()?;
    let hls_timeline = |v: &Variant| DashTimeline::from_hls(&v.classical, opt(args, "--period-id").unwrap_or("p0"), 0).map_err(|e| format!("{}: {e}", v.uri));
    let timeline = match &classical_mpd {
        Some(text) => {
            // Expanded no further than the most segments a variant lists.
            let most = list.variants.iter().map(|v| v.segments.len()).max().unwrap_or(0);
            DashTimeline::from_mpd(text, opt(args, "--period-id"), opt(args, "--classical-set"), most).map_err(|e| format!("the classical MPD: {e}"))?
        }
        None => hls_timeline(first)?,
    };
    let mut reps = Vec::new();
    let mut all = Vec::new();
    for v in &list.variants {
        if v.segments.len() != timeline.segments.len() {
            return Err(format!("{}: {} segments, and the classical timeline has {}", v.uri, v.segments.len(), timeline.segments.len()));
        }
        if classical_mpd.is_none() && hls_timeline(v)? != timeline {
            return Err(format!("{}: its classical timeline is not the first variant's; one AdaptationSet shares one timeline", v.uri));
        }
        let (peak, _) = rates(v)?;
        let id = v.uri.trim_end_matches(".m3u8").rsplit('/').next().unwrap_or(&v.uri).to_string();
        // A gap offers no WAI object there: its S and SegmentURL are left out.
        let media = v.segments.iter()
            .map(|s| (!s.gap).then(|| format!("{}{}.wai#n={}{}", list.uri_prefix, hex(&s.cid), s.n, s.h.map(|h| format!("&h={h}")).unwrap_or_default())))
            .collect();
        reps.push(WaiRepresentation { id, bandwidth: peak, media });
        all.extend(v.envelopes.iter().cloned());
    }
    let (union, floor) = union_floor(&all)?;
    let text = wai_mpd(&timeline, set_id, &union, &floor, &reps).map_err(|e| e.to_string())?;
    if let Some(c) = &classical_mpd {
        check_dash_adjacent(&text, c).map_err(|e| format!("the WAI MPD is not adjacent to the classical one: {e}"))?;
    }
    write(&out, text.as_bytes())?;
    println!("{}", out.display());
    Ok(())
}

#[cfg(feature = "video_manifest")]
pub fn seal(args: &[String]) -> Result<(), String> {
    use wai::http_binding::playlist_receipt::seal_playlist;
    use wai::wai_video_manifest::ManifestReceipt;
    let path = args.first().filter(|a| !a.starts_with("--")).ok_or("seal-playlist needs the playlist")?;
    let text = String::from_utf8(read(Path::new(path))?).map_err(|_| format!("{path}: not UTF-8"))?;
    let key = crate::dx::key::signing_key(opt(args, "--key").ok_or("seal-playlist needs --key")?, true).map_err(|e| e.message)?;
    let parent = opt(args, "--parent").map(|p| ManifestReceipt::from_json(&String::from_utf8_lossy(&read(Path::new(p))?)).map_err(|e| format!("{p}: {e}"))).transpose()?;
    let stem = Path::new(path).file_name().and_then(|n| n.to_str()).unwrap_or("playlist");
    // The publisher's grant (Appendix H's form), or none: a grant with no ceilings.
    let grant = match opt(args, "--grant") {
        Some(g) => {
            let v: serde_json::Value = serde_json::from_slice(&read(Path::new(g))?).map_err(|e| format!("{g}: {e}"))?;
            wai::wai_video_manifest::grant_from_json(&v).map_err(|e| format!("{g}: {e}"))?
        }
        None => wai::quantum_ops::GrantRef::unbounded("video.manifest"),
    };
    let r = seal_playlist(&text, &key, opt(args, "--signer-id").unwrap_or("unconfirmed"), opt(args, "--content-id").unwrap_or(stem),
        grant, parent.as_ref()).map_err(|e| format!("{path}: {e}"))?;
    let dir = opt(args, "--out").map(PathBuf::from).unwrap_or_else(|| Path::new(path).parent().unwrap_or(Path::new(".")).to_path_buf());
    // §8.6: at its manifest hash, where a client checks the playlist, and at its receipt
    // hash, where a client that skipped this revision fetches it by a later one's link.
    let file = dir.join("mr").join(r.receipt_file_name());
    write(&file, r.to_json().as_bytes())?;
    write(&dir.join("mr").join(format!("{}.json", hex(&r.receipt_hash()))), r.to_json().as_bytes())?;
    println!("{}\treceipt {}", file.display(), hex(&r.receipt_hash()));
    Ok(())
}

#[cfg(not(feature = "video_manifest"))]
pub fn seal(_: &[String]) -> Result<(), String> {
    Err("built without video_manifest: sealing a playlist needs a build with the features http and video_manifest".into())
}

#[cfg(feature = "video_manifest")]
pub fn check(args: &[String]) -> Result<bool, String> {
    use wai::http_binding::hls::check_adjacent;
    use wai::http_binding::playlist_receipt::{check_playlist, LiveChain, Step};
    use wai::wai_video_manifest::ManifestReceipt;
    let path = args.first().filter(|a| !a.starts_with("--")).ok_or("check-playlist needs the playlist")?;
    let text = read(Path::new(path))?;
    let rpath = opt(args, "--receipt").ok_or("check-playlist needs --receipt")?;
    let name = Path::new(rpath).file_name().and_then(|n| n.to_str());
    let held = match check_playlist(&text, &read(Path::new(rpath))?, name) {
        Ok(r) => r,
        Err(e) => {
            println!("refused\t{}", e.code());
            return Ok(false);
        }
    };
    let mut walked = None;
    if let Some(p) = opt(args, "--parent") {
        // The head the client holds; parents between it and this revision are fetched
        // by their links from the receipt's own directory (§8.6).
        let head = ManifestReceipt::from_json(&String::from_utf8_lossy(&read(Path::new(p))?)).map_err(|e| format!("{p}: {e}"))?;
        let dir = Path::new(rpath).parent().unwrap_or(Path::new(".")).to_path_buf();
        // Lines resolve against the playlist's own path, the same for every revision.
        let mut chain = LiveChain::for_playlist(path, wai::http_binding::playlist_receipt::WALK_BOUND);
        chain.advance(&head, |_| None).map_err(|e| format!("{p}: {e}"))?;
        match chain.advance(&held, |h| std::fs::read(dir.join(format!("{}.json", hex(h)))).ok()) {
            Ok(Step::Chained { walked: n }) => walked = Some(n),
            Ok(Step::Reanchored { walked: n }) => {
                println!("reanchored\t{n} parents walked; the revisions between are not signed");
                walked = Some(n);
            }
            Ok(_) => walked = Some(0),
            Err(e) => {
                println!("refused\t{}", e.code());
                return Ok(false);
            }
        }
    }
    if let Some(c) = opt(args, "--classical") {
        let ctext = String::from_utf8(read(Path::new(c))?).map_err(|_| format!("{c}: not UTF-8"))?;
        if let Err(e) = check_adjacent(&String::from_utf8_lossy(&text), &ctext) {
            println!("refused\t{}", e.code());
            return Ok(false);
        }
    }
    let chain = walked.map(|n| format!("\tchained ({n} parents walked)")).unwrap_or_default();
    println!("signed\treceipt {}\tsigner {}{chain}\t(signer unconfirmed until anchored)", hex(&held.receipt_hash()), hex(&held.signer_pubkey));
    Ok(true)
}

#[cfg(not(feature = "video_manifest"))]
pub fn check(_: &[String]) -> Result<bool, String> {
    Err("built without video_manifest: checking a playlist receipt needs a build with the features http and video_manifest".into())
}
