// A gRPC service config: the JSON document a name resolver (or a caller) can hand a
// channel to set per-method retry, hedging, timeout and wait-for-ready, plus the
// channel-wide load-balancing policy and retry throttling.
// https://github.com/grpc/grpc/blob/master/doc/service_config.md

///|
/// One method's configuration. `service` and `name` are the two halves of a method
/// name; either may be left out to match every method of that service or of every
/// service.
pub(all) struct MethodConfig {
  service : String?
  name : String?
  retry : Retry?
  hedging : Hedging?
  timeout : @moondate.Span?
  wait_for_ready : Bool?
}

///|
/// A parsed service config: the per-method entries in the order they were written
/// (first match wins), the channel-wide load-balancing policy and retry throttling.
pub(all) struct ServiceConfig {
  methods : Array[MethodConfig]
  load_balancing : LbPolicy?
  throttling : Throttling?
}

///|
/// An empty config: no method overrides, no policy.
pub fn ServiceConfig::empty() -> ServiceConfig {
  { methods: [], load_balancing: None, throttling: None, }
}

///|
/// Parse a service config document, as grpc-go's `encoding/json` does.
pub fn ServiceConfig::parse(
  text : String,
) -> ServiceConfig raise @json.ParseError {
  ServiceConfig::of_json(@json.parse(text))
}

///|
/// The entry that governs `path` (`/pkg.Service/Method`), the first whose service and
/// method both match (an omitted half matches anything).
pub fn ServiceConfig::for_method(
  self : ServiceConfig,
  path : String,
) -> MethodConfig? {
  let (service, name) = match split_path(path) {
    Some(parts) => parts
    None => return None
  }
  for m in self.methods {
    let service_ok = match m.service {
      Some(s) => s == service
      None => true
    }
    let name_ok = match m.name {
      Some(n) => n == name
      None => true
    }
    if service_ok && name_ok {
      return Some(m)
    }
  }
  None
}

///|
/// The timeout in whole milliseconds, for the `attempting` loop.
pub fn MethodConfig::timeout_millis(self : MethodConfig) -> Int32? {
  self.timeout.map(span => (span.nanos / 1_000_000L).to_int())
}

///|
/// Split `/pkg.Service/Method` into its service and method halves.
fn split_path(path : String) -> (String, String)? {
  let mut first = -1
  let mut second = -1
  let mut i = 0
  for ch in path {
    if ch == '/' {
      if first < 0 {
        first = i
      } else if second < 0 {
        second = i
      }
    }
    i = i + 1
  }
  if first < 0 || second < 0 {
    return None
  }
  let service = path.exact_view(start=first + 1, end=second).to_owned()
  let name = path.exact_view(start=second + 1).to_owned()
  Some((service, name))
}

///|
fn ServiceConfig::of_json(json : @json.Json) -> ServiceConfig {
  let obj = match object_of(json) {
    Some(o) => o
    None => return ServiceConfig::empty()
  }
  let methods : Array[MethodConfig] = []
  match array_field(obj, "methodConfig") {
    Some(arr) =>
      for j in arr {
        match method_of_json(j) {
          Some(m) => methods.push(m)
          None => ()
        }
      }
    None => ()
  }
  {
    methods,
    load_balancing: match string_field(obj, "loadBalancingPolicy") {
      Some(s) => lb_policy(s)
      None => None
    },
    throttling: match obj.get("retryThrottling") {
      Some(tj) => throttling_of_json(tj)
      None => None
    },
  }
}

///|
fn method_of_json(json : @json.Json) -> MethodConfig? {
  let obj = match object_of(json) {
    Some(o) => o
    None => return None
  }
  let mut service : String? = None
  let mut name : String? = None
  match array_field(obj, "name") {
    Some(names) =>
      match names {
        [first, ..] =>
          match object_of(first) {
            Some(no) => {
              service = string_field(no, "service")
              name = string_field(no, "method")
            }
            None => ()
          }
        [] => ()
      }
    None => ()
  }
  Some({
    service,
    name,
    retry: match obj.get("retryPolicy") {
      Some(r) => retry_of_json(r)
      None => None
    },
    hedging: match obj.get("hedgingPolicy") {
      Some(h) => hedging_of_json(h)
      None => None
    },
    timeout: match string_field(obj, "timeout") {
      Some(s) => parse_span(s)
      None => None
    },
    wait_for_ready: bool_field(obj, "waitForReady"),
  })
}

