//! The import of extensions/channel-lineup.md §4: an XMLTV schedule, Media RSS catalogues, the
//! JSON form and a Media Playlist's SCTE-35 date ranges, into a `wai.video.channel` receipt, a
//! lineup and the breaks' cues. Every step refuses with the reason its section names, in the
//! order of §4.10.

use std::collections::{BTreeMap, HashMap, HashSet};

use ed25519_dalek::SigningKey;
use sha2::{Digest, Sha256};

use super::{
    duration_ms, first, geo, hls_date_time, is_long, is_short, read_feed_xml, xmltv_time, Asset, Lineup, Reason, FEED_KIND, ID_SYSTEM,
    MAX_ARTWORK, MAX_CATEGORIES, MAX_COUNT, MAX_DOCUMENT, MAX_INSTANT,
};
use crate::quantum_ops::GrantRef;
use crate::stitched_presentation::markers::{attribute_list, Element, Value as Attr};
use crate::strict_json::{self, Json};
use crate::wai_video_channel::{ChannelReceipt, EntryKind, ScheduleEntry};

/// The cue id domain (§4.8).
const CUE_ID_DOMAIN: &[u8] = b"wai:channel-cue-id\x01";

/// An import's inputs (§4.1), but the operator's key and the receipt's signer id and parent.
#[derive(Clone, Debug, Default)]
pub struct Feeds {
    pub channel: String,
    pub xmltv: Option<Vec<u8>>,
    /// Each Media RSS document, with its owner when one is given.
    pub mrss: Vec<(Vec<u8>, Option<String>)>,
    pub json: Option<Vec<u8>>,
    pub playlist: Option<Vec<u8>>,
    pub owner: Option<String>,
    pub id_system: Option<String>,
    pub xmltv_channel: Option<String>,
    pub start_ms: Option<u64>,
    /// The artwork set: each file's bytes by its name.
    pub artwork: BTreeMap<String, Vec<u8>>,
    pub window: Option<(u64, u64)>,
}

/// A break's cue (§4.8): its break's id and start, and the live-events §4.3 `cue` payload.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Cue {
    pub break_id: String,
    pub start_ms: u64,
    pub payload: Vec<u8>,
}

/// The cues as `cues.json` holds them: each break's id, its start and its payload in
/// hexadecimal.
pub fn cues_json(cues: &[Cue]) -> serde_json::Value {
    serde_json::Value::Array(cues.iter().map(|c| serde_json::json!({"break": c.break_id, "start_ms": c.start_ms, "payload": super::hex(&c.payload)})).collect())
}

/// What an import yields (§4.9).
#[derive(Clone, Debug, PartialEq)]
pub struct Imported {
    pub receipt: ChannelReceipt,
    pub lineup: Lineup,
    pub cues: Vec<Cue>,
}

type R<T> = Result<T, Reason>;

/// An asset's artwork as a feed gives it: by URI (XMLTV, Media RSS) or with its hash (the
/// JSON form).
#[derive(Clone, Debug)]
enum Art {
    Uris(Vec<String>),
    Hashed(Vec<([u8; 32], String)>),
}

/// An asset's description as a feed gives it (§4.6 step 1).
#[derive(Clone, Debug)]
struct Desc {
    title: String,
    description: String,
    categories: Vec<String>,
    rating: (String, String),
    art: Art,
    geo: (u8, Vec<[u8; 2]>),
    content_uri: String,
    duration_ms: u64,
}

/// A programme: its span and its asset id.
#[derive(Clone, Debug)]
struct Prog {
    start: u64,
    stop: u64,
    asset: String,
}

/// A Media RSS item (§4.3).
#[derive(Clone, Debug)]
struct Item {
    asset: String,
    desc: Desc,
    duration: Option<u64>,
    owner: Option<String>,
}

/// The programmes of an XMLTV document, each with its description (§4.2).
type Described = Vec<(Prog, Desc)>;

/// The JSON form's assets, each its id, its description and its rights holder (§4.4).
type Catalogue = Vec<(String, Desc, Option<String>)>;

/// The JSON form as §4.4 reads it.
struct JsonRead {
    channel_name: String,
    programmes: Vec<Prog>,
    assets: Catalogue,
    breaks: Vec<Break>,
}

/// A break (§4.5).
#[derive(Clone, Debug)]
struct Break {
    id: String,
    start: u64,
    duration: u64,
    section: Option<Vec<u8>>,
}

