///|
/// Frontend Model -> ApiIr lowering.
///
/// Consumes the versioned Frontend Model JSON produced by the adapter
/// and builds a typed Canonical Client IR.

///|
/// Stable ordering for operations: by (path, method_rank, operation_id).
fn sort_operations(ops : Array[Json]) -> Array[Json] {
  let out : Array[Json] = []
  let mut rem = ops
  while rem.length() > 0 {
    let mut best = 0
    for i = 1; i < rem.length(); i = i + 1 {
      if before(rem[i], rem[best]) {
        best = i
      }
    }
    out.push(rem[best])
    rem = remove_at_json(rem, best)
  }
  out
}

///|
fn remove_at_json(xs : Array[Json], skip : Int) -> Array[Json] {
  let out : Array[Json] = []
  for i, x in xs {
    if i != skip {
      out.push(x)
    }
  }
  out
}

///|
fn method_rank(m : String) -> Int {
  if m == "DELETE" {
    0
  } else if m == "GET" {
    1
  } else if m == "POST" {
    2
  } else if m == "PUT" {
    3
  } else if m == "PATCH" {
    4
  } else {
    9
  }
}

///|
fn before(a : Json, b : Json) -> Bool {
  let pa = text(get(a, "path"))
  let pb = text(get(b, "path"))
  if pa != pb {
    pa < pb
  } else {
    let ra = method_rank(text(get(a, "method")))
    let rb = method_rank(text(get(b, "method")))
    if ra != rb {
      ra < rb
    } else {
      text(get(a, "operationId")) < text(get(b, "operationId"))
    }
  }
}

///|
/// Map string method to HttpMethod.
fn parse_method(m : String) -> HttpMethod {
  if m == "GET" {
    Get
  } else if m == "POST" {
    Post
  } else if m == "PUT" {
    Put
  } else if m == "PATCH" {
    Patch
  } else {
    Delete
  }
}

///|
/// Map string location to ParamLocation.
fn parse_location(loc : String) -> ParamLocation {
  if loc == "path" {
    Path
  } else if loc == "query" {
    Query
  } else {
    Header
  }
}

///|
/// Parse serialization style from JSON, applying OpenAPI defaults.
fn parse_style(style_json : Json, loc : String) -> SerializationStyle {
  let style_str = text(style_json)
  if style_str == "" {
    if loc == "query" {
      Form
    } else {
      Simple
    }
  } else if style_str == "simple" {
    Simple
  } else if style_str == "form" {
    Form
  } else {
    UnsupportedStyle(style_str)
  }
}

///|
/// Parse status code from a response JSON.
fn parse_status(r : Json) -> Int {
  let status_str = text(get(r, "status"))
  let mut n = 0
  for c in status_str {
    let ch = c.to_int()
    if ch >= 48 && ch <= 57 {
      n = n * 10 + ch - 48
    }
  }
  n
}

///|
/// Parse a schema JSON node into a model IR.
fn parse_model(schema_json : Json, model_name : String) -> ModelIr {
  let kind = text(get(schema_json, "kind"))
  if kind == "string" {
    let enum_values = arr(get(schema_json, "enum"))
    if enum_values.length() > 0 {
      let taken : Map[String, Bool] = Map([])
      let members : Array[(String, String)] = []
      for ev in enum_values {
        let wire = text(ev)
        let name = unique(pascal(wire), taken)
        members.push((name, wire))
      }
      return EnumModel({ name: model_name, members })
    }
  }
  // Parse as struct
  let properties = obj(get(schema_json, "properties"))
  let required_list = arr(get(schema_json, "required"))
  let required_set : Map[String, Bool] = Map([])
  for r in required_list {
    required_set[text(r)] = true
  }
  let taken_fields : Map[String, Bool] = Map([])
  let parsed_fields : Array[FieldIr] = []
  let opt_fields : Array[FieldIr] = []
  // `properties` is an unordered JSON map. The runtime Map preserves insertion
  // order, so walking it directly would leak the key order of the incoming
  // normalized JSON into the field order of the emitted struct.
  let property_names : Array[String] = []
  for name, _child_schema in properties {
    property_names.push(name)
  }
  for name in sorted_strings(property_names) {
    // The key came from this very map, so the indexed read cannot miss.
    let child_schema = properties[name]
    let is_req = required_set.contains(name)
    let nullable = truth(get(child_schema, "nullable"))
    let presence_val = compute_presence(is_req, nullable)
    let f = {
      name: unique(snake(name), taken_fields),
      wire_name: name,
      type_ref: map_schema(child_schema),
      presence: presence_val,
    }
    if is_req {
      parsed_fields.push(f)
    } else {
      opt_fields.push(f)
    }
  }
  for f in opt_fields {
    parsed_fields.push(f)
  }
  let has_additional = truth(get(schema_json, "additionalProperties"))
  let additional_field = if has_additional {
    Some(unique("additional_properties", taken_fields))
  } else {
    None
  }
  StructModel({
    name: model_name,
    fields: parsed_fields,
    additional_properties: has_additional,
    additional_properties_field: additional_field,
  })
}

