///|
priv enum Timeout {
  Finite(Int32)
  Disabled
  Default
}

///|
priv struct Mark {
  name : String
  session : Timeout
  scoped : Timeout?
}

///|
priv struct TimeoutState {
  mut session : Timeout
  mut scoped : Timeout?
  mut at_begin : Timeout
  marks : Array[Mark]
}

///|
fn TimeoutState::new(initial : Int32?) -> TimeoutState {
  let session = match initial {
    None => Default
    Some(0) => Disabled
    Some(value) => Finite(value)
  }
  // An external transaction's earlier session baseline is not supplied.
  { session, scoped: None, at_begin: Default, marks: [], }
}

///|
fn TimeoutState::begin(self : TimeoutState) -> Unit {
  self.at_begin = self.session
  self.scoped = None
  self.marks.clear()
}

///|
fn TimeoutState::finish(self : TimeoutState, rollback~ : Bool) -> Unit {
  if rollback {
    self.session = self.at_begin
  }
  self.scoped = None
  self.marks.clear()
}

///|
fn TimeoutState::taint(self : TimeoutState) -> Unit {
  self.session = Default
  self.scoped = None
  self.at_begin = Default
  self.marks.clear()
}

///|
fn TimeoutState::mark(self : TimeoutState, name : String) -> Unit {
  self.marks.push({ name, session: self.session, scoped: self.scoped, })
}

///|
fn TimeoutState::restore(
  self : TimeoutState,
  name : String,
  release~ : Bool,
) -> Bool {
  for i = self.marks.length(); i > 0; i = i - 1 {
    let mark = self.marks[i - 1]
    if mark.name == name {
      if release {
        self.marks.truncate(i - 1)
      } else {
        self.session = mark.session
        self.scoped = mark.scoped
        self.marks.truncate(i)
      }
      return true
    }
  }
  false
}

///|
fn TimeoutState::check(
  self : TimeoutState,
  span : Span,
  out : Array[Diagnostic],
  maximum : Int32?,
) -> Unit {
  match self.scoped.unwrap_or(self.session) {
    Finite(milliseconds) =>
      if maximum is Some(limit) && milliseconds > limit {
        out.push(
          finding(
            "ML205",
            Warning,
            span,
            "Effective lock_timeout of \{milliseconds}ms exceeds policy maximum of \{limit}ms",
            "Set a positive timeout at or below the configured maximum in the effective scope.",
          ),
        )
      }
    Disabled =>
      out.push(
        finding(
          "ML201",
          Warning,
          span,
          "lock_timeout is explicitly disabled",
          "Establish a positive finite lock_timeout in the effective scope before this index operation.",
        ),
      )
    Default =>
      out.push(
        finding(
          "ML202",
          Warning,
          span,
          "A finite effective lock_timeout has not been established by this input",
          "Check session defaults or set a positive timeout; unknown does not mean disabled.",
        ),
      )
  }
}

///|
// Restrict to integer milliseconds or quoted integer durations. Values which
// PostgreSQL rounds, evaluates or obtains from configuration are never guessed.
fn parse_timeout(raw : String) -> Timeout? {
  if raw == "DEFAULT" {
    return Some(Default)
  }
  let quoted = raw.has_prefix("'") && raw.has_suffix("'")
  let value = if quoted { raw[1:raw.length() - 1].to_owned() } else { raw }
  let mut end = 0
  while end < value.length() {
    let c = value.code_units()[end]
    if c < 48 || c > 57 {
      break
    }
    end += 1
  }
  if end == 0 {
    return None
  }
  let factor : Int64 = match value[end:].to_owned() {
    "" => 1L
    "ms" if quoted => 1L
    "s" if quoted => 1000L
    "min" if quoted => 60000L
    "h" if quoted => 3600000L
    "d" if quoted => 86400000L
    _ => return None
  }
  let number = @string.parse_int64(value[:end]) catch { _ => return None }
  if number > 2147483647L / factor {
    return None
  }
  Some(if number == 0L { Disabled } else { Finite((number * factor).to_int()) })
}

///|
// PostgreSQL identifier truncation is byte-based. Restrict savepoint names to
// ASCII <=63 bytes, and use the dialect tokenizer to reject reserved keywords.
fn savepoint_name(raw : String) -> String? {
  if raw.has_prefix("\"") && raw.has_suffix("\"") {
    let out = StringBuilder()
    let mut i = 1
    while i < raw.length() - 1 {
      let c = raw.code_units()[i]
      if c == 0 || c > 127 {
        return None
      }
      if c == 34 {
        if raw.get(i + 1) != Some(34) {
          return None
        }
        i += 1
      }
      out.write_stringview(raw[i:i + 1])
      i += 1
    }
    let name = out.to_string()
    return if name.is_empty() || name.length() > 63 { None } else { Some(name) }
  }
  let tokens = @sql.tokenize(dialect=@sql.Postgres::{ }, raw) catch {
    _ => return None
  }
  match tokens[:] {
    [@sql.Identifier(name)] => {
      if name.is_empty() ||
        name.length() > 63 ||
        name.code_units().any(c => c == 0 || c > 127) {
        return None
      }
      Some(name.to_lower())
    }
    _ => None
  }
}