//! An observation as a CMCD version 2 report (CTA-5004-B; `extensions/delivery-telemetry.md`
//! §3, §4): which mode reports it, which standard and custom keys carry which members,
//! and, for Request mode, the header shards.
//!
//! A report is an RFC 9651 dictionary with its keys in bytewise order and no optional
//! whitespace. A key is written only when its value is not `null`. Custom keys carry
//! Strings, which is the only custom value type CTA-5004-B admits besides Tokens, so an
//! integer member is written as its decimal digits.

use std::collections::BTreeMap;

use super::observation::Observation;
use super::sf::{parse_dict, serialize_dict, SfBare, SfMember};
use super::TelemetryError;

/// The prefix of every custom key this extension defines.
pub const PREFIX: &str = "science.transaction.wai-";

/// The keys of CMCD version 2: request, response and event keys (CTA-5004-B), and the
/// version 1 key `nrr`.
pub const CMCD_KEYS: &[&str] = &[
    "ab", "bg", "bl", "br", "bs", "bsa", "bsd", "bsda", "cdn", "cid", "cs", "d", "dfa", "dl", "ec", "lab", "lb", "ltc", "msd", "mtp", "nor",
    "nr", "nrr", "ot", "pb", "pr", "pt", "rtp", "sf", "sid", "sn", "st", "sta", "su", "tab", "tb", "tbl", "tpb", "v", "cmsdd", "cmsds", "rc",
    "smrt", "ttfb", "ttfbb", "ttlb", "url", "cen", "e", "h", "ts",
];

/// The CMCD header shard a Request-mode key goes to (CTA-5004-B), custom keys included.
pub fn header_shard(key: &str) -> Option<&'static str> {
    Some(match key {
        "ab" | "br" | "d" | "lab" | "lb" | "ot" | "tab" | "tb" | "tpb" => "CMCD-Object",
        "bl" | "cs" | "dfa" | "dl" | "ltc" | "mtp" | "nor" | "nrr" | "pb" | "sn" | "sta" | "su" | "tbl" => "CMCD-Request",
        "cid" | "msd" | "sf" | "sid" | "st" | "v" => "CMCD-Session",
        "bg" | "bs" | "bsa" | "bsd" | "bsda" | "cdn" | "ec" | "nr" | "pr" | "pt" | "rtp" => "CMCD-Status",
        _ => {
            let x = key.strip_prefix(PREFIX)?;
            match x {
                "b" | "tx" | "ch" | "cap" | "ri" | "l" | "lt" | "p" | "rcpt" => "CMCD-Object",
                "lc" => "CMCD-Request",
                "coh" => "CMCD-Session",
                _ => return None,
            }
        }
    })
}

/// Whether `k` has the form of a CMCD custom key: a lowercase first letter, then
/// lowercase letters, digits, `.` and `-`, with a hyphen that is neither first nor last.
pub fn is_custom_key(k: &str) -> bool {
    let b = k.as_bytes();
    // An inner hyphen needs a character on each side, so a key has at least three.
    b.len() >= 3
        && b[0].is_ascii_lowercase()
        && b.iter().all(|&c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == b'.' || c == b'-')
        && b[1..b.len() - 1].contains(&b'-')
}

/// How an observation is reported.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Report {
    /// Request mode: the dictionary a request carries.
    Request(String),
    /// Event mode: one report line.
    Event(String),
    /// Not reported in CMCD (a `present` that is not the session's first).
    None,
}

fn s(v: impl ToString) -> SfMember {
    SfMember::Item(SfBare::Str(v.to_string()))
}

fn int(v: u64) -> SfMember {
    SfMember::Item(SfBare::Int(v as i64))
}

fn tok(v: &str) -> SfMember {
    SfMember::Item(SfBare::Token(v.to_string()))
}

fn path_token(p: &str) -> &'static str {
    if p == "primary" { "p" } else { "f" }
}

