//! The subset of RFC 9651 Structured Field Values that CMCD and CMSD reports use:
//! dictionaries whose members are bare items (Integer, String, Token, Boolean) or inner
//! lists of Integers, and lists of items with parameters. Serialisation follows RFC
//! 9651 §4.1 and writes no optional whitespace; parsing follows §4.2 and refuses what
//! this subset does not hold.

/// A bare item (RFC 9651 §3.3).
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SfBare {
    /// −999 999 999 999 999 … 999 999 999 999 999.
    Int(i64),
    /// Printable ASCII.
    Str(String),
    Token(String),
    Bool(bool),
}

/// A dictionary member: a bare item, or an inner list of Integers with no parameters.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SfMember {
    Item(SfBare),
    IntList(Vec<i64>),
}

/// An item of a list, with its parameters in order.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SfItem {
    pub value: SfBare,
    pub params: Vec<(String, SfBare)>,
}

/// Why a value cannot be serialised or a field cannot be parsed.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SfError(pub String);

impl std::fmt::Display for SfError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "structured field: {}", self.0)
    }
}

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

fn err(s: impl Into<String>) -> SfError {
    SfError(s.into())
}

const INT_MAX: i64 = 999_999_999_999_999;

/// Whether `k` is a key (RFC 9651 §3.1.2): `lcalpha` or `*` first, then `lcalpha`,
/// digits, `_`, `-`, `.` or `*`.
pub fn is_key(k: &str) -> bool {
    let b = k.as_bytes();
    !b.is_empty()
        && (b[0].is_ascii_lowercase() || b[0] == b'*')
        && b.iter().all(|&c| c.is_ascii_lowercase() || c.is_ascii_digit() || matches!(c, b'_' | b'-' | b'.' | b'*'))
}

fn is_tchar(c: u8) -> bool {
    c.is_ascii_alphanumeric() || b"!#$%&'*+-.^_`|~".contains(&c)
}

/// Whether `t` is a Token (RFC 9651 §3.3.4).
pub fn is_token(t: &str) -> bool {
    let b = t.as_bytes();
    !b.is_empty() && (b[0].is_ascii_alphabetic() || b[0] == b'*') && b.iter().all(|&c| is_tchar(c) || c == b':' || c == b'/')
}

fn bare(v: &SfBare, out: &mut String) -> Result<(), SfError> {
    match v {
        SfBare::Int(i) => {
            if !(-INT_MAX..=INT_MAX).contains(i) {
                return Err(err(format!("integer {i} out of range")));
            }
            out.push_str(&i.to_string());
        }
        SfBare::Str(s) => {
            if !s.bytes().all(|c| (0x20..=0x7e).contains(&c)) {
                return Err(err("a string holds a byte outside printable ASCII"));
            }
            out.push('"');
            for c in s.chars() {
                if c == '"' || c == '\\' {
                    out.push('\\');
                }
                out.push(c);
            }
            out.push('"');
        }
        SfBare::Token(t) => {
            if !is_token(t) {
                return Err(err(format!("`{t}` is not a token")));
            }
            out.push_str(t);
        }
        SfBare::Bool(b) => out.push_str(if *b { "?1" } else { "?0" }),
    }
    Ok(())
}

fn params(ps: &[(String, SfBare)], out: &mut String) -> Result<(), SfError> {
    for (k, v) in ps {
        if !is_key(k) {
            return Err(err(format!("`{k}` is not a key")));
        }
        out.push(';');
        out.push_str(k);
        if *v != SfBare::Bool(true) {
            out.push('=');
            bare(v, out)?;
        }
    }
    Ok(())
}

/// Serialise a dictionary, members in the order given (RFC 9651 §4.1.2). A Boolean
/// true is written as its key alone.
pub fn serialize_dict(m: &[(String, SfMember)]) -> Result<String, SfError> {
    let mut out = String::new();
    for (i, (k, v)) in m.iter().enumerate() {
        if !is_key(k) {
            return Err(err(format!("`{k}` is not a key")));
        }
        if i > 0 {
            out.push(',');
        }
        out.push_str(k);
        match v {
            SfMember::Item(SfBare::Bool(true)) => {}
            SfMember::Item(b) => {
                out.push('=');
                bare(b, &mut out)?;
            }
            SfMember::IntList(l) => {
                out.push_str("=(");
                for (j, x) in l.iter().enumerate() {
                    if j > 0 {
                        out.push(' ');
                    }
                    bare(&SfBare::Int(*x), &mut out)?;
                }
                out.push(')');
            }
        }
    }
    Ok(out)
}

