///|
/// GraphQL introspection (spec ยง4): answer `__schema`, `__type` and `__typename`
/// from the schema. The introspection type system (`__Schema`, `__Type`,
/// `__Field`, `__InputValue`, `__EnumValue`, `__Directive`) is defined here as
/// synthetic `ObjectType`s so the executor's normal field-completion machinery
/// can walk an introspection selection set, while the data is materialised as
/// JSON that the default resolver reads through.

///|
/// A synthetic introspection object type: a name and its `(field, type)` list.
fn intro_obj(name : String, fields : Array[(String, GqlType)]) -> ObjectType {
  let fs : Array[Field] = []
  for f in fields {
    fs.push({ name: f.0, args: [], typ: f.1 })
  }
  { name, fields: fs, kind: Object, interfaces: [] }
}

///|
/// The synthetic definition of an introspection type by name, or `None`. These
/// drive leaf-vs-composite completion when executing an introspection query.
fn introspection_type_def(name : String) -> ObjectType? {
  let t = Scalar("String")
  let b = NonNull(Scalar("Boolean"))
  match name {
    "__Schema" =>
      Some(
        intro_obj("__Schema", [
          ("description", t),
          ("types", NonNull(ListOf(NonNull(Named("__Type"))))),
          ("queryType", NonNull(Named("__Type"))),
          ("mutationType", Named("__Type")),
          ("subscriptionType", Named("__Type")),
          ("directives", NonNull(ListOf(NonNull(Named("__Directive"))))),
        ]),
      )
    "__Type" =>
      Some(
        intro_obj("__Type", [
          ("kind", NonNull(Named("__TypeKind"))),
          ("name", t),
          ("description", t),
          ("fields", ListOf(NonNull(Named("__Field")))),
          ("interfaces", ListOf(NonNull(Named("__Type")))),
          ("possibleTypes", ListOf(NonNull(Named("__Type")))),
          ("enumValues", ListOf(NonNull(Named("__EnumValue")))),
          ("inputFields", ListOf(NonNull(Named("__InputValue")))),
          ("ofType", Named("__Type")),
        ]),
      )
    "__Field" =>
      Some(
        intro_obj("__Field", [
          ("name", NonNull(Scalar("String"))),
          ("description", t),
          ("args", NonNull(ListOf(NonNull(Named("__InputValue"))))),
          ("type", NonNull(Named("__Type"))),
          ("isDeprecated", b),
          ("deprecationReason", t),
        ]),
      )
    "__InputValue" =>
      Some(
        intro_obj("__InputValue", [
          ("name", NonNull(Scalar("String"))),
          ("description", t),
          ("type", NonNull(Named("__Type"))),
          ("defaultValue", t),
        ]),
      )
    "__EnumValue" =>
      Some(
        intro_obj("__EnumValue", [
          ("name", NonNull(Scalar("String"))),
          ("description", t),
          ("isDeprecated", b),
          ("deprecationReason", t),
        ]),
      )
    "__Directive" =>
      Some(
        intro_obj("__Directive", [
          ("name", NonNull(Scalar("String"))),
          ("description", t),
          ("locations", NonNull(ListOf(NonNull(Named("__DirectiveLocation"))))),
          ("args", NonNull(ListOf(NonNull(Named("__InputValue"))))),
          ("isRepeatable", b),
        ]),
      )
    _ => None
  }
}

///|
/// The introspection `__TypeKind` enum name for a type name in this schema.
fn Exec::type_kind(self : Exec, name : String) -> String {
  if is_builtin_scalar(name) {
    return "SCALAR"
  }
  match self.schema.type_by_name(name) {
    Some(t) =>
      match t.kind {
        Object => "OBJECT"
        Input => "INPUT_OBJECT"
        Interface => "INTERFACE"
      }
    None =>
      if self.schema.enum_by_name(name) is Some(_) {
        "ENUM"
      } else if self.schema.union_by_name(name) is Some(_) {
        "UNION"
      } else {
        "SCALAR"
      }
  }
}

///|
/// A shallow `__Type` reference: `{ kind, name, ofType: null }` with the detail
/// fields null. Used wherever introspection points at a type by reference
/// (`ofType`, `queryType`, a field's `type`, interface / possibleType entries).
fn Exec::named_type_ref(self : Exec, name : String) -> Json {
  jobj([
    ("kind", self.type_kind(name).to_json()),
    ("name", name.to_json()),
    ("description", Json::null()),
    ("fields", Json::null()),
    ("interfaces", Json::null()),
    ("possibleTypes", Json::null()),
    ("enumValues", Json::null()),
    ("inputFields", Json::null()),
    ("ofType", Json::null()),
  ])
}

