//! What a `signer_id` names — `extensions/jwp-receipts.md` §7.1.
//!
//! Every WAI receipt family that signs a `signer_pubkey` signs a `signer_id` beside it.
//! This module reads that string, and nothing else: it has no dependency, no I/O and no
//! clock, so every surface that shows a signer can say what its id is before any key log
//! is consulted (the confirmation itself is `crate::key_log`, feature `key_discovery`).
//!
//! A `signer_id` has one of three forms:
//!
//! - **origin-bound** — `did:web:<host>[%3A<port>]#<key-id>`: it names the web origin
//!   whose key log, published at `<origin>/.well-known/wai-keys`, lists the signer's key;
//! - **Ed25519 `did:key`** — `did:key:z` and the base58btc encoding of `0xed 0x01` and a
//!   32-byte key, in its one canonical spelling: it names that key and nothing else;
//! - **label** — any other string. A verifier reads nothing from a label.
//!
//! Each origin and key-id has one spelling, so a signer cannot be confirmed under a
//! second spelling of an origin a verifier anchored: a capital letter in the host, an
//! escape other than `%3A`, a path, a missing fragment, the default port written out and
//! an IP address are not the origin-bound form.

/// Where an origin publishes its key log, after the origin (jwp-receipts §7.1).
pub const KEY_LOG_PATH: &str = "/.well-known/wai-keys";

/// The form of a `signer_id` (jwp-receipts §7.1).
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SignerIdForm {
    /// `did:web:<host>[%3A<port>]#<key-id>`: the web origin (`https://<host>[:<port>]`)
    /// and the key-id its key log lists the key under.
    OriginBound { origin: String, key_id: String },
    /// An Ed25519 `did:key` in its canonical spelling, naming this key.
    DidKey([u8; 32]),
    /// Anything else: the signer's own label, from which a verifier reads nothing.
    Label,
}

/// The level a confirmation reports beside an item's own verdict (jwp-receipts §7).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Level {
    /// An origin the verifier anchored lists the item's key, in force, with its authority.
    Confirmed,
    /// What the verifier holds contradicts the item's claim to speak for its `signer_id`.
    Refused,
    /// The verifier holds nothing it can confirm against.
    Unconfirmed,
}

impl Level {
    pub fn label(self) -> &'static str {
        match self {
            Level::Confirmed => "confirmed",
            Level::Refused => "refused",
            Level::Unconfirmed => "unconfirmed",
        }
    }
}

/// Read a `signer_id` under §7.1's grammar.
pub fn classify(signer_id: &str) -> SignerIdForm {
    if let Some(rest) = signer_id.strip_prefix("did:web:")
        && let Some((authority, key_id)) = rest.split_once('#')
        && key_id_ok(key_id)
    {
        let (host, port) = match authority.split_once("%3A") {
            Some((h, p)) => (h, Some(p)),
            None => (authority, None),
        };
        if host_ok(host) && port.is_none_or(port_ok) {
            let origin = match port {
                Some(p) => format!("https://{host}:{p}"),
                None => format!("https://{host}"),
            };
            return SignerIdForm::OriginBound { origin, key_id: key_id.to_owned() };
        }
        return SignerIdForm::Label;
    }
    if let Some(enc) = signer_id.strip_prefix("did:key:z")
        && let Some(bytes) = b58_decode(enc)
        && bytes.len() == 34
        && bytes[0] == 0xed
        && bytes[1] == 0x01
    {
        let key: [u8; 32] = bytes[2..].try_into().expect("34 bytes less the two-byte prefix");
        // One spelling per key: the text must be what encoding the key gives.
        if did_key(&key) == signer_id {
            return SignerIdForm::DidKey(key);
        }
    }
    SignerIdForm::Label
}

/// The Ed25519 `did:key` of a key: `did:key:z` + base58btc(`0xed 0x01` || key).
pub fn did_key(key: &[u8; 32]) -> String {
    format!("did:key:z{}", multibase_ed25519(key))
}

/// The base58btc text (without the multibase `z`) of `0xed 0x01` || key — the
/// `publicKeyMultibase` of an Ed25519 key, less its leading `z`.
pub fn multibase_ed25519(key: &[u8; 32]) -> String {
    let mut b = Vec::with_capacity(34);
    b.extend_from_slice(&[0xed, 0x01]);
    b.extend_from_slice(key);
    b58_encode(&b)
}

