///|
/// Formal Frontend Adapter for oas2moon Phase 1.
///
/// Parsing is delegated to `Han-Wentao/mooncontract`. This program re-emits the
/// parsed model as deterministic JSON (the Frontend Model) so that the Client IR
/// builder can consume it. It never generates code and never walks raw OpenAPI
/// JSON to drive codegen.
///
/// The only raw-JSON access is the narrow sidecar below: mooncontract does not
/// expose `servers`, `components.securitySchemes` or `security`, so those fields
/// are read directly and merged into the same Frontend Model. No second OpenAPI
/// parser is created here.
fn string_or_null(value : String?) -> Json {
  match value {
    Some(text) => Json::string(text)
    None => Json::null()
  }
}

///|
/// Raw sidecar: read one key, `null` when absent.
#warnings("-deprecated")
fn raw_get(json : Json, key : String) -> Json {
  match json.value(key) {
    Some(value) => value
    None => Json::null()
  }
}

///|
#warnings("-deprecated")
fn raw_as_array(json : Json) -> Array[Json]? {
  json.as_array()
}

///|
#warnings("-deprecated")
fn raw_as_object(json : Json) -> Map[String, Json]? {
  json.as_object()
}

///|
#warnings("-deprecated")
fn raw_as_string(json : Json) -> String? {
  json.as_string()
}

///|
fn raw_boolean(json : Json) -> Bool? {
  match json {
    True => Some(true)
    False => Some(false)
    _ => None
  }
}

///|
fn raw_array(json : Json) -> Array[Json] {
  match raw_as_array(json) {
    Some(values) => values
    None => []
  }
}

///|
fn raw_object_entries(json : Json) -> Map[String, Json] {
  match raw_as_object(json) {
    Some(entries) => entries
    None => Map([])
  }
}

///|
/// Build an object whose keys are inserted in `keys` order.
///
/// JSON object key order carries no meaning, so the Frontend Model never
/// forwards source order: every unordered map is re-emitted through this
/// helper with sorted keys. The runtime `Map` preserves insertion order, so
/// without this the parser would leak document order into the normalized
/// model.
fn object_with_sorted_keys(
  keys : Array[String],
  lookup : (String) -> Json,
) -> Json {
  let out : Map[String, Json] = Map([])
  for key in keys {
    out[key] = lookup(key)
  }
  Json::object(out)
}

///|
/// The keys of a JSON object in a deterministic (ascending) order.
fn sorted_object_keys(json : Json) -> Array[String] {
  let keys : Array[String] = []
  for key, _value in raw_object_entries(json) {
    keys.push(key)
  }
  sort_keys(keys)
}

///|
/// Selection sort with strictly ascending comparison, so equal keys keep their
/// relative order. Mirrors `sorted_strings` in the core authority.
fn sort_keys(keys : Array[String]) -> Array[String] {
  let out : Array[String] = []
  let mut rem = keys
  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])
    let rest : Array[String] = []
    for i, key in rem {
      if i != best {
        rest.push(key)
      }
    }
    rem = rest
  }
  out
}

///|
/// Declared server URLs, in document order.
fn raw_servers(root : Json) -> Json {
  let urls : Array[Json] = []
  for server in raw_array(raw_get(root, "servers")) {
    match raw_as_string(raw_get(server, "url")) {
      Some(url) => urls.push(Json::string(url))
      None => ()
    }
  }
  Json::array(urls)
}

///|
/// Declared security schemes.
fn raw_security_schemes(root : Json) -> Json {
  let schemes = raw_get(raw_get(root, "components"), "securitySchemes")
  object_with_sorted_keys(sorted_object_keys(schemes), name => {
    let scheme = raw_get(schemes, name)
    Json::object(
      Map([
        ("type", string_or_null(raw_as_string(raw_get(scheme, "type")))),
        ("scheme", string_or_null(raw_as_string(raw_get(scheme, "scheme")))),
        ("in", string_or_null(raw_as_string(raw_get(scheme, "in")))),
        ("name", string_or_null(raw_as_string(raw_get(scheme, "name")))),
      ]),
    )
  })
}

///|
/// `security` is a list of alternatives; each alternative lists scheme names.
/// Scopes are kept at the adapter boundary only as scheme names; the Phase 1
/// model deliberately does not claim OAuth-flow semantics.
fn raw_security_alternatives(owner : Json) -> Array[Json] {
  let out : Array[Json] = []
  for alternative in raw_array(raw_get(owner, "security")) {
    let names : Array[Json] = []
    for name, _scopes in raw_object_entries(alternative) {
      names.push(Json::string(name))
    }
    out.push(Json::array(names))
  }
  out
}

///|
/// Operation-level security, falling back to the document root when the
/// operation does not declare its own `security` list.
fn raw_operation_security(
  root : Json,
  http_method : String,
  path : String,
) -> Array[Json] {
  let operation = raw_get(
    raw_get(raw_get(root, "paths"), path),
    http_method.to_lower(),
  )
  match raw_as_array(raw_get(operation, "security")) {
    Some(_) => raw_security_alternatives(operation)
    None => raw_security_alternatives(root)
  }
}

