///|
/// Validation options for JWT verification.
pub(all) struct ValidationOptions {
  /// Expected issuer; if set, the token's iss claim must match.
  mut expected_issuer : String?
  /// Expected audience; if set, the token's aud claim must contain it.
  mut expected_audience : String?
  /// Expected subject; if set, the token's sub claim must match.
  mut expected_subject : String?
  /// Current time in seconds since epoch (for exp/nbf checks).
  /// Current time in seconds since epoch.
  mut now_time : Int
  /// Leeway in seconds for time-based checks (clock skew tolerance).
  mut leeway : Int
}

///|
/// Default validation options: no issuer/audience check, leeway=0.
pub fn ValidationOptions::new(now : Int) -> ValidationOptions {
  {
    expected_issuer: None,
    expected_audience: None,
    expected_subject: None,
    now_time: now,
    leeway: 0,
  }
}

///|
/// Set expected issuer.
pub fn ValidationOptions::with_issuer(
  self : ValidationOptions,
  iss : String,
) -> ValidationOptions {
  self.expected_issuer = Some(iss)
  self
}

///|
/// Set expected audience.
pub fn ValidationOptions::with_audience(
  self : ValidationOptions,
  aud : String,
) -> ValidationOptions {
  self.expected_audience = Some(aud)
  self
}

///|
/// Set clock skew leeway in seconds.
pub fn ValidationOptions::with_leeway(
  self : ValidationOptions,
  seconds : Int,
) -> ValidationOptions {
  self.leeway = seconds
  self
}

///|
/// Extract a string claim from a JSON claims object.
fn get_string_claim(claims : Json, key : String) -> String? {
  match claims {
    Object(o) =>
      match o.get(key) {
        Some(String(s)) => Some(s)
        _ => None
      }
    _ => None
  }
}

///|
/// Extract a numeric claim (as Int) from a JSON claims object.
fn get_numeric_claim(claims : Json, key : String) -> Int? {
  match claims {
    Object(o) =>
      match o.get(key) {
        Some(Number(n, ..)) => Some(n.to_int())
        _ => None
      }
    _ => None
  }
}

///|
/// Validate a token's signature AND claims according to [ValidationOptions].
///
/// This is the high-level entry point: it verifies the HMAC signature,
/// then checks exp, nbf, iss, aud, sub claims as configured.
pub fn validate(
  token : String,
  secret : Bytes,
  options : ValidationOptions,
) -> Result[Json, JwtError] {
  // Step 1: verify signature
  match verify(token, secret) {
    Err(e) => return Err(e)
    Ok(_) => ()
  }
  // Step 2: decode claims
  let claims = match decode_claims(token) {
    Ok(c) => c
    Err(e) => return Err(e)
  }
  // Step 3: check expiration (exp)
  match get_numeric_claim(claims, claim_exp) {
    Some(exp) => {
      if options.now_time > exp + options.leeway {
        return Err(TokenExpired)
      }
      ()
    }
    None => ()
  }
  // Step 4: check not-before (nbf)
  match get_numeric_claim(claims, claim_nbf) {
    Some(nbf) => {
      if options.now_time + options.leeway < nbf {
        return Err(InvalidClaims("token not yet valid (nbf)"))
      }
      ()
    }
    None => ()
  }
  // Step 5: check issuer (iss)
  match options.expected_issuer {
    Some(expected) =>
      match get_string_claim(claims, claim_iss) {
        Some(actual) =>
          if actual != expected {
            return Err(
              InvalidClaims(
                "issuer mismatch: expected " + expected + ", got " + actual,
              ),
            )
          }
        None => return Err(InvalidClaims("expected issuer but token has none"))
      }
    None => ()
  }
  // Step 6: check audience (aud)
  match options.expected_audience {
    Some(expected) =>
      match get_string_claim(claims, claim_aud) {
        Some(actual) =>
          if actual != expected {
            return Err(InvalidClaims("audience mismatch"))
          }
        None =>
          return Err(InvalidClaims("expected audience but token has none"))
      }
    None => ()
  }
  // Step 7: check subject (sub)
  match options.expected_subject {
    Some(expected) =>
      match get_string_claim(claims, claim_sub) {
        Some(actual) =>
          if actual != expected {
            return Err(InvalidClaims("subject mismatch"))
          }
        None => return Err(InvalidClaims("expected subject but token has none"))
      }
    None => ()
  }
  Ok(claims)
}

///|
/// Convenience: validate with only signature + expiration check.
pub fn validate_with_expiry(
  token : String,
  secret : Bytes,
  now : Int,
) -> Result[Json, JwtError] {
  let opts = ValidationOptions::new(now)
  validate(token, secret, opts)
}

///|
/// Check if a token is expired without verifying signature.
pub fn is_expired(token : String, now : Int, leeway : Int) -> Bool {
  match decode_claims(token) {
    Err(_) => true
    Ok(claims) =>
      match get_numeric_claim(claims, claim_exp) {
        Some(exp) => now > exp + leeway
        None => false
      }
  }
}