//! Six-rule validator (spec §8).
//!
//! The receiver MUST run all six rules in order. The first failing rule
//! aborts validation with a specific [`IddropError`]. Rule 6 (Commitment
//! Discipline) is enforced *at the API surface*: callers MUST NOT
//! materialise a claim that has not passed `validate(...)`, and
//! materialisation itself is outside the protocol-engine.

use crate::claim::IdClaim;
use crate::errors::{IddropError, Result};
use crate::nonce::ChallengeNonce;
use serde::{Deserialize, Serialize};

/// The six rules from `draft-vandemeent-iddrop-00` §8.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ValidatorRule {
    /// §8.1 — sealed carrier authentication checked (= TBZ v2 verified).
    CarrierTruth,
    /// §8.2 — semantic class explicitly an identity object.
    SemanticClass,
    /// §8.3 — receiver_pubkey matches our local identity.
    ReceiverBinding,
    /// §8.4 — claim's sender_pubkey confirmed against AINS / JIS.
    ClaimProvenance,
    /// §8.5 — request_id + challenge_nonce match outstanding request
    /// (request-first) or acceptance/transfer share an offer session
    /// (offer-first).
    CausalBinding,
    /// §8.6 — never auto-materialise; enforced at API surface.
    CommitmentDiscipline,
}

/// Result of running the six rules against a claim.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ValidationOutcome {
    /// Which rules were checked and passed.
    pub passed: Vec<ValidatorRule>,
    /// The current mode (= "offer-first" or "request-first") for §8.5.
    pub mode: String,
    /// The provenance reference checked in §8.4 (e.g. `"ains:jasper.aint"`).
    pub provenance_ref: String,
}

/// Local receiver context against which a claim is validated.
pub struct ReceiverContext<'a> {
    /// Local Ed25519 pubkey (hex). Used in §8.3 receiver binding.
    pub local_pubkey: &'a str,
    /// Outstanding requests this receiver issued — `(request_id, nonce)`.
    pub outstanding_requests: &'a [(String, ChallengeNonce)],
    /// Outstanding offer sessions this receiver accepted — `offer_id`s.
    pub outstanding_offers: &'a [String],
    /// AINS resolver — implements [`crate::AinsResolver`]. In v0.1 this
    /// was a closure; v0.2 uses a trait object so the same context can
    /// hold a live [`crate::HttpAinsResolver`] or a [`crate::StubAinsResolver`].
    pub ains_resolver: &'a dyn crate::ains::AinsResolver,
}

/// Run all six rules in spec order. Returns the outcome on success,
/// or the specific [`IddropError`] for the first failing rule.
///
/// `carrier_verified` reflects Rule 1 from outside this function (= the
/// caller must have already unpacked the TBZ v2 envelope and confirmed
/// AEAD + signature). The validator trusts this assertion.
pub fn validate(
    claim: &IdClaim,
    carrier_verified: bool,
    ctx: &ReceiverContext<'_>,
    mode: &str,
) -> Result<ValidationOutcome> {
    let mut passed = Vec::new();

    // Rule 1: Carrier Truth (§8.1) — asserted by caller
    if !carrier_verified {
        return Err(IddropError::Envelope(
            "Rule 1 (carrier truth): caller asserts envelope NOT verified".to_string(),
        ));
    }
    passed.push(ValidatorRule::CarrierTruth);

    // Rule 2: Semantic Class (§8.2)
    use crate::claim::SemanticType;
    match claim.semantic_type {
        SemanticType::Identity | SemanticType::Capability
        | SemanticType::Credential | SemanticType::SubIdentity => {}
        // Any other variant would fail at deserialise; reaching here is fine.
    }
    passed.push(ValidatorRule::SemanticClass);

    // Rule 3: Receiver Binding (§8.3)
    if claim.receiver_pubkey.to_lowercase() != ctx.local_pubkey.to_lowercase() {
        let prefix = claim.receiver_pubkey.chars().take(16).collect::<String>();
        return Err(IddropError::ReceiverMismatch {
            expected_prefix: prefix,
        });
    }
    passed.push(ValidatorRule::ReceiverBinding);

    // Rule 4: Claim Provenance (§8.4) — AINS / JIS lookup
    let provenance_ref = match ctx.ains_resolver.resolve(&claim.claimed_aint)? {
        Some(expected_pk) => {
            if expected_pk.to_lowercase() != claim.sender_pubkey.to_lowercase() {
                return Err(IddropError::SenderAinsMismatch {
                    claimed_aint: claim.claimed_aint.clone(),
                });
            }
            claim.provenance_ref.clone()
        }
        None => {
            return Err(IddropError::AinsResolutionFailed {
                claimed_aint: claim.claimed_aint.clone(),
                reason: "no record found".to_string(),
            });
        }
    };
    passed.push(ValidatorRule::ClaimProvenance);

    // Rule 5: Causal Binding (§8.5)
    match mode {
        "request-first" => {
            let nonce = claim.challenge_nonce.ok_or_else(|| {
                IddropError::NonceMismatch
            })?;
            let matched = ctx.outstanding_requests.iter().any(|(rid, n)| {
                rid == &claim.request_id && n == &nonce
            });
            if !matched {
                return Err(IddropError::RequestIdUnknown(claim.request_id.clone()));
            }
        }
        "offer-first" => {
            if !ctx.outstanding_offers.iter().any(|oid| oid == &claim.request_id) {
                return Err(IddropError::RequestIdUnknown(claim.request_id.clone()));
            }
        }
        other => {
            return Err(IddropError::Internal(format!(
                "unknown mode '{}', expected 'offer-first' or 'request-first'",
                other
            )));
        }
    }
    passed.push(ValidatorRule::CausalBinding);

    // §8.6 (Commitment Discipline) — passing validation is the
    // *precondition*, not the action. Caller still must explicitly
    // accept before materialising.
    let now = crate::claim::current_unix_seconds();
    if now > claim.expires_at || now < claim.issued_at {
        return Err(IddropError::TimeWindow {
            now,
            issued_at: claim.issued_at,
            expires_at: claim.expires_at,
        });
    }
    passed.push(ValidatorRule::CommitmentDiscipline);

    Ok(ValidationOutcome {
        passed,
        mode: mode.to_string(),
        provenance_ref,
    })
}
