///|
pub(all) enum BreakerState {
  Closed
  Open
  HalfOpen
} derive(Eq, Debug)

///|
pub(all) struct CircuitBreakerConfig {
  failure_threshold : Int
  success_threshold : Int
  open_window_ms : Int
  half_open_max_calls : Int
} derive(Eq, Debug)

///|
pub(all) struct CircuitBreaker {
  config : CircuitBreakerConfig
  state : BreakerState
  consecutive_failures : Int
  half_open_successes : Int
  half_open_in_flight : Int
  opened_at_ms : Int
  open_until_ms : Int
  transition_count : Int
} derive(Eq, Debug)

///|
pub(all) struct BreakerSnapshot {
  state : BreakerState
  consecutive_failures : Int
  half_open_successes : Int
  half_open_in_flight : Int
  open_until_ms : Int
  remaining_open_ms : Int
  transition_count : Int
} derive(Eq, Debug)

///|
pub(all) enum BreakerDecision {
  BreakerAllowed(CircuitBreaker, Bool)
  BreakerRejected(CircuitBreaker, Int, String)
} derive(Eq, Debug)

///|
pub fn circuit_breaker_config(
  failure_threshold : Int,
  success_threshold : Int,
  open_window_ms : Int,
  half_open_max_calls : Int,
) -> CircuitBreakerConfig {
  {
    failure_threshold: clamp_at_least_one(failure_threshold),
    success_threshold: clamp_at_least_one(success_threshold),
    open_window_ms: clamp_non_negative(open_window_ms),
    half_open_max_calls: clamp_at_least_one(half_open_max_calls),
  }
}

///|
pub fn default_circuit_breaker() -> CircuitBreaker {
  new_circuit_breaker(circuit_breaker_config(5, 2, 30000, 1))
}

///|
pub fn new_circuit_breaker(config : CircuitBreakerConfig) -> CircuitBreaker {
  {
    config: circuit_breaker_config(
      config.failure_threshold,
      config.success_threshold,
      config.open_window_ms,
      config.half_open_max_calls,
    ),
    state: Closed,
    consecutive_failures: 0,
    half_open_successes: 0,
    half_open_in_flight: 0,
    opened_at_ms: 0,
    open_until_ms: 0,
    transition_count: 0,
  }
}

///|
pub fn breaker_before_call(
  breaker : CircuitBreaker,
  now_ms : Int,
) -> BreakerDecision {
  match breaker.state {
    Closed => BreakerAllowed(breaker, false)
    Open =>
      if now_ms < breaker.open_until_ms {
        BreakerRejected(
          breaker,
          breaker.open_until_ms,
          "recovery window has not elapsed",
        )
      } else {
        let half_open = to_half_open(breaker)
        reserve_half_open_probe(half_open, true)
      }
    HalfOpen => reserve_half_open_probe(breaker, false)
  }
}

///|
pub fn breaker_record_success(
  breaker : CircuitBreaker,
  _now_ms : Int,
) -> CircuitBreaker {
  match breaker.state {
    Closed => { ..breaker, consecutive_failures: 0 }
    Open => breaker
    HalfOpen => {
      let remaining = max_int(0, breaker.half_open_in_flight - 1)
      let successes = breaker.half_open_successes + 1
      if successes >= breaker.config.success_threshold {
        close_breaker(breaker)
      } else {
        {
          ..breaker,
          half_open_successes: successes,
          half_open_in_flight: remaining,
        }
      }
    }
  }
}

///|
pub fn breaker_record_failure(
  breaker : CircuitBreaker,
  now_ms : Int,
) -> CircuitBreaker {
  match breaker.state {
    Open => breaker
    HalfOpen => open_breaker(breaker, now_ms)
    Closed => {
      let failures = breaker.consecutive_failures + 1
      if failures >= breaker.config.failure_threshold {
        open_breaker({ ..breaker, consecutive_failures: failures }, now_ms)
      } else {
        { ..breaker, consecutive_failures: failures }
      }
    }
  }
}

///|
pub fn breaker_cancel_call(breaker : CircuitBreaker) -> CircuitBreaker {
  match breaker.state {
    HalfOpen =>
      {
        ..breaker,
        half_open_in_flight: max_int(0, breaker.half_open_in_flight - 1),
      }
    _ => breaker
  }
}

///|
pub fn breaker_force_open(
  breaker : CircuitBreaker,
  now_ms : Int,
) -> CircuitBreaker {
  open_breaker(breaker, now_ms)
}

///|
pub fn breaker_force_close(breaker : CircuitBreaker) -> CircuitBreaker {
  close_breaker(breaker)
}

///|
pub fn breaker_snapshot(
  breaker : CircuitBreaker,
  now_ms : Int,
) -> BreakerSnapshot {
  {
    state: breaker.state,
    consecutive_failures: breaker.consecutive_failures,
    half_open_successes: breaker.half_open_successes,
    half_open_in_flight: breaker.half_open_in_flight,
    open_until_ms: breaker.open_until_ms,
    remaining_open_ms: if breaker.state == Open {
      max_int(0, breaker.open_until_ms - now_ms)
    } else {
      0
    },
    transition_count: breaker.transition_count,
  }
}

///|
pub fn breaker_state_name(state : BreakerState) -> String {
  match state {
    Closed => "closed"
    Open => "open"
    HalfOpen => "half-open"
  }
}

///|
pub fn format_breaker_snapshot(snapshot : BreakerSnapshot) -> String {
  "state=" +
  breaker_state_name(snapshot.state) +
  " failures=" +
  snapshot.consecutive_failures.to_string() +
  " half_open_successes=" +
  snapshot.half_open_successes.to_string() +
  " half_open_in_flight=" +
  snapshot.half_open_in_flight.to_string() +
  " remaining_open_ms=" +
  snapshot.remaining_open_ms.to_string() +
  " transitions=" +
  snapshot.transition_count.to_string()
}

///|
fn reserve_half_open_probe(
  breaker : CircuitBreaker,
  transitioned : Bool,
) -> BreakerDecision {
  if breaker.half_open_in_flight >= breaker.config.half_open_max_calls {
    BreakerRejected(
      breaker,
      breaker.open_until_ms,
      "half-open probe capacity exhausted",
    )
  } else {
    BreakerAllowed(
      { ..breaker, half_open_in_flight: breaker.half_open_in_flight + 1 },
      transitioned,
    )
  }
}

///|
fn to_half_open(breaker : CircuitBreaker) -> CircuitBreaker {
  {
    ..breaker,
    state: HalfOpen,
    consecutive_failures: 0,
    half_open_successes: 0,
    half_open_in_flight: 0,
    transition_count: breaker.transition_count + 1,
  }
}

///|
fn open_breaker(breaker : CircuitBreaker, now_ms : Int) -> CircuitBreaker {
  {
    ..breaker,
    state: Open,
    half_open_successes: 0,
    half_open_in_flight: 0,
    opened_at_ms: now_ms,
    open_until_ms: now_ms + breaker.config.open_window_ms,
    transition_count: breaker.transition_count + 1,
  }
}

///|
fn close_breaker(breaker : CircuitBreaker) -> CircuitBreaker {
  {
    ..breaker,
    state: Closed,
    consecutive_failures: 0,
    half_open_successes: 0,
    half_open_in_flight: 0,
    opened_at_ms: 0,
    open_until_ms: 0,
    transition_count: breaker.transition_count + 1,
  }
}