///|
pub(all) enum RetryIssueClass {
  NoIssue
  FormatIssue
  SemanticIssue
  SafetyNoRetry
  ProviderNoRetry
} derive(Debug, Eq, ToJson, FromJson)

///|
pub fn RetryIssueClass::label(self : RetryIssueClass) -> String {
  match self {
    NoIssue => "no_issue"
    FormatIssue => "format_issue"
    SemanticIssue => "semantic_issue"
    SafetyNoRetry => "safety_no_retry"
    ProviderNoRetry => "provider_no_retry"
  }
}

///|
pub(all) struct RetryDecisionInput {
  stage : @routine.ModelStage
  report : @domain.ValidationReport
  attempt : Int
  policy : @job.RetryPolicy
} derive(Debug, Eq, ToJson, FromJson)

///|
pub(all) struct RetryDecision {
  stage : @routine.ModelStage
  attempt : Int
  max_attempts : Int
  issue_class : RetryIssueClass
  should_retry : Bool
  feedback_targets : Array[String]
  immutable_fields : Array[String]
  message : String
} derive(Debug, Eq, ToJson, FromJson)

///|
pub fn retry_decision_input(
  stage : @routine.ModelStage,
  report : @domain.ValidationReport,
  attempt? : Int = 0,
  policy? : @job.RetryPolicy = @job.default_retry_policy(),
) -> RetryDecisionInput {
  { stage, report, attempt, policy }
}

///|
fn issue_text(issue : @domain.ValidationIssue) -> String {
  "\{issue.path} \{issue.message}"
}

///|
fn issue_mentions(issue : @domain.ValidationIssue, needle : String) -> Bool {
  issue_text(issue).contains(needle)
}

///|
fn any_issue_mentions(
  report : @domain.ValidationReport,
  needle : String,
) -> Bool {
  report.issues.any(fn(issue) { issue_mentions(issue, needle) })
}

///|
fn classify_report(report : @domain.ValidationReport) -> RetryIssueClass {
  if report.ok {
    NoIssue
  } else if any_issue_mentions(report, "provider") ||
    any_issue_mentions(report, "quota") ||
    any_issue_mentions(report, "gateway") {
    ProviderNoRetry
  } else if any_issue_mentions(report, "order_direction") ||
    any_issue_mentions(report, "broker") ||
    any_issue_mentions(report, "safety") {
    SafetyNoRetry
  } else if any_issue_mentions(report, "confidence") ||
    any_issue_mentions(report, "rationale") ||
    any_issue_mentions(report, "strategy_files_needed") ||
    any_issue_mentions(report, "required") ||
    any_issue_mentions(report, "schema") {
    FormatIssue
  } else {
    SemanticIssue
  }
}

///|
fn max_attempts_for_class(
  stage : @routine.ModelStage,
  issue_class : RetryIssueClass,
  policy : @job.RetryPolicy,
) -> Int {
  if stage is Stage2 && !policy.retry_stage2 {
    0
  } else {
    match issue_class {
      NoIssue | SafetyNoRetry | ProviderNoRetry => 0
      FormatIssue => policy.max_attempts
      SemanticIssue => policy.semantic_max_attempts
    }
  }
}

///|
fn push_unique(items : Array[String], item : String) -> Unit {
  if !items.contains(item) {
    items.push(item)
  }
}

///|
fn feedback_targets(report : @domain.ValidationReport) -> Array[String] {
  let targets : Array[String] = []
  for issue in report.issues {
    if issue_mentions(issue, "direction") {
      push_unique(targets, "direction")
    }
    if issue_mentions(issue, "cycle_position") {
      push_unique(targets, "cycle_position")
    }
    if issue_mentions(issue, "confidence") {
      push_unique(targets, "confidence")
    }
    if issue_mentions(issue, "rationale") {
      push_unique(targets, "rationale")
    }
    if issue_mentions(issue, "strategy_files_needed") {
      push_unique(targets, "strategy_files_needed")
    }
  }
  targets
}

///|
fn immutable_fields(stage : @routine.ModelStage) -> Array[String] {
  match stage {
    Stage1 => ["direction", "cycle_position"]
    Stage2 => ["diagnosis_summary.cycle_position"]
  }
}

///|
fn decision_message(
  issue_class : RetryIssueClass,
  should_retry : Bool,
  attempt : Int,
  max_attempts : Int,
) -> String {
  if issue_class is NoIssue {
    "validation passed; no retry needed"
  } else if should_retry {
    "retry allowed for \{attempt + 1}/\{max_attempts} under \{issue_class.label()}"
  } else {
    "retry denied for \{issue_class.label()} at attempt \{attempt}/\{max_attempts}"
  }
}

///|
pub fn prepare_retry_decision(input : RetryDecisionInput) -> RetryDecision {
  let issue_class = classify_report(input.report)
  let max_attempts = max_attempts_for_class(
    input.stage,
    issue_class,
    input.policy,
  )
  let should_retry = !input.report.ok && input.attempt < max_attempts
  {
    stage: input.stage,
    attempt: input.attempt,
    max_attempts,
    issue_class,
    should_retry,
    feedback_targets: feedback_targets(input.report),
    immutable_fields: immutable_fields(input.stage),
    message: decision_message(
      issue_class,
      should_retry,
      input.attempt,
      max_attempts,
    ),
  }
}