///|
pub(all) enum ReleaseDecision {
  ReleasePass
  ReleaseWarn
  ReleaseBlock
} derive(Eq)

///|
pub(all) enum ReleaseImpact {
  ImpactNone
  ImpactPatch
  ImpactMinor
  ImpactMajor
} derive(Eq)

///|
pub(all) struct ReleaseGate {
  min_score : Int
  allow_warnings : Bool
  max_warnings : Int
  max_entries : Int
  max_total_bytes : Int
  max_asset_bytes : Int
  require_tags : Bool
  forbid_runtime : Bool
  forbid_no_store : Bool
  require_readme_asset : Bool
  require_license_asset : Bool
} derive(Eq)

///|
pub(all) struct ReleaseGateResult {
  decision : ReleaseDecision
  quality : QualityReport
  blockers : Array[String]
  warnings : Array[String]
  notes : Array[String]
} derive(Eq)

///|
pub(all) struct ReleaseDelta {
  added : Int
  removed : Int
  changed : Int
  byte_delta : Int
  policy_changes : Int
  kind_changes : Int
  fingerprint_changes : Int
  tag_changes : Int
  impact : ReleaseImpact
} derive(Eq)

///|
pub fn ReleaseDecision::to_wire(self : ReleaseDecision) -> String {
  match self {
    ReleasePass => "pass"
    ReleaseWarn => "warn"
    ReleaseBlock => "block"
  }
}

///|
pub fn ReleaseImpact::to_wire(self : ReleaseImpact) -> String {
  match self {
    ImpactNone => "none"
    ImpactPatch => "patch"
    ImpactMinor => "minor"
    ImpactMajor => "major"
  }
}

///|
pub fn default_release_gate() -> ReleaseGate {
  {
    min_score: 70,
    allow_warnings: true,
    max_warnings: 12,
    max_entries: 5000,
    max_total_bytes: 52428800,
    max_asset_bytes: 5242880,
    require_tags: false,
    forbid_runtime: false,
    forbid_no_store: false,
    require_readme_asset: false,
    require_license_asset: false,
  }
}

///|
pub fn strict_release_gate() -> ReleaseGate {
  {
    min_score: 85,
    allow_warnings: false,
    max_warnings: 0,
    max_entries: 2000,
    max_total_bytes: 20971520,
    max_asset_bytes: 2097152,
    require_tags: true,
    forbid_runtime: true,
    forbid_no_store: true,
    require_readme_asset: true,
    require_license_asset: true,
  }
}

///|
pub fn evaluate_release_gate(
  manifest : Manifest,
  gate : ReleaseGate,
) -> ReleaseGateResult {
  let quality = assess_quality(manifest)
  let blockers : Array[String] = []
  let warnings : Array[String] = []
  let notes : Array[String] = []
  if quality.errors > 0 {
    blockers.push("manifest has validation errors")
  }
  if quality.score < gate.min_score {
    blockers.push("quality score is below release gate")
  }
  if quality.entries == 0 {
    blockers.push("manifest has no assets")
  }
  if quality.entries > gate.max_entries {
    blockers.push("manifest has too many assets for this gate")
  }
  if quality.total_bytes > gate.max_total_bytes {
    blockers.push("manifest exceeds total byte budget")
  }
  if quality.oversized_assets > 0 {
    warnings.push(
      "some assets are larger than the recommended single asset budget",
    )
  }
  if has_asset_over(manifest, gate.max_asset_bytes) {
    blockers.push("at least one asset exceeds the gate single asset budget")
  }
  if gate.require_tags && quality.missing_tag_assets > 0 {
    blockers.push("gate requires every asset to have at least one tag")
  }
  if gate.forbid_runtime && quality.runtime_assets > 0 {
    blockers.push("gate forbids runtime assets")
  }
  if gate.forbid_no_store && quality.no_store_assets > 0 {
    blockers.push("gate forbids no-store assets")
  }
  if !gate.allow_warnings && quality.warnings > 0 {
    blockers.push("gate does not allow warnings")
  } else if quality.warnings > gate.max_warnings {
    warnings.push("warning count is above gate preference")
  }
  if gate.require_readme_asset && !manifest_has_readme_asset(manifest) {
    blockers.push("gate requires a README-like asset")
  }
  if gate.require_license_asset && !manifest_has_license_asset(manifest) {
    blockers.push("gate requires a license-like asset")
  }
  if quality.policy_advice.length() > 0 {
    warnings.push("cache policy advice is available")
  }
  if quality.generated_assets > 0 {
    notes.push(
      "manifest includes generated assets; verify producer determinism",
    )
  }
  if quality.duplicate_fingerprint_assets > 0 {
    notes.push(
      "some fingerprints are reused; this can be intentional for duplicated content",
    )
  }
  if quality.directory_count <= 1 && quality.entries > 4 {
    notes.push(
      "most assets live in one directory; directory structure may be too flat",
    )
  }
  {
    decision: release_decision(blockers, warnings),
    quality,
    blockers,
    warnings,
    notes,
  }
}

