///|
pub fn town_task_status_for_execution(
  status : TaskExecutionStatus,
) -> TownTaskStatus {
  match status {
    PacketReady => Planned
    ProposalImported => Assigned
    RunConfirmed => Assigned
    Running => Running
    AwaitingPersistence => Running
    ReviewQueued => Blocked
    Completed => Done
    Stale => Blocked
    Failed => Failed
  }
}

///|
pub fn copy_task_with_status(
  task : TownTask,
  status : TownTaskStatus,
) -> TownTask {
  TownTask::new(
    id=task.id,
    book_id=task.book_id,
    title=task.title,
    prompt=task.prompt,
    domain=task.domain,
    required_role=task.required_role,
    required_capabilities=task.required_capabilities,
    status~,
  )
}

///|
pub fn schedulable_task_status(status : TownTaskStatus) -> Bool {
  match status {
    Pending => true
    Planned => true
    Assigned => true
    Running => true
    Blocked => false
    Done => false
    Failed => false
  }
}

///|
pub fn terminal_execution_status(status : TaskExecutionStatus) -> Bool {
  match status {
    Completed => true
    Stale => true
    Failed => true
    _ => false
  }
}

///|
pub fn live_execution_status(status : TaskExecutionStatus) -> Bool {
  match status {
    ProposalImported => true
    RunConfirmed => true
    Running => true
    _ => false
  }
}

///|
pub fn active_work_execution_status(status : TaskExecutionStatus) -> Bool {
  live_execution_status(status) || status is AwaitingPersistence
}

///|
pub fn execution_status_rank(status : TaskExecutionStatus) -> Int {
  match status {
    PacketReady => 1
    ProposalImported => 2
    RunConfirmed | Running | AwaitingPersistence => 3
    ReviewQueued | Completed | Stale | Failed => 4
  }
}

///|
pub fn pollable_execution_status(status : TaskExecutionStatus) -> Bool {
  live_execution_status(status) || status is Stale
}

///|
pub fn polled_execution_stale_window_ticks(
  execution : TaskExecutionRecord,
) -> Int {
  match execution.run_id {
    Some(_) => 120
    None => 15
  }
}

///|
pub fn external_execution_stale_window_ticks() -> Int {
  30
}

///|
pub fn no_run_id_live_execution_stale(
  execution : TaskExecutionRecord,
  tick : Int,
) -> Bool {
  if !live_execution_status(execution.status) {
    return false
  }
  tick >= execution.stale_after_tick ||
  (
    execution.run_id is None &&
    tick >=
    execution.heartbeat_tick + polled_execution_stale_window_ticks(execution)
  )
}

///|
pub fn live_external_execution_count(state : TownState) -> Int {
  let mut count = 0
  for execution in state.executions {
    match execution.status {
      ProposalImported | RunConfirmed | Running =>
        if execution.run_id is Some(_) {
          count += 1
        }
      _ => ()
    }
  }
  count
}

///|
pub fn max_live_external_executions() -> Int {
  8
}

///|
pub fn has_live_executions(state : TownState) -> Bool {
  for execution in state.executions {
    if live_execution_status(execution.status) {
      return true
    }
  }
  false
}

///|
pub fn has_active_work_executions(state : TownState) -> Bool {
  for execution in state.executions {
    if active_work_execution_status(execution.status) {
      return true
    }
  }
  false
}

///|
pub fn has_retry_pending_executions(state : TownState, tick : Int) -> Bool {
  ignore(tick)
  for execution in state.executions {
    match execution.status {
      Stale | Failed | ReviewQueued =>
        match execution.next_retry_tick {
          Some(_) if execution.attempt < 3 => return true
          _ => ()
        }
      _ => ()
    }
  }
  false
}

///|
pub fn tick_from_task_id(task_id : String) -> Int {
  match tick_marker_index(task_id) {
    Some(index) => first_non_negative_int(task_id[index + 5:].to_owned())
    None => 0
  }
}

///|
fn tick_marker_index(task_id : String) -> Int? {
  let marker = "tick-"
  let limit = task_id.length() - marker.length()
  if limit < 0 {
    return None
  }
  let mut found : Int? = None
  for index in 0..<=limit {
    if task_id[index:index + marker.length()].to_owned() == marker {
      found = Some(index)
    }
  }
  found
}

///|
fn first_non_negative_int(text : String) -> Int {
  let mut value = 0
  let mut seen = false
  for char in text {
    if char >= '0' && char <= '9' {
      value = value * 10 + (char.to_int() - '0'.to_int())
      seen = true
    } else if seen {
      return value
    }
  }
  value
}