// Policy evaluation — scope matching + condition evaluation.
// Reference: cedar-policy-core/src/evaluator.rs

///|
/// Evaluate a single condition (body of When or Unless).
/// Returns Value(Bool(true)) if condition passes (When is true, Unless is false).
/// Returns Value(Bool(false)) if condition fails (When is false, Unless is true).
/// Returns Residual(expr) if the condition body could not be fully reduced.
fn[S : EntityStore] eval_single_condition(
  cond : @ast.Condition,
  req : Request,
  store : S,
) -> @ast.PartialValue raise EvalError {
  match eval_expr(cond.body, req, store) {
    @ast.PartialValue::Value(@ast.Value::Bool(b)) =>
      match cond.kind {
        @ast.ConditionKind::When =>
          @ast.PartialValue::Value(@ast.Value::Bool(b))
        @ast.ConditionKind::Unless =>
          @ast.PartialValue::Value(@ast.Value::Bool(!b))
      }
    @ast.PartialValue::Value(_) =>
      @ast.PartialValue::Value(@ast.Value::Bool(false))
    @ast.PartialValue::Residual(re) =>
      match cond.kind {
        @ast.ConditionKind::When => @ast.PartialValue::Residual(re)
        @ast.ConditionKind::Unless =>
          @ast.PartialValue::Residual(
            @ast.Expr::UnaryApp(@ast.UnaryOp::Not, re),
          )
      }
  }
}

///|
/// Convert a ScopeConstraint + Unknown Expr into an Expr condition.
/// For example: Eq(User::"alice") + Unknown("principal") → principal == User::"alice"
fn scope_to_expr(sc : @ast.ScopeConstraint, e : @ast.Expr) -> @ast.Expr {
  match sc {
    @ast.ScopeConstraint::All => @ast.Expr::Lit(@ast.Literal::Bool(true))
    @ast.ScopeConstraint::Eq(uid) =>
      @ast.Expr::BinaryApp(
        @ast.BinaryOp::Eq,
        e,
        @ast.Expr::Lit(@ast.Literal::EntityUID(uid)),
      )
    @ast.ScopeConstraint::In(parent) =>
      @ast.Expr::BinaryApp(
        @ast.BinaryOp::In_,
        e,
        @ast.Expr::Lit(@ast.Literal::EntityUID(parent)),
      )
    @ast.ScopeConstraint::InSet(uids) =>
      @ast.Expr::BinaryApp(
        @ast.BinaryOp::In_,
        e,
        @ast.Expr::Set(
          uids.map(fn(uid) { @ast.Expr::Lit(@ast.Literal::EntityUID(uid)) }),
        ),
      )
    @ast.ScopeConstraint::Is(ty) => @ast.Expr::Is(e, ty)
    @ast.ScopeConstraint::IsIn(ty, parent) =>
      @ast.Expr::And(
        @ast.Expr::Is(e, ty),
        @ast.Expr::BinaryApp(
          @ast.BinaryOp::In_,
          e,
          @ast.Expr::Lit(@ast.Literal::EntityUID(parent)),
        ),
      )
  }
}

///|
/// Build an AND chain from a list of residual expressions.
fn merge_residuals(residuals : Array[@ast.Expr]) -> @ast.Expr {
  if residuals.length() == 0 {
    @ast.Expr::Lit(@ast.Literal::Bool(true))
  } else if residuals.length() == 1 {
    residuals[0]
  } else {
    let mut acc = residuals[0]
    for i = 1; i < residuals.length(); i = i + 1 {
      acc = @ast.Expr::And(acc, residuals[i])
    }
    acc
  }
}

///|
/// Evaluate a single policy against a Request and EntityStore.
/// Returns:
///   Value(Bool(true)) — policy satisfied (scope matched, all conditions passed)
///   Value(Bool(false)) — policy not satisfied (scope didn't match, or condition failed)
///   Residual(Expr) — partial eval: scope matched but some conditions are residual
///   raise EvalError — evaluation error
pub fn[S : EntityStore] eval_policy(
  p : @ast.Policy,
  req : Request,
  store : S,
) -> @ast.PartialValue raise EvalError {
  // Check scope constraints.
  // Unknown PARC slots generate residual conditions instead of concrete checks.
  let residuals : Array[@ast.Expr] = []
  match req.principal {
    EntityUIDEntry::Concrete(uid) =>
      if !scope_match(p.principal, uid, store) {
        return @ast.PartialValue::Value(@ast.Value::Bool(false))
      }
    EntityUIDEntry::Unknown(ty) =>
      residuals.push(
        scope_to_expr(
          p.principal,
          @ast.Expr::Unknown("principal", Some(@ast.Type::Entity(ty))),
        ),
      )
  }
  match req.action {
    EntityUIDEntry::Concrete(uid) =>
      if !scope_match(p.action, uid, store) {
        return @ast.PartialValue::Value(@ast.Value::Bool(false))
      }
    EntityUIDEntry::Unknown(ty) =>
      residuals.push(
        scope_to_expr(
          p.action,
          @ast.Expr::Unknown("action", Some(@ast.Type::Entity(ty))),
        ),
      )
  }
  match req.resource {
    EntityUIDEntry::Concrete(uid) =>
      if !scope_match(p.resource, uid, store) {
        return @ast.PartialValue::Value(@ast.Value::Bool(false))
      }
    EntityUIDEntry::Unknown(ty) =>
      residuals.push(
        scope_to_expr(
          p.resource,
          @ast.Expr::Unknown("resource", Some(@ast.Type::Entity(ty))),
        ),
      )
  }

  // Evaluate conditions in order, accumulating into shared residuals array
  for i = 0; i < p.conditions.length(); i = i + 1 {
    let cond = p.conditions[i]
    match eval_single_condition(cond, req, store) {
      @ast.PartialValue::Value(@ast.Value::Bool(true)) => continue
      @ast.PartialValue::Value(@ast.Value::Bool(false)) =>
        return @ast.PartialValue::Value(@ast.Value::Bool(false))
      @ast.PartialValue::Value(_) =>
        return @ast.PartialValue::Value(@ast.Value::Bool(false))
      @ast.PartialValue::Residual(re) => {
        residuals.push(re)
        continue
      }
    }
  }

  if residuals.length() == 0 {
    @ast.PartialValue::Value(@ast.Value::Bool(true))
  } else {
    @ast.PartialValue::Residual(merge_residuals(residuals))
  }
}