fn field(ok: bool) -> R<()> {
    if ok { Ok(()) } else { Err(Reason::ImportFieldInvalid) }
}

fn sized(b: &[u8]) -> R<()> {
    if b.len() > MAX_DOCUMENT { Err(Reason::ImportOversized) } else { Ok(()) }
}

// ---------------------------------------------------------------------------------------
// §4.1 Inputs
// ---------------------------------------------------------------------------------------

fn check_inputs(f: &Feeds, signer_id: &str) -> R<()> {
    let bad = Err(Reason::ImportInputsInvalid);
    let set = if f.json.is_some() { f.xmltv.is_none() && f.mrss.is_empty() } else { f.xmltv.is_some() || !f.mrss.is_empty() };
    let owners = f.mrss.iter().all(|(_, o)| o.is_some()) || f.mrss.iter().all(|(_, o)| o.is_none());
    let short_opt = |o: &Option<String>| o.as_deref().is_none_or(is_short);
    let alone = f.json.is_none() && f.xmltv.is_none() && !f.mrss.is_empty();
    if !is_short(&f.channel)
        || !set
        || !owners
        || !f.mrss.iter().all(|(_, o)| short_opt(o))
        || !short_opt(&f.owner)
        || !short_opt(&f.id_system)
        || !short_opt(&f.xmltv_channel)
        || !is_short(signer_id)
        || (alone && f.start_ms.is_none())
        || f.start_ms.is_some_and(|s| s > MAX_INSTANT)
    {
        return bad;
    }
    Ok(())
}

// ---------------------------------------------------------------------------------------
// §4.2 XMLTV, §4.3 Media RSS
// ---------------------------------------------------------------------------------------

/// The categories, artwork URIs and rating of an XMLTV programme or a Media RSS item (§4.2
/// step 4, §4.3 step 3): `art` the child that names an artwork and `attr` its URI attribute,
/// `scheme` the rating's system attribute and its default.
fn description(e: &Element, desc_name: &str, art: (&str, &str), scheme: (&str, &str)) -> R<Desc> {
    let title = first(e, "title").map(Element::text).unwrap_or_default();
    field(!title.is_empty() && is_long(&title))?;
    let description = first(e, desc_name).map(Element::text).unwrap_or_default();
    field(is_long(&description))?;
    let categories: Vec<String> = e.children_named("category").map(Element::text).collect();
    field(categories.len() <= MAX_CATEGORIES && categories.iter().all(|c| is_short(c)))?;
    let uris: Vec<Option<&str>> = e.children_named(art.0).map(|i| i.attr(art.1)).collect();
    field(uris.len() <= MAX_ARTWORK && uris.iter().all(|u| u.is_some_and(|u| !u.is_empty() && is_long(u))))?;
    let rating = match first(e, "rating") {
        None => (String::new(), String::new()),
        Some(r) => {
            let system = r.attr(scheme.0).unwrap_or(scheme.1).to_string();
            field(system.is_empty() || is_short(&system))?;
            let value = if desc_name == "desc" { first(r, "value").map(Element::text).unwrap_or_default() } else { r.text() };
            field(is_short(&value))?;
            (system, value)
        }
    };
    Ok(Desc {
        title,
        description,
        categories,
        rating,
        art: Art::Uris(uris.into_iter().map(|u| u.expect("checked").to_string()).collect()),
        geo: (geo::EVERYWHERE, Vec::new()),
        content_uri: String::new(),
        duration_ms: 0,
    })
}

/// §4.2: the channel name, the programmes and each one's description.
fn read_xmltv(b: &[u8], xch: &str, id_system: &str) -> R<(Option<String>, Described)> {
    sized(b)?;
    let root = read_feed_xml(b).ok_or(Reason::ImportXmlMalformed)?;
    if root.name != "tv" {
        return Err(Reason::ImportXmlMalformed);
    }
    let progs: Vec<&Element> = root.children_named("programme").filter(|p| p.attr("channel") == Some(xch)).collect();
    if progs.len() > MAX_COUNT {
        return Err(Reason::ImportOversized);
    }
    let name = root.children_named("channel").find(|c| c.attr("id") == Some(xch)).and_then(|c| first(c, "display-name")).map(Element::text).filter(|n| !n.is_empty());
    if let Some(n) = &name {
        field(is_short(n))?;
    }
    if progs.is_empty() {
        return Err(Reason::ImportEmpty);
    }
    let mut out = Vec::with_capacity(progs.len());
    for p in progs {
        let start = xmltv_time(p.attr("start").ok_or(Reason::ImportTimeMalformed)?)?;
        let stop = xmltv_time(p.attr("stop").ok_or(Reason::ImportNoStop)?)?;
        if stop <= start {
            return Err(Reason::ImportTimeMalformed);
        }
        let asset = p.children_named("episode-num").find(|e| e.attr("system").unwrap_or("onscreen") == id_system).map(Element::text).filter(|a| is_short(a)).ok_or(Reason::ImportAssetUnknown)?;
        let desc = description(p, "desc", ("icon", "src"), ("system", ""))?;
        out.push((Prog { start, stop, asset }, desc));
    }
    Ok((name, out))
}