/// The report's members, in bytewise key order, and whether it is Event mode; `None`
/// when the observation is not reported.
pub fn members(o: &Observation) -> Result<Option<(bool, Vec<(String, SfMember)>)>, TelemetryError> {
    let event = o.is_event_mode();
    if o.event == "present" && !event {
        return Ok(None);
    }
    let mut m: BTreeMap<String, SfMember> = BTreeMap::new();
    let mut put = |k: &str, v: Option<SfMember>| {
        if let Some(v) = v {
            m.insert(k.to_string(), v);
        }
    };
    let c = |x: &str| format!("{PREFIX}{x}");
    put("v", Some(int(2)));
    put("sid", Some(s(&o.sid)));
    put("sn", Some(int(o.sn)));
    put("cid", o.cid.as_ref().map(s));
    put("st", Some(tok(&o.st)));
    put("sf", Some(tok(&o.sf)));
    put(&c("coh"), o.cohort.as_ref().map(s));
    if let Some(x) = &o.metrics {
        put("msd", x.msd_ms.map(int));
        put("bsa", x.bsa.map(|n| SfMember::IntList(vec![n as i64])));
        put("bsda", x.bsda_ms.map(|n| SfMember::IntList(vec![n as i64])));
        put("dfa", x.dfa.map(int));
    }
    if let Some(ob) = &o.object {
        put(&c("b"), ob.counted.map(s));
        put(&c("tx"), ob.transferred.map(s));
        put(&c("ch"), ob.content_hash.as_ref().map(s));
        put(&c("cap"), ob.capability.as_ref().map(s));
        put(&c("ri"), ob.rendition.map(s));
        put(&c("l"), ob.layer.map(s));
        put(&c("lt"), ob.layers_total.map(s));
        put(&c("p"), ob.path.as_deref().map(|p| s(path_token(p))));
        put(&c("rcpt"), ob.receipt.as_ref().map(s));
        // Request keys, and response keys (Event mode, `e=rr`); the observation's
        // checks keep each on its own side.
        put("ot", ob.ot.as_deref().map(tok));
        put("d", ob.d_ms.map(int));
        put("dl", ob.dl_ms.map(int));
        put(&c("lc"), ob.layer_cap.map(s));
        put("rc", ob.rc.map(int));
        put("ttfb", ob.ttfb_ms.map(int));
        put("ttlb", ob.ttlb_ms.map(int));
        put("url", ob.url.as_ref().map(s));
    }
    if event {
        put("ts", o.ts_ms.map(int));
        let (e, sta, cen) = match o.event.as_str() {
            "response" => ("rr", None, None),
            "start" => ("ps", Some("s"), None),
            "present" | "stall-end" => ("ps", Some("p"), None),
            "stall-start" => ("ps", Some("r"), None),
            "abandon" => ("ps", Some("q"), None),
            "end" => ("ps", Some("e"), None),
            "switch" => ("ce", None, Some("wai-switch")),
            "capable" => ("ce", None, Some("wai-capable")),
            "layer-change" => ("ce", None, Some("wai-layer-change")),
            "frame-drop" => ("ce", None, Some("wai-frame-drop")),
            other => return Err(TelemetryError::Invalid(format!("event `{other}` has no Event-mode report"))),
        };
        put("e", Some(tok(e)));
        put("sta", sta.map(tok));
        put("cen", cen.map(s));
        if let Some(p) = &o.present {
            put(&c("u"), Some(s(p.unit)));
            put(&c("pl"), Some(s(p.layers)));
            put(&c("late"), p.late_ms.map(s));
            put(&c("dec"), p.decode_us.map(s));
        }
        if let Some(w) = &o.switch {
            put(&c("u"), Some(s(w.unit)));
            put(&c("p"), Some(s(path_token(&w.to))));
            put(&c("sw"), Some(s(&w.reason)));
        }
        if let Some(k) = &o.capable {
            put(&c("co"), Some(s(&k.outcome)));
            put(&c("cr"), k.reason.as_ref().map(s));
            put(&c("cf"), k.classical_floor.as_ref().map(s));
        }
        if let Some(l) = &o.layer_change {
            put(&c("lc"), l.to.map(s));
            put(&c("lcf"), l.from.map(s));
        }
        put(&c("clm"), o.claim.as_ref().map(s));
    }
    Ok(Some((event, m.into_iter().collect())))
}

/// The CMCD report of an observation (§3).
pub fn cmcd(o: &Observation) -> Result<Report, TelemetryError> {
    Ok(match members(o)? {
        None => Report::None,
        Some((event, m)) => {
            let d = serialize_dict(&m).map_err(|e| TelemetryError::Invalid(e.to_string()))?;
            if event { Report::Event(d) } else { Report::Request(d) }
        }
    })
}

/// The header shards of a Request-mode report: each shard's dictionary, keys in bytewise
/// order; a shard with no key is not written.
pub fn cmcd_headers(o: &Observation) -> Result<BTreeMap<&'static str, String>, TelemetryError> {
    let Some((false, m)) = members(o)? else {
        return Err(TelemetryError::Invalid("header shards carry Request-mode reports only".into()));
    };
    let mut shards: BTreeMap<&'static str, Vec<(String, SfMember)>> = BTreeMap::new();
    for (k, v) in m {
        let h = header_shard(&k).ok_or_else(|| TelemetryError::Invalid(format!("`{k}` has no header shard")))?;
        shards.entry(h).or_default().push((k, v));
    }
    shards
        .into_iter()
        .map(|(h, m)| serialize_dict(&m).map(|d| (h, d)).map_err(|e| TelemetryError::Invalid(e.to_string())))
        .collect()
}

