//! COSE as live-provenance §4.3, §7.1 and §7.2 use it: `COSE_Sign1_Tagged` (RFC 9052 §4.2)
//! with EdDSA (RFC 9053 §2.2), its Sig_structure (§4.4 there, context `Signature1`, empty
//! external data), and the OKP COSE_Key of an Ed25519 public key (RFC 9053 §7.2).

use crate::live_watermark::cbor::{encode, read_one, Item};
use ed25519_dalek::{Signer, SigningKey};

/// COSE's `alg` label.
pub const H_ALG: u64 = 1;
/// COSE's `kid` label.
pub const H_KID: u64 = 4;
/// COSE's `x5chain` label (RFC 9360).
pub const H_X5CHAIN: u64 = 33;
/// EdDSA (RFC 9053 §2.2): −8.
pub const ALG_EDDSA: i64 = -8;
/// The tag of `COSE_Sign1_Tagged`.
pub const TAG_SIGN1: u64 = 18;

/// The negative CBOR integer `n` (< 0).
pub fn nint(n: i64) -> Item {
    debug_assert!(n < 0);
    Item::Nint((-1 - n) as u64)
}

/// A map with unsigned and negative integer keys, `(key, value)` as given.
pub fn int_map(entries: Vec<(i64, Item)>) -> Item {
    Item::Map(entries.into_iter().map(|(k, v)| (if k < 0 { nint(k) } else { Item::Uint(k as u64) }, v)).collect())
}

/// The Sig_structure of a `COSE_Sign1` (RFC 9052 §4.4) over `payload`, encoded.
pub fn sig_structure(protected: &[u8], payload: &[u8]) -> Vec<u8> {
    encode(&Item::Array(vec![
        Item::Text("Signature1".into()),
        Item::Bytes(protected.to_vec()),
        Item::Bytes(Vec::new()),
        Item::Bytes(payload.to_vec()),
    ]))
}

/// A `COSE_Sign1_Tagged` by `key` over `signed`, with the protected header `protected`, the
/// unprotected header `unprotected`, and `payload` present or detached (`None`).
pub fn sign1(protected: &Item, unprotected: Item, payload: Option<&[u8]>, signed: &[u8], key: &SigningKey) -> Vec<u8> {
    let p = encode(protected);
    let sig = key.sign(&sig_structure(&p, signed)).to_bytes();
    encode(&Item::Tag(
        TAG_SIGN1,
        Box::new(Item::Array(vec![
            Item::Bytes(p),
            unprotected,
            payload.map_or(Item::Null, |b| Item::Bytes(b.to_vec())),
            Item::Bytes(sig.to_vec()),
        ])),
    ))
}

/// A `COSE_Sign1_Tagged`, read.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Sign1 {
    pub protected_bytes: Vec<u8>,
    pub protected: Item,
    pub unprotected: Item,
    pub payload: Option<Vec<u8>>,
    pub signature: [u8; 64],
}

impl Sign1 {
    /// Read a `COSE_Sign1_Tagged` whose protected header is a map, whose unprotected header
    /// is a map, whose payload is a byte string or `nil`, and whose signature is 64 bytes; with
    /// `alg` EdDSA in the protected header and in no other bucket.
    pub fn read(b: &[u8]) -> Option<Sign1> {
        let Item::Tag(TAG_SIGN1, inner) = read_one(b).ok()? else { return None };
        let Item::Array(parts) = *inner else { return None };
        let [Item::Bytes(p), u @ Item::Map(_), payload, Item::Bytes(sig)] = parts.as_slice() else { return None };
        let protected = read_one(p).ok()?;
        if !matches!(protected, Item::Map(_)) {
            return None;
        }
        if protected.get(H_ALG).and_then(Item::int) != Some(ALG_EDDSA) || u.get(H_ALG).is_some() {
            return None;
        }
        let payload = match payload {
            Item::Null => None,
            Item::Bytes(x) => Some(x.clone()),
            _ => return None,
        };
        Some(Sign1 {
            protected_bytes: p.clone(),
            protected,
            unprotected: u.clone(),
            payload,
            signature: sig.as_slice().try_into().ok()?,
        })
    }

    /// The signature under `key` over `signed`, by jwp-receipts §3.1 step 3's strict rule.
    pub fn verify(&self, key: &[u8; 32], signed: &[u8]) -> bool {
        crate::sig::verify_strict(key, &sig_structure(&self.protected_bytes, signed), &self.signature).is_ok()
    }
}

/// The OKP COSE_Key of §7.1: `{1: 1, 2: kid, -1: 6, -2: x}`, the `kid` being `x`.
pub fn okp_key(x: &[u8; 32]) -> Item {
    int_map(vec![(1, Item::Uint(1)), (2, Item::Bytes(x.to_vec())), (-1, Item::Uint(6)), (-2, Item::Bytes(x.to_vec()))])
}

/// The public key of an OKP COSE_Key as §7.1 writes one: exactly those four labels, the key
/// type OKP, the curve Ed25519, a 32-byte `x` and a `kid` equal to it.
pub fn read_okp_key(k: &Item) -> Option<[u8; 32]> {
    let Item::Map(m) = k else { return None };
    if m.len() != 4 || k.get(1) != Some(&Item::Uint(1)) {
        return None;
    }
    let crv = m.iter().find(|(l, _)| *l == nint(-1)).map(|(_, v)| v)?;
    let x = m.iter().find(|(l, _)| *l == nint(-2)).map(|(_, v)| v)?;
    let kid = k.get(2)?;
    let (Item::Uint(6), Item::Bytes(x), Item::Bytes(kid)) = (crv, x, kid) else { return None };
    let x: [u8; 32] = x.as_slice().try_into().ok()?;
    (kid.as_slice() == x).then_some(x)
}

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

    #[test]
    fn a_sign1_round_trips_and_its_signature_binds_the_payload() {
        let k = SigningKey::from_bytes(&[7; 32]);
        let protected = int_map(vec![(1, nint(ALG_EDDSA))]);
        let s = sign1(&protected, Item::Map(vec![]), None, b"claim", &k);
        let r = Sign1::read(&s).unwrap();
        assert_eq!(r.payload, None);
        let pk = k.verifying_key().to_bytes();
        assert!(r.verify(&pk, b"claim"));
        assert!(!r.verify(&pk, b"claim!"));
        // An algorithm other than EdDSA does not read.
        let other = sign1(&int_map(vec![(1, nint(-7))]), Item::Map(vec![]), None, b"claim", &k);
        assert!(Sign1::read(&other).is_none());
    }

    #[test]
    fn an_okp_key_reads_back_and_its_kid_is_its_key() {
        let x = [9u8; 32];
        assert_eq!(read_okp_key(&okp_key(&x)), Some(x));
        let other = int_map(vec![(1, Item::Uint(1)), (2, Item::Bytes(vec![1])), (-1, Item::Uint(6)), (-2, Item::Bytes(x.to_vec()))]);
        assert_eq!(read_okp_key(&other), None);
    }
}