/// Serialise a list of items with parameters (RFC 9651 §4.1.1).
pub fn serialize_list(items: &[SfItem]) -> Result<String, SfError> {
    let mut out = String::new();
    for (i, it) in items.iter().enumerate() {
        if i > 0 {
            out.push(',');
        }
        bare(&it.value, &mut out)?;
        params(&it.params, &mut out)?;
    }
    Ok(out)
}

struct P<'a> {
    b: &'a [u8],
    i: usize,
}

impl P<'_> {
    fn peek(&self) -> Option<u8> {
        self.b.get(self.i).copied()
    }
    fn ows(&mut self) {
        while matches!(self.peek(), Some(b' ' | b'\t')) {
            self.i += 1;
        }
    }
    fn sp(&mut self) {
        while self.peek() == Some(b' ') {
            self.i += 1;
        }
    }
    fn key(&mut self) -> Result<String, SfError> {
        let s = self.i;
        match self.peek() {
            Some(c) if c.is_ascii_lowercase() || c == b'*' => {}
            _ => return Err(err(format!("a key expected at {}", self.i))),
        }
        while matches!(self.peek(), Some(c) if c.is_ascii_lowercase() || c.is_ascii_digit() || matches!(c, b'_' | b'-' | b'.' | b'*')) {
            self.i += 1;
        }
        Ok(String::from_utf8_lossy(&self.b[s..self.i]).into_owned())
    }
    fn bare(&mut self) -> Result<SfBare, SfError> {
        match self.peek() {
            Some(b'-' | b'0'..=b'9') => {
                let s = self.i;
                if self.peek() == Some(b'-') {
                    self.i += 1;
                }
                let d = self.i;
                while matches!(self.peek(), Some(b'0'..=b'9')) {
                    self.i += 1;
                }
                if self.peek() == Some(b'.') {
                    return Err(err("a decimal is not in this subset"));
                }
                if self.i == d || self.i - d > 15 {
                    return Err(err("an integer has no digits or more than 15"));
                }
                let t = std::str::from_utf8(&self.b[s..self.i]).expect("ASCII");
                Ok(SfBare::Int(t.parse().map_err(|_| err("an integer"))?))
            }
            Some(b'"') => {
                self.i += 1;
                let mut out = String::new();
                loop {
                    match self.peek() {
                        None => return Err(err("an unterminated string")),
                        Some(b'"') => {
                            self.i += 1;
                            return Ok(SfBare::Str(out));
                        }
                        Some(b'\\') => {
                            self.i += 1;
                            match self.peek() {
                                Some(c @ (b'"' | b'\\')) => {
                                    out.push(c as char);
                                    self.i += 1;
                                }
                                _ => return Err(err("a backslash escapes only a quote or a backslash")),
                            }
                        }
                        Some(c) if (0x20..=0x7e).contains(&c) => {
                            out.push(c as char);
                            self.i += 1;
                        }
                        Some(_) => return Err(err("a string holds a byte outside printable ASCII")),
                    }
                }
            }
            Some(b'?') => {
                self.i += 1;
                let v = match self.peek() {
                    Some(b'1') => true,
                    Some(b'0') => false,
                    _ => return Err(err("a boolean is ?0 or ?1")),
                };
                self.i += 1;
                Ok(SfBare::Bool(v))
            }
            Some(c) if c.is_ascii_alphabetic() || c == b'*' => {
                let s = self.i;
                while matches!(self.peek(), Some(c) if is_tchar(c) || c == b':' || c == b'/') {
                    self.i += 1;
                }
                Ok(SfBare::Token(String::from_utf8_lossy(&self.b[s..self.i]).into_owned()))
            }
            _ => Err(err(format!("an item expected at {}", self.i))),
        }
    }
    fn params(&mut self) -> Result<Vec<(String, SfBare)>, SfError> {
        let mut out: Vec<(String, SfBare)> = Vec::new();
        while self.peek() == Some(b';') {
            self.i += 1;
            self.sp();
            let k = self.key()?;
            let v = if self.peek() == Some(b'=') {
                self.i += 1;
                self.bare()?
            } else {
                SfBare::Bool(true)
            };
            match out.iter_mut().find(|(x, _)| *x == k) {
                Some(slot) => slot.1 = v,
                None => out.push((k, v)),
            }
        }
        Ok(out)
    }
    fn sep(&mut self) -> Result<bool, SfError> {
        self.ows();
        if self.i == self.b.len() {
            return Ok(false);
        }
        if self.peek() != Some(b',') {
            return Err(err(format!("a comma expected at {}", self.i)));
        }
        self.i += 1;
        self.ows();
        if self.i == self.b.len() {
            return Err(err("a trailing comma"));
        }
        Ok(true)
    }
}

