///|
pub(all) enum PlanChangeKind {
  ChangeAdded
  ChangeRemoved
  ChangeIncreased
  ChangeDecreased
  ChangeModified
  ChangeUnchanged
} derive(Eq, Debug)

///|
pub(all) struct PlanChange {
  field : String
  kind : PlanChangeKind
  before : String
  after : String
  severity : Severity
  detail : String
} derive(Eq, Debug)

///|
pub fn PlanChange::PlanChange(
  field? : String = "",
  kind? : PlanChangeKind = ChangeUnchanged,
  before? : String = "",
  after? : String = "",
  severity? : Severity = Pass,
  detail? : String = "",
) -> PlanChange {
  { field, kind, before, after, severity, detail }
}

///|
pub(all) struct PlanDiff {
  verdict : String
  risk_delta : Int
  changes : Array[PlanChange]
} derive(Eq, Debug)

///|
pub fn compare_plans(base : RolloutPlan, candidate : RolloutPlan) -> PlanDiff {
  let changes = Array::new(capacity=32)
  push_string_change(changes, "name", base.name, candidate.name, severity=Warn)
  push_string_change(changes, "salt", base.salt, candidate.salt, severity=Fail)
  push_string_change(
    changes,
    "owner",
    base.owner,
    candidate.owner,
    severity=Fail,
  )
  push_string_change(
    changes,
    "repository",
    base.repository,
    candidate.repository,
    severity=Warn,
  )
  push_string_change(
    changes,
    "package",
    base.package_name,
    candidate.package_name,
    severity=Fail,
  )
  push_int_change(
    changes,
    "phase.count",
    base.phases.length(),
    candidate.phases.length(),
    higher_is_risk=false,
  )
  push_int_change(
    changes,
    "segment.count",
    base.segments.length(),
    candidate.segments.length(),
    higher_is_risk=false,
  )
  push_int_change(
    changes,
    "duration.total",
    plan_duration(base),
    plan_duration(candidate),
    higher_is_risk=false,
  )
  push_int_change(
    changes,
    "phase.max_percent_bp",
    max_phase_percent(base),
    max_phase_percent(candidate),
    higher_is_risk=true,
  )
  push_int_change(
    changes,
    "budget.max_error_per_million",
    base.budget.max_error_per_million,
    candidate.budget.max_error_per_million,
    higher_is_risk=true,
  )
  push_int_change(
    changes,
    "budget.max_latency_p95_ms",
    base.budget.max_latency_p95_ms,
    candidate.budget.max_latency_p95_ms,
    higher_is_risk=true,
  )
  push_int_change(
    changes,
    "budget.min_samples",
    base.budget.min_samples,
    candidate.budget.min_samples,
    higher_is_risk=false,
  )
  push_int_change(
    changes,
    "budget.max_saturation_bp",
    base.budget.max_saturation_bp,
    candidate.budget.max_saturation_bp,
    higher_is_risk=true,
  )
  push_int_change(
    changes,
    "budget.max_blast_radius_bp",
    base.budget.max_blast_radius_bp,
    candidate.budget.max_blast_radius_bp,
    higher_is_risk=true,
  )
  push_int_change(
    changes,
    "budget.max_dependency_failures",
    base.budget.max_dependency_failures,
    candidate.budget.max_dependency_failures,
    higher_is_risk=true,
  )
  push_bool_change(
    changes,
    "budget.require_synthetic_pass",
    base.budget.require_synthetic_pass,
    candidate.budget.require_synthetic_pass,
  )
  push_phase_name_changes(changes, base, candidate)
  push_segment_name_changes(changes, base, candidate)
  let delta = plan_diff_risk_delta(changes)
  let verdict = if has_failed_change(changes) {
    "breaking-change"
  } else if delta > 30 {
    "needs-release-review"
  } else if delta > 0 {
    "minor-risk"
  } else {
    "compatible"
  }
  { verdict, risk_delta: delta, changes }
}

///|
fn push_string_change(
  changes : Array[PlanChange],
  field : String,
  before : String,
  after : String,
  severity? : Severity = Warn,
) -> Unit {
  if before != after {
    changes.push(
      PlanChange(
        field~,
        kind=ChangeModified,
        before~,
        after~,
        severity~,
        detail="string value changed",
      ),
    )
  }
}

///|
fn push_int_change(
  changes : Array[PlanChange],
  field : String,
  before : Int,
  after : Int,
  higher_is_risk? : Bool = true,
) -> Unit {
  if before == after {
    return
  }
  let kind = if after > before { ChangeIncreased } else { ChangeDecreased }
  let risk = if higher_is_risk { after > before } else { after < before }
  changes.push(
    PlanChange(
      field~,
      kind~,
      before=before.to_string(),
      after=after.to_string(),
      severity=if risk { Warn } else { Pass },
      detail=if risk {
        "change increases release risk"
      } else {
        "change reduces release risk"
      },
    ),
  )
}

