//! A parameter set's usage statement (extensions/prior-carriage.md §18.1).
//!
//! A JSON object of exactly the members `kind` (`wai-set-usage`), `licence` (an SPDX licence
//! expression or `null`), `terms` (an `https` URI or `null`), `content_derived`,
//! `redistribute` and `ai_usage` (an [ai-usage](crate::ai_usage) statement or `null`). It is
//! bound to a set only as part of the bytes the set's pin covers: the file `wai-usage.json` of a
//! `wai.prior.set`, or the statement in a sealed set's header.

use crate::ai_usage::Statement as AiUsage;
use serde_json::Value;

/// The reason a statement that does not read is refused with.
pub const USAGE_MALFORMED: &str = "set_usage_malformed";
/// The file of a `wai.prior.set` that holds its statement.
pub const USAGE_FILE: &str = "wai-usage.json";
/// The longest statement, in bytes.
pub const MAX_USAGE: usize = 65536;

/// A statement as read.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Usage {
    pub licence: Option<String>,
    pub terms: Option<String>,
    pub content_derived: bool,
    pub redistribute: bool,
    /// The ai-usage statement's digest (ai-usage §3.6: SHA-256 over its canonical form).
    pub ai_usage: Option<[u8; 32]>,
}

/// One token of an SPDX licence expression.
fn tokens(s: &str) -> Option<Vec<&str>> {
    let mut out = Vec::new();
    for word in s.split(' ') {
        if word.is_empty() {
            return None;
        }
        let mut rest = word;
        while let Some(r) = rest.strip_prefix('(') {
            out.push("(");
            rest = r;
        }
        let mut closes = 0;
        while let Some(r) = rest.strip_suffix(')') {
            closes += 1;
            rest = r;
        }
        if !rest.is_empty() {
            out.push(rest);
        }
        out.extend(std::iter::repeat_n(")", closes));
    }
    Some(out)
}

fn idstring(s: &str) -> bool {
    !s.is_empty() && s.bytes().all(|c| c.is_ascii_alphanumeric() || c == b'-' || c == b'.')
}

/// A simple expression: a licence identifier, with `+` or not, or a `LicenseRef-`, with a
/// `DocumentRef-…:` before it or not.
fn simple(t: &str) -> bool {
    if ["AND", "OR", "WITH", "(", ")"].contains(&t) {
        return false;
    }
    let t = match t.split_once(':') {
        Some((doc, r)) => match doc.strip_prefix("DocumentRef-") {
            Some(d) if idstring(d) => {
                return r.strip_prefix("LicenseRef-").is_some_and(idstring);
            }
            _ => return false,
        },
        None => t,
    };
    if let Some(r) = t.strip_prefix("LicenseRef-") {
        return idstring(r);
    }
    idstring(t.strip_suffix('+').unwrap_or(t))
}

/// Whether `s` is an SPDX licence expression (SPDX 2.3 Annex D): simple expressions joined by
/// `AND` and `OR`, each with `WITH` and an exception identifier or not, in parentheses or not,
/// tokens separated by single spaces. Identifiers are checked for their characters only.
pub fn is_licence_expression(s: &str) -> bool {
    fn expr(t: &[&str], i: &mut usize, depth: usize) -> bool {
        if depth > 32 {
            return false;
        }
        loop {
            match t.get(*i) {
                Some(&"(") => {
                    *i += 1;
                    if !expr(t, i, depth + 1) || t.get(*i) != Some(&")") {
                        return false;
                    }
                    *i += 1;
                }
                Some(x) if simple(x) => {
                    *i += 1;
                    if t.get(*i) == Some(&"WITH") {
                        *i += 1;
                        match t.get(*i) {
                            Some(e) if idstring(e) && !["AND", "OR", "WITH"].contains(e) => *i += 1,
                            _ => return false,
                        }
                    }
                }
                _ => return false,
            }
            match t.get(*i) {
                Some(&"AND") | Some(&"OR") => *i += 1,
                _ => return true,
            }
        }
    }
    if s.is_empty() || s.len() > 1024 {
        return false;
    }
    let Some(t) = tokens(s) else { return false };
    let mut i = 0;
    expr(&t, &mut i, 0) && i == t.len()
}

fn terms_ok(s: &str) -> bool {
    (8..=2048).contains(&s.len()) && s.starts_with("https://") && s.bytes().all(|b| (0x21..=0x7e).contains(&b))
}

/// §18.1's statement from its JSON text: no member named twice, exactly the members above, each
/// of its type and within its range; or [`USAGE_MALFORMED`].
pub fn read(bytes: &[u8]) -> Result<Usage, &'static str> {
    let bad = USAGE_MALFORMED;
    if bytes.len() > MAX_USAGE {
        return Err(bad);
    }
    let text = std::str::from_utf8(bytes).map_err(|_| bad)?;
    let v = crate::claim_text::read(text).map_err(|_| bad)?;
    let m = v.as_object().ok_or(bad)?;
    let names = ["kind", "licence", "terms", "content_derived", "redistribute", "ai_usage"];
    if m.len() != names.len() || !names.iter().all(|n| m.contains_key(*n)) {
        return Err(bad);
    }
    if m["kind"] != Value::String("wai-set-usage".into()) {
        return Err(bad);
    }
    let licence = match &m["licence"] {
        Value::Null => None,
        Value::String(s) if is_licence_expression(s) => Some(s.clone()),
        _ => return Err(bad),
    };
    let terms = match &m["terms"] {
        Value::Null => None,
        Value::String(s) if terms_ok(s) => Some(s.clone()),
        _ => return Err(bad),
    };
    let (Some(content_derived), Some(redistribute)) = (m["content_derived"].as_bool(), m["redistribute"].as_bool()) else {
        return Err(bad);
    };
    let ai_usage = match &m["ai_usage"] {
        Value::Null => None,
        a => Some(AiUsage::read(a).map_err(|_| bad)?.digest()),
    };
    Ok(Usage { licence, terms, content_derived, redistribute, ai_usage })
}

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

    #[test]
    fn licence_expressions() {
        for ok in [
            "Apache-2.0",
            "MIT OR Apache-2.0",
            "(MIT OR Apache-2.0) AND CC-BY-4.0",
            "GPL-2.0-or-later WITH Classpath-exception-2.0",
            "LicenseRef-studio-catalog-1",
            "DocumentRef-terms:LicenseRef-x",
            "GPL-2.0+",
            "((MIT))",
        ] {
            assert!(is_licence_expression(ok), "{ok}");
        }
        for bad in ["", "MIT OR", "MIT  OR Apache-2.0", "(MIT", "MIT)", "MIT and Apache-2.0", "WITH", "MIT WITH", "a/b", "MIT WITH x+"] {
            assert!(!is_licence_expression(bad), "{bad}");
        }
    }

    #[test]
    fn statements() {
        let s = br#"{"kind":"wai-set-usage","licence":"Apache-2.0","terms":null,"content_derived":false,"redistribute":true,"ai_usage":null}"#;
        let u = read(s).expect("reads");
        assert!(u.redistribute && !u.content_derived);
        let twice = br#"{"kind":"wai-set-usage","kind":"wai-set-usage","licence":null,"terms":null,"content_derived":false,"redistribute":true,"ai_usage":null}"#;
        assert_eq!(read(twice), Err(USAGE_MALFORMED));
        let extra = br#"{"kind":"wai-set-usage","licence":null,"terms":null,"content_derived":false,"redistribute":true,"ai_usage":null,"x":1}"#;
        assert_eq!(read(extra), Err(USAGE_MALFORMED));
    }
}
