///|
/// Release policy for CI and package publication gates.
pub(all) struct ReleasePolicy {
  max_files : Int
  max_total_bytes : Int64
  required_paths : Array[String]
  allowed_extensions : Array[String]
  max_file_bytes : Int64
  reject_generated : Bool
}

///|
/// Construct a conservative release policy for source packages.
pub fn ReleasePolicy::source_package() -> ReleasePolicy {
  {
    max_files: 10000,
    max_total_bytes: 100_000_000L,
    required_paths: ["README.md", "LICENSE", "moon.mod"],
    allowed_extensions: [
      ".mbt", ".mbti", ".md", ".toml", ".json", ".yml", ".yaml", "LICENSE", "moon.mod",
    ],
    max_file_bytes: 10_000_000L,
    reject_generated: true,
  }
}

///|
/// Validate a manifest against a release policy and append policy findings.
pub fn Manifest::release_check(
  self : Manifest,
  policy : ReleasePolicy,
) -> AuditReport {
  let base = self.audit_with_policy(
    policy.allowed_extensions,
    policy.max_file_bytes,
  )
  let findings = base.findings
  let stats = self.statistics()
  if self.files.length() > policy.max_files {
    findings.push({
      code: "RELEASE_FILE_COUNT",
      severity: AuditSeverity::Error,
      path: "",
      message: "release contains more files than the configured limit",
      remediation: "remove accidental artifacts or update the reviewed policy",
    })
  }
  if stats.total_bytes > policy.max_total_bytes {
    findings.push({
      code: "RELEASE_TOTAL_SIZE",
      severity: AuditSeverity::Error,
      path: "",
      message: "release exceeds the configured total size limit",
      remediation: "exclude build caches and large unrelated assets",
    })
  }
  for required in policy.required_paths {
    if self.find(required) is None {
      findings.push({
        code: "REQUIRED_FILE_MISSING",
        severity: AuditSeverity::Error,
        path: required,
        message: "required release file is missing",
        remediation: "add the file before creating a release manifest",
      })
    }
  }
  if policy.reject_generated {
    for file in self.files {
      if is_release_generated(file.path) {
        findings.push({
          code: "GENERATED_FILE_REJECTED",
          severity: AuditSeverity::Error,
          path: file.path,
          message: "generated or build output is not permitted by this release policy",
          remediation: "publish source inputs or explicitly document the generated artifact",
        })
      }
    }
  }
  { findings, files_checked: base.files_checked, total_bytes: base.total_bytes }
}

///|
/// Return a compact policy result suitable for a CI exit decision.
pub fn Manifest::is_release_ready(
  self : Manifest,
  policy : ReleasePolicy,
) -> Bool {
  !self.release_check(policy).has_errors()
}

///|
/// Validate a manifest against the default source-package policy.
pub fn Manifest::is_source_release_ready(self : Manifest) -> Bool {
  self.is_release_ready(ReleasePolicy::source_package())
}

///|
/// Return paths that are missing from a required-path list.
pub fn Manifest::missing_paths(
  self : Manifest,
  required : Array[String],
) -> Array[String] {
  let missing : Array[String] = []
  for path in required {
    if self.find(path) is None {
      missing.push(path)
    }
  }
  missing
}

///|
/// Return true when a manifest contains no files rejected by a policy.
pub fn Manifest::contains_only_extensions(
  self : Manifest,
  allowed : Array[String],
) -> Bool {
  for file in self.files {
    if !release_extension_allowed(file.path, allowed) {
      return false
    }
  }
  true
}

///|
fn is_release_generated(path : String) -> Bool {
  release_ends_with(path, ".wasm") ||
  release_ends_with(path, ".map") ||
  release_ends_with(path, ".mbti") ||
  release_starts_with(path, "_build/") ||
  release_starts_with(path, "target/")
}

///|
fn release_extension_allowed(path : String, allowed : Array[String]) -> Bool {
  for extension in allowed {
    if release_ends_with(path, extension) {
      return true
    }
  }
  allowed.length() == 0
}

///|
fn release_starts_with(value : String, prefix : String) -> Bool {
  if value.length() < prefix.length() {
    return false
  }
  for i = 0; i < prefix.length(); i = i + 1 {
    if value[i] != prefix[i] {
      return false
    }
  }
  true
}

///|
fn release_ends_with(value : String, suffix : String) -> Bool {
  if value.length() < suffix.length() {
    return false
  }
  let offset = value.length() - suffix.length()
  for i = 0; i < suffix.length(); i = i + 1 {
    if value[offset + i] != suffix[i] {
      return false
    }
  }
  true
}