//! Wire: the registry export (`wai/registry/capabilities.json`, written by `wai registry
//! export` from REGISTRY.md and held to it by `registry export --check`). It names every
//! capability a reader dispatches on, the deterministic identifiers, and the wire
//! identifiers: magics, document kinds, headers, paths, schemes and tags.
//!
//! A capability, identifier or wire identifier that is gone breaks: a reader that
//! dispatches on it, or a writer that emits it, no longer conforms. So does a capability
//! whose kind, media, role, tier, licence, replicate class, edition or operator set
//! changed (one the old export did not state at all is new, and compatible), one that left a set or joined the mandatory floor (every old reader lacks
//! it), a registered capability demoted to candidate, a wire identifier (named by its kind
//! and name together) whose status fell back from `defined`, and an identifier whose
//! meaning changed. A section moving between documents is editorial and is not compared.

use super::jsondiff;
use super::{Category, Finding};
use serde_json::{Map, Value};

pub const PATH: &str = "wai/registry/capabilities.json";

const COMPARED: &[&str] = &["kind", "media", "role", "tier", "licence", "replicate", "edition", "ops"];

fn by<'a>(doc: &'a Value, list: &str, key: &str) -> Vec<(String, &'a Map<String, Value>)> {
    doc.get(list)
        .and_then(Value::as_array)
        .map(|a| a.iter().filter_map(|x| Some((x.get(key)?.as_str()?.to_string(), x.as_object()?))).collect())
        .unwrap_or_default()
}

/// The wire identifiers, each named by its kind and its name together (`fourcc/wai1`): one
/// name can be, say, a `codecs` prefix and a sample-entry 4CC at once.
fn wire(doc: &Value) -> Vec<(String, &Map<String, Value>)> {
    by(doc, "wire_identifiers", "name").into_iter().map(|(n, m)| (format!("{}/{n}", m.get("kind").and_then(Value::as_str).unwrap_or("identifier")), m)).collect()
}

fn s(m: &Map<String, Value>, k: &str) -> String {
    match m.get(k) {
        None => "absent".into(),
        Some(Value::String(x)) => x.clone(),
        Some(v) => jsondiff::short(v),
    }
}