/// Read a `publicKeyMultibase` value holding an Ed25519 key (`z` + base58btc of
/// `0xed 0x01` || key), in its canonical spelling.
pub fn read_multibase_ed25519(text: &str) -> Option<[u8; 32]> {
    let enc = text.strip_prefix('z')?;
    let b = b58_decode(enc)?;
    if b.len() != 34 || b[0] != 0xed || b[1] != 0x01 {
        return None;
    }
    let key: [u8; 32] = b[2..].try_into().ok()?;
    (multibase_ed25519(&key) == enc).then_some(key)
}

/// The key log's location for an origin of §7.1's form.
pub fn key_log_url(origin: &str) -> String {
    format!("{origin}{KEY_LOG_PATH}")
}

/// The `did:web` DID (no fragment) of an origin of §7.1's form, or `None` for a string
/// that is not one: `https://host` → `did:web:host`, `https://host:8443` →
/// `did:web:host%3A8443`.
pub fn did_web_of_origin(origin: &str) -> Option<String> {
    let rest = origin.strip_prefix("https://")?;
    let (host, port) = match rest.split_once(':') {
        Some((h, p)) => (h, Some(p)),
        None => (rest, None),
    };
    if !host_ok(host) || !port.is_none_or(port_ok) {
        return None;
    }
    Some(match port {
        Some(p) => format!("did:web:{host}%3A{p}"),
        None => format!("did:web:{host}"),
    })
}

/// Whether `origin` is an origin of §7.1's form: `https://` and a host, and a port where
/// one other than 443 is written.
pub fn origin_ok(origin: &str) -> bool {
    did_web_of_origin(origin).is_some()
}

/// §7.4 step 1: the origin and key-id of an origin-bound `signer_id`, or the level and
/// reason label at which the confirmation stops.
pub fn step1(signer_id: &str, signer_pubkey: &[u8; 32]) -> Result<(String, String), (Level, &'static str)> {
    match classify(signer_id) {
        SignerIdForm::OriginBound { origin, key_id } => Ok((origin, key_id)),
        SignerIdForm::DidKey(k) if k != *signer_pubkey => Err((Level::Refused, "signer_id_names_another_key")),
        SignerIdForm::DidKey(_) | SignerIdForm::Label => Err((Level::Unconfirmed, "signer_id_not_origin_bound")),
    }
}

/// What a surface that holds no key log can report about a signer: its level and reason
/// after §7.4 step 1, with an origin-bound id stopping at step 2 (`origin_not_anchored`).
pub fn level_without_anchors(signer_id: &str, signer_pubkey: &[u8; 32]) -> (Level, &'static str) {
    match step1(signer_id, signer_pubkey) {
        Ok(_) => (Level::Unconfirmed, "origin_not_anchored"),
        Err(e) => e,
    }
}

/// A host of §7.1: dot-separated labels of 1–63 characters from `a-z`, `0-9` and `-`,
/// none beginning or ending with `-`, at least two labels, the last not all digits, at
/// most 253 characters in all.
fn host_ok(host: &str) -> bool {
    let labels: Vec<&str> = host.split('.').collect();
    host.len() <= 253
        && labels.len() >= 2
        && labels.iter().all(|l| {
            (1..=63).contains(&l.len())
                && l.bytes().all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-')
                && !l.starts_with('-')
                && !l.ends_with('-')
        })
        && !labels.last().is_some_and(|l| l.bytes().all(|b| b.is_ascii_digit()))
}

/// A port of §7.1: 1–65535 in decimal, no leading zero, not 443.
fn port_ok(p: &str) -> bool {
    !p.is_empty()
        && p.len() <= 5
        && p.bytes().all(|b| b.is_ascii_digit())
        && !p.starts_with('0')
        && p.parse::<u32>().is_ok_and(|n| (1..=65535).contains(&n) && n != 443)
}