///|
fn retry_of_json(json : @json.Json) -> Retry? {
  let obj = match object_of(json) {
    Some(o) => o
    None => return None
  }
  Some(
    Retry::new(
      attempts=match number_field(obj, "maxAttempts") {
        Some(d) => d.to_int()
        None => retry.backoff.attempts
      },
      base=match string_field(obj, "initialBackoff") {
        Some(s) => parse_span(s).unwrap_or(retry.backoff.base)
        None => retry.backoff.base
      },
      cap=match string_field(obj, "maxBackoff") {
        Some(s) => parse_span(s).unwrap_or(retry.backoff.cap)
        None => retry.backoff.cap
      },
      factor=match number_field(obj, "backoffMultiplier") {
        Some(d) => d
        None => retry.backoff.factor
      },
      codes=match array_field(obj, "retryableStatusCodes") {
        Some(a) => statuses(a)
        None => retry.codes
      },
    ),
  )
}

///|
fn hedging_of_json(json : @json.Json) -> Hedging? {
  let obj = match object_of(json) {
    Some(o) => o
    None => return None
  }
  Some(
    Hedging::new(
      attempts=match number_field(obj, "maxAttempts") {
        Some(d) => d.to_int()
        None => hedging.attempts
      },
      delay=match string_field(obj, "hedgingDelay") {
        Some(s) => parse_span(s).unwrap_or(hedging.delay)
        None => hedging.delay
      },
      codes=match array_field(obj, "nonFatalStatusCodes") {
        Some(a) => statuses(a)
        None => hedging.codes
      },
    ),
  )
}

///|
fn throttling_of_json(json : @json.Json) -> Throttling? {
  let obj = match object_of(json) {
    Some(o) => o
    None => return None
  }
  let tokens = match number_field(obj, "maxTokens") {
    Some(d) => d.to_int()
    None => return None
  }
  let ratio = match number_field(obj, "tokenRatio") {
    Some(d) => d
    None => return None
  }
  Some(Throttling::new(tokens~, ratio~))
}

///|
fn string_field(obj : Map[String, @json.Json], key : String) -> String? {
  match obj.get(key) {
    Some(j) => string_of(j)
    None => None
  }
}

///|
fn bool_field(obj : Map[String, @json.Json], key : String) -> Bool? {
  match obj.get(key) {
    Some(j) => bool_of(j)
    None => None
  }
}

///|
fn number_field(obj : Map[String, @json.Json], key : String) -> Double? {
  match obj.get(key) {
    Some(j) => number_of(j)
    None => None
  }
}

///|
fn array_field(
  obj : Map[String, @json.Json],
  key : String,
) -> Array[@json.Json]? {
  match obj.get(key) {
    Some(j) => array_of(j)
    None => None
  }
}

///|
fn statuses(arr : Array[@json.Json]) -> Array[@wire.Status] {
  let out : Array[@wire.Status] = []
  for j in arr {
    match string_of(j) {
      Some(s) =>
        match @wire.Status::from_name(s) {
          Some(st) => out.push(st)
          None => ()
        }
      None => ()
    }
  }
  out
}

///|
fn lb_policy(name : String) -> LbPolicy? {
  if name == "round_robin" {
    Some(RoundRobin)
  } else if name == "pick_first" {
    Some(PickFirst)
  } else {
    None
  }
}

///|
/// A gRPC duration: decimal seconds with an `s` suffix (`"1.5s"`, `"0.1s"`).
fn parse_span(text : String) -> @moondate.Span? {
  let n = text.length()
  if n < 2 {
    return None
  }
  if text.exact_view(start=n - 1, end=n).to_owned() != "s" {
    return None
  }
  let mut whole : Int64 = 0
  let mut frac : Int64 = 0
  let mut digits = 0
  let mut dot = false
  for ch in text.exact_view(end=n - 1) {
    if ch == '.' {
      if dot {
        return None
      }
      dot = true
      continue
    }
    let d = ch.to_int() - '0'.to_int()
    if d < 0 || d > 9 {
      return None
    }
    if dot {
      if digits < 9 {
        frac = frac * 10L + d.to_int64()
        digits = digits + 1
      }
    } else {
      whole = whole * 10L + d.to_int64()
    }
  }
  let mut scale = 1L
  for i = digits; i < 9; i = i + 1 {
    scale = scale * 10L
  }
  Some({ nanos: whole * 1_000_000_000L + frac * scale, })
}

///|
fn object_of(json : @json.Json) -> Map[String, @json.Json]? {
  if json is Object(m) {
    Some(m)
  } else {
    None
  }
}

///|
fn string_of(json : @json.Json) -> String? {
  if json is String(s) {
    Some(s)
  } else {
    None
  }
}

///|
fn bool_of(json : @json.Json) -> Bool? {
  if json is True {
    Some(true)
  } else if json is False {
    Some(false)
  } else {
    None
  }
}

///|
fn number_of(json : @json.Json) -> Double? {
  if json is Number(n, ..) {
    Some(n)
  } else {
    None
  }
}

///|
fn array_of(json : @json.Json) -> Array[@json.Json]? {
  if json is Array(a) {
    Some(a)
  } else {
    None
  }
}