//! Verifying a presentation: check the issuer signature, that every revealed
//! claim was committed by digest, and that the presenter controls the bound key
//! over this exact request.
//!
//! Verification needs only the issuer's public key (resolved from a trust
//! anchor by a higher layer) and never contacts the issuer (I4). It returns the
//! disclosed and visible claims separately from any decision, leaving policy to
//! a layer that cannot be swayed by presentation transport (I12).

use crate::base64url;
use crate::disclosure::Disclosure;
use crate::error::Error;
use crate::issue::SD_JWT_VC_TYP;
use crate::jwk::Jwk;
use crate::jws;
use crate::present::KEY_BINDING_TYP;
use chrono::{DateTime, Utc};
use p256::ecdsa::VerifyingKey;
use serde_json::{Map, Value};
use sha2::{Digest as _, Sha256};
use std::collections::HashSet;

const STANDARD_CLAIMS: [&str; 10] = [
    "iss", "sub", "vct", "iat", "nbf", "exp", "cnf", "status", "_sd", "_sd_alg",
];

/// What a verifier supplies to check a presentation.
#[derive(Debug, Clone)]
pub struct VerifyParams<'a> {
    /// The issuer's public key, resolved through a trust chain to an anchor.
    pub issuer_key: &'a VerifyingKey,
    /// The verifier's own identifier; the key-binding `aud` must match it.
    pub audience: &'a str,
    /// The nonce the verifier issued for this request.
    pub nonce: &'a str,
    /// The instant validity is judged against.
    pub now: DateTime<Utc>,
}

/// The cryptographically verified content of a presentation, with disclosed and
/// always-visible claims kept apart from any eligibility decision.
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct VerifiedCredential {
    /// `iss` from the credential.
    pub issuer: String,
    /// `sub` from the credential (a pairwise pseudonym).
    pub subject: String,
    /// `vct` credential type.
    pub vct: String,
    /// `nbf`, when present.
    pub valid_from: Option<DateTime<Utc>>,
    /// `exp`, when present.
    pub valid_until: Option<DateTime<Utc>>,
    /// The holder key the presenter proved control of.
    pub holder_key: Jwk,
    /// Claims the holder chose to reveal, by name.
    pub disclosed: Map<String, Value>,
    /// Issuer claims signed in the clear (never blinded), by name.
    pub visible: Map<String, Value>,
    /// The status claim, if the credential carried one.
    pub status: Option<Value>,
}

/// Verify a key-bound presentation.
///
/// # Errors
///
/// Returns an [`Error`] if any signature fails, a disclosure is uncommitted,
/// the credential is outside its validity window, or the key-binding does not
/// match the requested audience, nonce, or disclosure set.
pub fn verify(
    presentation: &crate::present::Presentation,
    params: &VerifyParams<'_>,
) -> Result<VerifiedCredential, Error> {
    let raw = presentation.as_str();
    let separator = raw.rfind('~').ok_or_else(|| Error::MalformedSdJwt {
        reason: "presentation has no key-binding separator".to_owned(),
    })?;
    let covered = &raw[..=separator];
    let key_binding = &raw[separator + 1..];
    if key_binding.is_empty() {
        return Err(Error::MalformedSdJwt {
            reason: "presentation is missing its key-binding JWT".to_owned(),
        });
    }

    let mut segments = covered.split('~').filter(|s| !s.is_empty());
    let jws_token = segments.next().ok_or_else(|| Error::MalformedSdJwt {
        reason: "presentation has no JWS segment".to_owned(),
    })?;

    let (header, payload) = jws::decode_verify(jws_token, params.issuer_key, "issuer credential")?;
    if header.typ != SD_JWT_VC_TYP {
        return Err(Error::MalformedSdJwt {
            reason: format!("unexpected credential type header {:?}", header.typ),
        });
    }
    let claims = payload.as_object().ok_or_else(|| Error::MalformedSdJwt {
        reason: "credential payload is not a JSON object".to_owned(),
    })?;

    let valid_from = timestamp_claim(claims, "nbf");
    let valid_until = timestamp_claim(claims, "exp");
    enforce_window(params.now, valid_from, valid_until)?;

    let holder_key = holder_key(claims)?;
    let disclosed = collect_disclosures(claims, segments)?;
    check_key_binding(key_binding, &holder_key, covered, params)?;

    Ok(VerifiedCredential {
        issuer: required_str(claims, "iss")?,
        subject: required_str(claims, "sub")?,
        vct: required_str(claims, "vct")?,
        valid_from,
        valid_until,
        holder_key,
        disclosed,
        visible: visible_claims(claims),
        status: claims.get("status").cloned(),
    })
}