///|
/// Parse an operation JSON node into an OperationIr.
fn parse_operation(
  op_json : Json,
  auth_kind_map : Map[String, String],
) -> OperationIr {
  let operation_id = text(get(op_json, "operationId"))
  let method_str = text(get(op_json, "method"))
  let path = text(get(op_json, "path"))
  let taken_params : Map[String, Bool] = Map([])
  taken_params["self"] = true
  taken_params["client"] = true
  let parsed_params : Array[ParameterIr] = []
  let opt_params : Array[ParameterIr] = []
  for p in arr(get(op_json, "parameters")) {
    let p_name = text(get(p, "name"))
    let sc = get(p, "schema")
    let moon_name = unique(snake(p_name), taken_params)
    let required = truth(get(p, "required"))
    let nullable = truth(get(sc, "nullable"))
    let param = {
      name: moon_name,
      wire_name: p_name,
      location: parse_location(text(get(p, "in"))),
      required,
      nullable,
      style: parse_style(get(p, "style"), text(get(p, "in"))),
      explode: truth(get(p, "explode")),
      type_ref: map_schema(sc),
      source: { pointer: "" },
    }
    if required {
      parsed_params.push(param)
    } else {
      opt_params.push(param)
    }
  }
  for p in opt_params {
    parsed_params.push(p)
  }
  // Parse request body
  let rb = match get(op_json, "requestBody") {
    Object(body_fields) => {
      let body_map : Map[String, Json] = body_fields
      let body_req = match body_map.get("required") {
        Some(v) => truth(v)
        None => false
      }
      let body_media = match body_map.get("media_type") {
        Some(String(s)) => Some(s)
        _ => None
      }
      let body_type = match body_map.get("schema") {
        Some(s) => Some(map_schema(s))
        None => None
      }
      Some({ type_ref: body_type, required: body_req, media_type: body_media })
    }
    _ => None
  }
  // Compute body parameter name from the type or default to "body"
  let body_name = match rb {
    Some(body) =>
      match body.type_ref {
        Some(Named(name)) => Some(unique(snake(name), taken_params))
        Some(_) => Some(unique("body", taken_params))
        None => None
      }
    None => None
  }
  // Parse responses
  let success : Array[ResponseIr] = []
  let errors : Array[ResponseIr] = []
  for r in arr(get(op_json, "responses")) {
    let status = parse_status(r)
    let rtype = match get(r, "schema") {
      Null => None
      x => Some(map_schema(x))
    }
    let media = match get(r, "media_type") {
      Null => None
      String(s) => Some(s)
      _ => None
    }
    let response = { status, type_ref: rtype, media_type: media }
    if status >= 200 && status < 300 {
      success.push(response)
    } else {
      errors.push(response)
    }
  }
  // Parse security using auth_kind_map
  let security : Array[String] = []
  for alt in arr(get(op_json, "security")) {
    let names_arr = arr(alt)
    for name_json in names_arr {
      let name = text(name_json)
      let label = match auth_kind_map.get(name) {
        Some(kind) => kind
        None => name
      }
      if !security.contains(label) {
        security.push(label)
      }
    }
  }
  // Parse tags
  let tags : Array[String] = []
  for t in arr(get(op_json, "tags")) {
    tags.push(text(t))
  }
  let fn_name = operation_fn_name(operation_id)
  let strategy = determine_response_strategy(success)
  {
    operation_id,
    fn_name,
    http_method: parse_method(method_str),
    path,
    tags,
    parameters: parsed_params,
    request_body: rb,
    body_name,
    success_responses: success,
    error_responses: errors,
    response_strategy: strategy,
    security,
    source: { pointer: "" },
  }
}

///|
/// Check if a media type is supported for JSON body responses.
fn is_supported_media_type(media_type : String?) -> Bool {
  match media_type {
    Some(mt) => mt == "application/json"
    None => true
  }
}

///|
/// Determine the response strategy from success responses.
fn determine_response_strategy(
  responses : Array[ResponseIr],
) -> ResponseStrategy {
  if responses.length() == 0 {
    return NoContent
  }
  let all_204 = {
    let mut ok = true
    for r in responses {
      if r.status != 204 {
        ok = false
      }
    }
    ok
  }
  if all_204 {
    return UnitResult
  }
  let with_body : Array[ResponseIr] = []
  for r in responses {
    match r.type_ref {
      Some(_) => with_body.push(r)
      None => ()
    }
  }
  if with_body.length() == 0 {
    return NoContent
  }
  for r in with_body {
    if !is_supported_media_type(r.media_type) {
      let mt_msg = match r.media_type {
        Some(mt) => mt
        None => "unknown"
      }
      return UnsupportedMediaType(mt_msg)
    }
  }
  let first_type = with_body[0].type_ref
  let all_same = {
    let mut ok = true
    for i = 1; i < with_body.length(); i = i + 1 {
      if with_body[i].type_ref != first_type {
        ok = false
      }
    }
    ok
  }
  if all_same {
    match first_type {
      Some(t) => return SingleResult(t)
      None => return NoContent
    }
  }
  let variants : Array[ResponseVariant] = []
  for r in with_body {
    match r.type_ref {
      Some(t) => variants.push({ status: r.status, type_ref: t })
      None => ()
    }
  }
  ResponseEnum(variants)
}

