///|
/// GraphQL input value used for variables, literals after coercion, and
/// resolver arguments.
///
/// `InputEnum` preserves the distinction between enum tokens and strings while
/// allowing later coercion against enum definitions.
pub(all) enum InputValue {
  InputNull
  InputString(String)
  InputBoolean(Bool)
  InputInt(Int64)
  InputFloat(Double)
  InputEnum(String)
  InputList(Array[InputValue])
  InputObject(Map[String, InputValue])
} derive(Eq, Debug)

///|
/// Convert an input value into JSON for transport or diagnostics.
pub fn InputValue::to_json(self : InputValue) -> Json {
  match self {
    InputNull => Json::null()
    InputString(value) => Json::string(value)
    InputBoolean(value) => Json::boolean(value)
    InputInt(value) => Json::number(value.to_double(), repr=value.to_string())
    InputFloat(value) => Json::number(value)
    InputEnum(value) => Json::string(value)
    InputList(values) => Json::array(values.map(value => value.to_json()))
    InputObject(fields) => {
      let object : Map[String, Json] = Map([])
      for name, value in fields.iter2() {
        object[name] = value.to_json()
      }
      Json::object(object)
    }
  }
}

///|
/// Convert JSON request data into GraphQL input values.
///
/// JSON numbers with no fractional component become `InputInt`; other numbers
/// become `InputFloat`. Enum values cannot be represented distinctly in JSON and
/// are therefore parsed as `InputString`.
pub fn InputValue::from_json(json : Json) -> InputValue {
  match json {
    Null => InputNull
    True => InputBoolean(true)
    False => InputBoolean(false)
    Number(value, ..) => {
      let truncated = value.trunc()
      if value == truncated {
        InputInt(truncated.to_int64())
      } else {
        InputFloat(value)
      }
    }
    String(value) => InputString(value)
    Array(values) =>
      InputList(values.map(value => InputValue::from_json(value)))
    Object(fields) => {
      let object : Map[String, InputValue] = Map([])
      for name, value in fields.iter2() {
        object[name] = InputValue::from_json(value)
      }
      InputObject(object)
    }
  }
}

///|
/// Runtime value returned by resolvers and completed into GraphQL response data.
///
/// Use scalar variants for leaf fields, `GqlObject` for object-like values, and
/// `GqlTypedObject` when interface or union completion needs an explicit
/// runtime type.
pub(all) enum GqlValue {
  GqlNull
  GqlString(String)
  GqlBoolean(Bool)
  GqlInt(Int32)
  GqlInt64(Int64)
  GqlFloat(Double)
  GqlEnum(String)
  GqlList(Array[GqlValue])
  GqlObject(Map[String, GqlValue])
  GqlTypedObject(String, Map[String, GqlValue])
} derive(Eq, Debug)

///|
/// Construct an object value from a compact field array.
pub fn GqlValue::object(fields : ArrayView[(String, GqlValue)]) -> GqlValue {
  GqlObject(Map::from_array(fields))
}

///|
/// Construct an object value with an explicit GraphQL runtime type.
///
/// This is required when returning interface or union members whose concrete
/// type cannot be inferred from the field's declared type.
pub fn GqlValue::typed_object(
  type_name : String,
  fields : ArrayView[(String, GqlValue)],
) -> GqlValue {
  GqlTypedObject(type_name, Map::from_array(fields))
}

///|
/// Convert a completed GraphQL runtime value into JSON response data.
pub fn GqlValue::to_json(self : GqlValue) -> Json {
  match self {
    GqlNull => Json::null()
    GqlString(value) => Json::string(value)
    GqlBoolean(value) => Json::boolean(value)
    GqlInt(value) => Json::number(value.to_double(), repr=value.to_string())
    GqlInt64(value) => Json::number(value.to_double(), repr=value.to_string())
    GqlFloat(value) => Json::number(value)
    GqlEnum(value) => Json::string(value)
    GqlList(values) => Json::array(values.map(value => value.to_json()))
    GqlObject(fields) | GqlTypedObject(_, fields) => {
      let object : Map[String, Json] = Map([])
      for name, value in fields.iter2() {
        object[name] = value.to_json()
      }
      Json::object(object)
    }
  }
}

