///|
/// Create a schema that validates JSON objects.
///
/// Each key in `spec` defines a required field and its schema.
/// By default, extra fields in the input JSON are silently stripped (Strip mode).
/// Use `.passthrough()` to allow extra fields, or `.strict()` to reject them.
pub fn object(
  spec : Map[String, Schema],
  required_error~ : String = "",
  invalid_type_error~ : String = "",
) -> Schema {
  {
    id: new_schema_id(),
    schema_type: ObjectType(spec.copy(), Strip),
    rules: [],
    description: "",
    required_error,
    invalid_type_error,
    name: "",
    brand: "",
  }
}

///|
/// Reject fields not defined in the schema spec.
pub fn Schema::strict(self : Schema) -> Schema {
  match self.schema_type {
    ObjectType(spec, _) =>
      { ..self, id: new_schema_id(), schema_type: ObjectType(spec, Strict), }
    _ => abort("strict() is only valid for object schemas")
  }
}

///|
/// Allow fields not defined in the schema spec.
pub fn Schema::passthrough(self : Schema) -> Schema {
  match self.schema_type {
    ObjectType(spec, _) =>
      {
        ..self,
        id: new_schema_id(),
        schema_type: ObjectType(spec, Passthrough),
      }
    _ => abort("passthrough() is only valid for object schemas")
  }
}

///|
/// Silently strip extra fields not defined in the schema spec.
/// This is the default mode.
pub fn Schema::strip(self : Schema) -> Schema {
  match self.schema_type {
    ObjectType(spec, _) =>
      {
        ..self,
        id: new_schema_id(),
        schema_type: ObjectType(spec.copy(), Strip),
      }
    _ => abort("strip() is only valid for object schemas")
  }
}

///|
/// Select only the specified fields from an object schema.
/// Returns a new object schema with the same mode (Strip/Passthrough/Strict).
/// Aborts if a requested key is not present in the original spec.
///
/// # Example
/// ```
/// let s = object({"a": string(), "b": number()}).pick(["a"])
/// s.parse(json)  // validates only field "a"
/// ```
pub fn Schema::pick(self : Schema, keys : Array[String]) -> Schema {
  guard self.rules.is_empty() else {
    abort(
      "pick() cannot change the shape of a refined object; compose the shape before refine()",
    )
  }
  match self.schema_type {
    ObjectType(spec, mode) => {
      let new_spec : Map[String, Schema] = Map([])
      for key in keys {
        match spec.get(key) {
          Some(s) => new_spec.set(key, s)
          None => abort("Unrecognized key: \"\{key}\"")
        }
      }
      {
        id: new_schema_id(),
        schema_type: ObjectType(new_spec, mode),
        rules: self.rules,
        description: self.description,
        required_error: self.required_error,
        invalid_type_error: self.invalid_type_error,
        name: self.name,
        brand: self.brand,
      }
    }
    _ => abort("pick() is only valid for object schemas")
  }
}

///|
/// Omit the specified fields from an object schema.
/// Returns a new object schema with the same mode (Strip/Passthrough/Strict).
/// Aborts if a requested key is not present in the original spec.
///
/// # Example
/// ```
/// let s = object({"a": string(), "b": number()}).omit(["b"])
/// s.parse(json)  // validates only field "a"
/// ```
pub fn Schema::omit(self : Schema, keys : Array[String]) -> Schema {
  guard self.rules.is_empty() else {
    abort(
      "omit() cannot change the shape of a refined object; compose the shape before refine()",
    )
  }
  match self.schema_type {
    ObjectType(spec, mode) => {
      for key in keys {
        if !spec.contains(key) {
          abort("Unrecognized key: \"\{key}\"")
        }
      }
      let new_spec : Map[String, Schema] = Map([])
      for key in spec.keys() {
        if !value_in_array(key, keys) {
          match spec.get(key) {
            Some(s) => new_spec.set(key, s)
            None => ()
          }
        }
      }
      {
        id: new_schema_id(),
        schema_type: ObjectType(new_spec, mode),
        rules: self.rules,
        description: self.description,
        required_error: self.required_error,
        invalid_type_error: self.invalid_type_error,
        name: self.name,
        brand: self.brand,
      }
    }
    _ => abort("omit() is only valid for object schemas")
  }
}