///|
/// Sort names deterministically.
///
/// Only call this for values OpenAPI models as *unordered* maps (schema names,
/// property names, security scheme names). OpenAPI arrays whose order carries
/// meaning (parameters, responses, enum members, servers, required, tags) keep
/// their source order and must never be routed through here.
fn sorted_strings(names : Array[String]) -> Array[String] {
  let out : Array[String] = []
  let mut rem = names
  while rem.length() > 0 {
    let mut best = 0
    for i = 1; i < rem.length(); i = i + 1 {
      if rem[i] < rem[best] {
        best = i
      }
    }
    out.push(rem[best])
    rem = remove_at_string(rem, best)
  }
  out
}

///|
fn remove_at_string(xs : Array[String], skip : Int) -> Array[String] {
  let out : Array[String] = []
  for i, x in xs {
    if i != skip {
      out.push(x)
    }
  }
  out
}

///|
/// Sort models: enums before structs, then by name.
fn sort_models(xs : Array[ModelIr]) -> Array[ModelIr] {
  let out : Array[ModelIr] = []
  let mut rem = xs
  while rem.length() > 0 {
    let mut best = 0
    for i = 1; i < rem.length(); i = i + 1 {
      let n_i = model_name(rem[i])
      let n_b = model_name(rem[best])
      let e_i = model_is_enum(rem[i])
      let e_b = model_is_enum(rem[best])
      if (e_i && !e_b) || (e_i == e_b && n_i < n_b) {
        best = i
      }
    }
    out.push(rem[best])
    rem = remove_at_model(rem, best)
  }
  out
}

///|
fn remove_at_model(xs : Array[ModelIr], skip : Int) -> Array[ModelIr] {
  let out : Array[ModelIr] = []
  for i, x in xs {
    if i != skip {
      out.push(x)
    }
  }
  out
}

///|
fn model_name(m : ModelIr) -> String {
  match m {
    EnumModel(e) => e.name
    StructModel(s) => s.name
  }
}

///|
fn model_is_enum(m : ModelIr) -> Bool {
  match m {
    EnumModel(_) => true
    _ => false
  }
}

///|
/// Parse the Frontend Model JSON root and produce an ApiIr.
pub fn lower(root : Json, module_name : String) -> ApiIr {
  let title = text(get(root, "title"))
  let version = text(get(root, "openapi"))
  // Servers
  let servers : Array[String] = []
  for s in arr(get(root, "servers")) {
    servers.push(text(s))
  }
  // Auth schemes + auth_kind_map
  let auth_schemes : Array[AuthSchemeIr] = []
  let auth_kind_map : Map[String, String] = Map([])
  // Same reasoning as struct properties: `securitySchemes` is an unordered map.
  let scheme_names : Array[String] = []
  for name, _scheme in obj(get(root, "securitySchemes")) {
    scheme_names.push(name)
  }
  for name in sorted_strings(scheme_names) {
    let scheme = obj(get(root, "securitySchemes"))[name]
    let kind = if text(get(scheme, "type")) == "http" &&
      text(get(scheme, "scheme")) == "bearer" {
      "bearer"
    } else if text(get(scheme, "type")) == "http" &&
      text(get(scheme, "scheme")) == "basic" {
      "basic"
    } else {
      text(get(scheme, "type"))
    }
    let location = match get(scheme, "in") {
      Null => None
      String(s) => Some(s)
      _ => None
    }
    let key_name = match get(scheme, "name") {
      Null => None
      String(s) => Some(s)
      _ => None
    }
    auth_schemes.push({ name, kind, location, key_name })
    auth_kind_map[name] = kind
  }
  // Models
  let models : Array[ModelIr] = []
  let taken_model_names : Map[String, Bool] = Map([])
  let schema_names : Array[String] = []
  for name, _ in obj(get(root, "schemas")) {
    schema_names.push(name)
  }
  for schema_name in sorted_strings(schema_names) {
    let model_name = unique(pascal(schema_name), taken_model_names)
    let schema = get(get(root, "schemas"), schema_name)
    models.push(parse_model(schema, model_name))
  }
  let sorted_models = sort_models(models)
  // Operations
  let ops = arr(get(root, "operations"))
  let sorted_ops = sort_operations(ops)
  let operations : Array[OperationIr] = []
  for op in sorted_ops {
    operations.push(parse_operation(op, auth_kind_map))
  }
  {
    module_name,
    title,
    version,
    servers,
    auth_schemes,
    models: sorted_models,
    operations,
  }
}