/// A key-id of §7.1: 1–64 characters from `A-Z`, `a-z`, `0-9`, `-`, `_` and `.`.
pub fn key_id_ok(k: &str) -> bool {
    (1..=64).contains(&k.len()) && k.bytes().all(|b| b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.'))
}

const B58: &[u8; 58] = b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";

/// The length of the base58btc text of `0xed 0x01` and a 32-byte key: every such value
/// lies between 58^46 and 58^47, so its one spelling has exactly this many characters.
/// A longer text is not that spelling, and is refused before any decoding: decoding is
/// quadratic in its input, and a `signer_id` arrives from whoever signed the item.
const ED25519_B58_LEN: usize = 47;

/// base58btc (the Bitcoin alphabet), each leading zero byte written as `1`.
fn b58_encode(input: &[u8]) -> String {
    let zeros = input.iter().take_while(|&&b| b == 0).count();
    let mut digits: Vec<u8> = Vec::new(); // base-58 digits, least significant first
    for &byte in &input[zeros..] {
        let mut carry = u32::from(byte);
        for d in digits.iter_mut() {
            carry += u32::from(*d) << 8;
            *d = (carry % 58) as u8;
            carry /= 58;
        }
        while carry > 0 {
            digits.push((carry % 58) as u8);
            carry /= 58;
        }
    }
    let mut s = String::with_capacity(zeros + digits.len());
    s.extend(std::iter::repeat_n('1', zeros));
    s.extend(digits.iter().rev().map(|&d| B58[d as usize] as char));
    s
}

/// The inverse of [`b58_encode`] for a text of at most [`ED25519_B58_LEN`] characters;
/// `None` for a longer text or a character outside the alphabet.
fn b58_decode(s: &str) -> Option<Vec<u8>> {
    if s.len() > ED25519_B58_LEN {
        return None;
    }
    let zeros = s.bytes().take_while(|&b| b == b'1').count();
    let mut bytes: Vec<u8> = Vec::new(); // base-256, least significant first
    for c in s.bytes().skip(zeros) {
        let mut carry = B58.iter().position(|&a| a == c)? as u32;
        for b in bytes.iter_mut() {
            carry += u32::from(*b) * 58;
            *b = (carry & 0xff) as u8;
            carry >>= 8;
        }
        while carry > 0 {
            bytes.push((carry & 0xff) as u8);
            carry >>= 8;
        }
    }
    let mut out = vec![0u8; zeros];
    out.extend(bytes.iter().rev());
    Some(out)
}

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

    fn unhex32(s: &str) -> [u8; 32] {
        let v: Vec<u8> = (0..64).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap()).collect();
        v.try_into().unwrap()
    }

    #[test]
    fn origin_bound_form_and_its_origin() {
        assert_eq!(
            classify("did:web:publisher.example#d1"),
            SignerIdForm::OriginBound { origin: "https://publisher.example".into(), key_id: "d1".into() }
        );
        assert_eq!(
            classify("did:web:publisher.example%3A8443#m_1.v-2"),
            SignerIdForm::OriginBound { origin: "https://publisher.example:8443".into(), key_id: "m_1.v-2".into() }
        );
        assert_eq!(key_log_url("https://publisher.example"), "https://publisher.example/.well-known/wai-keys");
        assert_eq!(did_web_of_origin("https://publisher.example:8443").as_deref(), Some("did:web:publisher.example%3A8443"));
        assert_eq!(did_web_of_origin("https://publisher.example:443"), None);
    }

    /// Each origin has one spelling: every other spelling is a label, so it can never be
    /// confirmed under an origin the verifier anchored.
    #[test]
    fn boundary_spellings() {
        let long_id = "k".repeat(65);
        let labels = [
            "did:web:Publisher.example#d1",
            "did:web:publisher.example:users:a#k",
            "did:web:publisher.example%3a8443#d1",
            "did:web:publisher.example%3A443#d1",
            "did:web:publisher.example%3A0443#d1",
            "did:web:publisher.example%3A65536#d1",
            "did:web:publisher.example%3A#d1",
            "did:web:publisher.example",
            "did:web:publisher.example#",
            "did:web:127.0.0.1#d1",
            "did:web:-a.example#d1",
            "did:web:a-.example#d1",
            "did:web:publisher..example#d1",
            "did:web:localhost#d1",
            "did:web:publisher.example#a/b",
            "did:web:publisher.example#a#b",
            "did:web:publisher.example/#d1",
        ];
        for l in labels {
            assert_eq!(classify(l), SignerIdForm::Label, "{l}");
        }
        assert_eq!(classify(&format!("did:web:publisher.example#{long_id}")), SignerIdForm::Label);
        assert!(matches!(classify(&format!("did:web:publisher.example#{}", &long_id[..64])), SignerIdForm::OriginBound { .. }));
        assert!(matches!(classify("did:web:publisher.example%3A65535#d1"), SignerIdForm::OriginBound { .. }));
        assert!(matches!(classify("did:web:a1.b2c.example#d1"), SignerIdForm::OriginBound { .. }));
    }

    /// Every DID-shaped id the tree's pins, tests and demos write is a label (or, for a
    /// canonical `did:key`, names one key): none is origin-bound.
    #[test]
    fn tree_ids_are_labels() {
        for id in [
            "did:key:pin",
            "did:key:demo-lab",
            "did:key:z6Mk",
            "did:key:test",
            "did:key:wai-quantum-cli",
            "did:web:pin#audio",
            "did:web:wai#prov",
            "did:web:meter",
            "did:web:publisher.example",
            "did:example:meter",
            "did:example:wai-meter",
        ] {
            assert_eq!(classify(id), SignerIdForm::Label, "{id}");
        }
    }

    /// Cross-checked against literals another pack in the repository computed
    /// (`jcp/conformance/v1/vectors.json`) and against keys from the claim corpus's seeds.
    #[test]
    fn did_key_literals() {
        let cases = [
            (
                "8a88e3dd7409f195fd52db2d3cba5d72ca6709bf1d94121bf3748801b40f6f5c",
                "did:key:z6Mkon3Necd6NkkyfoGoHxid2znGc59LU3K7mubaRcFbLfLX",
            ),
            (
                "8139770ea87d175f56a35466c34c7ecccb8d8a91b4ee37a25df60f5b8fc9b394",
                "did:key:z6Mko9hTggMwjSTEaJaPUfE6tqcy2xvU6BnNq3e3o8qVBiyH",
            ),
        ];
        for (k, d) in cases {
            let key = unhex32(k);
            assert_eq!(did_key(&key), d);
            assert_eq!(classify(d), SignerIdForm::DidKey(key));
            assert_eq!(step1(d, &key), Err((Level::Unconfirmed, "signer_id_not_origin_bound")));
            assert_eq!(step1(d, &[0u8; 32]), Err((Level::Refused, "signer_id_names_another_key")));
            assert_eq!(read_multibase_ed25519(&d["did:key:".len()..]), Some(key));
        }
    }

    /// A `did:key` that decodes to a key but is not that key's one spelling — an extra
    /// leading `1` adds a zero byte — is a label.
    #[test]
    fn did_key_noncanonical_is_label() {
        let key = unhex32("8a88e3dd7409f195fd52db2d3cba5d72ca6709bf1d94121bf3748801b40f6f5c");
        let canonical = did_key(&key);
        let extra = canonical.replacen("did:key:z", "did:key:z1", 1);
        assert_eq!(classify(&extra), SignerIdForm::Label);
        assert_eq!(classify("did:key:z0OIl"), SignerIdForm::Label);
        assert_eq!(b58_decode("1112").unwrap(), vec![0, 0, 0, 1]);
        assert_eq!(b58_encode(&[0, 0, 0, 1]), "1112");
    }

    /// An over-long `did:key` is a label, and is refused before it is decoded: a
    /// `signer_id` is chosen by whoever signed the item, and decoding is quadratic.
    #[test]
    fn long_did_key_is_refused_before_decoding() {
        assert!(b58_decode(&"2".repeat(ED25519_B58_LEN + 1)).is_none());
        assert!(b58_decode(&"2".repeat(ED25519_B58_LEN)).is_some());
        let key = unhex32("8a88e3dd7409f195fd52db2d3cba5d72ca6709bf1d94121bf3748801b40f6f5c");
        assert_eq!(did_key(&key).len(), "did:key:z".len() + ED25519_B58_LEN);
        let long = format!("did:key:z{}", "2".repeat(4 << 20));
        let t = std::time::Instant::now();
        assert_eq!(classify(&long), SignerIdForm::Label);
        assert_eq!(read_multibase_ed25519(&long["did:key:".len()..]), None);
        assert_eq!(level_without_anchors(&long, &key), (Level::Unconfirmed, "signer_id_not_origin_bound"));
        assert!(t.elapsed() < std::time::Duration::from_secs(5), "{:?}", t.elapsed());
    }

    #[test]
    fn level_without_anchors_stops_at_step_two() {
        assert_eq!(
            level_without_anchors("did:web:publisher.example#d1", &[0; 32]),
            (Level::Unconfirmed, "origin_not_anchored")
        );
        assert_eq!(level_without_anchors("did:key:wai-quantum-cli", &[0; 32]), (Level::Unconfirmed, "signer_id_not_origin_bound"));
    }
}
