//! A commitment list derived from a CPIX document (extensions/key-release.md §4.4).
//!
//! The reader is the XML 1.0 a CPIX document uses: UTF-8, namespaces, attributes in
//! either quote, the five predefined entities and character references, comments,
//! processing instructions and CDATA sections. A document type declaration is refused,
//! so no entity is ever defined or expanded. It is written here rather than taken from a
//! crate so that the one thing it reads is exactly what §4.4 names.

use super::{key_commitment, sha256, unb64, ListKey, Period, Scheme};

/// The CPIX namespace (CPIX §7.4.1).
pub const CPIX_NS: &str = "urn:dashif:org:cpix";
/// The namespace of `Secret`, `PlainValue` and `EncryptedValue` (RFC 6030).
pub const PSKC_NS: &str = "urn:ietf:params:xml:ns:keyprov:pskc";
const XML_NS: &str = "http://www.w3.org/XML/1998/namespace";
const MAX_DEPTH: usize = 64;

/// Why a CPIX document is refused (§4.4).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CpixError {
    Malformed,
    Unsupported,
}

/// One key of a CPIX document: what §4.4 reads of it, the key included.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CpixKey {
    pub kid: [u8; 16],
    pub scheme: Scheme,
    pub key: Vec<u8>,
    pub period: Option<Period>,
}

/// What §4.4 derives from a document.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Derived {
    pub content_id: String,
    pub sha256: [u8; 32],
    /// In ascending KID order.
    pub keys: Vec<CpixKey>,
}

impl Derived {
    /// The list's keys: each key's KID, scheme, commitment and period.
    pub fn list_keys(&self) -> Vec<ListKey> {
        self.keys
            .iter()
            .map(|k| ListKey { kid: k.kid, scheme: k.scheme, key_commit: key_commitment(&k.key), period: k.period })
            .collect()
    }
}

/// An element: its namespace, local name, attributes (unprefixed, so in no namespace,
/// unless prefixed), children and text.
#[derive(Clone, Debug, Default)]
pub struct Element {
    pub ns: Option<String>,
    pub name: String,
    pub attrs: Vec<(Option<String>, String, String)>,
    pub children: Vec<Element>,
    pub text: String,
}

impl Element {
    /// The value of an attribute in no namespace.
    pub fn attr(&self, name: &str) -> Option<&str> {
        self.attrs.iter().find(|(ns, n, _)| ns.is_none() && n == name).map(|(_, _, v)| v.as_str())
    }

    /// The children of this namespace and name.
    pub fn kids<'a>(&'a self, ns: &'a str, name: &'a str) -> impl Iterator<Item = &'a Element> + 'a {
        self.children.iter().filter(move |c| c.ns.as_deref() == Some(ns) && c.name == name)
    }

    pub fn kid(&self, ns: &str, name: &str) -> Option<&Element> {
        self.children.iter().find(|c| c.ns.as_deref() == Some(ns) && c.name == name)
    }
}

