///|
/// A prepared conversion context for protocol converters that need the complete
/// schema document plus schemas for individual members or filtered structures.
/// The encoded model and contextual names are prepared once, not per binding.
pub struct SchemaIndex {
  shapes : Map[String, Json]
  names : Map[String, String]
  config : Config
  priv value_contracts : Bool
  document : Json
  definitions : Map[String, @smithy.ShapeId]
}

///|
pub fn SchemaIndex::new(
  model : @smithy.Model,
  service~ : String = "",
  config~ : Config = Config::new(),
) -> SchemaIndex raise {
  SchemaIndex::with_profile(model, service~, config~, value_contracts=false)
}

///|
/// Prepare value-compatible schemas without changing reference conversion.
pub fn SchemaIndex::for_values(
  model : @smithy.Model,
  service~ : String = "",
  config~ : Config = Config::new(),
) -> SchemaIndex raise {
  SchemaIndex::with_profile(model, service~, config~, value_contracts=true)
}

///|
fn SchemaIndex::with_profile(
  model : @smithy.Model,
  service~ : String = "",
  config~ : Config = Config::new(),
  value_contracts~ : Bool = false,
) -> SchemaIndex raise {
  let document = convert_with_profile(
    model,
    service~,
    config~,
    value_contracts~,
  )
  let config : ConversionConfig = { settings: config, value_contracts, }
  let ast = @syntax.encode_json_ast(model.to_json_ast())
  guard ast is { "shapes": Object(shapes), .. } else {
    raise InvalidConfiguration("Model has no shape table")
  }
  let names = schema_names(shapes, service, config)
  let connected = connected_shapes(service, shapes)
  if service == "" {
    for id, _ in shapes {
      connected[id] = true
    }
  }
  deconflict_names(shapes, connected, names, service, config)
  if config.value_contracts {
    value_member_names(shapes, connected, names, service)
  }
  let definitions : Map[String, @smithy.ShapeId] = Map([])
  for id, _ in connected {
    if @smithy.ShapeId::parse(id) is Some(shape_id) &&
      names.get(id) is Some(name) &&
      shapes.get(id) is Some(shape) &&
      is_definition_shape(shape, config) {
      definitions[name] = shape_id
    }
  }
  if config.value_contracts {
    for id, name in names {
      if id.contains("$") && @smithy.ShapeId::parse(id) is Some(shape_id) {
        definitions[name] = shape_id
      }
    }
  }
  {
    shapes,
    names,
    config: config.settings,
    value_contracts: config.value_contracts,
    document,
    definitions,
  }
}

///|
/// Resolve a contextual component name within the selected service closure.
pub fn SchemaIndex::definition(
  self : SchemaIndex,
  name : String,
) -> @smithy.ShapeId? {
  self.definitions.get(name)
}

///|
pub fn SchemaIndex::document(self : SchemaIndex) -> Json {
  self.document
}

///|
pub fn SchemaIndex::name(self : SchemaIndex, id : @smithy.ShapeId) -> String {
  self.names.get(id.root().to_string()).unwrap_or(id.name())
}

///|
/// Convert a value shape inline, retaining the shared contextual references.
pub fn SchemaIndex::shape(
  self : SchemaIndex,
  id : @smithy.ShapeId,
  config~ : Config? = None,
) -> Json raise JsonSchemaError {
  let effective : ConversionConfig = {
    settings: config.unwrap_or(self.config),
    value_contracts: self.value_contracts,
  }
  validate_value_config(effective)
  if id.to_string() == "smithy.api#Unit" {
    return if effective.value_contracts {
      Json::object({
        "type": Json::string("object"),
        "properties": Json::object(Map([])),
        "additionalProperties": Json::boolean(false),
      })
    } else {
      Json::object({ "type": Json::string("object") })
    }
  }
  guard self.shapes.get(id.to_string()) is Some(shape) else {
    raise InvalidConfiguration("Unknown schema shape `" + id.to_string() + "`")
  }
  shape_schema(shape, self.shapes, [id.to_string()], effective, self.names)
}

///|
/// Convert a member using the same inline/reference rules as a full document.
pub fn SchemaIndex::schema_for_member(
  self : SchemaIndex,
  id : @smithy.ShapeId,
  config~ : Config? = None,
) -> Json raise JsonSchemaError {
  let effective : ConversionConfig = {
    settings: config.unwrap_or(self.config),
    value_contracts: self.value_contracts,
  }
  validate_value_config(effective)
  guard id.member_name() is Some(name) &&
    self.shapes.get(id.root().to_string()) is Some(Object(parent)) else {
    raise InvalidConfiguration("Unknown schema member `" + id.to_string() + "`")
  }
  let entries = match parent.get("members") {
    Some(Object(members)) => members
    _ => parent
  }
  guard entries.get(name) is Some(entry) && member_target(entry) is Some(_) else {
    raise InvalidConfiguration("Unknown schema member `" + id.to_string() + "`")
  }
  member_schema(entry, self.shapes, [], effective, self.names)
}

///|
/// Convert only the selected members of a structure. Filtering occurs before
/// required/property generation, so omitted HTTP bindings cannot leave stale
/// required properties in synthesized document payloads.
pub fn SchemaIndex::structure(
  self : SchemaIndex,
  id : @smithy.ShapeId,
  selected : Array[@smithy.ShapeId],
) -> Json raise JsonSchemaError {
  guard self.shapes.get(id.to_string()) is Some(Object(fields)) &&
    fields.get("type") == Some(Json::string("structure")) else {
    raise InvalidConfiguration(
      "Expected schema structure `" + id.to_string() + "`",
    )
  }
  let original = match fields.get("members") {
    Some(Object(members)) => members
    _ => Map([])
  }
  let members : Map[String, Json] = Map([])
  for selected_id in selected {
    guard selected_id.root() == id &&
      selected_id.member_name() is Some(name) &&
      original.get(name) is Some(value) else {
      raise InvalidConfiguration(
        "Selected member does not belong to `" + id.to_string() + "`",
      )
    }
    members[name] = value
  }
  let filtered : Map[String, Json] = Map([])
  for key, value in fields {
    filtered[key] = value
  }
  filtered["members"] = Json::object(members)
  shape_schema(
    Json::object(filtered),
    self.shapes,
    [id.to_string()],
    { settings: self.config, value_contracts: self.value_contracts, },
    self.names,
  )
}