///|
/// Strict in-memory view of the Canonical Client IR consumed by the codegen
/// emitters. JSON names, MoonBit names, presence and wire strings all arrive
/// precomputed from the Phase 1.5 authority; this module only validates and
/// copies them.

///|
pub struct TypeRef {
  kind : String
  name : String?
  item : TypeRef?
} derive(Eq, Debug)

///|
pub struct FieldIr {
  name : String
  wire_name : String
  type_ref : TypeRef
  presence : String
} derive(Eq, Debug)

///|
pub struct EnumMemberIr {
  name : String
  wire : String
} derive(Eq, Debug)

///|
pub struct EnumModelIr {
  name : String
  members : Array[EnumMemberIr]
} derive(Eq, Debug)

///|
pub struct StructModelIr {
  name : String
  fields : Array[FieldIr]
  additional_properties : Bool
  additional_properties_field : String?
} derive(Eq, Debug)

///|
pub enum ModelIr {
  EnumModel(EnumModelIr)
  StructModel(StructModelIr)
} derive(Eq, Debug)

///|
pub enum ParamLocation {
  PathLoc
  QueryLoc
  HeaderLoc
} derive(Eq, Debug)

///|
pub struct ParameterIr {
  name : String
  wire_name : String
  location : ParamLocation
  type_ref : TypeRef
  required : Bool
  nullable : Bool
  presence : String
} derive(Eq, Debug)

///|
pub struct RequestBodyIr {
  type_ref : TypeRef?
  required : Bool
  media_type : String?
} derive(Eq, Debug)

///|
pub struct ResponseIr {
  status : Int
  type_ref : TypeRef?
  media_type : String?
} derive(Eq, Debug)

///|
pub struct ResponseVariant {
  status : Int
  type_ref : TypeRef
} derive(Eq, Debug)

///|
pub enum ResponseStrategy {
  SingleResult(TypeRef)
  UnitResult
  ResponseEnum(Array[ResponseVariant])
  NoContent
  UnsupportedMediaType(String)
} derive(Eq, Debug)

///|
pub struct AuthSchemeIr {
  name : String
  kind : String
  location : String?
  key_name : String?
} derive(Eq, Debug)

///|
pub struct OperationIr {
  operation_id : String
  fn_name : String
  http_method : String
  path : String
  tags : Array[String]
  parameters : Array[ParameterIr]
  request_body : RequestBodyIr?
  body_name : String?
  success_responses : Array[ResponseIr]
  error_responses : Array[ResponseIr]
  response_strategy : ResponseStrategy
  security : Array[String]
  source_pointer : String
} derive(Eq, Debug)

///|
pub struct ApiIr {
  module_name : String
  title : String
  version : String
  servers : Array[String]
  auth_schemes : Array[AuthSchemeIr]
  models : Array[ModelIr]
  operations : Array[OperationIr]
} derive(Eq, Debug)

// ---------------------------------------------------------------------------
// JSON helpers
// ---------------------------------------------------------------------------

///|
fn require_object(value : Json, location : String) -> Map[String, Json] raise {
  match value {
    Object(fields) => fields
    _ => raise Failure(location + ": expected object")
  }
}

///|
fn require_array(value : Json, location : String) -> Array[Json] raise {
  match value {
    Array(items) => items
    _ => raise Failure(location + ": expected array")
  }
}

///|
fn require_string(value : Json, location : String) -> String raise {
  match value {
    String(text) => text
    _ => raise Failure(location + ": expected string")
  }
}

///|
fn require_bool(value : Json, location : String) -> Bool raise {
  match value {
    True => true
    False => false
    _ => raise Failure(location + ": expected boolean")
  }
}

///|
fn require_field(
  fields : Map[String, Json],
  key : String,
  location : String,
) -> Json raise {
  match fields.get(key) {
    Some(value) => value
    None => raise Failure(location + ": missing field " + key.escape())
  }
}

///|
fn require_string_field(
  fields : Map[String, Json],
  key : String,
  location : String,
) -> String raise {
  require_string(require_field(fields, key, location), location + "." + key)
}

///|
fn require_bool_field(
  fields : Map[String, Json],
  key : String,
  location : String,
) -> Bool raise {
  require_bool(require_field(fields, key, location), location + "." + key)
}

