///|
/// Approval metadata attached to a release review.
pub(all) struct ApprovalRecord {
  reviewer : String
  ticket : String
  reviewed_root : String
  approved_at : String
  notes : String
}

///|
/// Validate that an approval refers to the exact manifest root under review.
pub fn ApprovalRecord::matches(
  self : ApprovalRecord,
  manifest : Manifest,
) -> Bool {
  self.reviewer.length() > 0 &&
  self.ticket.length() > 0 &&
  self.approved_at.length() > 0 &&
  self.reviewed_root == manifest.merkle_root
}

///|
/// Return a stable serialized approval line for audit logs.
pub fn ApprovalRecord::to_text(self : ApprovalRecord) -> String {
  "reviewer=" +
  self.reviewer +
  " ticket=" +
  self.ticket +
  " reviewed_root=" +
  self.reviewed_root +
  " approved_at=" +
  self.approved_at +
  " notes=" +
  self.notes
}

///|
/// A release gate that requires both technical and human approval.
pub(all) struct ApprovalGate {
  policy : ReleasePolicy
  require_clean_report : Bool
}

///|
/// Construct a strict gate for a public package release.
pub fn ApprovalGate::strict() -> ApprovalGate {
  { policy: ReleasePolicy::source_package(), require_clean_report: true }
}

///|
/// Check policy and approval evidence together.
pub fn ApprovalGate::evaluate(
  self : ApprovalGate,
  manifest : Manifest,
  approval : ApprovalRecord?,
) -> Bool {
  let report = manifest.release_check(self.policy)
  if report.has_errors() {
    return false
  }
  if self.require_clean_report && !report.is_clean() {
    return false
  }
  match approval {
    Some(record) => record.matches(manifest)
    None => false
  }
}

///|
/// Return the exact root that an external signer should approve.
pub fn Manifest::approval_payload(self : Manifest) -> String {
  "manifest=" +
  self.name +
  " version=" +
  self.version +
  " root=" +
  self.merkle_root +
  " files=" +
  self.files.length().to_string()
}

///|
/// Return a finding when approval metadata is missing or stale.
pub fn Manifest::approval_finding(
  self : Manifest,
  approval : ApprovalRecord?,
) -> AuditFinding? {
  match approval {
    Some(record) =>
      if record.matches(self) {
        None
      } else {
        Some({
          code: "APPROVAL_ROOT_MISMATCH",
          severity: AuditSeverity::Error,
          path: "",
          message: "approval does not bind to the current manifest root",
          remediation: "re-run the review and approve the exact published root",
        })
      }
    None =>
      Some({
        code: "APPROVAL_MISSING",
        severity: AuditSeverity::Error,
        path: "",
        message: "release has no reviewer approval record",
        remediation: "attach a review ticket and the exact Merkle root",
      })
  }
}