///|
/// A `__Type` wrapper reference (`NON_NULL` / `LIST`) around `of_type`.
fn wrapper_ref(kind : String, of_type : Json) -> Json {
  jobj([
    ("kind", kind.to_json()),
    ("name", Json::null()),
    ("description", Json::null()),
    ("fields", Json::null()),
    ("interfaces", Json::null()),
    ("possibleTypes", Json::null()),
    ("enumValues", Json::null()),
    ("inputFields", Json::null()),
    ("ofType", of_type),
  ])
}

///|
/// The introspection `__Type` reference for a possibly-wrapped `GqlType`,
/// nesting `ofType` for each `!` / `[]` wrapper.
fn Exec::type_ref_json(self : Exec, t : GqlType) -> Json {
  match t {
    NonNull(inner) => wrapper_ref("NON_NULL", self.type_ref_json(inner))
    ListOf(inner) => wrapper_ref("LIST", self.type_ref_json(inner))
    Scalar(n) => self.named_type_ref(n)
    Named(n) => self.named_type_ref(n)
  }
}

///|
/// Materialise a `__InputValue` for an argument or input field.
fn Exec::input_value_json(self : Exec, name : String, typ : GqlType) -> Json {
  jobj([
    ("name", name.to_json()),
    ("description", Json::null()),
    ("type", self.type_ref_json(typ)),
    ("defaultValue", Json::null()),
  ])
}

///|
/// Materialise a `__Field` for an output field.
fn Exec::field_json(self : Exec, f : Field) -> Json {
  let args : Array[Json] = []
  for a in f.args {
    args.push(self.input_value_json(a.0, a.1))
  }
  jobj([
    ("name", f.name.to_json()),
    ("description", Json::null()),
    ("args", args.to_json()),
    ("type", self.type_ref_json(f.typ)),
    ("isDeprecated", false.to_json()),
    ("deprecationReason", Json::null()),
  ])
}

///|
/// Materialise the full `__Type` for a declared composite type (object / input
/// object / interface), including its fields, interfaces and possibleTypes.
fn Exec::composite_type_json(self : Exec, def : ObjectType) -> Json {
  let kind = match def.kind {
    Object => "OBJECT"
    Input => "INPUT_OBJECT"
    Interface => "INTERFACE"
  }
  let fields = if def.kind is Input {
    Json::null()
  } else {
    let fs : Array[Json] = []
    for f in def.fields {
      // @inaccessible fields are present in the subgraph but hidden from the
      // composed/public schema, so introspection must not report them.
      if self.schema.field_has_directive(def.name, f.name, "inaccessible") {
        continue
      }
      fs.push(self.field_json(f))
    }
    fs.to_json()
  }
  let input_fields = if def.kind is Input {
    let fs : Array[Json] = []
    for f in def.fields {
      if self.schema.field_has_directive(def.name, f.name, "inaccessible") {
        continue
      }
      fs.push(self.input_value_json(f.name, f.typ))
    }
    fs.to_json()
  } else {
    Json::null()
  }
  let interfaces = if def.kind is Input {
    Json::null()
  } else {
    let is_ : Array[Json] = []
    for i in def.interfaces {
      is_.push(self.named_type_ref(i))
    }
    is_.to_json()
  }
  let possible_types = if def.kind is Interface {
    let ps : Array[Json] = []
    for t in self.schema.types {
      for i in t.interfaces {
        if i == def.name {
          ps.push(self.named_type_ref(t.name))
          break
        }
      }
    }
    ps.to_json()
  } else {
    Json::null()
  }
  jobj([
    ("kind", kind.to_json()),
    ("name", def.name.to_json()),
    ("description", Json::null()),
    ("fields", fields),
    ("interfaces", interfaces),
    ("possibleTypes", possible_types),
    ("enumValues", Json::null()),
    ("inputFields", input_fields),
    ("ofType", Json::null()),
  ])
}

///|
/// Materialise the full `__Type` for an enum type.
fn Exec::enum_type_json(self : Exec, def : EnumType) -> Json {
  ignore(self)
  let vals : Array[Json] = []
  for v in def.values {
    vals.push(
      jobj([
        ("name", v.to_json()),
        ("description", Json::null()),
        ("isDeprecated", false.to_json()),
        ("deprecationReason", Json::null()),
      ]),
    )
  }
  jobj([
    ("kind", "ENUM".to_json()),
    ("name", def.name.to_json()),
    ("description", Json::null()),
    ("fields", Json::null()),
    ("interfaces", Json::null()),
    ("possibleTypes", Json::null()),
    ("enumValues", vals.to_json()),
    ("inputFields", Json::null()),
    ("ofType", Json::null()),
  ])
}

///|
/// Materialise the full `__Type` for a union type: `kind = UNION` and
/// `possibleTypes` listing its members as type references.
fn Exec::union_type_json(self : Exec, def : UnionType) -> Json {
  let members : Array[Json] = []
  for m in def.members {
    members.push(self.named_type_ref(m))
  }
  jobj([
    ("kind", "UNION".to_json()),
    ("name", def.name.to_json()),
    ("description", Json::null()),
    ("fields", Json::null()),
    ("interfaces", Json::null()),
    ("possibleTypes", members.to_json()),
    ("enumValues", Json::null()),
    ("inputFields", Json::null()),
    ("ofType", Json::null()),
  ])
}

