//! Issuing an SD-JWT VC: sign the always-visible claims, commit to the
//! selectively-disclosable ones by digest, and bind the holder's key via `cnf`.
//!
//! The issuer ships every disclosure alongside the signed token; selecting
//! which to reveal is the holder's job at presentation time. After this, the
//! issuer is out of the verification loop (I4).

use crate::disclosure::Disclosure;
use crate::error::Error;
use crate::jwk::Jwk;
use crate::jws::{self, Header};
use crate::key::SigningKey;
use chrono::{DateTime, Utc};
use serde_json::{Map, Value};

/// Media type of an SD-JWT VC, per the SD-JWT VC draft.
pub const SD_JWT_VC_TYP: &str = "dc+sd-jwt";

/// Everything the issuer commits to when minting a credential.
#[derive(Debug, Clone)]
pub struct IssuerParams<'a> {
    /// `iss`: the issuer's federation entity identifier (an `https` URL).
    pub issuer: &'a str,
    /// `sub`: the holder's pairwise pseudonymous subject identifier.
    pub subject: &'a str,
    /// `vct`: the verifiable-credential type URI.
    pub vct: &'a str,
    /// `kid` placed in the JWS header so a verifier can select the issuer key.
    pub key_id: Option<String>,
    /// The holder public key bound into `cnf`.
    pub holder_key: &'a Jwk,
    /// `iat`: when the credential is issued.
    pub issued_at: DateTime<Utc>,
    /// `nbf`: when the credential becomes valid.
    pub valid_from: DateTime<Utc>,
    /// `exp`: when the credential expires, if it does.
    pub valid_until: Option<DateTime<Utc>>,
    /// Claims signed in the clear (e.g. authority class) — never blinded.
    pub visible_claims: Map<String, Value>,
    /// Claims committed by digest and revealed only by disclosure.
    pub selective_claims: Map<String, Value>,
    /// An optional status claim for post-issuance revocation lookups.
    pub status: Option<Value>,
}

/// A freshly issued SD-JWT VC: the signed JWS followed by every disclosure, in
/// the `JWS~D1~…~DN~` serialization with an empty key-binding slot.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IssuedSdJwt(String);

impl IssuedSdJwt {
    /// Borrow the serialized credential.
    #[must_use]
    pub fn as_str(&self) -> &str {
        &self.0
    }

    /// Wrap an already-serialized SD-JWT (e.g. one loaded from a wallet store).
    #[must_use]
    pub fn from_string(value: String) -> Self {
        Self(value)
    }
}

/// Issue and sign an SD-JWT VC.
///
/// # Errors
///
/// Returns an [`Error`] if a disclosure salt cannot be gathered, a claim cannot
/// be encoded, or signing fails.
pub fn issue(params: &IssuerParams<'_>, key: &SigningKey) -> Result<IssuedSdJwt, Error> {
    let mut disclosures = Vec::with_capacity(params.selective_claims.len());
    for (name, value) in &params.selective_claims {
        disclosures.push(Disclosure::new(name, value.clone())?);
    }
    let mut digests: Vec<Value> = disclosures
        .iter()
        .map(|d| Value::String(d.digest()))
        .collect();
    // Sort so digest order never echoes claim insertion order.
    digests.sort_by(|a, b| a.as_str().cmp(&b.as_str()));

    let payload = build_payload(params, digests)?;
    let header = Header::new(SD_JWT_VC_TYP, params.key_id.clone());
    let jws = jws::encode(&header, &payload, key)?;

    let mut serialized = String::with_capacity(jws.len() + 64);
    serialized.push_str(&jws);
    serialized.push('~');
    for disclosure in &disclosures {
        serialized.push_str(disclosure.encoded());
        serialized.push('~');
    }
    Ok(IssuedSdJwt(serialized))
}

fn build_payload(params: &IssuerParams<'_>, digests: Vec<Value>) -> Result<Value, Error> {
    let mut payload = params.visible_claims.clone();
    payload.insert("iss".to_owned(), Value::String(params.issuer.to_owned()));
    payload.insert("sub".to_owned(), Value::String(params.subject.to_owned()));
    payload.insert("vct".to_owned(), Value::String(params.vct.to_owned()));
    payload.insert("iat".to_owned(), Value::from(params.issued_at.timestamp()));
    payload.insert("nbf".to_owned(), Value::from(params.valid_from.timestamp()));
    if let Some(exp) = params.valid_until {
        payload.insert("exp".to_owned(), Value::from(exp.timestamp()));
    }
    let cnf = serde_json::to_value(params.holder_key).map_err(Error::json("cnf jwk"))?;
    payload.insert("cnf".to_owned(), serde_json::json!({ "jwk": cnf }));
    payload.insert("_sd_alg".to_owned(), Value::String("sha-256".to_owned()));
    payload.insert("_sd".to_owned(), Value::Array(digests));
    if let Some(status) = &params.status {
        payload.insert("status".to_owned(), status.clone());
    }
    Ok(Value::Object(payload))
}