///|
/// Make all fields of an object schema optional.
/// Useful for partial updates (e.g., PATCH operations).
/// Existing optional fields remain optional.
///
/// # Example
/// ```
/// let s = object({"name": string(), "age": number()}).partial()
/// s.parse(json)  // all fields are now optional
/// ```
pub fn Schema::partial(self : Schema) -> Schema {
  guard self.rules.is_empty() else {
    abort(
      "partial() cannot change the shape of a refined object; compose the shape before refine()",
    )
  }
  match self.schema_type {
    ObjectType(spec, mode) => {
      let new_spec : Map[String, Schema] = Map([])
      for key in spec.keys() {
        match spec.get(key) {
          Some(s) => new_spec.set(key, s.optional())
          None => ()
        }
      }
      {
        id: new_schema_id(),
        schema_type: ObjectType(new_spec, mode),
        rules: self.rules,
        description: self.description,
        required_error: self.required_error,
        invalid_type_error: self.invalid_type_error,
        name: self.name,
        brand: self.brand,
      }
    }
    _ => abort("partial() is only valid for object schemas")
  }
}

///|
/// Extend an object schema with additional fields.
/// Fields in `extension` override fields with the same name in the base schema.
pub fn Schema::extend_with(
  self : Schema,
  extension : Map[String, Schema],
) -> Schema {
  guard self.rules.is_empty() else {
    abort(
      "extend_with() cannot change the shape of a refined object; compose the shape before refine()",
    )
  }
  match self.schema_type {
    ObjectType(spec, mode) => {
      let new_spec : Map[String, Schema] = Map([])
      for key in spec.keys() {
        match spec.get(key) {
          Some(s) => new_spec.set(key, s)
          None => ()
        }
      }
      for key in extension.keys() {
        match extension.get(key) {
          Some(s) => new_spec.set(key, s)
          None => ()
        }
      }
      {
        id: new_schema_id(),
        schema_type: ObjectType(new_spec, mode),
        rules: self.rules,
        description: self.description,
        required_error: self.required_error,
        invalid_type_error: self.invalid_type_error,
        name: self.name,
        brand: self.brand,
      }
    }
    _ => abort("extend_with() is only valid for object schemas")
  }
}

///|
/// Merge two object schemas.
/// Fields from `other` override fields with the same name in `self`.
/// The merged schema inherits the right schema's object mode.
pub fn Schema::merge(self : Schema, other : Schema) -> Schema {
  guard self.rules.is_empty() && other.rules.is_empty() else {
    abort(
      "merge() cannot change the shape of a refined object; compose the shape before refine()",
    )
  }
  match (self.schema_type, other.schema_type) {
    (ObjectType(spec, _), ObjectType(other_spec, other_mode)) => {
      let new_spec : Map[String, Schema] = Map([])
      for key in spec.keys() {
        match spec.get(key) {
          Some(s) => new_spec.set(key, s)
          None => ()
        }
      }
      for key in other_spec.keys() {
        match other_spec.get(key) {
          Some(s) => new_spec.set(key, s)
          None => ()
        }
      }
      {
        id: new_schema_id(),
        schema_type: ObjectType(new_spec, other_mode),
        rules: self.rules,
        description: self.description,
        required_error: self.required_error,
        invalid_type_error: self.invalid_type_error,
        name: self.name,
        brand: self.brand,
      }
    }
    (_, ObjectType(_, _)) => abort("merge() is only valid for object schemas")
    _ => abort("merge() requires an object schema")
  }
}

///|
pub fn Schema::parse_object(
  self : Schema,
  spec : Map[String, Schema],
  mode : ObjectMode,
  json : Json,
  path_stack : Array[String],
) -> RawSchemaResult {
  parse_inner(
    { ..self, id: new_schema_id(), schema_type: ObjectType(spec, mode), },
    json,
    path_stack,
  )
}