/// The registry's findings, from the two exports (`None` where a tree has none).
pub fn compare(old: Option<&Value>, new: Option<&Value>) -> Vec<Finding> {
    let mut out = Vec::new();
    let (old, new) = match (old, new) {
        (None, None) => return out,
        (None, Some(_)) => {
            out.push(Finding::compatible(Category::Wire, "registry", PATH.into(), "the registry export is new".into()));
            return out;
        }
        (Some(_), None) => {
            out.push(Finding::broke(
                Category::Wire,
                "registry",
                PATH.into(),
                "removed".into(),
                "the registry export is gone".into(),
                "every capability and wire identifier a reader dispatches on loses its registration".into(),
            ));
            return out;
        }
        (Some(a), Some(b)) => (a, b),
    };

    let caps_new: std::collections::HashMap<String, &Map<String, Value>> = by(new, "capabilities", "capability").into_iter().collect();
    let caps_old: std::collections::HashSet<String> = by(old, "capabilities", "capability").into_iter().map(|x| x.0).collect();
    for (name, a) in by(old, "capabilities", "capability") {
        let id = name.clone();
        let Some(b) = caps_new.get(&name) else {
            out.push(Finding::broke(Category::Wire, "capability", id, "removed".into(), format!("capability {name} is no longer registered"), "a reader that dispatches on it, or a writer that names it, no longer conforms".into()));
            continue;
        };
        let mut what = Vec::new();
        let mut gained = Vec::new();
        for k in COMPARED {
            match (a.get(*k), b.get(*k)) {
                (x, y) if x == y => {}
                // A member the old export did not have is new information, not a change.
                (None, Some(_)) => gained.push(format!("{k} {}", s(b, k))),
                _ => what.push(format!("{k} {} → {}", s(a, k), s(b, k))),
            }
        }
        if a.get("status").and_then(Value::as_str) == Some("registered") && b.get("status").and_then(Value::as_str) != Some("registered") {
            what.push(format!("status registered → {}", s(b, "status")));
        }
        let sets = |m: &Map<String, Value>| -> Vec<String> { m.get("sets").and_then(Value::as_array).map(|a| a.iter().filter_map(|x| x.as_str().map(String::from)).collect()).unwrap_or_default() };
        let (sa, sb) = (sets(a), sets(b));
        for x in sa.iter().filter(|x| !sb.contains(x)) {
            what.push(format!("left the set {x}"));
        }
        if sb.iter().any(|x| x == "floor") && !sa.iter().any(|x| x == "floor") {
            what.push("joined the mandatory floor, which every older reader lacks".into());
        }
        if !what.is_empty() {
            out.push(Finding::broke(Category::Wire, "capability", id, "changed".into(), what.join("; "), "readers and writers dispatch, select and verify by these fields".into()));
        } else if a.get("status") != b.get("status") || sb.iter().any(|x| !sa.contains(x)) || !gained.is_empty() {
            gained.insert(0, format!("status {} → {}, sets {:?} → {:?}", s(a, "status"), s(b, "status"), sa, sb));
            out.push(Finding::compatible(Category::Wire, "capability", name.clone(), gained.join("; ")));
        }
    }
    for (name, _) in by(new, "capabilities", "capability") {
        if !caps_old.contains(&name) {
            out.push(Finding::compatible(Category::Wire, "capability", name.clone(), format!("capability {name} is registered")));
        }
    }

    let ids_new: std::collections::HashMap<String, &Map<String, Value>> = by(new, "identifiers", "identifier").into_iter().collect();
    let ids_old: std::collections::HashSet<String> = by(old, "identifiers", "identifier").into_iter().map(|x| x.0).collect();
    for (name, a) in by(old, "identifiers", "identifier") {
        let id = name.clone();
        match ids_new.get(&name) {
            None => out.push(Finding::broke(Category::Wire, "identifier", id, "removed".into(), format!("identifier {name} is gone"), "a manifest that names it is no longer read as it was".into())),
            Some(b) if a.get("meaning") != b.get("meaning") => out.push(Finding::broke(
                Category::Wire,
                "identifier",
                id,
                "changed".into(),
                format!("its meaning changed: {} → {}", s(a, "meaning"), s(b, "meaning")),
                "a deterministic identifier names one arithmetic; another meaning gives other bytes under the same name".into(),
            )),
            Some(_) => {}
        }
    }
    for (name, _) in by(new, "identifiers", "identifier") {
        if !ids_old.contains(&name) {
            out.push(Finding::compatible(Category::Wire, "identifier", name.clone(), format!("identifier {name} is new")));
        }
    }

    let rank = |m: &Map<String, Value>| match m.get("status").and_then(Value::as_str) {
        Some("defined") => 2,
        Some("provisional") => 1,
        _ => 0,
    };
    let wire_new: std::collections::HashMap<String, &Map<String, Value>> = wire(new).into_iter().collect();
    let wire_old: std::collections::HashSet<String> = wire(old).into_iter().map(|x| x.0).collect();
    for (id, a) in wire(old) {
        let kind = a.get("kind").and_then(Value::as_str).unwrap_or("identifier");
        let name = a.get("name").and_then(Value::as_str).unwrap_or("?");
        match wire_new.get(&id) {
            None => out.push(Finding::broke(
                Category::Wire,
                "wire-identifier",
                id.clone(),
                "removed".into(),
                format!("the {kind} {name} ({}) is gone", s(a, "document")),
                format!("a reader that matches this {kind} refuses what is written without it, and what was written with it is no longer defined"),
            )),
            Some(b) if rank(b) < rank(a) => out.push(Finding::broke(
                Category::Wire,
                "wire-identifier",
                id.clone(),
                "changed".into(),
                format!("the {kind} {name}: status {} → {}", s(a, "status"), s(b, "status")),
                format!("readers and writers rely on this {kind} as it was defined"),
            )),
            Some(_) => {}
        }
    }
    for (id, b) in wire(new) {
        if !wire_old.contains(&id) {
            let kind = b.get("kind").and_then(Value::as_str).unwrap_or("identifier");
            let name = b.get("name").and_then(Value::as_str).unwrap_or("?");
            out.push(Finding::compatible(Category::Wire, "wire-identifier", id.clone(), format!("the {kind} {name} is new")));
        }
    }
    out
}