/// §4.3 step 4: where an item may be shown.
fn restriction(item: &Element) -> R<(u8, Vec<[u8; 2]>)> {
    let Some(r) = item.children_named("restriction").find(|r| r.attr("type") == Some("country")) else {
        return Ok((geo::EVERYWHERE, Vec::new()));
    };
    let allow = match r.attr("relationship") {
        Some("allow") => true,
        Some("deny") => false,
        _ => return Err(Reason::ImportFieldInvalid),
    };
    let text = r.text();
    let tokens: Vec<&str> = text.split([' ', '\t', '\r', '\n']).filter(|t| !t.is_empty()).collect();
    match (tokens.as_slice(), allow) {
        (["all"], true) | (["none"], false) => Ok((geo::EVERYWHERE, Vec::new())),
        (["none"], true) | (["all"], false) => Ok((geo::ONLY, Vec::new())),
        ([], _) => Err(Reason::ImportFieldInvalid),
        (t, _) if t.iter().all(|c| c.len() == 2 && c.bytes().all(|x| x.is_ascii_alphabetic())) => {
            let mut codes: Vec<[u8; 2]> = t.iter().map(|c| [c.as_bytes()[0].to_ascii_uppercase(), c.as_bytes()[1].to_ascii_uppercase()]).collect();
            codes.sort_unstable();
            codes.dedup();
            Ok((if allow { geo::ONLY } else { geo::EXCEPT }, codes))
        }
        _ => Err(Reason::ImportFieldInvalid),
    }
}

/// §4.3: the channel name and the items of one document.
fn read_mrss(b: &[u8], owner: &Option<String>, seen: &mut HashSet<String>, count: &mut usize) -> R<(Option<String>, Vec<Item>)> {
    sized(b)?;
    let root = read_feed_xml(b).ok_or(Reason::ImportXmlMalformed)?;
    let channel = (root.name == "rss").then(|| first(&root, "channel")).flatten().ok_or(Reason::ImportXmlMalformed)?;
    let items: Vec<&Element> = channel.children_named("item").collect();
    *count += items.len();
    if *count > MAX_COUNT {
        return Err(Reason::ImportOversized);
    }
    let name = first(channel, "title").map(Element::text).filter(|n| !n.is_empty());
    if let Some(n) = &name {
        field(is_short(n))?;
    }
    let mut out = Vec::with_capacity(items.len());
    for item in items {
        let asset = first(item, "guid").map(Element::text).filter(|a| is_short(a)).ok_or(Reason::ImportAssetUnknown)?;
        if !seen.insert(asset.clone()) {
            return Err(Reason::ImportAssetDuplicate);
        }
        let content = first(item, "content").or_else(|| first(item, "group").and_then(|g| first(g, "content")));
        let content_uri = content.and_then(|c| c.attr("url")).unwrap_or("").to_string();
        field(is_long(&content_uri))?;
        let duration = match content.and_then(|c| c.attr("duration")) {
            None => None,
            Some(d) => Some(duration_ms(d).and_then(|d| if d == 0 { Err(Reason::ImportTimeMalformed) } else { Ok(d) })?),
        };
        let mut desc = description(item, "description", ("thumbnail", "url"), ("scheme", "urn:simple"))?;
        desc.geo = restriction(item)?;
        desc.content_uri = content_uri;
        desc.duration_ms = duration.unwrap_or(0);
        out.push(Item { asset, desc, duration, owner: owner.clone() });
    }
    Ok((name, out))
}

// ---------------------------------------------------------------------------------------
// §4.4 The JSON form
// ---------------------------------------------------------------------------------------