///|
fn require_array_field(
  fields : Map[String, Json],
  key : String,
  location : String,
) -> Array[Json] raise {
  require_array(require_field(fields, key, location), location + "." + key)
}

///|
fn optional_string_field(
  fields : Map[String, Json],
  key : String,
  location : String,
) -> String? raise {
  match fields.get(key) {
    Some(Null) => None
    Some(value) => Some(require_string(value, location + "." + key))
    None => raise Failure(location + ": missing field " + key.escape())
  }
}

///|
fn optional_type_ref_field(
  fields : Map[String, Json],
  key : String,
  location : String,
) -> TypeRef? raise {
  match fields.get(key) {
    Some(Null) => None
    Some(value) => Some(parse_type_ref(value, location + "." + key))
    None => raise Failure(location + ": missing field " + key.escape())
  }
}

// ---------------------------------------------------------------------------
// Type parsing
// ---------------------------------------------------------------------------

///|
fn is_known_scalar(name : String) -> Bool {
  name == "String" ||
  name == "Bool" ||
  name == "Int" ||
  name == "Int64" ||
  name == "Double" ||
  name == "Json"
}

///|
fn parse_type_ref(value : Json, location : String) -> TypeRef raise {
  let fields = require_object(value, location)
  let kind = require_string_field(fields, "kind", location)
  match kind {
    "scalar" => {
      let name = require_string_field(fields, "name", location)
      if !is_known_scalar(name) {
        raise Failure(location + ": unsupported scalar " + name.escape())
      }
      { kind, name: Some(name), item: None }
    }
    "named" => {
      let name = require_string_field(fields, "name", location)
      if name.length() == 0 {
        raise Failure(location + ": named type name must not be empty")
      }
      { kind, name: Some(name), item: None }
    }
    "array" => {
      let item_value = require_field(fields, "item", location)
      let item = parse_type_ref(item_value, location + ".item")
      { kind, name: None, item: Some(item) }
    }
    _ => raise Failure(location + ": unsupported type kind " + kind.escape())
  }
}

///|
fn expected_presence(required : Bool, nullable : Bool) -> String {
  if required && nullable {
    "required_nullable"
  } else if required {
    "required"
  } else if nullable {
    "optional_nullable"
  } else {
    "optional"
  }
}

// ---------------------------------------------------------------------------
// Model parsing
// ---------------------------------------------------------------------------

///|
fn parse_field(value : Json, location : String) -> FieldIr raise {
  let fields = require_object(value, location)
  let name = require_string_field(fields, "name", location)
  let wire_name = require_string_field(fields, "wire_name", location)
  let required = require_bool_field(fields, "required", location)
  let nullable = require_bool_field(fields, "nullable", location)
  let presence = require_string_field(fields, "presence", location)
  let expected = expected_presence(required, nullable)
  if presence != expected {
    raise Failure(
      location +
      ": presence mismatch: got " +
      presence.escape() +
      ", expected " +
      expected,
    )
  }
  let type_value = require_field(fields, "type", location)
  let type_ref = parse_type_ref(type_value, location + ".type")
  { name, wire_name, type_ref, presence }
}

///|
fn parse_enum_member(value : Json, location : String) -> (String, String) raise {
  let pair = require_array(value, location)
  guard pair.length() == 2 else {
    raise Failure(location + ": enum member must be a 2-element array")
  }
  let name = require_string(pair[0], location + "[0]")
  let wire = require_string(pair[1], location + "[1]")
  (name, wire)
}

///|
fn parse_enum(
  fields : Map[String, Json],
  location : String,
) -> EnumModelIr raise {
  let name = require_string_field(fields, "name", location)
  let member_values = require_array_field(fields, "members", location)
  let members : Array[EnumMemberIr] = []
  let taken_names : Map[String, Bool] = Map([])
  let taken_wires : Map[String, Bool] = Map([])
  for index, value in member_values {
    let member_where = location + ".members[" + index.to_string() + "]"
    let (n, w) = parse_enum_member(value, member_where)
    if taken_names.get(n) is Some(_) {
      raise Failure(member_where + ": duplicate enum member name")
    }
    if taken_wires.get(w) is Some(_) {
      raise Failure(member_where + ": duplicate enum wire value")
    }
    taken_names[n] = true
    taken_wires[w] = true
    members.push({ name: n, wire: w })
  }
  { name, members }
}