///|
/// Read a field from `GqlObject` or `GqlTypedObject`.
///
/// The executor uses this as the default resolver when no explicit field
/// resolver is registered.
pub fn GqlValue::field(self : GqlValue, name : String) -> GqlValue? {
  match self {
    GqlObject(fields) | GqlTypedObject(_, fields) => fields.get(name)
    _ => None
  }
}

///|
/// Return the explicit runtime type carried by a typed object, or `fallback`.
pub fn GqlValue::runtime_type(self : GqlValue, fallback~ : String) -> String {
  match self {
    GqlTypedObject(type_name, _) => type_name
    _ => fallback
  }
}

///|
/// Convert a MoonBit value into a resolver-facing GraphQL runtime value.
pub(open) trait ToGraphQL {
  fn to_graphql(Self) -> GqlValue
}

///|
/// Convert a coerced GraphQL input value into a MoonBit resolver argument type.
pub(open) trait FromGraphQL {
  fn from_graphql(InputValue) -> Self raise CoercionError
}

///|
pub impl ToGraphQL for String with fn to_graphql(self) {
  GqlString(self)
}

///|
pub impl FromGraphQL for String with fn from_graphql(value) {
  match value {
    InputString(value) | InputEnum(value) => value
    InputInt(value) => value.to_string()
    _ => coercion_error("expected String")
  }
}

///|
pub impl ToGraphQL for Bool with fn to_graphql(self) {
  GqlBoolean(self)
}

///|
pub impl FromGraphQL for Bool with fn from_graphql(value) {
  match value {
    InputBoolean(value) => value
    _ => coercion_error("expected Boolean")
  }
}

///|
pub impl ToGraphQL for Int32 with fn to_graphql(self) {
  GqlInt(self)
}

///|
pub impl FromGraphQL for Int32 with fn from_graphql(value) {
  match value {
    InputInt(value) => value.to_int()
    _ => coercion_error("expected Int")
  }
}

///|
pub impl ToGraphQL for Int64 with fn to_graphql(self) {
  GqlInt64(self)
}

///|
pub impl FromGraphQL for Int64 with fn from_graphql(value) {
  match value {
    InputInt(value) => value
    _ => coercion_error("expected Int64")
  }
}

///|
pub impl ToGraphQL for Double with fn to_graphql(self) {
  GqlFloat(self)
}

///|
pub impl FromGraphQL for Double with fn from_graphql(value) {
  match value {
    InputFloat(value) => value
    InputInt(value) => value.to_double()
    _ => coercion_error("expected Float")
  }
}

///|
pub impl ToGraphQL for Json with fn to_graphql(self) {
  gql_value_from_json(self)
}

///|
pub impl FromGraphQL for Json with fn from_graphql(value) {
  value.to_json()
}

///|
pub impl[T : ToGraphQL] ToGraphQL for T? with fn to_graphql(self) {
  match self {
    None => GqlNull
    Some(value) => value.to_graphql()
  }
}

///|
pub impl[T : FromGraphQL] FromGraphQL for T? with fn from_graphql(value) {
  match value {
    InputNull => None
    _ => Some(FromGraphQL::from_graphql(value))
  }
}

///|
pub impl[T : ToGraphQL] ToGraphQL for Array[T] with fn to_graphql(self) {
  GqlList(self.map(value => value.to_graphql()))
}

///|
pub impl[T : FromGraphQL] FromGraphQL for Array[T] with fn from_graphql(value) {
  match value {
    InputList(values) => values.map(value => FromGraphQL::from_graphql(value))
    _ => [FromGraphQL::from_graphql(value)]
  }
}