/// Derive a list's content, digest and keys from a document (§4.4).
pub fn derive(doc: &[u8]) -> Result<Derived, CpixError> {
    use CpixError::*;
    // Step 1.
    let text = std::str::from_utf8(doc).map_err(|_| Malformed)?;
    let root = parse(text).ok_or(Malformed)?;
    if root.ns.as_deref() != Some(CPIX_NS) || root.name != "CPIX" {
        return Err(Malformed);
    }
    let sha = sha256(doc);
    // Step 2.
    let content_id = root.attr("contentId").ok_or(Unsupported)?.to_string();
    // Step 3.
    let mut keys: Vec<CpixKey> = Vec::new();
    let list = root.kid(CPIX_NS, "ContentKeyList");
    for ck in list.into_iter().flat_map(|l| l.kids(CPIX_NS, "ContentKey")) {
        let kid = ck.attr("kid").and_then(uuid).ok_or(Malformed)?;
        if keys.iter().any(|k| k.kid == kid) {
            return Err(Malformed);
        }
        if ck.attr("contentId").is_some() || ck.attr("dependsOnKey").is_some() {
            return Err(Unsupported);
        }
        let scheme = ck.attr("commonEncryptionScheme").and_then(Scheme::from_label).ok_or(Unsupported)?;
        let plain = ck
            .kid(CPIX_NS, "Data")
            .and_then(|d| d.kid(PSKC_NS, "Secret"))
            .filter(|s| s.kid(PSKC_NS, "EncryptedValue").is_none())
            .and_then(|s| s.kid(PSKC_NS, "PlainValue"))
            .ok_or(Unsupported)?;
        let key = unb64(&plain.text).filter(|k| k.len() == scheme.key_len()).ok_or(Malformed)?;
        keys.push(CpixKey { kid, scheme, key, period: None });
    }
    if keys.is_empty() {
        return Err(Malformed);
    }
    // Step 4.
    let periods: Vec<&Element> =
        root.kid(CPIX_NS, "ContentKeyPeriodList").into_iter().flat_map(|l| l.kids(CPIX_NS, "ContentKeyPeriod")).collect();
    let mut named: Vec<([u8; 16], usize)> = Vec::new();
    for rule in root.kid(CPIX_NS, "ContentKeyUsageRuleList").into_iter().flat_map(|l| l.kids(CPIX_NS, "ContentKeyUsageRule")) {
        let kid = rule.attr("kid").and_then(uuid).filter(|k| keys.iter().any(|x| x.kid == *k)).ok_or(Malformed)?;
        for f in rule.kids(CPIX_NS, "KeyPeriodFilter") {
            let id = f.attr("periodId").ok_or(Malformed)?;
            let p = periods.iter().position(|p| p.attr("id") == Some(id)).ok_or(Malformed)?;
            match named.iter().find(|(k, _)| *k == kid) {
                Some((_, q)) if *q != p => return Err(Unsupported),
                Some(_) => {}
                None => named.push((kid, p)),
            }
        }
    }
    // Step 5.
    keys.sort_by_key(|k| k.kid);
    for k in &mut keys {
        if let Some((_, p)) = named.iter().find(|(kid, _)| *kid == k.kid) {
            k.period = Some(period(periods[*p])?);
        }
    }
    Ok(Derived { content_id, sha256: sha, keys })
}

fn period(p: &Element) -> Result<Period, CpixError> {
    use CpixError::*;
    if ["startOffset", "endOffset", "duration"].iter().any(|a| p.attr(a).is_some()) {
        return Err(Unsupported);
    }
    let index = match p.attr("index") {
        None => None,
        Some(s) => Some(decimal_u32(s).ok_or(Unsupported)?),
    };
    let window = match (p.attr("start"), p.attr("end")) {
        (None, None) => None,
        (Some(s), Some(e)) => {
            let (s, e) = (date_time(s).ok_or(Unsupported)?, date_time(e).ok_or(Unsupported)?);
            if s >= e {
                return Err(Malformed);
            }
            Some((s, e))
        }
        _ => return Err(Unsupported),
    };
    if index.is_none() && window.is_none() {
        return Err(Unsupported);
    }
    Ok(Period { index, window })
}

fn decimal_u32(s: &str) -> Option<u32> {
    if s.is_empty() || !s.bytes().all(|c| c.is_ascii_digit()) {
        return None;
    }
    s.parse().ok()
}

/// A UUID in its canonical text form, either case, as its 16 bytes in order.
pub fn uuid(s: &str) -> Option<[u8; 16]> {
    let b = s.as_bytes();
    if b.len() != 36 || [8, 13, 18, 23].iter().any(|&i| b[i] != b'-') {
        return None;
    }
    let digits: String = s.chars().filter(|&c| c != '-').collect();
    if digits.len() != 32 || !digits.bytes().all(|c| c.is_ascii_hexdigit()) {
        return None;
    }
    let mut o = [0u8; 16];
    for (i, x) in o.iter_mut().enumerate() {
        *x = u8::from_str_radix(&digits[2 * i..2 * i + 2], 16).ok()?;
    }
    Some(o)
}

/// The UUID text form of 16 bytes, lowercase.
pub fn uuid_text(b: &[u8; 16]) -> String {
    let h = super::hex(b);
    format!("{}-{}-{}-{}-{}", &h[..8], &h[8..12], &h[12..16], &h[16..20], &h[20..])
}