fn exactly<'a>(j: &'a Json, want: &[&str]) -> Option<&'a Json> {
    let m = j.as_obj()?;
    (m.len() == want.len() && want.iter().all(|k| m.iter().any(|(n, _)| n == k))).then_some(j)
}

fn int(j: Option<&Json>) -> Option<u64> {
    j?.as_u64().filter(|&v| v <= MAX_INSTANT)
}

fn strs(j: Option<&Json>) -> Option<Vec<String>> {
    j?.as_arr()?.iter().map(|s| s.as_str().map(str::to_string)).collect()
}

/// An asset of the JSON form, as its structure reads (§4.4's table).
struct JsonAsset {
    asset_id: String,
    title: String,
    description: String,
    content_uri: String,
    categories: Vec<String>,
    rating: Option<(String, String)>,
    artwork: Vec<(String, String)>,
    geo: Option<(bool, Vec<String>)>,
    rights_holder: Option<String>,
    duration_ms: u64,
}

struct JsonBreak {
    id: String,
    start_ms: u64,
    duration_ms: u64,
    scte35: Option<String>,
}

struct JsonForm {
    channel_name: String,
    programmes: Vec<(u64, u64, String)>,
    assets: Vec<JsonAsset>,
    breaks: Vec<JsonBreak>,
}

/// The JSON form's structure (§4.4): `None` when it does not read so.
fn json_structure(b: &[u8]) -> Option<JsonForm> {
    let j = strict_json::read(b).ok()?;
    exactly(&j, &["kind", "channel_name", "programmes", "assets", "breaks"])?;
    if j.get("kind")?.as_str()? != FEED_KIND {
        return None;
    }
    let channel_name = j.get("channel_name")?.as_str()?.to_string();
    let mut programmes = Vec::new();
    for p in j.get("programmes")?.as_arr()? {
        exactly(p, &["start_ms", "stop_ms", "asset_id"])?;
        programmes.push((int(p.get("start_ms"))?, int(p.get("stop_ms"))?, p.get("asset_id")?.as_str()?.to_string()));
    }
    let mut assets = Vec::new();
    for a in j.get("assets")?.as_arr()? {
        exactly(a, &["asset_id", "title", "description", "content_uri", "categories", "rating", "artwork", "geo", "rights_holder", "duration_ms"])?;
        let s = |k: &str| a.get(k)?.as_str().map(str::to_string);
        let rating = match a.get("rating")? {
            Json::Null => None,
            r => {
                exactly(r, &["system", "value"])?;
                Some((r.get("system")?.as_str()?.to_string(), r.get("value")?.as_str()?.to_string()))
            }
        };
        let mut artwork = Vec::new();
        for w in a.get("artwork")?.as_arr()? {
            exactly(w, &["uri", "sha256"])?;
            artwork.push((w.get("uri")?.as_str()?.to_string(), w.get("sha256")?.as_str()?.to_string()));
        }
        let geo = match a.get("geo")? {
            Json::Null => None,
            g @ Json::Obj(m) if m.len() == 1 => {
                let allow = m[0].0 == "allow";
                if !allow && m[0].0 != "deny" {
                    return None;
                }
                Some((allow, strs(g.get(&m[0].0))?))
            }
            _ => return None,
        };
        let rights_holder = match a.get("rights_holder")? {
            Json::Null => None,
            Json::Str(r) => Some(r.clone()),
            _ => return None,
        };
        assets.push(JsonAsset {
            asset_id: s("asset_id")?,
            title: s("title")?,
            description: s("description")?,
            content_uri: s("content_uri")?,
            categories: strs(a.get("categories"))?,
            rating,
            artwork,
            geo,
            rights_holder,
            duration_ms: int(a.get("duration_ms"))?,
        });
    }
    let mut breaks = Vec::new();
    for k in j.get("breaks")?.as_arr()? {
        exactly(k, &["id", "start_ms", "duration_ms", "scte35"])?;
        let scte35 = match k.get("scte35")? {
            Json::Null => None,
            Json::Str(s) => Some(s.clone()),
            _ => return None,
        };
        breaks.push(JsonBreak { id: k.get("id")?.as_str()?.to_string(), start_ms: int(k.get("start_ms"))?, duration_ms: int(k.get("duration_ms"))?, scte35 });
    }
    Some(JsonForm { channel_name, programmes, assets, breaks })
}

fn is_lower_hex64(s: &str) -> bool {
    s.len() == 64 && s.bytes().all(|c| c.is_ascii_digit() || (b'a'..=b'f').contains(&c))
}