/// Read a CMCD dictionary: every key a CMCD version 2 key or a custom key, and every
/// custom key's value a String or a Token.
pub fn from_cmcd(dict: &str) -> Result<Vec<(String, SfMember)>, TelemetryError> {
    let m = parse_dict(dict).map_err(|e| TelemetryError::Invalid(e.to_string()))?;
    for (k, v) in &m {
        if CMCD_KEYS.contains(&k.as_str()) {
            continue;
        }
        if !is_custom_key(k) {
            return Err(TelemetryError::Invalid(format!("`{k}` is neither a CMCD key nor a custom key")));
        }
        if !matches!(v, SfMember::Item(SfBare::Str(_) | SfBare::Token(_))) {
            return Err(TelemetryError::Invalid(format!("custom key `{k}` carries neither a String nor a Token")));
        }
    }
    Ok(m)
}

/// The class of a key (§3.4): what a deployment does with it, never a permission.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum KeyClass {
    /// What the host serving the request needs to serve it.
    Delivery,
    /// What is only aggregated afterwards.
    Measurement,
}

impl KeyClass {
    pub fn name(self) -> &'static str {
        match self {
            KeyClass::Delivery => "delivery",
            KeyClass::Measurement => "measurement",
        }
    }
}

/// The class of each key this mapping writes (§3.4); `None` for a key it does not write.
pub fn key_class(key: &str) -> Option<KeyClass> {
    use KeyClass::*;
    Some(match key {
        "v" | "st" | "ot" | "d" | "dl" => Delivery,
        "sid" | "sn" | "cid" | "sf" | "msd" | "bsa" | "bsda" | "dfa" | "ts" | "e" | "sta" | "rc" | "ttfb" | "ttlb" | "url" | "cen" => Measurement,
        _ => match key.strip_prefix(PREFIX)? {
            "ch" | "lc" => Delivery,
            "b" | "tx" | "cap" | "ri" | "l" | "lt" | "p" | "rcpt" | "lcf" | "coh" | "u" | "pl" | "late" | "dec" | "sw" | "co" | "cr" | "cf" | "clm" => Measurement,
            _ => return None,
        },
    })
}

fn delivery_members(o: &Observation) -> Result<Vec<(String, SfMember)>, TelemetryError> {
    match members(o)? {
        Some((false, m)) => Ok(m.into_iter().filter(|(k, _)| key_class(k) == Some(KeyClass::Delivery)).collect()),
        _ => Err(TelemetryError::Invalid("a delivery-only report is a Request-mode report".into())),
    }
}

/// The delivery-only Request-mode report of an observation (§4.3): its delivery-class
/// members alone, in the order and form §3.3 writes them.
pub fn cmcd_delivery_only(o: &Observation) -> Result<String, TelemetryError> {
    serialize_dict(&delivery_members(o)?).map_err(|e| TelemetryError::Invalid(e.to_string()))
}

/// The header shards of a delivery-only report (§4.2, §4.3).
pub fn cmcd_delivery_only_headers(o: &Observation) -> Result<BTreeMap<&'static str, String>, TelemetryError> {
    let mut shards: BTreeMap<&'static str, Vec<(String, SfMember)>> = BTreeMap::new();
    for (k, v) in delivery_members(o)? {
        let h = header_shard(&k).ok_or_else(|| TelemetryError::Invalid(format!("`{k}` has no header shard")))?;
        shards.entry(h).or_default().push((k, v));
    }
    shards
        .into_iter()
        .map(|(h, m)| serialize_dict(&m).map(|d| (h, d)).map_err(|e| TelemetryError::Invalid(e.to_string())))
        .collect()
}

/// Check a report a reader is told is delivery-only (§4.3): it reads as CMCD, and every key
/// is of the delivery class. The first key that is not is the error.
pub fn check_delivery_only(dict: &str) -> Result<(), String> {
    let m = from_cmcd(dict).map_err(|e| format!("does not read: {e}"))?;
    match m.iter().find(|(k, _)| key_class(k) != Some(KeyClass::Delivery)) {
        Some((k, _)) => Err(k.clone()),
        None => Ok(()),
    }
}