///|
pub fn evaluate_default_release_gate(manifest : Manifest) -> ReleaseGateResult {
  evaluate_release_gate(manifest, default_release_gate())
}

///|
pub fn evaluate_strict_release_gate(manifest : Manifest) -> ReleaseGateResult {
  evaluate_release_gate(manifest, strict_release_gate())
}

///|
pub fn release_gate_passed(result : ReleaseGateResult) -> Bool {
  result.decision == ReleasePass
}

///|
pub fn release_gate_blocked(result : ReleaseGateResult) -> Bool {
  result.decision == ReleaseBlock
}

///|
pub fn release_gate_warned(result : ReleaseGateResult) -> Bool {
  result.decision == ReleaseWarn
}

///|
pub fn render_release_gate(result : ReleaseGateResult) -> String {
  let out = StringBuilder()
  out.write_string("release ")
  out.write_string(result.decision.to_wire())
  out.write_string("\nscore ")
  out.write_string(result.quality.score.to_string())
  out.write_string("\nrisk ")
  out.write_string(result.quality.risk.to_wire())
  out.write_string("\nentries ")
  out.write_string(result.quality.entries.to_string())
  out.write_string("\nbytes ")
  out.write_string(result.quality.total_bytes.to_string())
  out.write_string("\n")
  render_release_section(out, "blockers", result.blockers)
  render_release_section(out, "warnings", result.warnings)
  render_release_section(out, "notes", result.notes)
  out.to_string()
}

///|
pub fn summarize_release_delta(
  before : Manifest,
  after : Manifest,
) -> ReleaseDelta {
  let diffs = diff_manifest(before, after)
  release_delta_from_diffs(diffs)
}

///|
pub fn release_delta_from_diffs(diffs : Array[EntryDiff]) -> ReleaseDelta {
  let mut added = 0
  let mut removed = 0
  let mut changed = 0
  let mut byte_delta = 0
  let mut policy_changes = 0
  let mut kind_changes = 0
  let mut fingerprint_changes = 0
  let mut tag_changes = 0
  for diff in diffs {
    match diff.kind {
      Added => {
        added = added + 1
        byte_delta = byte_delta + asset_bytes_or_zero(diff.after)
      }
      Removed => {
        removed = removed + 1
        byte_delta = byte_delta - asset_bytes_or_zero(diff.before)
      }
      Changed => {
        changed = changed + 1
        byte_delta = byte_delta +
          asset_bytes_or_zero(diff.after) -
          asset_bytes_or_zero(diff.before)
        if diff.reasons.contains("policy") {
          policy_changes = policy_changes + 1
        }
        if diff.reasons.contains("kind") {
          kind_changes = kind_changes + 1
        }
        if diff.reasons.contains("fingerprint") {
          fingerprint_changes = fingerprint_changes + 1
        }
        if diff.reasons.contains("tags") {
          tag_changes = tag_changes + 1
        }
      }
    }
  }
  let impact = release_impact(
    added, removed, changed, policy_changes, kind_changes,
  )
  {
    added,
    removed,
    changed,
    byte_delta,
    policy_changes,
    kind_changes,
    fingerprint_changes,
    tag_changes,
    impact,
  }
}

///|
pub fn release_impact(
  added : Int,
  removed : Int,
  changed : Int,
  policy_changes : Int,
  kind_changes : Int,
) -> ReleaseImpact {
  if added == 0 && removed == 0 && changed == 0 {
    return ImpactNone
  }
  if removed > 0 || kind_changes > 0 {
    return ImpactMajor
  }
  if added > 0 || policy_changes > 0 {
    return ImpactMinor
  }
  ImpactPatch
}