///|
fn parse_struct(
  fields : Map[String, Json],
  location : String,
) -> StructModelIr raise {
  let name = require_string_field(fields, "name", location)
  let additional_properties = require_bool_field(
    fields, "additional_properties", location,
  )
  let additional_properties_field = optional_string_field(
    fields, "additional_properties_field", location,
  )
  if additional_properties && additional_properties_field is None {
    raise Failure(
      location + ": additionalProperties=true requires a field name",
    )
  }
  if !additional_properties && additional_properties_field is Some(_) {
    raise Failure(
      location + ": additionalProperties=false must use a null field name",
    )
  }
  let field_values = require_array_field(fields, "fields", location)
  let parsed_fields : Array[FieldIr] = []
  let taken_names : Map[String, Bool] = Map([])
  let taken_wires : Map[String, Bool] = Map([])
  for index, value in field_values {
    let field_where = location + ".fields[" + index.to_string() + "]"
    let field = parse_field(value, field_where)
    if taken_names.get(field.name) is Some(_) {
      raise Failure(field_where + ": duplicate field name")
    }
    if taken_wires.get(field.wire_name) is Some(_) {
      raise Failure(field_where + ": duplicate wire_name")
    }
    taken_names[field.name] = true
    taken_wires[field.wire_name] = true
    parsed_fields.push(field)
  }
  match additional_properties_field {
    Some(extra_name) =>
      if taken_names.get(extra_name) is Some(_) {
        raise Failure(
          location +
          ": additional properties field collides with a declared field",
        )
      }
    None => ()
  }
  {
    name,
    fields: parsed_fields,
    additional_properties,
    additional_properties_field,
  }
}

///|
fn validate_named_type(
  type_ref : TypeRef,
  location : String,
  model_names : Map[String, Bool],
) -> Unit raise {
  match type_ref.kind {
    "named" =>
      match type_ref.name {
        Some(name) =>
          if model_names.get(name) is None {
            raise Failure(location + ": unresolved named type " + name.escape())
          }
        None => abort("validated named type is missing its name")
      }
    "array" =>
      match type_ref.item {
        Some(item) => validate_named_type(item, location + ".item", model_names)
        None => abort("validated array type is missing its item")
      }
    _ => ()
  }
}

///|
fn validate_model_references(
  model : ModelIr,
  model_names : Map[String, Bool],
) -> Unit raise {
  match model {
    EnumModel(_) => ()
    StructModel(value) =>
      for index, field in value.fields {
        validate_named_type(
          field.type_ref,
          "canonical.models." +
          value.name +
          ".fields[" +
          index.to_string() +
          "].type",
          model_names,
        )
      }
  }
}

///|
fn parse_model(value : Json, location : String) -> ModelIr raise {
  let fields = require_object(value, location)
  let kind = require_string_field(fields, "kind", location)
  match kind {
    "enum" => EnumModel(parse_enum(fields, location))
    "struct" => StructModel(parse_struct(fields, location))
    _ => raise Failure(location + ": unsupported model kind " + kind.escape())
  }
}

// ---------------------------------------------------------------------------
// Operation parsing
// ---------------------------------------------------------------------------

///|
fn parse_param_location(loc : String, location : String) -> ParamLocation raise {
  match loc {
    "path" => PathLoc
    "query" => QueryLoc
    "header" => HeaderLoc
    _ =>
      raise Failure(
        location + ": unsupported parameter location " + loc.escape(),
      )
  }
}

///|
fn parse_parameter(value : Json, location : String) -> ParameterIr raise {
  let fields = require_object(value, location)
  let name = require_string_field(fields, "name", location)
  let wire_name = require_string_field(fields, "wire_name", location)
  let loc_str = require_string_field(fields, "location", location)
  let loc = parse_param_location(loc_str, location + ".location")
  let required = require_bool_field(fields, "required", location)
  let nullable = require_bool_field(fields, "nullable", location)
  let presence = require_string_field(fields, "presence", location)
  let expected = expected_presence(required, nullable)
  if presence != expected {
    raise Failure(
      location +
      ": presence mismatch: got " +
      presence.escape() +
      ", expected " +
      expected,
    )
  }
  let type_value = require_field(fields, "type", location)
  let type_ref = parse_type_ref(type_value, location + ".type")
  { name, wire_name, location: loc, type_ref, required, nullable, presence }
}