///|
fn push_bool_change(
  changes : Array[PlanChange],
  field : String,
  before : Bool,
  after : Bool,
) -> Unit {
  if before != after {
    changes.push(
      PlanChange(
        field~,
        kind=ChangeModified,
        before=before.to_string(),
        after=after.to_string(),
        severity=if before && !after { Fail } else { Pass },
        detail=if before && !after {
          "candidate disables a safety gate"
        } else {
          "candidate enables a safety gate"
        },
      ),
    )
  }
}

///|
fn push_phase_name_changes(
  changes : Array[PlanChange],
  base : RolloutPlan,
  candidate : RolloutPlan,
) -> Unit {
  for phase in base.phases {
    if !has_phase_named(candidate, phase.name) {
      changes.push(
        PlanChange(
          field="phase." + phase.name,
          kind=ChangeRemoved,
          before=mode_text(phase.mode),
          after="missing",
          severity=Warn,
          detail="phase removed from candidate",
        ),
      )
    }
  }
  for phase in candidate.phases {
    if !has_phase_named(base, phase.name) {
      changes.push(
        PlanChange(
          field="phase." + phase.name,
          kind=ChangeAdded,
          before="missing",
          after=mode_text(phase.mode),
          severity=if phase.mode == Live { Warn } else { Pass },
          detail="phase added in candidate",
        ),
      )
    }
  }
}

///|
fn push_segment_name_changes(
  changes : Array[PlanChange],
  base : RolloutPlan,
  candidate : RolloutPlan,
) -> Unit {
  for segment in base.segments {
    if !has_segment_named(candidate, segment.name) {
      changes.push(
        PlanChange(
          field="segment." + segment.name,
          kind=ChangeRemoved,
          before=segment_summary(segment),
          after="missing",
          severity=Warn,
          detail="segment removed from candidate",
        ),
      )
    }
  }
  for segment in candidate.segments {
    if !has_segment_named(base, segment.name) {
      changes.push(
        PlanChange(
          field="segment." + segment.name,
          kind=ChangeAdded,
          before="missing",
          after=segment_summary(segment),
          severity=Pass,
          detail="segment added in candidate",
        ),
      )
    }
  }
}

///|
fn has_phase_named(plan : RolloutPlan, name : String) -> Bool {
  for phase in plan.phases {
    if phase.name == name {
      return true
    }
  }
  false
}

///|
fn has_segment_named(plan : RolloutPlan, name : String) -> Bool {
  for segment in plan.segments {
    if segment.name == name {
      return true
    }
  }
  false
}

///|
pub fn max_phase_percent(plan : RolloutPlan) -> Int {
  let mut max_percent = 0
  for phase in plan.phases {
    if phase.percent_bp > max_percent {
      max_percent = phase.percent_bp
    }
  }
  max_percent
}

///|
pub fn plan_diff_risk_delta(changes : Array[PlanChange]) -> Int {
  let mut total = 0
  for change in changes {
    if change.severity == Fail {
      total += 20
    } else if change.severity == Warn {
      total += 8
    }
    if change.kind == ChangeIncreased && change.severity != Pass {
      total += 4
    }
    if change.kind == ChangeRemoved {
      total += 3
    }
  }
  total
}

///|
fn has_failed_change(changes : Array[PlanChange]) -> Bool {
  for change in changes {
    if change.severity == Fail {
      return true
    }
  }
  false
}

///|
pub fn change_kind_text(kind : PlanChangeKind) -> String {
  match kind {
    ChangeAdded => "added"
    ChangeRemoved => "removed"
    ChangeIncreased => "increased"
    ChangeDecreased => "decreased"
    ChangeModified => "modified"
    ChangeUnchanged => "unchanged"
  }
}

///|
pub fn PlanDiff::to_markdown(self : PlanDiff) -> String {
  let out = StringBuilder()
  out.write_string("# CanaryRing Plan Diff\n\n")
  out.write_string("- Verdict: " + self.verdict + "\n")
  out.write_string("- Risk delta: " + self.risk_delta.to_string() + "\n\n")
  for change in self.changes {
    out.write_string("- [" + severity_text(change.severity) + "] ")
    out.write_string(change.field + " " + change_kind_text(change.kind))
    out.write_string(" from `" + change.before + "` to `" + change.after + "`")
    out.write_string(" -- " + change.detail + "\n")
  }
  out.to_string()
}

///|
pub fn PlanDiff::to_json(self : PlanDiff) -> String {
  let out = StringBuilder()
  out.write_string("{")
  out.write_string("\"verdict\":\"" + json_escape(self.verdict) + "\",")
  out.write_string("\"risk_delta\":" + self.risk_delta.to_string() + ",")
  out.write_string("\"changes\":[")
  for i in 0.. 0 {
      out.write_string(",")
    }
    out.write_string("{")
    out.write_string("\"field\":\"" + json_escape(change.field) + "\",")
    out.write_string("\"kind\":\"" + change_kind_text(change.kind) + "\",")
    out.write_string("\"severity\":\"" + severity_text(change.severity) + "\",")
    out.write_string("\"before\":\"" + json_escape(change.before) + "\",")
    out.write_string("\"after\":\"" + json_escape(change.after) + "\"")
    out.write_string("}")
  }
  out.write_string("]}")
  out.to_string()
}