///|
/// Creates an empty resource state. Token zero is never issued.
pub fn LeaseState::new(resource : String) -> LeaseState {
  { resource, lease: None, next_token: 1, revision: 0 }
}

///|
/// Returns whether this lease is valid at the supplied logical time.
pub fn Lease::is_active(self : Lease, now : Int) -> Bool {
  now >= self.issued_at && now < self.expires_at
}

///|
/// Returns the active lease, treating expired or not-yet-issued records as absent.
pub fn LeaseState::active_lease(self : LeaseState, now : Int) -> Lease? {
  match self.lease {
    Some(lease) => if lease.is_active(now) { Some(lease) } else { None }
    None => None
  }
}

///|
/// Remaining logical ticks, clamped to zero.
pub fn Lease::remaining(self : Lease, now : Int) -> Int {
  if self.expires_at > now {
    self.expires_at - now
  } else {
    0
  }
}

///|
fn rejected(state : LeaseState, message : String) -> LeaseDecision {
  {
    state,
    accepted: false,
    kind: Rejected,
    lease: state.lease,
    event: None,
    message,
  }
}

///|
fn accepted(
  state : LeaseState,
  kind : DecisionKind,
  now : Int,
  holder : String,
  token : Int,
  detail : String,
) -> LeaseDecision {
  let event = {
    resource: state.resource,
    kind,
    at: now,
    holder,
    token,
    revision: state.revision,
    detail,
  }
  {
    state,
    accepted: true,
    kind,
    lease: state.lease,
    event: Some(event),
    message: detail,
  }
}

///|
fn validate_identity(holder : String) -> String? {
  if holder == "" {
    Some("holder must not be empty")
  } else {
    None
  }
}

///|
/// Applies one command at an explicit logical time.
pub fn LeaseState::apply(
  self : LeaseState,
  command : LeaseCommand,
  now : Int,
) -> LeaseDecision {
  if self.resource == "" {
    return rejected(self, "resource must not be empty")
  }
  if now < 0 {
    return rejected(self, "logical time must not be negative")
  }
  match command {
    Acquire(holder, ttl, expected_revision) => {
      match validate_identity(holder) {
        Some(message) => return rejected(self, message)
        None => ()
      }
      if ttl <= 0 {
        return rejected(self, "ttl must be positive")
      }
      match expected_revision {
        Some(expected) =>
          if expected != self.revision {
            return rejected(self, "revision precondition failed")
          }
        None => ()
      }
      match self.active_lease(now) {
        Some(_) => rejected(self, "resource already has an active lease")
        None => {
          let revision = self.revision + 1
          let lease = {
            resource: self.resource,
            holder,
            token: self.next_token,
            revision,
            issued_at: now,
            expires_at: now + ttl,
          }
          let state = {
            resource: self.resource,
            lease: Some(lease),
            next_token: self.next_token + 1,
            revision,
          }
          accepted(state, Acquired, now, holder, lease.token, "lease acquired")
        }
      }
    }
    Renew(holder, token, ttl) => {
      match validate_identity(holder) {
        Some(message) => return rejected(self, message)
        None => ()
      }
      if ttl <= 0 {
        return rejected(self, "ttl must be positive")
      }
      match self.active_lease(now) {
        None => rejected(self, "cannot renew an absent or expired lease")
        Some(current) =>
          if current.holder != holder || current.token != token {
            rejected(self, "holder or fencing token mismatch")
          } else {
            let revision = self.revision + 1
            let lease = {
              ..current,
              revision,
              issued_at: now,
              expires_at: now + ttl,
            }
            let state = { ..self, lease: Some(lease), revision }
            accepted(state, Renewed, now, holder, token, "lease renewed")
          }
      }
    }
    Release(holder, token) =>
      match self.active_lease(now) {
        None => rejected(self, "cannot release an absent or expired lease")
        Some(current) =>
          if current.holder != holder || current.token != token {
            rejected(self, "holder or fencing token mismatch")
          } else {
            let state = { ..self, lease: None, revision: self.revision + 1 }
            accepted(state, Released, now, holder, token, "lease released")
          }
      }
    Transfer(holder, token, successor, ttl) => {
      match validate_identity(successor) {
        Some(_) => return rejected(self, "successor must not be empty")
        None => ()
      }
      if successor == holder {
        return rejected(self, "successor must differ from current holder")
      }
      if ttl <= 0 {
        return rejected(self, "ttl must be positive")
      }
      match self.active_lease(now) {
        None => rejected(self, "cannot transfer an absent or expired lease")
        Some(current) =>
          if current.holder != holder || current.token != token {
            rejected(self, "holder or fencing token mismatch")
          } else {
            let revision = self.revision + 1
            let lease = {
              resource: self.resource,
              holder: successor,
              token: self.next_token,
              revision,
              issued_at: now,
              expires_at: now + ttl,
            }
            let state = {
              resource: self.resource,
              lease: Some(lease),
              next_token: self.next_token + 1,
              revision,
            }
            accepted(
              state,
              Transferred,
              now,
              successor,
              lease.token,
              "lease transferred",
            )
          }
      }
    }
    Reap =>
      match self.lease {
        Some(current) =>
          if current.is_active(now) {
            rejected(self, "active lease cannot be reaped")
          } else {
            let state = { ..self, lease: None, revision: self.revision + 1 }
            accepted(
              state,
              Reaped,
              now,
              current.holder,
              current.token,
              "expired lease reaped",
            )
          }
        None => rejected(self, "no lease to reap")
      }
  }
}