///|
pub fn render_release_delta(delta : ReleaseDelta) -> String {
  let out = StringBuilder()
  out.write_string("impact ")
  out.write_string(delta.impact.to_wire())
  out.write_string("\nadded ")
  out.write_string(delta.added.to_string())
  out.write_string("\nremoved ")
  out.write_string(delta.removed.to_string())
  out.write_string("\nchanged ")
  out.write_string(delta.changed.to_string())
  out.write_string("\nbyte-delta ")
  out.write_string(delta.byte_delta.to_string())
  out.write_string("\npolicy-changes ")
  out.write_string(delta.policy_changes.to_string())
  out.write_string("\nkind-changes ")
  out.write_string(delta.kind_changes.to_string())
  out.write_string("\nfingerprint-changes ")
  out.write_string(delta.fingerprint_changes.to_string())
  out.write_string("\ntag-changes ")
  out.write_string(delta.tag_changes.to_string())
  out.write_string("\n")
  out.to_string()
}

///|
pub fn manifest_has_readme_asset(manifest : Manifest) -> Bool {
  for entry in manifest.entries {
    if path_is_readme_like(entry.path) is Ok(true) {
      return true
    }
  }
  false
}

///|
pub fn manifest_has_license_asset(manifest : Manifest) -> Bool {
  for entry in manifest.entries {
    if path_is_license_like(entry.path) is Ok(true) {
      return true
    }
  }
  false
}

///|
pub fn manifest_release_notes(before : Manifest, after : Manifest) -> String {
  let delta = summarize_release_delta(before, after)
  let out = StringBuilder()
  out.write_string("Release impact: ")
  out.write_string(delta.impact.to_wire())
  out.write_string("\n")
  out.write_string(render_release_delta(delta))
  let diffs = diff_manifest(before, after)
  if !diffs.is_empty() {
    out.write_string("\nChanged assets:\n")
    out.write_string(render_diff(diffs))
  }
  out.to_string()
}

///|
pub fn gate_with_score(gate : ReleaseGate, min_score : Int) -> ReleaseGate {
  {
    min_score,
    allow_warnings: gate.allow_warnings,
    max_warnings: gate.max_warnings,
    max_entries: gate.max_entries,
    max_total_bytes: gate.max_total_bytes,
    max_asset_bytes: gate.max_asset_bytes,
    require_tags: gate.require_tags,
    forbid_runtime: gate.forbid_runtime,
    forbid_no_store: gate.forbid_no_store,
    require_readme_asset: gate.require_readme_asset,
    require_license_asset: gate.require_license_asset,
  }
}

///|
pub fn gate_with_size_budget(
  gate : ReleaseGate,
  max_total_bytes : Int,
  max_asset_bytes : Int,
) -> ReleaseGate {
  {
    min_score: gate.min_score,
    allow_warnings: gate.allow_warnings,
    max_warnings: gate.max_warnings,
    max_entries: gate.max_entries,
    max_total_bytes,
    max_asset_bytes,
    require_tags: gate.require_tags,
    forbid_runtime: gate.forbid_runtime,
    forbid_no_store: gate.forbid_no_store,
    require_readme_asset: gate.require_readme_asset,
    require_license_asset: gate.require_license_asset,
  }
}

///|
pub fn gate_requiring_metadata(gate : ReleaseGate) -> ReleaseGate {
  {
    min_score: gate.min_score,
    allow_warnings: gate.allow_warnings,
    max_warnings: gate.max_warnings,
    max_entries: gate.max_entries,
    max_total_bytes: gate.max_total_bytes,
    max_asset_bytes: gate.max_asset_bytes,
    require_tags: true,
    forbid_runtime: gate.forbid_runtime,
    forbid_no_store: gate.forbid_no_store,
    require_readme_asset: true,
    require_license_asset: true,
  }
}

///|
fn release_decision(
  blockers : Array[String],
  warnings : Array[String],
) -> ReleaseDecision {
  if !blockers.is_empty() {
    ReleaseBlock
  } else if !warnings.is_empty() {
    ReleaseWarn
  } else {
    ReleasePass
  }
}

///|
fn has_asset_over(manifest : Manifest, max_asset_bytes : Int) -> Bool {
  for entry in manifest.entries {
    if entry.bytes > max_asset_bytes {
      return true
    }
  }
  false
}

///|
fn asset_bytes_or_zero(asset : Asset?) -> Int {
  match asset {
    Some(entry) => entry.bytes
    None => 0
  }
}

///|
fn render_release_section(
  out : StringBuilder,
  title : String,
  rows : Array[String],
) -> Unit {
  out.write_string("\n[")
  out.write_string(title)
  out.write_string("]\n")
  if rows.is_empty() {
    out.write_string("(none)\n")
  } else {
    for row in rows {
      out.write_string("- ")
      out.write_string(row)
      out.write_string("\n")
    }
  }
}