/// Hexadecimal digits of either case, an even count, as bytes.
fn hex_any(s: &str) -> Option<Vec<u8>> {
    if !s.len().is_multiple_of(2) || !s.bytes().all(|c| c.is_ascii_hexdigit()) {
        return None;
    }
    (0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).ok()).collect()
}

/// A `splice_info_section()` as live-events §4.3 reads form 1 (§4.5 step 3).
pub(crate) fn section_reads(b: &[u8]) -> bool {
    b.len() >= 3 && b[0] == 0xFC && (u16::from_be_bytes([b[1], b[2]]) & 0x0FFF) as usize + 3 == b.len()
}

/// A break's duration, as §4.5 step 4 bounds it.
fn break_duration(d: u64) -> R<u64> {
    if d == 0 || d > u64::from(u32::MAX) { Err(Reason::ImportBreakDuration) } else { Ok(d) }
}

/// §4.4: the JSON form, its values read in document order.
fn read_json(b: &[u8], breaks_count: &mut usize) -> R<JsonRead> {
    sized(b)?;
    let f = json_structure(b).ok_or(Reason::ImportJsonMalformed)?;
    *breaks_count += f.breaks.len();
    if f.programmes.len() > MAX_COUNT || f.assets.len() > MAX_COUNT || *breaks_count > MAX_COUNT {
        return Err(Reason::ImportOversized);
    }
    field(is_short(&f.channel_name))?;
    let mut progs = Vec::with_capacity(f.programmes.len());
    for (start, stop, asset) in f.programmes {
        field(is_short(&asset))?;
        if stop <= start {
            return Err(Reason::ImportTimeMalformed);
        }
        progs.push(Prog { start, stop, asset });
    }
    let mut seen = HashSet::new();
    let mut assets = Vec::with_capacity(f.assets.len());
    for a in f.assets {
        field(is_short(&a.asset_id))?;
        if !seen.insert(a.asset_id.clone()) {
            return Err(Reason::ImportAssetDuplicate);
        }
        field(!a.title.is_empty() && is_long(&a.title))?;
        field(is_long(&a.description) && is_long(&a.content_uri))?;
        field(a.categories.len() <= MAX_CATEGORIES && a.categories.iter().all(|c| is_short(c)))?;
        let rating = match a.rating {
            None => (String::new(), String::new()),
            Some((s, v)) => {
                field((s.is_empty() || is_short(&s)) && is_short(&v))?;
                (s, v)
            }
        };
        field(a.artwork.len() <= MAX_ARTWORK && a.artwork.iter().all(|(u, h)| !u.is_empty() && is_long(u) && is_lower_hex64(h)))?;
        let art = a.artwork.into_iter().map(|(u, h)| (super::unhex::<32>(&h).expect("checked"), u)).collect();
        let geo = match a.geo {
            None => (geo::EVERYWHERE, Vec::new()),
            Some((allow, codes)) => {
                let ok = codes.iter().all(|c| c.len() == 2 && c.bytes().all(|x| x.is_ascii_uppercase())) && codes.windows(2).all(|w| w[0] < w[1]) && (allow || !codes.is_empty());
                field(ok)?;
                (if allow { geo::ONLY } else { geo::EXCEPT }, codes.iter().map(|c| [c.as_bytes()[0], c.as_bytes()[1]]).collect())
            }
        };
        if let Some(r) = &a.rights_holder {
            field(is_short(r))?;
        }
        let desc = Desc { title: a.title, description: a.description, categories: a.categories, rating, art: Art::Hashed(art), geo, content_uri: a.content_uri, duration_ms: a.duration_ms };
        assets.push((a.asset_id, desc, a.rights_holder));
    }
    let mut breaks = Vec::with_capacity(f.breaks.len());
    for k in f.breaks {
        field(is_short(&k.id))?;
        let section = match &k.scte35 {
            None => None,
            Some(h) => Some(hex_any(h).filter(|s| section_reads(s)).ok_or(Reason::ImportScte35Malformed)?),
        };
        breaks.push(Break { id: k.id, start: k.start_ms, duration: break_duration(k.duration_ms)?, section });
    }
    Ok(JsonRead { channel_name: f.channel_name, programmes: progs, assets, breaks })
}

// ---------------------------------------------------------------------------------------
// §4.5 Breaks from a Media Playlist
// ---------------------------------------------------------------------------------------

