///|
/// Purpose-bound access decisions for privacy-sensitive de-identification
/// services. The core engine never grants access by itself; callers provide a
/// principal, purpose, and resource scope which are evaluated together.
pub(all) enum AccessPermission {
  AccessReadRaw
  AccessReadMasked
  AccessWriteMasked
  AccessRunPipeline
  AccessReviewFinding
  AccessExportAudit
  AccessManagePolicy
  AccessManageKeys
} derive(Debug, Eq)

///|
pub(all) enum AccessDecision {
  AccessAllowed
  AccessDenied
  AccessRequiresApproval
  AccessExpired
  AccessScopeMismatch
} derive(Debug, Eq)

///|
pub(all) enum AccessPurpose {
  AccessTreatment
  AccessOperations
  AccessResearch
  AccessQuality
  AccessSupport
  AccessSecurity
} derive(Debug, Eq)

///|
pub(all) struct AccessPrincipal {
  principal_id : String
  display_name : String
  roles : Array[String]
  department : String
  active : Bool
  mfa_verified : Bool
} derive(Debug, Eq)

///|
pub(all) struct AccessGrant {
  grant_id : String
  principal_id : String
  permission : AccessPermission
  purpose : AccessPurpose
  resource_prefix : String
  expires_at : String
  requires_mfa : Bool
  requires_approval : Bool
  approved_by : String
  approved_at : String
  reason : String
} derive(Debug, Eq)

///|
pub(all) struct AccessRequest {
  request_id : String
  principal : AccessPrincipal
  permission : AccessPermission
  purpose : AccessPurpose
  resource_id : String
  requested_at : String
  reason : String
  emergency : Bool
} derive(Debug)

///|
pub(all) struct AccessEvaluation {
  request_id : String
  decision : AccessDecision
  matched_grant : String
  reasons : Array[String]
  evaluated_at : String
  audit_checksum : String
} derive(Debug)

///|
pub fn access_permission_name(permission : AccessPermission) -> String {
  match permission {
    AccessReadRaw => "read_raw"
    AccessReadMasked => "read_masked"
    AccessWriteMasked => "write_masked"
    AccessRunPipeline => "run_pipeline"
    AccessReviewFinding => "review_finding"
    AccessExportAudit => "export_audit"
    AccessManagePolicy => "manage_policy"
    AccessManageKeys => "manage_keys"
  }
}

///|
pub fn access_decision_name(decision : AccessDecision) -> String {
  match decision {
    AccessAllowed => "allowed"
    AccessDenied => "denied"
    AccessRequiresApproval => "requires_approval"
    AccessExpired => "expired"
    AccessScopeMismatch => "scope_mismatch"
  }
}

///|
pub fn access_purpose_name(purpose : AccessPurpose) -> String {
  match purpose {
    AccessTreatment => "treatment"
    AccessOperations => "operations"
    AccessResearch => "research"
    AccessQuality => "quality"
    AccessSupport => "support"
    AccessSecurity => "security"
  }
}

///|
pub fn access_principal(
  principal_id : String,
  display_name : String,
  roles : Array[String],
  department : String,
) -> AccessPrincipal {
  {
    principal_id,
    display_name,
    roles,
    department,
    active: true,
    mfa_verified: false,
  }
}

///|
pub fn AccessPrincipal::deactivate(self : AccessPrincipal) -> AccessPrincipal {
  { ..self, active: false }
}

///|
pub fn AccessPrincipal::verify_mfa(self : AccessPrincipal) -> AccessPrincipal {
  { ..self, mfa_verified: true }
}

///|
pub fn AccessPrincipal::has_role(self : AccessPrincipal, role : String) -> Bool {
  self.roles.contains(role)
}

///|
pub fn AccessPrincipal::is_valid(self : AccessPrincipal) -> Bool {
  self.principal_id.length() > 0 &&
  self.display_name.length() > 0 &&
  self.department.length() > 0 &&
  self.active &&
  !self.roles.is_empty()
}

///|
pub fn access_grant(
  principal_id : String,
  permission : AccessPermission,
  purpose : AccessPurpose,
  resource_prefix : String,
  expires_at : String,
  reason : String,
) -> AccessGrant {
  {
    grant_id: stable_hash(
      principal_id +
      ":" +
      access_permission_name(permission) +
      ":" +
      resource_prefix,
    ),
    principal_id,
    permission,
    purpose,
    resource_prefix,
    expires_at,
    requires_mfa: permission == AccessReadRaw || permission == AccessManageKeys,
    requires_approval: permission == AccessExportAudit ||
    permission == AccessManageKeys,
    approved_by: "",
    approved_at: "",
    reason,
  }
}

///|
pub fn AccessGrant::approve(
  grant : AccessGrant,
  approver : String,
  approved_at : String,
) -> AccessGrant {
  { ..grant, approved_by: approver, approved_at }
}

///|
pub fn AccessGrant::is_approved(self : AccessGrant) -> Bool {
  !self.requires_approval ||
  (self.approved_by.length() > 0 && self.approved_at.length() > 0)
}

///|
pub fn AccessGrant::is_expired(self : AccessGrant, now : String) -> Bool {
  self.expires_at.length() > 0 && self.expires_at < now
}

///|
pub fn AccessGrant::covers(self : AccessGrant, request : AccessRequest) -> Bool {
  self.principal_id == request.principal.principal_id &&
  self.permission == request.permission &&
  self.purpose == request.purpose &&
  (
    self.resource_prefix.is_empty() ||
    request.resource_id.has_prefix(self.resource_prefix)
  )
}