/// `YYYY-MM-DDThh:mm:ssZ` as seconds since 1970-01-01T00:00:00Z.
pub fn date_time(s: &str) -> Option<u64> {
    let b = s.as_bytes();
    if b.len() != 20 || b[4] != b'-' || b[7] != b'-' || b[10] != b'T' || b[13] != b':' || b[16] != b':' || b[19] != b'Z' {
        return None;
    }
    let num = |r: std::ops::Range<usize>| -> Option<u64> {
        let t = &s[r];
        t.bytes().all(|c| c.is_ascii_digit()).then(|| t.parse().ok())?
    };
    let (y, mo, d, h, mi, se) = (num(0..4)?, num(5..7)?, num(8..10)?, num(11..13)?, num(14..16)?, num(17..19)?);
    let leap = (y % 4 == 0 && y % 100 != 0) || y % 400 == 0;
    let days_in = [31, if leap { 29 } else { 28 }, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
    if y < 1970 || !(1..=12).contains(&mo) || d == 0 || d > days_in[mo as usize - 1] || h > 23 || mi > 59 || se > 59 {
        return None;
    }
    Some((days_from_civil(y, mo, d) * 86_400) + h * 3600 + mi * 60 + se)
}

/// Days from 1970-01-01 to a date of the proleptic Gregorian calendar, 1970 or later.
fn days_from_civil(y: u64, m: u64, d: u64) -> u64 {
    let y = if m <= 2 { y - 1 } else { y };
    let era = y / 400;
    let yoe = y - era * 400;
    let mp = (m + 9) % 12;
    let doy = (153 * mp + 2) / 5 + d - 1;
    let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
    era * 146_097 + doe - 719_468
}

/// `seconds` as `YYYY-MM-DDThh:mm:ssZ`.
pub fn date_time_text(seconds: u64) -> String {
    let (days, rem) = (seconds / 86_400, seconds % 86_400);
    let z = days + 719_468;
    let era = z / 146_097;
    let doe = z - era * 146_097;
    let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
    let mp = (5 * doy + 2) / 153;
    let d = doy - (153 * mp + 2) / 5 + 1;
    let m = if mp < 10 { mp + 3 } else { mp - 9 };
    let y = yoe + era * 400 + if m <= 2 { 1 } else { 0 };
    format!("{y:04}-{m:02}-{d:02}T{:02}:{:02}:{:02}Z", rem / 3600, rem % 3600 / 60, rem % 60)
}

// ---------------------------------------------------------------------------------------
// The XML reader
// ---------------------------------------------------------------------------------------

/// Parse a well-formed XML 1.0 document with no document type declaration into its root.
pub fn parse(doc: &str) -> Option<Element> {
    let doc = doc.strip_prefix('\u{feff}').unwrap_or(doc);
    if doc.chars().any(|c| ((c as u32) < 0x20 && !matches!(c, '\t' | '\n' | '\r')) || matches!(c, '\u{fffe}' | '\u{ffff}')) {
        return None;
    }
    let norm = doc.replace("\r\n", "\n").replace('\r', "\n");
    let mut p = Parser { s: norm.as_bytes(), at: 0, text: &norm };
    if p.starts("<?xml") && p.s.get(5).is_some_and(|c| c.is_ascii_whitespace() || *c == b'?') {
        let end = p.find("?>")?;
        let decl = &norm[5..end];
        if let Some(i) = decl.find("encoding") {
            let rest = decl[i + 8..].trim_start().strip_prefix('=')?.trim_start();
            let q = rest.chars().next()?;
            let v = rest[1..].split(q).next()?;
            if !v.eq_ignore_ascii_case("utf-8") {
                return None;
            }
        }
        p.at = end + 2;
    }
    p.misc()?;
    let mut scopes = vec![vec![("xml".to_string(), XML_NS.to_string())]];
    let root = p.element(&mut scopes, 0)?;
    p.misc()?;
    (p.at == p.s.len()).then_some(root)
}

struct Parser<'a> {
    s: &'a [u8],
    at: usize,
    text: &'a str,
}