fn enforce_window(
    now: DateTime<Utc>,
    valid_from: Option<DateTime<Utc>>,
    valid_until: Option<DateTime<Utc>>,
) -> Result<(), Error> {
    if valid_from.is_some_and(|from| now < from) {
        return Err(Error::NotValidNow {
            reason: "not yet valid",
        });
    }
    if valid_until.is_some_and(|until| now >= until) {
        return Err(Error::NotValidNow { reason: "expired" });
    }
    Ok(())
}

fn collect_disclosures<'a>(
    claims: &Map<String, Value>,
    disclosures: impl Iterator<Item = &'a str>,
) -> Result<Map<String, Value>, Error> {
    let mut committed: HashSet<String> = match claims.get("_sd") {
        Some(Value::Array(items)) => items
            .iter()
            .filter_map(|v| v.as_str().map(str::to_owned))
            .collect(),
        _ => HashSet::new(),
    };
    let mut disclosed = Map::new();
    for encoded in disclosures {
        let disclosure = Disclosure::parse(encoded)?;
        if !committed.remove(&disclosure.digest()) {
            return Err(Error::UncommittedDisclosure {
                name: disclosure.name().to_owned(),
            });
        }
        disclosed.insert(disclosure.name().to_owned(), disclosure.value().clone());
    }
    Ok(disclosed)
}

fn check_key_binding(
    key_binding: &str,
    holder_key: &Jwk,
    covered: &str,
    params: &VerifyParams<'_>,
) -> Result<(), Error> {
    let holder_vk = holder_key.to_verifying_key()?;
    let (header, payload) = jws::decode_verify(key_binding, &holder_vk, "key-binding")?;
    if header.typ != KEY_BINDING_TYP {
        return Err(Error::KeyBinding { reason: "type" });
    }
    let claims = payload
        .as_object()
        .ok_or(Error::KeyBinding { reason: "payload" })?;
    if claims.get("aud").and_then(Value::as_str) != Some(params.audience) {
        return Err(Error::KeyBinding { reason: "audience" });
    }
    if claims.get("nonce").and_then(Value::as_str) != Some(params.nonce) {
        return Err(Error::KeyBinding { reason: "nonce" });
    }
    let expected = base64url::encode(Sha256::digest(covered.as_bytes()));
    if claims.get("sd_hash").and_then(Value::as_str) != Some(expected.as_str()) {
        return Err(Error::KeyBinding { reason: "sd_hash" });
    }
    Ok(())
}

fn holder_key(claims: &Map<String, Value>) -> Result<Jwk, Error> {
    let jwk = claims
        .get("cnf")
        .and_then(|cnf| cnf.get("jwk"))
        .ok_or_else(|| Error::MalformedSdJwt {
            reason: "credential has no cnf.jwk holder key".to_owned(),
        })?;
    serde_json::from_value(jwk.clone()).map_err(Error::json("cnf jwk"))
}

fn visible_claims(claims: &Map<String, Value>) -> Map<String, Value> {
    claims
        .iter()
        .filter(|(name, _)| !STANDARD_CLAIMS.contains(&name.as_str()))
        .map(|(name, value)| (name.clone(), value.clone()))
        .collect()
}

fn required_str(claims: &Map<String, Value>, key: &'static str) -> Result<String, Error> {
    claims
        .get(key)
        .and_then(Value::as_str)
        .map(str::to_owned)
        .ok_or_else(|| Error::MalformedSdJwt {
            reason: format!("credential is missing required claim {key:?}"),
        })
}

fn timestamp_claim(claims: &Map<String, Value>, key: &str) -> Option<DateTime<Utc>> {
    claims
        .get(key)
        .and_then(Value::as_i64)
        .and_then(|secs| DateTime::<Utc>::from_timestamp(secs, 0))
}