///|
pub fn access_request(
  request_id : String,
  principal : AccessPrincipal,
  permission : AccessPermission,
  purpose : AccessPurpose,
  resource_id : String,
  requested_at : String,
  reason : String,
) -> AccessRequest {
  {
    request_id,
    principal,
    permission,
    purpose,
    resource_id,
    requested_at,
    reason,
    emergency: false,
  }
}

///|
pub fn AccessRequest::as_emergency(self : AccessRequest) -> AccessRequest {
  { ..self, emergency: true }
}

///|
pub fn AccessRequest::is_valid(self : AccessRequest) -> Bool {
  self.request_id.length() > 0 &&
  self.principal.is_valid() &&
  self.resource_id.length() > 0 &&
  self.requested_at.length() > 0 &&
  self.reason.length() > 0
}

///|
pub fn evaluate_access(
  request : AccessRequest,
  grants : Array[AccessGrant],
  now : String,
) -> AccessEvaluation {
  let mut matched = ""
  let mut decision = AccessDenied
  let reasons : Array[String] = []
  if !request.is_valid() {
    reasons.push("invalid request")
  } else if !request.principal.active {
    reasons.push("principal is inactive")
  } else {
    let mut scope_found = false
    for grant in grants {
      if grant.principal_id == request.principal.principal_id &&
        grant.permission == request.permission &&
        grant.purpose == request.purpose {
        if grant.covers(request) {
          scope_found = true
          matched = grant.grant_id
          if grant.is_expired(now) {
            decision = AccessExpired
            reasons.push("grant expired")
          } else if grant.requires_mfa && !request.principal.mfa_verified {
            decision = AccessDenied
            reasons.push("multi-factor verification required")
          } else if !grant.is_approved() {
            decision = AccessRequiresApproval
            reasons.push("independent approval required")
          } else {
            decision = AccessAllowed
            reasons.push("matching active grant")
          }
        }
      }
    }
    if !scope_found && decision == AccessDenied {
      let same_permission = grants.any(fn(grant) {
        grant.principal_id == request.principal.principal_id &&
        grant.permission == request.permission &&
        grant.purpose == request.purpose
      })
      if same_permission {
        decision = AccessScopeMismatch
        reasons.push("resource is outside the granted scope")
      } else {
        reasons.push("no matching grant")
      }
    }
    if request.emergency &&
      decision == AccessDenied &&
      request.principal.has_role("incident-response") {
      decision = AccessRequiresApproval
      reasons.push("emergency access requires post-event approval")
    }
  }
  {
    request_id: request.request_id,
    decision,
    matched_grant: matched,
    reasons,
    evaluated_at: now,
    audit_checksum: stable_hash(
      request.request_id +
      ":" +
      access_decision_name(decision) +
      ":" +
      reasons.join("|"),
    ),
  }
}

///|
pub fn AccessEvaluation::is_allowed(self : AccessEvaluation) -> Bool {
  self.decision == AccessAllowed
}

///|
pub fn AccessEvaluation::summary(self : AccessEvaluation) -> String {
  [
    "request_id=" + self.request_id,
    "decision=" + access_decision_name(self.decision),
    "matched_grant=" + self.matched_grant,
    "reasons=" + self.reasons.join("; "),
    "evaluated_at=" + self.evaluated_at,
    "audit_checksum=" + self.audit_checksum,
  ].join("\n")
}

///|
pub fn AccessEvaluation::to_json(self : AccessEvaluation) -> String {
  "{" +
  "\"request_id\":\"" +
  json_escape(self.request_id) +
  "\"," +
  "\"decision\":\"" +
  access_decision_name(self.decision) +
  "\"," +
  "\"matched_grant\":\"" +
  json_escape(self.matched_grant) +
  "\"," +
  "\"reasons\":[" +
  self.reasons.map(json_escape).join(",") +
  "]," +
  "\"audit_checksum\":\"" +
  json_escape(self.audit_checksum) +
  "\"}"
}

///|
pub fn access_requires_raw_protection(permission : AccessPermission) -> Bool {
  permission == AccessReadRaw || permission == AccessManageKeys
}

///|
pub fn access_purpose_allows_raw(purpose : AccessPurpose) -> Bool {
  purpose == AccessTreatment || purpose == AccessSecurity
}

///|
pub fn access_policy_is_safe(
  permission : AccessPermission,
  purpose : AccessPurpose,
) -> Bool {
  !access_requires_raw_protection(permission) ||
  access_purpose_allows_raw(purpose)
}

///|
pub fn filter_accessible_grants(
  grants : Array[AccessGrant],
  principal_id : String,
  purpose : AccessPurpose,
) -> Array[AccessGrant] {
  grants.filter(fn(grant) {
    grant.principal_id == principal_id && grant.purpose == purpose
  })
}

///|
pub fn access_grant_checksum(grants : Array[AccessGrant]) -> String {
  stable_hash(
    grants
    .map(fn(grant) {
      grant.grant_id +
      ":" +
      grant.principal_id +
      ":" +
      access_permission_name(grant.permission) +
      ":" +
      access_purpose_name(grant.purpose)
    })
    .join("\n"),
  )
}

///|
pub fn access_evaluation_is_auditable(evaluation : AccessEvaluation) -> Bool {
  evaluation.request_id.length() > 0 &&
  evaluation.evaluated_at.length() > 0 &&
  evaluation.audit_checksum.length() > 0 &&
  !evaluation.reasons.is_empty()
}