/// Parse a dictionary (RFC 9651 §4.2.2). A key given twice keeps its first place and
/// takes its last value. Members with parameters are not in this subset.
pub fn parse_dict(s: &str) -> Result<Vec<(String, SfMember)>, SfError> {
    let mut p = P { b: s.as_bytes(), i: 0 };
    p.sp();
    let mut out: Vec<(String, SfMember)> = Vec::new();
    if p.i == p.b.len() {
        return Ok(out);
    }
    loop {
        let k = p.key()?;
        let v = if p.peek() == Some(b'=') {
            p.i += 1;
            if p.peek() == Some(b'(') {
                p.i += 1;
                let mut l = Vec::new();
                loop {
                    p.sp();
                    if p.peek() == Some(b')') {
                        p.i += 1;
                        break;
                    }
                    match p.bare()? {
                        SfBare::Int(x) => l.push(x),
                        _ => return Err(err("an inner list here holds integers only")),
                    }
                    if !matches!(p.peek(), Some(b' ' | b')')) {
                        return Err(err("an inner list item is followed by a space or `)`"));
                    }
                }
                SfMember::IntList(l)
            } else {
                SfMember::Item(p.bare()?)
            }
        } else {
            SfMember::Item(SfBare::Bool(true))
        };
        if p.peek() == Some(b';') {
            return Err(err("parameters on a dictionary member are not in this subset"));
        }
        match out.iter_mut().find(|(x, _)| *x == k) {
            Some(slot) => slot.1 = v,
            None => out.push((k, v)),
        }
        if !p.sep()? {
            return Ok(out);
        }
    }
}

/// Parse a list of items with parameters (RFC 9651 §4.2.1).
pub fn parse_list(s: &str) -> Result<Vec<SfItem>, SfError> {
    let mut p = P { b: s.as_bytes(), i: 0 };
    p.sp();
    let mut out = Vec::new();
    if p.i == p.b.len() {
        return Ok(out);
    }
    loop {
        let value = p.bare()?;
        let params = p.params()?;
        out.push(SfItem { value, params });
        if !p.sep()? {
            return Ok(out);
        }
    }
}

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

    fn item(b: SfBare) -> SfMember {
        SfMember::Item(b)
    }

    #[test]
    fn dictionary_round_trip() {
        let d = vec![
            ("bs".to_string(), item(SfBare::Bool(true))),
            ("bsa".to_string(), SfMember::IntList(vec![2, 3])),
            ("cid".to_string(), item(SfBare::Str("a\"b\\c".into()))),
            ("ot".to_string(), item(SfBare::Token("v".into()))),
            ("su".to_string(), item(SfBare::Bool(false))),
            ("ts".to_string(), item(SfBare::Int(-999_999_999_999_999))),
        ];
        let s = serialize_dict(&d).unwrap();
        assert_eq!(s, r#"bs,bsa=(2 3),cid="a\"b\\c",ot=v,su=?0,ts=-999999999999999"#);
        assert_eq!(parse_dict(&s).unwrap(), d);
        // Whitespace around commas is read; a later value of a key replaces the first.
        assert_eq!(parse_dict("a=1 ,  b=2,a=3").unwrap(), vec![("a".into(), item(SfBare::Int(3))), ("b".into(), item(SfBare::Int(2)))]);
    }

    #[test]
    fn what_this_subset_refuses() {
        for bad in ["A=1", "a=1,", "a=1;p", "a=1.5", "a=1234567890123456", "a=\"\\x\"", "a=(1 \"s\")", "a=?2", "a,,b", "a=\"open"] {
            assert!(parse_dict(bad).is_err(), "{bad}");
        }
        assert!(serialize_dict(&[("a".into(), item(SfBare::Int(1_000_000_000_000_000)))]).is_err());
        assert!(serialize_dict(&[("a".into(), item(SfBare::Str("é".into())))]).is_err());
        assert!(serialize_dict(&[("a".into(), item(SfBare::Token("1x".into())))]).is_err());
        assert!(serialize_dict(&[("A".into(), item(SfBare::Int(1)))]).is_err());
    }

    #[test]
    fn list_with_parameters() {
        let l = vec![
            SfItem { value: SfBare::Str("edge-1".into()), params: vec![("du".into(), SfBare::Bool(true)), ("etp".into(), SfBare::Int(25000))] },
            SfItem { value: SfBare::Str("origin".into()), params: vec![("rd".into(), SfBare::Int(12))] },
        ];
        let s = serialize_list(&l).unwrap();
        assert_eq!(s, r#""edge-1";du;etp=25000,"origin";rd=12"#);
        assert_eq!(parse_list(&s).unwrap(), l);
    }
}
