//! A P-256 public key in JWK form — the shape a credential carries in its
//! `cnf` claim and a verifier reads to check a key-binding proof.
//!
//! Only `EC` / `P-256` is modeled: it is what Apple's Secure Enclave and the
//! common WebAuthn authenticators produce, and what ES256 signs with. The JWK
//! is the *only* place the holder's authenticator choice shows through — the
//! credential model never names "Secure Enclave" or "WebAuthn", just a key.

use crate::base64url;
use crate::error::Error;
use p256::ecdsa::VerifyingKey;
use serde::{Deserialize, Serialize};
use sha2::{Digest as _, Sha256};

/// A public key on the NIST P-256 curve, JWK-encoded.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Jwk {
    /// Key type; always `EC` here.
    pub kty: String,
    /// Curve; always `P-256` here.
    pub crv: String,
    /// base64url big-endian x coordinate (32 bytes).
    pub x: String,
    /// base64url big-endian y coordinate (32 bytes).
    pub y: String,
    /// Optional key identifier (an RFC 7638 thumbprint by convention).
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub kid: Option<String>,
}

impl Jwk {
    /// Build a JWK from a verifying key.
    #[must_use]
    pub fn from_verifying_key(key: &VerifyingKey) -> Self {
        let point = key.to_encoded_point(false);
        let bytes = point.as_bytes();
        // An uncompressed SEC1 point is `0x04 || x(32) || y(32)`; a verifying
        // key is never the identity, so the layout is fixed.
        let (x, y) = bytes
            .get(1..33)
            .zip(bytes.get(33..65))
            .unwrap_or((&[], &[]));
        Self {
            kty: "EC".to_owned(),
            crv: "P-256".to_owned(),
            x: base64url::encode(x),
            y: base64url::encode(y),
            kid: None,
        }
    }

    /// Recover the verifying key this JWK describes.
    ///
    /// # Errors
    ///
    /// Returns [`Error::UnsupportedJwk`] for a non-`EC`/`P-256` key and
    /// [`Error::InvalidKeyMaterial`] when the coordinates are the wrong length
    /// or do not lie on the curve.
    pub fn to_verifying_key(&self) -> Result<VerifyingKey, Error> {
        if self.kty != "EC" || self.crv != "P-256" {
            return Err(Error::UnsupportedJwk {
                reason: format!("expected EC/P-256, got {}/{}", self.kty, self.crv),
            });
        }
        let x = base64url::decode(&self.x, "jwk x")?;
        let y = base64url::decode(&self.y, "jwk y")?;
        if x.len() != 32 || y.len() != 32 {
            return Err(Error::InvalidKeyMaterial {
                reason: "P-256 coordinates must be 32 bytes".to_owned(),
            });
        }
        let mut sec1 = Vec::with_capacity(65);
        sec1.push(0x04);
        sec1.extend_from_slice(&x);
        sec1.extend_from_slice(&y);
        VerifyingKey::from_sec1_bytes(&sec1).map_err(|_| Error::InvalidKeyMaterial {
            reason: "coordinates are not a point on P-256".to_owned(),
        })
    }

    /// The RFC 7638 SHA-256 thumbprint, base64url-encoded — a stable key
    /// identifier derived only from the public coordinates.
    #[must_use]
    pub fn thumbprint_sha256(&self) -> String {
        // RFC 7638: hash the required members in lexicographic order with no
        // whitespace. For an EC key that is exactly {crv, kty, x, y}.
        let canonical = format!(
            r#"{{"crv":"{}","kty":"{}","x":"{}","y":"{}"}}"#,
            self.crv, self.kty, self.x, self.y
        );
        let digest = Sha256::digest(canonical.as_bytes());
        base64url::encode(digest)
    }

    /// Return a copy whose `kid` is its own thumbprint.
    #[must_use]
    pub fn with_thumbprint_kid(mut self) -> Self {
        self.kid = Some(self.thumbprint_sha256());
        self
    }
}

#[cfg(test)]
#[allow(clippy::expect_used, clippy::panic)]
mod tests {
    use super::*;
    use p256::ecdsa::SigningKey;
    use rand_core::OsRng;

    #[test]
    fn round_trips_through_verifying_key() {
        let signing = SigningKey::random(&mut OsRng);
        let verifying = *signing.verifying_key();
        let jwk = Jwk::from_verifying_key(&verifying);
        assert_eq!(jwk.kty, "EC");
        assert_eq!(jwk.crv, "P-256");
        let recovered = jwk.to_verifying_key().expect("recover verifying key");
        assert_eq!(recovered, verifying);
    }

    #[test]
    fn thumbprint_is_stable_and_url_safe() {
        let signing = SigningKey::random(&mut OsRng);
        let jwk = Jwk::from_verifying_key(signing.verifying_key());
        let direct = jwk.thumbprint_sha256();
        let via_kid = jwk.with_thumbprint_kid().kid.expect("kid is set");
        assert_eq!(direct, via_kid);
        assert!(!direct.contains(['+', '/', '=']));
    }

    #[test]
    fn rejects_non_p256_key() {
        let bad = Jwk {
            kty: "RSA".to_owned(),
            crv: "P-256".to_owned(),
            x: String::new(),
            y: String::new(),
            kid: None,
        };
        assert!(bad.to_verifying_key().is_err());
    }
}