impl Parser<'_> {
    fn starts(&self, t: &str) -> bool {
        self.s[self.at..].starts_with(t.as_bytes())
    }

    fn find(&self, t: &str) -> Option<usize> {
        self.text[self.at..].find(t).map(|i| self.at + i)
    }

    fn ws(&mut self) -> bool {
        let start = self.at;
        while self.at < self.s.len() && matches!(self.s[self.at], b' ' | b'\t' | b'\n') {
            self.at += 1;
        }
        self.at > start
    }

    /// Comments, processing instructions and white space; a document type declaration is
    /// refused.
    fn misc(&mut self) -> Option<()> {
        loop {
            self.ws();
            if self.starts("<!--") {
                self.comment()?;
            } else if self.starts("<?") {
                self.pi()?;
            } else if self.starts("<!") {
                return None;
            } else {
                return Some(());
            }
        }
    }

    fn comment(&mut self) -> Option<()> {
        let end = self.find("-->")?;
        let body = &self.text[self.at + 4..end];
        if body.contains("--") || body.ends_with('-') {
            return None;
        }
        self.at = end + 3;
        Some(())
    }

    fn pi(&mut self) -> Option<()> {
        self.at += 2;
        let name = self.name()?;
        if name.eq_ignore_ascii_case("xml") || name.contains(':') {
            return None;
        }
        let end = self.find("?>")?;
        if end > self.at && !self.s[self.at].is_ascii_whitespace() {
            return None;
        }
        self.at = end + 2;
        Some(())
    }

    fn name(&mut self) -> Option<String> {
        let start = self.at;
        let first = self.text[self.at..].chars().next()?;
        if !(first.is_ascii_alphabetic() || first == '_' || first == ':' || first as u32 >= 0x80) {
            return None;
        }
        for c in self.text[self.at..].chars() {
            if c.is_ascii_alphanumeric() || matches!(c, '_' | ':' | '-' | '.') || c as u32 >= 0x80 {
                self.at += c.len_utf8();
            } else {
                break;
            }
        }
        let n = &self.text[start..self.at];
        let colons = n.matches(':').count();
        if colons > 1 || n.starts_with(':') || n.ends_with(':') {
            return None;
        }
        Some(n.to_string())
    }

    /// Text with references resolved, up to the next `<` (or the quote, in an attribute).
    fn chars_until(&mut self, stop: u8, attribute: bool) -> Option<String> {
        let mut o = String::new();
        while self.at < self.s.len() && self.s[self.at] != stop {
            let c = self.s[self.at];
            if attribute && c == b'<' {
                return None;
            }
            if c == b'&' {
                let end = self.find(";")?;
                let r = &self.text[self.at + 1..end];
                let ch = match r {
                    "lt" => '<',
                    "gt" => '>',
                    "amp" => '&',
                    "quot" => '"',
                    "apos" => '\'',
                    _ => {
                        let v = if let Some(h) = r.strip_prefix("#x") {
                            if h.is_empty() || !h.bytes().all(|b| b.is_ascii_hexdigit()) {
                                return None;
                            }
                            u32::from_str_radix(h, 16).ok()?
                        } else {
                            let d = r.strip_prefix('#')?;
                            if d.is_empty() || !d.bytes().all(|b| b.is_ascii_digit()) {
                                return None;
                            }
                            d.parse().ok()?
                        };
                        let ch = char::from_u32(v)?;
                        if (v < 0x20 && !matches!(v, 0x9 | 0xA | 0xD)) || v == 0xFFFE || v == 0xFFFF {
                            return None;
                        }
                        ch
                    }
                };
                o.push(ch);
                self.at = end + 1;
            } else {
                let ch = self.text[self.at..].chars().next()?;
                if !attribute && self.text[self.at..].starts_with("]]>") {
                    return None;
                }
                o.push(if attribute && matches!(ch, '\t' | '\n') { ' ' } else { ch });
                self.at += ch.len_utf8();
            }
        }
        Some(o)
    }

    fn element(&mut self, scopes: &mut Vec<Vec<(String, String)>>, depth: usize) -> Option<Element> {
        if depth > MAX_DEPTH || !self.starts("<") {
            return None;
        }
        self.at += 1;
        let qname = self.name()?;
        let mut raw: Vec<(String, String)> = Vec::new();
        loop {
            let had_ws = self.ws();
            if self.starts("/>") || self.starts(">") {
                break;
            }
            if !had_ws {
                return None;
            }
            let an = self.name()?;
            self.ws();
            if !self.starts("=") {
                return None;
            }
            self.at += 1;
            self.ws();
            let q = *self.s.get(self.at)?;
            if q != b'"' && q != b'\'' {
                return None;
            }
            self.at += 1;
            let v = self.chars_until(q, true)?;
            if self.at >= self.s.len() {
                return None;
            }
            self.at += 1;
            if raw.iter().any(|(n, _)| *n == an) {
                return None;
            }
            raw.push((an, v));
        }
        let mut scope = Vec::new();
        for (n, v) in &raw {
            if n == "xmlns" {
                scope.push((String::new(), v.clone()));
            } else if let Some(p) = n.strip_prefix("xmlns:") {
                if v.is_empty() || p == "xmlns" || (p == "xml") != (v == XML_NS) {
                    return None;
                }
                scope.push((p.to_string(), v.clone()));
            }
        }
        scopes.push(scope);
        let resolve = |scopes: &Vec<Vec<(String, String)>>, prefix: &str| -> Option<Option<String>> {
            for s in scopes.iter().rev() {
                if let Some((_, u)) = s.iter().rev().find(|(p, _)| p == prefix) {
                    return Some(if u.is_empty() { None } else { Some(u.clone()) });
                }
            }
            if prefix.is_empty() { Some(None) } else { None }
        };
        let (prefix, local) = match qname.split_once(':') {
            Some((p, l)) => (p, l),
            None => ("", qname.as_str()),
        };
        let mut el = Element { ns: resolve(scopes, prefix)?, name: local.to_string(), ..Default::default() };
        for (n, v) in raw {
            if n == "xmlns" || n.starts_with("xmlns:") {
                continue;
            }
            let (ns, an) = match n.split_once(':') {
                Some((p, l)) => (Some(resolve(scopes, p)??), l.to_string()),
                None => (None, n.clone()),
            };
            if el.attrs.iter().any(|(ons, on, _)| *ons == ns && *on == an) {
                return None;
            }
            el.attrs.push((ns, an, v));
        }
        if self.starts("/>") {
            self.at += 2;
            scopes.pop();
            return Some(el);
        }
        self.at += 1;
        loop {
            let t = self.chars_until(b'<', false)?;
            el.text.push_str(&t);
            if self.at >= self.s.len() {
                return None;
            }
            if self.starts("</") {
                self.at += 2;
                if self.name()? != qname {
                    return None;
                }
                self.ws();
                if !self.starts(">") {
                    return None;
                }
                self.at += 1;
                scopes.pop();
                return Some(el);
            } else if self.starts("<!--") {
                self.comment()?;
            } else if self.starts("<![CDATA[") {
                let end = self.find("]]>")?;
                el.text.push_str(&self.text[self.at + 9..end]);
                self.at = end + 3;
            } else if self.starts("<?") {
                self.pi()?;
            } else if self.starts("<!") {
                return None;
            } else {
                let child = self.element(scopes, depth + 1)?;
                el.children.push(child);
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn the_reader_reads_namespaces_entities_and_cdata() {
        let doc = r#"<?xml version="1.0" encoding="UTF-8"?>
<!-- a comment -->
<c:CPIX xmlns:c="urn:dashif:org:cpix" xmlns:p="urn:ietf:params:xml:ns:keyprov:pskc" contentId='a&amp;b'>
  <c:ContentKeyList><c:ContentKey kid="0123"><p:Secret>x<![CDATA[<y>]]>&#x41;&#66;</p:Secret></c:ContentKey></c:ContentKeyList>
</c:CPIX>"#;
        let root = parse(doc).expect("well-formed");
        assert_eq!(root.ns.as_deref(), Some(CPIX_NS));
        assert_eq!(root.attr("contentId"), Some("a&b"));
        let ck = root.kid(CPIX_NS, "ContentKeyList").unwrap().kid(CPIX_NS, "ContentKey").unwrap();
        assert_eq!(ck.kid(PSKC_NS, "Secret").unwrap().text, "x<y>AB");
    }

    #[test]
    fn the_reader_refuses_what_is_not_well_formed() {
        for bad in [
            "<a><b></a></b>",
            "<a x='1' x='2'/>",
            "<!DOCTYPE a [<!ENTITY e 'x'>]><a>&e;</a>",
            "<a>&e;</a>",
            "<p:a/>",
            "<a/><b/>",
            "<a><!-- x -- y --></a>",
            "<a>\u{1}</a>",
            "<a x=1/>",
            "<?xml version='1.0' encoding='ISO-8859-1'?><a/>",
            "<a>]]></a>",
        ] {
            assert!(parse(bad).is_none(), "{bad:?} is not well-formed");
        }
    }

    #[test]
    fn times_and_uuids_round_trip() {
        assert_eq!(date_time("2026-10-25T00:00:00Z"), Some(1_792_886_400));
        assert_eq!(date_time_text(1_792_886_400), "2026-10-25T00:00:00Z");
        assert_eq!(date_time("2028-02-29T23:59:59Z").map(date_time_text).as_deref(), Some("2028-02-29T23:59:59Z"));
        assert_eq!(date_time("2027-02-29T00:00:00Z"), None);
        assert_eq!(date_time("2026-10-25T00:00:00.5Z"), None);
        let u = uuid("EDEF8BA9-79d6-4ace-a3c8-27dcd51d21ed").unwrap();
        assert_eq!(uuid_text(&u), "edef8ba9-79d6-4ace-a3c8-27dcd51d21ed");
        assert_eq!(uuid("edef8ba979d64acea3c827dcd51d21ed"), None);
    }
}
