///|
/// Safe rollout phase assigned to a generated migration step.
pub(all) enum MigrationPhase {
  ExpandReceivers
  MigrateProducers
  MigrateStoredData
  EnforceTarget
  ObserveRollout
} derive(Eq, Debug)

///|
/// One actionable, verifiable migration task.
pub(all) struct MigrationStep {
  id : String
  phase : MigrationPhase
  path : String
  action : String
  verification : String
  related_codes : Array[String]
} derive(Eq, Debug)

///|
/// Ordered rollout plan synthesized from compatibility findings.
pub(all) struct MigrationPlan {
  contract_name : String
  from_version : String
  to_version : String
  risk : String
  steps : Array[MigrationStep]
} derive(Eq, Debug)

///|
pub fn MigrationPhase::render(self : MigrationPhase) -> String {
  match self {
    ExpandReceivers => "1-expand-receivers"
    MigrateProducers => "2-migrate-producers"
    MigrateStoredData => "3-migrate-stored-data"
    EnforceTarget => "4-enforce-target"
    ObserveRollout => "5-observe-rollout"
  }
}

///|
/// Build an ordered plan from breaking findings. Compatible reports still get
/// a final observation step so releases have an explicit verification point.
pub fn plan_migration(report : AnalysisReport) -> MigrationPlan {
  let steps : Array[MigrationStep] = []
  for change in report.changes {
    if change.severity != Breaking {
      continue
    }
    append_change_plan(change, steps)
  }
  steps.sort_by((left, right) => {
    let phase_order = phase_rank(left.phase) - phase_rank(right.phase)
    if phase_order != 0 {
      phase_order
    } else {
      let path_order = left.path.compare(right.path)
      if path_order != 0 {
        path_order
      } else {
        left.id.compare(right.id)
      }
    }
  })
  append_unique_step(steps, {
    id: "observe-release",
    phase: ObserveRollout,
    path: "$",
    action: "Observe compatibility error rates through one complete rollout window.",
    verification: "No target-side rejection is attributable to the contract change.",
    related_codes: [],
  })
  let risk = migration_risk(report)
  {
    contract_name: report.contract_name,
    from_version: report.old_version,
    to_version: report.new_version,
    risk,
    steps,
  }
}

///|
fn append_change_plan(change : Change, steps : Array[MigrationStep]) -> Unit {
  match change.code {
    "REQUIRED_FIELD_ADDED" | "FIELD_BECAME_REQUIRED" => {
      append_step(
        steps,
        change,
        "accept-missing-" + slug(change.path),
        ExpandReceivers,
        "Deploy a receiver version that accepts the field as optional.",
        "The receiver accepts the omission witness.",
      )
      append_step(
        steps,
        change,
        "populate-" + slug(change.path),
        MigrateProducers,
        "Update every producer to populate the field on all new payloads.",
        "Producer telemetry shows the field on 100% of emitted payloads.",
      )
      append_step(
        steps,
        change,
        "require-" + slug(change.path),
        EnforceTarget,
        "Make the field required only after producer convergence.",
        "EvoWitness no longer reports the required-field break.",
      )
    }
    "FIELD_REMOVED" => {
      append_step(
        steps,
        change,
        "deprecate-" + slug(change.path),
        ExpandReceivers,
        "Keep accepting the field while marking it deprecated.",
        "Old and new receivers both accept payloads containing the field.",
      )
      append_step(
        steps,
        change,
        "stop-emitting-" + slug(change.path),
        MigrateProducers,
        "Stop emitting the deprecated field from every producer.",
        "No observed payload contains the deprecated field.",
      )
      append_step(
        steps,
        change,
        "remove-" + slug(change.path),
        EnforceTarget,
        "Remove the field after the compatibility window expires.",
        "Forward analysis has no FIELD_REMOVED result.",
      )
    }
    "ENUM_VALUE_REMOVED" | "FIELD_TYPE_NARROWED" => {
      append_step(
        steps,
        change,
        "retain-wide-type-" + slug(change.path),
        ExpandReceivers,
        "Retain the source value set in receivers during migration.",
        "The receiver accepts the generated type witness.",
      )
      append_step(
        steps,
        change,
        "rewrite-type-values-" + slug(change.path),
        MigrateProducers,
        "Move producers to values accepted by both source and target types.",
        "No producer emits a value outside the target set.",
      )
      append_step(
        steps,
        change,
        "backfill-type-values-" + slug(change.path),
        MigrateStoredData,
        "Rewrite or quarantine stored values excluded by the target type.",
        "A validation scan reports no excluded stored value.",
      )
      append_step(
        steps,
        change,
        "narrow-type-" + slug(change.path),
        EnforceTarget,
        "Apply the narrower target type after producers and stored data converge.",
        "The generated witness is absent from production and migration fixtures.",
      )
    }
    "MIN_INCREASED"
    | "MAX_DECREASED"
    | "MIN_LENGTH_INCREASED"
    | "MAX_LENGTH_DECREASED" => {
      append_step(
        steps,
        change,
        "retain-bound-" + slug(change.path),
        ExpandReceivers,
        "Keep the source bound active while rollout is mixed-version.",
        "The receiver accepts the boundary witness.",
      )
      append_step(
        steps,
        change,
        "constrain-producers-" + slug(change.path),
        MigrateProducers,
        "Update producers to emit values inside the target bound.",
        "Boundary metrics show no newly emitted out-of-range values.",
      )
      append_step(
        steps,
        change,
        "backfill-bound-" + slug(change.path),
        MigrateStoredData,
        "Repair or quarantine stored values outside the target bound.",
        "A validation scan passes against the target constraint.",
      )
      append_step(
        steps,
        change,
        "enforce-bound-" + slug(change.path),
        EnforceTarget,
        "Enforce the target bound after data convergence.",
        "Compatibility analysis and target validation pass for rollout fixtures.",
      )
    }
    "OBJECT_CLOSED" => {
      append_step(
        steps,
        change,
        "inventory-extra-fields-" + slug(change.path),
        ExpandReceivers,
        "Inventory additional fields before closing the object.",
        "Observed extra-field names have documented owners and disposition.",
      )
      append_step(
        steps,
        change,
        "remove-extra-fields-" + slug(change.path),
        MigrateProducers,
        "Stop emitting undeclared fields or add them to the target contract.",
        "No payload triggers UNKNOWN_FIELD in shadow validation.",
      )
      append_step(
        steps,
        change,
        "close-object-" + slug(change.path),
        EnforceTarget,
        "Change the object to closed after the extra-field window is clean.",
        "The '__extra' witness is the only intentionally rejected probe.",
      )
    }
    "TYPE_REMOVED" => {
      append_step(
        steps,
        change,
        "retain-type-" + slug(change.path),
        ExpandReceivers,
        "Retain the type declaration while any producer or stored record uses it.",
        "Both contract versions accept the type's minimal payload.",
      )
      append_step(
        steps,
        change,
        "migrate-type-users-" + slug(change.path),
        MigrateProducers,
        "Move all producers and consumers to the replacement type.",
        "Usage telemetry for the removed type reaches zero.",
      )
      append_step(
        steps,
        change,
        "remove-type-" + slug(change.path),
        EnforceTarget,
        "Remove the type after the retention window.",
        "No TYPE_REMOVED finding remains in the supported version matrix.",
      )
    }
    "CONTRACT_RENAMED" => {
      append_step(
        steps,
        change,
        "dual-publish-contract",
        ExpandReceivers,
        "Publish the new name while retaining an alias for the old contract name.",
        "Both names resolve to equivalent accepted-value sets.",
      )
      append_step(
        steps,
        change,
        "migrate-contract-name",
        MigrateProducers,
        "Move producers and consumers to the new contract name.",
        "Old-name usage reaches zero across the support window.",
      )
      append_step(
        steps,
        change,
        "retire-contract-alias",
        EnforceTarget,
        "Retire the old alias after downstream convergence.",
        "Supported clients no longer request the old name.",
      )
    }
    _ =>
      append_step(
        steps,
        change,
        "resolve-" + slug(change.path) + "-" + change.code.to_lower(),
        MigrateProducers,
        change.hint,
        "The related breaking finding is absent from a fresh analysis.",
      )
  }
}