///|
fn parse_request_body(value : Json, location : String) -> RequestBodyIr? raise {
  match value {
    Null => None
    _ => {
      let fields = require_object(value, location)
      let type_ref = optional_type_ref_field(fields, "type", location)
      let required = require_bool_field(fields, "required", location)
      let media_type = optional_string_field(fields, "media_type", location)
      Some({ type_ref, required, media_type })
    }
  }
}

///|
fn parse_response(value : Json, location : String) -> ResponseIr raise {
  let fields = require_object(value, location)
  let status = match require_field(fields, "status", location) {
    Number(n, ..) if n == n.trunc() => n.to_int()
    _ => raise Failure(location + ".status: expected number")
  }
  let type_ref = optional_type_ref_field(fields, "type", location)
  let media_type = optional_string_field(fields, "media_type", location)
  { status, type_ref, media_type }
}

///|
fn parse_responses_array(
  value : Json,
  location : String,
) -> Array[ResponseIr] raise {
  let items = require_array(value, location)
  let out : Array[ResponseIr] = []
  for index, item in items {
    out.push(parse_response(item, location + "[" + index.to_string() + "]"))
  }
  out
}

///|
fn parse_response_strategy(
  value : Json,
  location : String,
) -> ResponseStrategy raise {
  let fields = require_object(value, location)
  let kind = require_string_field(fields, "kind", location)
  match kind {
    "single_result" => {
      let type_value = require_field(fields, "type", location)
      SingleResult(parse_type_ref(type_value, location + ".type"))
    }
    "unit_result" => UnitResult
    "no_content" => NoContent
    "response_enum" => {
      let variants_value = require_field(fields, "variants", location)
      let variant_items = require_array(variants_value, location + ".variants")
      let variants : Array[ResponseVariant] = []
      for index, vv in variant_items {
        let vloc = location + ".variants[" + index.to_string() + "]"
        let vf = require_object(vv, vloc)
        let status = match require_field(vf, "status", vloc) {
          Number(n, ..) if n == n.trunc() => n.to_int()
          _ => raise Failure(vloc + ".status: expected number")
        }
        let tv = require_field(vf, "type", vloc)
        let type_ref = parse_type_ref(tv, vloc + ".type")
        variants.push({ status, type_ref })
      }
      ResponseEnum(variants)
    }
    "unsupported_media_type" => {
      let mt = require_string_field(fields, "media_type", location)
      UnsupportedMediaType(mt)
    }
    _ =>
      raise Failure(
        location + ": unsupported response strategy kind " + kind.escape(),
      )
  }
}

///|
fn parse_strings_array(value : Json, location : String) -> Array[String] raise {
  let items = require_array(value, location)
  let out : Array[String] = []
  for index, item in items {
    out.push(require_string(item, location + "[" + index.to_string() + "]"))
  }
  out
}

///|
fn parse_auth_scheme(value : Json, location : String) -> AuthSchemeIr raise {
  let fields = require_object(value, location)
  let name = require_string_field(fields, "name", location)
  let kind = require_string_field(fields, "kind", location)
  let loc = optional_string_field(fields, "location", location)
  let key_name = optional_string_field(fields, "key_name", location)
  { name, kind, location: loc, key_name }
}

///|
fn parse_auth_schemes(
  value : Json,
  location : String,
) -> Array[AuthSchemeIr] raise {
  let items = require_array(value, location)
  let out : Array[AuthSchemeIr] = []
  for index, item in items {
    out.push(parse_auth_scheme(item, location + "[" + index.to_string() + "]"))
  }
  out
}