fn hexadecimal_sequence(v: &Attr) -> Option<Vec<u8>> {
    match v {
        Attr::Plain(s) => hex_any(s.strip_prefix("0x").or_else(|| s.strip_prefix("0X"))?).filter(|b| section_reads(b)),
        Attr::Quoted(_) => None,
    }
}

/// §4.5: the breaks of a Media Playlist.
fn read_playlist(b: &[u8], breaks_count: &mut usize) -> R<Vec<Break>> {
    sized(b)?;
    let malformed = Reason::ImportPlaylistMalformed;
    let s = std::str::from_utf8(b).map_err(|_| malformed)?;
    let lines: Vec<&str> = s.split('\n').map(|l| l.strip_suffix('\r').unwrap_or(l)).collect();
    if lines.first() != Some(&"#EXTM3U") {
        return Err(malformed);
    }
    let mut groups: Vec<(String, Vec<(String, Attr)>)> = Vec::new();
    for l in lines.iter().filter_map(|l| l.strip_prefix("#EXT-X-DATERANGE:")) {
        let attrs = attribute_list(l).ok_or(malformed)?;
        let id = match attrs.iter().find(|(n, _)| n == "ID") {
            Some((_, Attr::Quoted(id))) => id.clone(),
            _ => return Err(malformed),
        };
        let at = match groups.iter().position(|(g, _)| *g == id) {
            Some(i) => i,
            None => {
                groups.push((id, Vec::new()));
                groups.len() - 1
            }
        };
        let g = &mut groups[at].1;
        for (n, v) in attrs {
            match g.iter().find(|(m, _)| *m == n) {
                Some((_, w)) if *w != v => return Err(malformed),
                Some(_) => {}
                None => g.push((n, v)),
            }
        }
    }
    let breaks: Vec<(String, Vec<(String, Attr)>)> = groups.into_iter().filter(|(_, a)| a.iter().any(|(n, _)| n == "SCTE35-OUT")).collect();
    *breaks_count += breaks.len();
    if *breaks_count > MAX_COUNT {
        return Err(Reason::ImportOversized);
    }
    let mut out = Vec::with_capacity(breaks.len());
    for (id, a) in breaks {
        let get = |n: &str| a.iter().find(|(m, _)| m == n).map(|(_, v)| v);
        field(is_short(&id))?;
        let start = match get("START-DATE") {
            Some(Attr::Quoted(d)) => hls_date_time(d)?,
            _ => return Err(malformed),
        };
        let section = hexadecimal_sequence(get("SCTE35-OUT").expect("a break")).ok_or(Reason::ImportScte35Malformed)?;
        if let Some(i) = get("SCTE35-IN") {
            hexadecimal_sequence(i).ok_or(Reason::ImportScte35Malformed)?;
        }
        let plain = |v: &Attr| match v {
            Attr::Plain(d) => duration_ms(d),
            Attr::Quoted(_) => Err(malformed),
        };
        let d = if let Some(v) = get("DURATION") {
            plain(v)?
        } else if let Some(v) = get("END-DATE") {
            let end = match v {
                Attr::Quoted(e) => hls_date_time(e)?,
                Attr::Plain(_) => return Err(malformed),
            };
            end.checked_sub(start).ok_or(Reason::ImportBreakDuration)?
        } else if let Some(v) = get("PLANNED-DURATION") {
            plain(v)?
        } else {
            return Err(Reason::ImportBreakDuration);
        };
        out.push(Break { id, start, duration: break_duration(d)?, section: Some(section) });
    }
    Ok(out)
}

// ---------------------------------------------------------------------------------------
// §4.6 The programmes and their assets, §4.7 the schedule, §4.8 the cues
// ---------------------------------------------------------------------------------------

/// An artwork URI's name (§4.6 step 3).
fn artwork_name(uri: &str) -> Option<&str> {
    let u = uri.split(['?', '#']).next().unwrap_or("");
    let name = u.rsplit('/').next().unwrap_or("");
    let ok = (1..=255).contains(&name.len()) && name != "." && name != ".." && name.bytes().all(|c| c.is_ascii_alphanumeric() || matches!(c, b'.' | b'_' | b'-'));
    ok.then_some(name)
}