///|
/// Convert arbitrary JSON into a GraphQL response value.
///
/// This supports custom scalar resolvers and the internal introspection bridge.
fn gql_value_from_json(json : Json) -> GqlValue {
  match json {
    Null => GqlNull
    True => GqlBoolean(true)
    False => GqlBoolean(false)
    Number(value, ..) => GqlFloat(value)
    String(value) => GqlString(value)
    Array(values) => GqlList(values.map(value => gql_value_from_json(value)))
    Object(fields) => {
      let object : Map[String, GqlValue] = Map([])
      for name, value in fields.iter2() {
        object[name] = gql_value_from_json(value)
      }
      GqlObject(object)
    }
  }
}

///|
/// Segment in a GraphQL response error path.
pub(all) enum PathSegment {
  PathField(String)
  PathIndex(Int32)
} derive(Eq, Debug)

///|
/// Convert a path segment into the JSON representation used in GraphQL errors.
pub fn PathSegment::to_json(self : PathSegment) -> Json {
  match self {
    PathField(name) => Json::string(name)
    PathIndex(index) => Json::number(index.to_double(), repr=index.to_string())
  }
}

///|
/// 1-based source location used in GraphQL errors.
pub(all) struct SourceLocation {
  line : Int32
  column : Int32
} derive(Eq, Debug)

///|
/// Convert a source location into the JSON representation used in errors.
pub fn SourceLocation::to_json(self : SourceLocation) -> Json {
  Json::object({ "line": Json(self.line), "column": Json(self.column) })
}

///|
/// GraphQL error entry with message, optional path, and optional source
/// locations.
pub(all) struct GraphQLError {
  message : String
  path : Array[PathSegment]
  locations : Array[SourceLocation]
} derive(Eq, Debug)

///|
/// Create a GraphQL error, defaulting absent path and locations to empty arrays.
pub fn GraphQLError::new(
  message : String,
  path? : Array[PathSegment],
  locations? : Array[SourceLocation],
) -> GraphQLError {
  { message, path: path.unwrap_or([]), locations: locations.unwrap_or([]), }
}

///|
/// Convert a GraphQL error into the standard JSON response shape.
pub fn GraphQLError::to_json(self : GraphQLError) -> Json {
  let object : Map[String, Json] = { "message": Json::string(self.message) }
  if !self.path.is_empty() {
    object["path"] = Json::array(self.path.map(segment => segment.to_json()))
  }
  if !self.locations.is_empty() {
    object["locations"] = Json::array(self.locations.map(loc => loc.to_json()))
  }
  Json::object(object)
}

///|
/// Executable GraphQL request.
///
/// `query` contains the operation document, `operation_name` selects one
/// operation from multi-operation documents, and `variables` holds JSON-style
/// GraphQL input values keyed by variable name without the leading `$`.
pub(all) struct GraphQLRequest {
  query : String
  operation_name : String?
  variables : Map[String, InputValue]
} derive(Eq, Debug)

///|
/// Build a request with empty variables by default.
pub fn GraphQLRequest::new(
  query : String,
  operation_name? : String,
  variables? : Map[String, InputValue],
) -> GraphQLRequest {
  { query, operation_name, variables: variables.unwrap_or({}), }
}

///|
/// GraphQL execution response.
///
/// Successful field data is stored in `data`; parse, validation, coercion, and
/// resolver failures are accumulated in `errors`.
pub(all) struct GraphQLResponse {
  data : GqlValue?
  errors : Array[GraphQLError]
} derive(Eq, Debug)

///|
/// Create a response containing only `data`.
pub fn GraphQLResponse::data(data : GqlValue) -> GraphQLResponse {
  { data: Some(data), errors: [], }
}

///|
/// Create a response containing a single top-level error message.
pub fn GraphQLResponse::error(message : String) -> GraphQLResponse {
  { data: None, errors: [GraphQLError::new(message)], }
}

///|
/// Convert the response into the standard GraphQL JSON envelope.
pub fn GraphQLResponse::to_json(self : GraphQLResponse) -> Json {
  let object : Map[String, Json] = Map([])
  match self.data {
    Some(data) => object["data"] = data.to_json()
    None => ()
  }
  if !self.errors.is_empty() {
    object["errors"] = Json::array(self.errors.map(error => error.to_json()))
  }
  Json::object(object)
}