///|
/// Materialise the full `__Type` for a built-in or custom scalar.
fn scalar_type_json(name : String) -> Json {
  jobj([
    ("kind", "SCALAR".to_json()),
    ("name", name.to_json()),
    ("description", Json::null()),
    ("fields", Json::null()),
    ("interfaces", Json::null()),
    ("possibleTypes", Json::null()),
    ("enumValues", Json::null()),
    ("inputFields", Json::null()),
    ("ofType", Json::null()),
  ])
}

///|
/// Resolve `__type(name:)` to a fully-materialised `__Type`, or JSON null.
fn Exec::introspection_type_by_name(self : Exec, name : String) -> Json {
  // An @inaccessible type is hidden from the composed schema, so `__type(name:)`
  // reports it as absent.
  if self.schema.type_has_directive(name, "inaccessible") {
    return Json::null()
  }
  match self.schema.type_by_name(name) {
    Some(def) => return self.composite_type_json(def)
    None => ()
  }
  match self.schema.enum_by_name(name) {
    Some(def) => return self.enum_type_json(def)
    None => ()
  }
  match self.schema.union_by_name(name) {
    Some(def) => return self.union_type_json(def)
    None => ()
  }
  if is_builtin_scalar(name) || self.schema.scalar_by_name(name) is Some(_) {
    return scalar_type_json(name)
  }
  Json::null()
}

///|
/// The three standard built-in directives, materialised as `__Directive`s.
fn Exec::directives_json(self : Exec) -> Json {
  let field_locs : Array[Json] = [
    "FIELD".to_json(),
    "FRAGMENT_SPREAD".to_json(),
    "INLINE_FRAGMENT".to_json(),
  ]
  let bool_if : Array[Json] = [
    self.input_value_json("if", NonNull(Scalar("Boolean"))),
  ]
  let deprecated_reason : Array[Json] = [
    self.input_value_json("reason", Scalar("String")),
  ]
  let directive = fn(
    name : String,
    locs : Array[Json],
    args : Array[Json],
  ) -> Json {
    jobj([
      ("name", name.to_json()),
      ("description", Json::null()),
      ("locations", locs.to_json()),
      ("args", args.to_json()),
      ("isRepeatable", false.to_json()),
    ])
  }
  let out : Array[Json] = [
    directive("include", field_locs, bool_if),
    directive("skip", field_locs, bool_if),
    directive(
      "deprecated",
      ["FIELD_DEFINITION".to_json(), "ENUM_VALUE".to_json()],
      deprecated_reason,
    ),
  ]
  // User-registered directives (@upper, @auth, ...) show up alongside the built-ins.
  for d in self.schema.directive_defs {
    let locs : Array[Json] = []
    for l in d.locations {
      locs.push(l.to_json())
    }
    let args : Array[Json] = []
    for a in d.args {
      args.push(self.input_value_json(a.0, a.1))
    }
    out.push(
      jobj([
        ("name", d.name.to_json()),
        ("description", Json::null()),
        ("locations", locs.to_json()),
        ("args", args.to_json()),
        ("isRepeatable", d.is_repeatable.to_json()),
      ]),
    )
  }
  out.to_json()
}

///|
/// Materialise the `__schema` root: query/mutation/subscription roots, every
/// declared type (user composites, enums and the built-in scalars), and the
/// built-in directives.
fn Exec::introspection_schema(self : Exec) -> Json {
  let types : Array[Json] = []
  for t in self.schema.types {
    // @inaccessible types are hidden from the composed schema's type list.
    if self.schema.type_has_directive(t.name, "inaccessible") {
      continue
    }
    types.push(self.composite_type_json(t))
  }
  for e in self.schema.enums {
    if self.schema.type_has_directive(e.name, "inaccessible") {
      continue
    }
    types.push(self.enum_type_json(e))
  }
  for u in self.schema.unions {
    if self.schema.type_has_directive(u.name, "inaccessible") {
      continue
    }
    types.push(self.union_type_json(u))
  }
  for s in ["String", "Int", "Float", "Boolean", "ID"] {
    types.push(scalar_type_json(s))
  }
  for sc in self.schema.scalars {
    types.push(scalar_type_json(sc.name))
  }
  let mutation = match self.schema.mutation {
    Some(m) => self.named_type_ref(m)
    None => Json::null()
  }
  let subscription = match self.schema.subscription {
    Some(s) => self.named_type_ref(s)
    None => Json::null()
  }
  jobj([
    ("description", Json::null()),
    ("queryType", self.named_type_ref(self.schema.query)),
    ("mutationType", mutation),
    ("subscriptionType", subscription),
    ("types", types.to_json()),
    ("directives", self.directives_json()),
  ])
}