/// A break's cue id (§4.8).
pub fn cue_id(break_id: &str, section: Option<&[u8]>) -> u32 {
    match section {
        Some(s) if s.len() >= 18 && s[4] & 0x80 == 0 && s[13] == 0x05 => u32::from_be_bytes(s[14..18].try_into().expect("4 bytes")),
        _ => {
            let d = Sha256::new().chain_update(CUE_ID_DOMAIN).chain_update(break_id.as_bytes()).finalize();
            u32::from_be_bytes(d[..4].try_into().expect("4 bytes"))
        }
    }
}

/// A break's `cue` payload of live-events §4.3 (§4.8).
fn cue_payload(k: &Break) -> Vec<u8> {
    break_cue(&k.id, k.duration, k.section.as_deref())
}

/// The `cue` payload of live-events §4.3 that §4.8 yields for a break of this id and duration,
/// with this section or none. A duration is at most 2^32 − 1 milliseconds (§4.5 step 4).
pub fn break_cue(id: &str, duration_ms: u64, section: Option<&[u8]>) -> Vec<u8> {
    let mut o = vec![4u8];
    o.extend_from_slice(&cue_id(id, section).to_le_bytes());
    o.extend_from_slice(&(duration_ms as u32).to_le_bytes());
    match section {
        None => o.push(0),
        Some(s) => {
            o.push(1);
            o.extend_from_slice(&(s.len() as u16).to_le_bytes());
            o.extend_from_slice(s);
        }
    }
    o
}

