///|
/// Strict parser settings preserve integer spelling and reject decoded duplicates.
fn document(source : String) -> Json raise PlanError {
  if source.length() > 8388608 {
    fail("input exceeds 8 Mi UTF-16 units")
  }
  @json.parse(
    source,
    max_nesting_depth=24,
    reject_duplicate_keys=true,
    preserve_number_repr=true,
  ) catch {
    _ => fail("invalid JSON, duplicate object key or nesting limit exceeded")
  }
}

///|
fn object(value : Json) -> Map[String, Json] raise PlanError {
  match value {
    Object(f) => f
    _ => fail("expected object")
  }
}

///|
fn only(f : Map[String, Json], keys : Array[String]) -> Unit raise PlanError {
  for key, _ in f {
    if !keys.contains(key) {
      fail("unknown or inapplicable field")
    }
  }
}

///|
fn required(f : Map[String, Json], key : String) -> Json raise PlanError {
  match f.get(key) {
    Some(v) => v
    None => fail("missing field: " + key)
  }
}

///|
fn text(value : Json) -> String raise PlanError {
  match value {
    String(s) => s
    _ => fail("expected string")
  }
}

///|
fn number(value : Json) -> Int32 raise PlanError {
  match value {
    Number(n, repr=Some(token)) => {
      if token.length() < 1 || token.length() > 10 {
        fail("integer token out of range")
      }
      for i in 0.. 57) && !(i == 0 && c == 45) {
          fail("use decimal integer tokens, no fractions or exponents")
        }
      }
      if n < 0.0 || n > 2000000.0 {
        fail("number outside supported nonnegative range")
      }
      n.to_int()
    }
    _ => fail("expected integer token")
  }
}

///|
fn int_default(
  f : Map[String, Json],
  key : String,
  default : Int32,
) -> Int32 raise PlanError {
  match f.get(key) {
    Some(v) => number(v)
    None => default
  }
}

///|
fn boolean(
  f : Map[String, Json],
  key : String,
  default : Bool,
) -> Bool raise PlanError {
  match f.get(key) {
    None => default
    Some(True) => true
    Some(False) => false
    _ => fail("expected boolean")
  }
}

///|
fn array(value : Json, max : Int32) -> Array[Json] raise PlanError {
  match value {
    Array(a) => {
      if a.length() > max {
        fail("array capacity exceeded")
      }
      a
    }
    _ => fail("expected array")
  }
}

///|
fn read_timetable(value : Json) -> Timetable raise PlanError {
  let f = object(value)
  only(f, ["name", "stops", "trips", "footpaths"])
  let stops = array(required(f, "stops"), 512).map(v => {
    let s = object(v)
    only(s, ["id", "name", "min_change", "accessible"])
    let id = text(required(s, "id"))
    let name = match s.get("name") {
      Some(v) => text(v)
      None => id
    }
    Stop::{
      id,
      name,
      min_change: int_default(s, "min_change", 0),
      accessible: boolean(s, "accessible", false),
    }
  })
  let trips = array(required(f, "trips"), 2048).map(v => {
    let s = object(v)
    only(s, ["id", "calls", "accessible", "cancelled", "delay"])
    let calls = array(required(s, "calls"), 128).map(v => {
      let c = object(v)
      only(c, ["stop", "arrival", "departure", "pickup", "dropoff"])
      Call::{
        stop: text(required(c, "stop")),
        arrival: number(required(c, "arrival")),
        departure: number(required(c, "departure")),
        pickup: boolean(c, "pickup", true),
        dropoff: boolean(c, "dropoff", true),
      }
    })
    Trip::{
      id: text(required(s, "id")),
      calls,
      accessible: boolean(s, "accessible", false),
      cancelled: boolean(s, "cancelled", false),
      delay: int_default(s, "delay", 0),
    }
  })
  let paths = match f.get("footpaths") {
    None => []
    Some(v) => array(v, 4096)
  }
  let footpaths = paths.map(v => {
    let s = object(v)
    only(s, ["id", "from", "to", "duration", "accessible"])
    Footpath::{
      id: text(required(s, "id")),
      from: text(required(s, "from")),
      to: text(required(s, "to")),
      duration: number(required(s, "duration")),
      accessible: boolean(s, "accessible", false),
    }
  })
  let t = Timetable::{
    name: text(required(f, "name")),
    stops,
    trips,
    footpaths,
  }
  validate(t)
  t
}

///|
fn read_query(value : Json) -> Query raise PlanError {
  let f = object(value)
  only(f, [
    "from", "to", "departure", "latest", "max_boardings", "max_walk", "accessible",
    "max_labels", "max_work",
  ])
  query(
    text(required(f, "from")),
    text(required(f, "to")),
    number(required(f, "departure")),
    latest=int_default(f, "latest", 259199),
    max_boardings=int_default(f, "max_boardings", 4),
    max_walk=int_default(f, "max_walk", 1800),
    accessible=boolean(f, "accessible", false),
    max_labels=int_default(f, "max_labels", 100000),
    max_work=int_default(f, "max_work", 2000000),
  )
}

///|
fn read_scenario(t : Timetable, value : Json) -> Timetable raise PlanError {
  let f = object(value)
  only(f, ["updates", "closed_paths"])
  let values = match f.get("updates") {
    None => []
    Some(v) => array(v, 2048)
  }
  let updates = values.map(v => {
    let u = object(v)
    only(u, ["id", "delay", "cancelled"])
    TripUpdate::{
      id: text(required(u, "id")),
      delay: int_default(u, "delay", 0),
      cancelled: boolean(u, "cancelled", false),
    }
  })
  let closed = match f.get("closed_paths") {
    None => []
    Some(v) => array(v, 4096).map(text)
  }
  scenario(t, updates, closed_paths=closed)
}

///|
pub fn decode_timetable(source : String) -> Timetable raise PlanError {
  read_timetable(document(source))
}

///|
/// Decode a complete versioned request, including optional scenario overrides.
pub fn decode_request(source : String) -> (Timetable, Query) raise PlanError {
  let f = object(document(source))
  only(f, ["schema_version", "timetable", "query", "scenario"])
  if number(required(f, "schema_version")) != 1 {
    fail("unsupported schema_version")
  }
  let base = read_timetable(required(f, "timetable"))
  let t = match f.get("scenario") {
    Some(v) => read_scenario(base, v)
    None => base
  }
  let q = read_query(required(f, "query"))
  validate_query(t, q)
  (t, q)
}

///|
pub fn plan_json(source : String) -> String raise PlanError {
  let (t, q) = decode_request(source)
  plan(t, q).to_json().stringify()
}