///|
fn parse_operation(
  value : Json,
  location : String,
  model_names : Map[String, Bool],
) -> OperationIr raise {
  let fields = require_object(value, location)
  let operation_id = require_string_field(fields, "operation_id", location)
  let fn_name = require_string_field(fields, "fn_name", location)
  let http_method = require_string_field(fields, "http_method", location)
  let path = require_string_field(fields, "path", location)
  let tags = match fields.get("tags") {
    Some(tv) => parse_strings_array(tv, location + ".tags")
    None => []
  }
  let parameters : Array[ParameterIr] = []
  match fields.get("parameters") {
    Some(pv) => {
      let pitems = require_array(pv, location + ".parameters")
      for index, p in pitems {
        parameters.push(
          parse_parameter(
            p,
            location + ".parameters[" + index.to_string() + "]",
          ),
        )
      }
    }
    None => ()
  }
  for index, p in parameters {
    validate_named_type(
      p.type_ref,
      location + ".parameters[" + index.to_string() + "].type",
      model_names,
    )
  }
  let request_body = match fields.get("request_body") {
    Some(rv) => parse_request_body(rv, location + ".request_body")
    None => None
  }
  let body_name = match fields.get("body_name") {
    Some(Null) => None
    Some(value) => Some(require_string(value, location + ".body_name"))
    None => None
  }
  match request_body {
    Some(body) =>
      match body.type_ref {
        Some(tr) =>
          validate_named_type(tr, location + ".request_body.type", model_names)
        None => ()
      }
    None => ()
  }
  let success_responses = match fields.get("success_responses") {
    Some(sv) => parse_responses_array(sv, location + ".success_responses")
    None => []
  }
  let error_responses = match fields.get("error_responses") {
    Some(ev) => parse_responses_array(ev, location + ".error_responses")
    None => []
  }
  for index, r in success_responses {
    match r.type_ref {
      Some(tr) =>
        validate_named_type(
          tr,
          location + ".success_responses[" + index.to_string() + "].type",
          model_names,
        )
      None => ()
    }
  }
  let response_strategy = match fields.get("response_strategy") {
    Some(rsv) => parse_response_strategy(rsv, location + ".response_strategy")
    None => raise Failure(location + ": missing field response_strategy")
  }
  match response_strategy {
    SingleResult(tr) =>
      validate_named_type(tr, location + ".response_strategy.type", model_names)
    ResponseEnum(variants) =>
      for index, v in variants {
        validate_named_type(
          v.type_ref,
          location +
          ".response_strategy.variants[" +
          index.to_string() +
          "].type",
          model_names,
        )
      }
    _ => ()
  }
  let security = match fields.get("security") {
    Some(sv) => parse_strings_array(sv, location + ".security")
    None => []
  }
  let source_pointer = match fields.get("source_pointer") {
    Some(sp) => require_string(sp, location + ".source_pointer")
    None => ""
  }
  {
    operation_id,
    fn_name,
    http_method,
    path,
    tags,
    parameters,
    request_body,
    body_name,
    success_responses,
    error_responses,
    response_strategy,
    security,
    source_pointer,
  }
}

// ---------------------------------------------------------------------------
// Top-level API parsing
// ---------------------------------------------------------------------------

///|
pub fn parse_api(value : Json) -> ApiIr raise {
  let root = require_object(value, "canonical")
  let module_name = require_string_field(root, "module", "canonical")
  let title = match root.get("title") {
    Some(t) => require_string(t, "canonical.title")
    None => ""
  }
  let version = match root.get("version") {
    Some(v) => require_string(v, "canonical.version")
    None => ""
  }
  let servers = match root.get("servers") {
    Some(sv) => parse_strings_array(sv, "canonical.servers")
    None => []
  }
  let auth_schemes = match root.get("auth_schemes") {
    Some(av) => parse_auth_schemes(av, "canonical.auth_schemes")
    None => []
  }
  let model_values = require_array_field(root, "models", "canonical")
  let models : Array[ModelIr] = []
  let model_names : Map[String, Bool] = Map([])
  for index, model_value in model_values {
    let location = "canonical.models[" + index.to_string() + "]"
    let model = parse_model(model_value, location)
    let name = match model {
      EnumModel(value) => value.name
      StructModel(value) => value.name
    }
    if model_names.get(name) is Some(_) {
      raise Failure(location + ": duplicate model name " + name.escape())
    }
    model_names[name] = true
    models.push(model)
  }
  for model in models {
    validate_model_references(model, model_names)
  }
  let operations : Array[OperationIr] = []
  match root.get("operations") {
    Some(ov) => {
      let op_items = require_array(ov, "canonical.operations")
      for index, op_value in op_items {
        let op_loc = "canonical.operations[" + index.to_string() + "]"
        operations.push(parse_operation(op_value, op_loc, model_names))
      }
    }
    None => ()
  }
  { module_name, title, version, servers, auth_schemes, models, operations }
}