/// Import a channel (§4), the receipt and the lineup signed by `key`.
pub fn import(f: &Feeds, key: &SigningKey, signer_id: &str, parent: Option<[u8; 32]>) -> R<Imported> {
    check_inputs(f, signer_id)?;
    let xch = f.xmltv_channel.as_deref().unwrap_or(&f.channel);
    let id_system = f.id_system.as_deref().unwrap_or(ID_SYSTEM);

    // The feeds, in the order of §4.10.
    let mut names: Vec<Option<String>> = Vec::new();
    let mut xmltv: Option<Described> = None;
    if let Some(b) = &f.xmltv {
        let (name, progs) = read_xmltv(b, xch, id_system)?;
        names.push(name);
        xmltv = Some(progs);
    }
    let (mut seen, mut count) = (HashSet::new(), 0usize);
    let mut items: Vec<Item> = Vec::new();
    for (k, (b, owner)) in f.mrss.iter().enumerate() {
        let (name, mut its) = read_mrss(b, owner, &mut seen, &mut count)?;
        if k == 0 {
            names.push(name);
        }
        items.append(&mut its);
    }
    let mut breaks_count = 0usize;
    let mut breaks: Vec<Break> = Vec::new();
    let mut json: Option<(Vec<Prog>, Catalogue)> = None;
    if let Some(b) = &f.json {
        let mut j = read_json(b, &mut breaks_count)?;
        names.push(Some(j.channel_name));
        breaks.append(&mut j.breaks);
        json = Some((j.programmes, j.assets));
    }
    if let Some(b) = &f.playlist {
        breaks.append(&mut read_playlist(b, &mut breaks_count)?);
    }

    // §4.6 The programmes.
    let mut progs: Vec<Prog> = if let Some(x) = &xmltv {
        x.iter().map(|(p, _)| p.clone()).collect()
    } else if let Some((p, _)) = &json {
        if p.is_empty() {
            return Err(Reason::ImportEmpty);
        }
        p.clone()
    } else {
        if items.is_empty() {
            return Err(Reason::ImportEmpty);
        }
        let mut at = f.start_ms.expect("checked in §4.1");
        let mut out = Vec::with_capacity(items.len());
        for it in &items {
            let d = it.duration.ok_or(Reason::ImportDurationUnknown)?;
            let stop = at.checked_add(d).filter(|&s| s <= MAX_INSTANT).ok_or(Reason::ImportTimeMalformed)?;
            out.push(Prog { start: at, stop, asset: it.asset.clone() });
            at = stop;
        }
        out
    };
    progs.sort_by_key(|p| p.start);
    for w in progs.windows(2) {
        if w[0].stop < w[1].start {
            return Err(Reason::ImportGap);
        }
        if w[0].stop > w[1].start {
            return Err(Reason::ImportOverlap);
        }
    }
    let last_stop = progs.last().expect("not empty").stop;
    if !progs.iter().all(|p| p.start.is_multiple_of(1000)) || !last_stop.is_multiple_of(1000) {
        return Err(Reason::ImportTimeMalformed);
    }

    // §4.6 Their assets.
    let item_of: HashMap<&str, &Item> = items.iter().map(|i| (i.asset.as_str(), i)).collect();
    let mut order: Vec<&str> = Vec::new();
    for p in &progs {
        if !order.contains(&p.asset.as_str()) {
            order.push(&p.asset);
        }
    }
    let mut assets: Vec<Asset> = Vec::with_capacity(order.len());
    for id in order {
        let (desc, holder): (Desc, Option<String>) = if let Some((_, js)) = &json {
            let (_, d, r) = js.iter().find(|(a, _, _)| a == id).ok_or(Reason::ImportAssetUnknown)?;
            (d.clone(), r.clone())
        } else if let Some(it) = item_of.get(id) {
            (it.desc.clone(), it.owner.clone())
        } else {
            let (_, d) = xmltv.as_ref().expect("an XMLTV programme").iter().find(|(p, _)| p.asset == id).expect("a programme of it");
            (d.clone(), None)
        };
        let rights_holder = holder.or_else(|| f.owner.clone()).ok_or(Reason::ImportOwnerUnknown)?;
        let artwork = match desc.art {
            Art::Hashed(h) => h,
            Art::Uris(uris) => {
                let mut out = Vec::with_capacity(uris.len());
                for u in uris {
                    let bytes = artwork_name(&u).and_then(|n| f.artwork.get(n)).ok_or(Reason::ImportArtworkMissing)?;
                    sized(bytes)?;
                    out.push((Sha256::digest(bytes).into(), u));
                }
                out
            }
        };
        assets.push(Asset {
            asset_id: id.to_string(),
            title: desc.title,
            description: desc.description,
            categories: desc.categories,
            rating_system: desc.rating.0,
            rating_value: desc.rating.1,
            artwork,
            geo: desc.geo.0,
            countries: desc.geo.1,
            rights_holder,
            content_uri: desc.content_uri,
            duration_ms: desc.duration_ms,
        });
    }

    // §4.7 The schedule.
    breaks.sort_by_key(|k| k.start);
    for w in breaks.windows(2) {
        if w[0].start + w[0].duration > w[1].start {
            return Err(Reason::ImportOverlap);
        }
    }
    let mut within: Vec<Vec<&Break>> = vec![Vec::new(); progs.len()];
    for k in &breaks {
        let at = progs.iter().position(|p| p.start <= k.start && k.start < p.stop).filter(|&i| k.start + k.duration <= progs[i].stop).ok_or(Reason::ImportBreakOutside)?;
        within[at].push(k);
    }
    let mut schedule: Vec<ScheduleEntry> = Vec::new();
    for (p, bks) in progs.iter().zip(&within) {
        let mut at = p.start;
        for k in bks {
            if k.start > at {
                schedule.push(ScheduleEntry { asset_id: p.asset.clone(), start_ms: at, duration_ms: k.start - at, kind: EntryKind::Program });
            }
            schedule.push(ScheduleEntry { asset_id: k.id.clone(), start_ms: k.start, duration_ms: k.duration, kind: EntryKind::Ad });
            at = k.start + k.duration;
        }
        if p.stop > at {
            schedule.push(ScheduleEntry { asset_id: p.asset.clone(), start_ms: at, duration_ms: p.stop - at, kind: EntryKind::Program });
        }
    }

    // §4.9 What it yields.
    let (first_start, last_end) = (progs[0].start, last_stop);
    let (ws, we) = f.window.unwrap_or((first_start, last_end));
    if ws >= we || ws < first_start || we > last_end {
        return Err(Reason::ImportWindowOutside);
    }
    let receipt = ChannelReceipt::seal(key, signer_id, f.channel.clone(), schedule, ws, we, 0, None, GrantRef::unbounded("video.channel"), parent);
    assets.sort_by(|a, b| a.asset_id.as_bytes().cmp(b.asset_id.as_bytes()));
    let channel_name = names.into_iter().flatten().next().unwrap_or_else(|| f.channel.clone());
    let lineup = Lineup {
        channel_id: f.channel.clone(),
        channel_name,
        schedule_root: receipt.schedule_root,
        schedule_start_ms: first_start,
        schedule_end_ms: last_end,
        programmes: progs.iter().map(|p| p.start).collect(),
        assets,
        signer: [0; 32],
        sig: [0; 64],
    }
    .sign(key);
    let cues = breaks.iter().map(|k| Cue { break_id: k.id.clone(), start_ms: k.start, payload: cue_payload(k) }).collect();
    Ok(Imported { receipt, lineup, cues })
}