///|
fn append_step(
  steps : Array[MigrationStep],
  change : Change,
  id : String,
  phase : MigrationPhase,
  action : String,
  verification : String,
) -> Unit {
  append_unique_step(steps, {
    id,
    phase,
    path: change.path,
    action,
    verification,
    related_codes: [change.code],
  })
}

///|
fn append_unique_step(
  steps : Array[MigrationStep],
  candidate : MigrationStep,
) -> Unit {
  match steps.search_by(step => step.id == candidate.id) {
    Some(index) => {
      let existing = steps[index]
      let codes = existing.related_codes.copy()
      for code in candidate.related_codes {
        if !codes.contains(code) {
          codes.push(code)
        }
      }
      steps[index] = {
        id: existing.id,
        phase: existing.phase,
        path: existing.path,
        action: existing.action,
        verification: existing.verification,
        related_codes: codes,
      }
    }
    None => steps.push(candidate)
  }
}

///|
fn phase_rank(phase : MigrationPhase) -> Int {
  match phase {
    ExpandReceivers => 1
    MigrateProducers => 2
    MigrateStoredData => 3
    EnforceTarget => 4
    ObserveRollout => 5
  }
}

///|
fn migration_risk(report : AnalysisReport) -> String {
  let count = report.breaking_count()
  if count == 0 {
    "low"
  } else if count <= 2 {
    "medium"
  } else if count <= 5 {
    "high"
  } else {
    "critical"
  }
}

///|
fn slug(value : String) -> String {
  let out = StringBuilder()
  let mut previous_dash = false
  for char in value.to_lower() {
    if char.is_ascii_alphabetic() || char.is_ascii_digit() {
      out.write_char(char)
      previous_dash = false
    } else if !previous_dash && !out.is_empty() {
      out.write_char('-')
      previous_dash = true
    }
  }
  let result = out.to_string()
  if result.has_suffix("-") {
    result[:result.length() - 1].to_owned()
  } else {
    result
  }
}

///|
pub fn MigrationPlan::to_markdown(self : MigrationPlan) -> String {
  let out = StringBuilder()
  out.write_string("# Migration plan for " + self.contract_name + "\n\n")
  out.write_string(
    "- Versions: `" + self.from_version + "` → `" + self.to_version + "`\n",
  )
  out.write_string("- Risk: **" + self.risk.to_upper() + "**\n")
  out.write_string("- Steps: " + self.steps.length().to_string() + "\n\n")
  let mut current_phase = ""
  for step in self.steps {
    let phase = step.phase.render()
    if phase != current_phase {
      out.write_string("## " + phase + "\n\n")
      current_phase = phase
    }
    out.write_string("- [ ] **" + step.id + "** (`" + step.path + "`): ")
    out.write_string(step.action + "\n")
    out.write_string("  - Verify: " + step.verification + "\n")
    if !step.related_codes.is_empty() {
      out.write_string(
        "  - Findings: `" + step.related_codes.join("`, `") + "`\n",
      )
    }
  }
  out.to_string()
}