///|
fn schema_to_json(schema : @openapi.Schema) -> Json {
  let property_names : Array[String] = []
  for name, _child in schema.properties {
    property_names.push(name)
  }
  // Keys come from the same map, so the indexed read cannot miss.
  let properties = object_with_sorted_keys(sort_keys(property_names), name => {
    schema_to_json(schema.properties[name])
  })
  let required : Array[Json] = []
  for name in schema.required {
    required.push(Json::string(name))
  }
  let items = match schema.items {
    Some(child) => schema_to_json(child)
    None => Json::null()
  }
  Json::object(
    Map([
      ("kind", Json::string(schema.kind.label())),
      ("ref", string_or_null(schema.ref_path)),
      ("format", string_or_null(schema.format)),
      ("nullable", Json::boolean(schema.nullable)),
      ("required", Json::array(required)),
      ("properties", properties),
      ("items", items),
      ("enum", Json::array(schema.enum_values.copy())),
      ("additionalProperties", Json::boolean(schema.additional_properties)),
    ]),
  )
}

///|
fn parameter_to_json(
  parameter : @openapi.Parameter,
  index : Int,
  root : Json,
  path : String,
  http_method : String,
) -> Json {
  let (style, explode) = raw_parameter_serialization(
    root, path, http_method, index,
  )
  Json::object(
    Map([
      ("name", Json::string(parameter.name)),
      ("in", Json::string(parameter.location.label())),
      ("required", Json::boolean(parameter.required)),
      ("schema", schema_to_json(parameter.schema)),
      ("ref", string_or_null(parameter.ref_path)),
      ("style", style),
      ("explode", explode),
    ]),
  )
}

///|
fn body_to_json(body : @openapi.BodySpec) -> Json {
  Json::object(
    Map([
      ("required", Json::boolean(body.required)),
      ("media_type", Json::string(body.media_type)),
      ("schema", schema_to_json(body.schema)),
    ]),
  )
}

///|
fn response_to_json(response : @openapi.ResponseSpec) -> Json {
  let schema = match response.schema {
    Some(value) => schema_to_json(value)
    None => Json::null()
  }
  Json::object(
    Map([
      ("status", Json::string(response.status)),
      ("description", Json::string(response.description)),
      ("media_type", string_or_null(response.media_type)),
      ("schema", schema),
    ]),
  )
}

///|
fn operation_to_json(
  operation : @openapi.Operation,
  security : Array[Json],
  root : Json,
) -> Json {
  let parameters : Array[Json] = []
  for index, parameter in operation.parameters {
    parameters.push(
      parameter_to_json(
        parameter,
        index,
        root,
        operation.path,
        operation.http_method.label(),
      ),
    )
  }
  let responses : Array[Json] = []
  for response in operation.responses {
    responses.push(response_to_json(response))
  }
  let body = match operation.request_body {
    Some(value) => body_to_json(value)
    None => Json::null()
  }
  Json::object(
    Map([
      ("operationId", Json::string(operation.operation_id)),
      ("method", Json::string(operation.http_method.label())),
      ("path", Json::string(operation.path)),
      ("security", Json::array(security)),
      ("parameters", Json::array(parameters)),
      ("requestBody", body),
      ("responses", Json::array(responses)),
    ]),
  )
}

///|
/// Whether a path names a YAML document. YAML is a convenience input; JSON stays
/// the authoritative format.
fn is_yaml_path(path : String) -> Bool {
  path.has_suffix(".yaml") || path.has_suffix(".yml")
}

///|
/// Convert a YAML document into JSON using the ecosystem YAML parser so that the
/// raw sidecar sees the same shape as the JSON input path.
fn raw_from_yaml(source : String) -> Json raise {
  let documents = @yaml.Yaml::load_from_string(source)
  match documents.get(0) {
    Some(document) => @json.to_json(document)
    None => Json::null()
  }
}

///|
fn main raise {
  let args = @env.args()
  guard args.length() >= 3 else {
    println("usage: oas2moon-frontend  ")
    @sys.exit(2)
    return
  }
  let source = @fs.read_file_to_string(args[1])
  let yaml_input = is_yaml_path(args[1])
  let raw = if yaml_input { raw_from_yaml(source) } else { @json.parse(source) }
  let parsed = if yaml_input {
    @openapi.parse_yaml(source)
  } else {
    @openapi.parse_json(source)
  }
  match parsed {
    Err(diagnostics) => {
      for diagnostic in diagnostics {
        println(diagnostic.to_text())
      }
      @sys.exit(3)
    }
    Ok(document) => {
      let operations : Array[Json] = []
      for operation in document.operations {
        operations.push(
          operation_to_json(
            operation,
            raw_operation_security(
              raw,
              operation.http_method.label(),
              operation.path,
            ),
            raw,
          ),
        )
      }
      let schema_names : Array[String] = []
      for name, _schema in document.components.schemas {
        schema_names.push(name)
      }
      // Keys come from the same map, so the indexed read cannot miss.
      let schemas = object_with_sorted_keys(sort_keys(schema_names), name => {
        schema_to_json(document.components.schemas[name])
      })
      let normalized = Json::object(
        Map([
          ("frontendModelVersion", Json::number(1.0)),
          ("openapi", Json::string(document.version)),
          ("title", Json::string(document.title)),
          ("servers", raw_servers(raw)),
          ("securitySchemes", raw_security_schemes(raw)),
          ("security", Json::array(raw_security_alternatives(raw))),
          ("operations", Json::array(operations)),
          ("schemas", schemas),
          ("issues", raw_unsupported_issues(raw)),
          ("ignored", raw_ignored_issues(raw)),
        ]),
      )
      @fs.write_string_to_file(args[2], normalized.stringify(indent=2) + "\n")
    }
  }
}