///|
/// Selects which fixes participate in a batch operation.
pub(all) enum FixMode {
  /// Include every fix, including suggestions that need review.
  AllFixes
  /// Include only fixes explicitly marked machine-applicable.
  AutomaticFixes
} derive(Eq, Debug)

///|
pub fn FixMode::name(self : FixMode) -> String {
  match self {
    AllFixes => "all"
    AutomaticFixes => "automatic"
  }
}

///|
/// The result of validating a selected batch of fixes.
pub(all) enum FixPlanValidation {
  PlanValid
  EmptyPlan
  NoSelectedFixes
  InvalidPlannedFix(Int, FixValidation)
  ConflictingPlannedFixes(FixValidation)
} derive(Eq, Debug)

///|
pub fn FixPlanValidation::is_valid(self : FixPlanValidation) -> Bool {
  self == PlanValid
}

///|
pub fn FixPlanValidation::message(self : FixPlanValidation) -> String {
  match self {
    PlanValid => "valid"
    EmptyPlan => "fix plan contains no fixes"
    NoSelectedFixes => "fix plan contains no fixes selected by this mode"
    InvalidPlannedFix(index, validation) =>
      "fix " + index.to_string() + " is invalid: " + validation.message()
    ConflictingPlannedFixes(validation) =>
      "selected fixes conflict: " + validation.message()
  }
}

///|
/// Counts describing a plan before it is applied.
pub struct FixPlanSummary {
  total_fixes : Int
  selected_fixes : Int
  skipped_fixes : Int
  selected_edits : Int
}

///|
pub fn FixPlanSummary::total_fixes(self : FixPlanSummary) -> Int {
  self.total_fixes
}

///|
pub fn FixPlanSummary::selected_fixes(self : FixPlanSummary) -> Int {
  self.selected_fixes
}

///|
pub fn FixPlanSummary::skipped_fixes(self : FixPlanSummary) -> Int {
  self.skipped_fixes
}

///|
pub fn FixPlanSummary::selected_edits(self : FixPlanSummary) -> Int {
  self.selected_edits
}

///|
/// An ordered collection of independent fixes.
///
/// A plan validates the complete edit set before changing any source. This
/// makes unattended formatter, linter, migration, and code-action workflows
/// atomic: either every selected edit is safe to apply, or nothing changes.
pub struct FixPlan {
  fixes : Array[Fix]
}

///|
pub fn FixPlan::new() -> FixPlan {
  { fixes: [] }
}

///|
pub fn FixPlan::with_fix(self : FixPlan, fix : Fix) -> FixPlan {
  { fixes: self.fixes + [fix] }
}

///|
pub fn FixPlan::length(self : FixPlan) -> Int {
  self.fixes.length()
}

///|
pub fn FixPlan::is_empty(self : FixPlan) -> Bool {
  self.fixes.is_empty()
}

///|
pub fn FixPlan::fixes(self : FixPlan) -> Array[Fix] {
  self.fixes.copy()
}

///|
fn FixPlan::selected(self : FixPlan, mode : FixMode) -> Array[Fix] {
  match mode {
    AllFixes => self.fixes.copy()
    AutomaticFixes => self.fixes.filter(fn(fix) { fix.is_automatic() })
  }
}

///|
pub fn FixPlan::summary(
  self : FixPlan,
  mode? : FixMode = AutomaticFixes,
) -> FixPlanSummary {
  let selected = self.selected(mode)
  let mut selected_edits = 0
  for fix in selected {
    selected_edits = selected_edits + fix.edits().length()
  }
  {
    total_fixes: self.fixes.length(),
    selected_fixes: selected.length(),
    skipped_fixes: self.fixes.length() - selected.length(),
    selected_edits,
  }
}

///|
fn FixPlan::combined(
  self : FixPlan,
  sources : SourceMap,
  mode : FixMode,
) -> (Fix?, FixPlanValidation) {
  if self.fixes.is_empty() {
    return (None, EmptyPlan)
  }
  let selected = self.selected(mode)
  if selected.is_empty() {
    return (None, NoSelectedFixes)
  }
  let mut combined = Fix::new(
    "apply " + mode.name() + " fixes",
    applicability=MachineApplicable,
  )
  for index, fix in selected {
    let validation = fix.validate(sources)
    if !validation.is_valid() {
      return (None, InvalidPlannedFix(index, validation))
    }
    for edit in fix.edits() {
      combined = combined.with_edit(edit)
    }
  }
  let validation = combined.validate(sources)
  if !validation.is_valid() {
    return (None, ConflictingPlannedFixes(validation))
  }
  (Some(combined), PlanValid)
}

///|
/// Validates every selected fix and then detects conflicts across fixes.
pub fn FixPlan::validate(
  self : FixPlan,
  sources : SourceMap,
  mode? : FixMode = AutomaticFixes,
) -> FixPlanValidation {
  self.combined(sources, mode).1
}

///|
/// Applies the selected fixes atomically to a copy of the source map.
///
/// The default is deliberately conservative: only machine-applicable fixes
/// are selected. Pass `AllFixes` when a user has reviewed every suggestion.
pub fn FixPlan::apply(
  self : FixPlan,
  sources : SourceMap,
  mode? : FixMode = AutomaticFixes,
) -> (SourceMap?, FixPlanValidation) {
  let (combined, validation) = self.combined(sources, mode)
  match combined {
    None => (None, validation)
    Some(fix) => {
      let (result, fix_validation) = fix.apply(sources)
      if fix_validation.is_valid() {
        (result, PlanValid)
      } else {
        (None, ConflictingPlannedFixes(fix_validation))
      }
    }
  }
}