///|
pub(all) struct FieldRule {
  name : String
  required : Bool
  max_values : Int
  max_length : Int
  allow_empty : Bool
} derive(Debug, Eq)

///|
pub(all) struct FileRule {
  name : String
  required : Bool
  max_files : Int
  max_length : Int
  allowed_content_types : Array[String]
  allowed_extensions : Array[String]
  allow_empty : Bool
} derive(Debug, Eq)

///|
pub(all) struct FormSchema {
  field_rules : Array[FieldRule]
  file_rules : Array[FileRule]
  allow_unknown_fields : Bool
  allow_unknown_files : Bool
} derive(Debug, Eq)

///|
pub fn field_rule(name : String) -> FieldRule {
  { name, required: false, max_values: -1, max_length: -1, allow_empty: true }
}

///|
pub fn required_field_rule(name : String) -> FieldRule {
  field_rule(name).with_required(true)
}

///|
pub fn file_rule(name : String) -> FileRule {
  {
    name,
    required: false,
    max_files: -1,
    max_length: -1,
    allowed_content_types: [],
    allowed_extensions: [],
    allow_empty: true,
  }
}

///|
pub fn required_file_rule(name : String) -> FileRule {
  file_rule(name).with_required(true)
}

///|
pub fn default_form_schema() -> FormSchema {
  {
    field_rules: [],
    file_rules: [],
    allow_unknown_fields: true,
    allow_unknown_files: true,
  }
}

///|
pub fn strict_form_schema() -> FormSchema {
  {
    field_rules: [],
    file_rules: [],
    allow_unknown_fields: false,
    allow_unknown_files: false,
  }
}

///|
pub fn FieldRule::with_required(self : FieldRule, required : Bool) -> FieldRule {
  {
    name: self.name,
    required,
    max_values: self.max_values,
    max_length: self.max_length,
    allow_empty: self.allow_empty,
  }
}

///|
pub fn FieldRule::with_max_values(
  self : FieldRule,
  max_values : Int,
) -> FieldRule {
  {
    name: self.name,
    required: self.required,
    max_values,
    max_length: self.max_length,
    allow_empty: self.allow_empty,
  }
}

///|
pub fn FieldRule::with_max_length(
  self : FieldRule,
  max_length : Int,
) -> FieldRule {
  {
    name: self.name,
    required: self.required,
    max_values: self.max_values,
    max_length,
    allow_empty: self.allow_empty,
  }
}

///|
pub fn FieldRule::with_allow_empty(
  self : FieldRule,
  allow_empty : Bool,
) -> FieldRule {
  {
    name: self.name,
    required: self.required,
    max_values: self.max_values,
    max_length: self.max_length,
    allow_empty,
  }
}

///|
pub fn FileRule::with_required(self : FileRule, required : Bool) -> FileRule {
  {
    name: self.name,
    required,
    max_files: self.max_files,
    max_length: self.max_length,
    allowed_content_types: clone_string_array(self.allowed_content_types),
    allowed_extensions: clone_string_array(self.allowed_extensions),
    allow_empty: self.allow_empty,
  }
}

///|
pub fn FileRule::with_max_files(self : FileRule, max_files : Int) -> FileRule {
  {
    name: self.name,
    required: self.required,
    max_files,
    max_length: self.max_length,
    allowed_content_types: clone_string_array(self.allowed_content_types),
    allowed_extensions: clone_string_array(self.allowed_extensions),
    allow_empty: self.allow_empty,
  }
}

///|
pub fn FileRule::with_max_length(self : FileRule, max_length : Int) -> FileRule {
  {
    name: self.name,
    required: self.required,
    max_files: self.max_files,
    max_length,
    allowed_content_types: clone_string_array(self.allowed_content_types),
    allowed_extensions: clone_string_array(self.allowed_extensions),
    allow_empty: self.allow_empty,
  }
}

///|
pub fn FileRule::with_allow_empty(
  self : FileRule,
  allow_empty : Bool,
) -> FileRule {
  {
    name: self.name,
    required: self.required,
    max_files: self.max_files,
    max_length: self.max_length,
    allowed_content_types: clone_string_array(self.allowed_content_types),
    allowed_extensions: clone_string_array(self.allowed_extensions),
    allow_empty,
  }
}

///|
pub fn FileRule::allow_content_type(
  self : FileRule,
  content_type : String,
) -> FileRule {
  let allowed = clone_string_array(self.allowed_content_types)
  allowed.push(to_lower_ascii(content_type))
  {
    name: self.name,
    required: self.required,
    max_files: self.max_files,
    max_length: self.max_length,
    allowed_content_types: allowed,
    allowed_extensions: clone_string_array(self.allowed_extensions),
    allow_empty: self.allow_empty,
  }
}

///|
pub fn FileRule::allow_extension(
  self : FileRule,
  extension : String,
) -> FileRule {
  let allowed = clone_string_array(self.allowed_extensions)
  allowed.push(normalize_rule_extension(extension))
  {
    name: self.name,
    required: self.required,
    max_files: self.max_files,
    max_length: self.max_length,
    allowed_content_types: clone_string_array(self.allowed_content_types),
    allowed_extensions: allowed,
    allow_empty: self.allow_empty,
  }
}

///|
pub fn FormSchema::with_unknown_fields(
  self : FormSchema,
  allow : Bool,
) -> FormSchema {
  {
    field_rules: clone_field_rules(self.field_rules),
    file_rules: clone_file_rules(self.file_rules),
    allow_unknown_fields: allow,
    allow_unknown_files: self.allow_unknown_files,
  }
}

///|
pub fn FormSchema::with_unknown_files(
  self : FormSchema,
  allow : Bool,
) -> FormSchema {
  {
    field_rules: clone_field_rules(self.field_rules),
    file_rules: clone_file_rules(self.file_rules),
    allow_unknown_fields: self.allow_unknown_fields,
    allow_unknown_files: allow,
  }
}

///|
pub fn FormSchema::add_field_rule(
  self : FormSchema,
  rule : FieldRule,
) -> FormSchema {
  let field_rules = clone_field_rules(self.field_rules)
  upsert_field_rule(field_rules, rule)
  {
    field_rules,
    file_rules: clone_file_rules(self.file_rules),
    allow_unknown_fields: self.allow_unknown_fields,
    allow_unknown_files: self.allow_unknown_files,
  }
}

///|
pub fn FormSchema::add_file_rule(
  self : FormSchema,
  rule : FileRule,
) -> FormSchema {
  let file_rules = clone_file_rules(self.file_rules)
  upsert_file_rule(file_rules, rule)
  {
    field_rules: clone_field_rules(self.field_rules),
    file_rules,
    allow_unknown_fields: self.allow_unknown_fields,
    allow_unknown_files: self.allow_unknown_files,
  }
}

///|
pub fn form_schema_require_field(
  schema : FormSchema,
  name : String,
) -> FormSchema {
  schema.add_field_rule(required_field_rule(name))
}

///|
pub fn form_schema_require_file(
  schema : FormSchema,
  name : String,
) -> FormSchema {
  schema.add_file_rule(required_file_rule(name))
}

///|
pub fn validate_form_schema(
  form : MultipartForm,
  schema : FormSchema,
) -> ValidationReport {
  let issues = Array::new()
  validate_schema_field_rules(form, schema, issues)
  validate_schema_file_rules(form, schema, issues)
  validate_unknown_text_fields(form, schema, issues)
  validate_unknown_file_fields(form, schema, issues)
  { issues, }
}

///|
pub fn MultipartForm::validate_schema(
  self : MultipartForm,
  schema : FormSchema,
) -> ValidationReport {
  validate_form_schema(self, schema)
}

///|
pub fn parse_and_validate_schema_request(
  content_type : String,
  body : String,
  schema : FormSchema,
) -> Result[ParseAndValidateResult, MultipartError] {
  match parse_multipart_request(content_type, body) {
    Ok(form) => Ok({ form, report: form.validate_schema(schema) })
    Err(err) => Err(err)
  }
}

///|
pub fn ValidationReport::has_issue_code(
  self : ValidationReport,
  code : String,
) -> Bool {
  let mut i = 0
  while i < self.issues.length() {
    if self.issues[i].code == code {
      return true
    }
    i = i + 1
  }
  false
}

///|
pub fn ValidationReport::issues_for_field(
  self : ValidationReport,
  name : String,
) -> Array[ValidationIssue] {
  let out = Array::new()
  let mut i = 0
  while i < self.issues.length() {
    match self.issues[i].field_name {
      Some(field) => if field == name { out.push(self.issues[i]) }
      None => ()
    }
    i = i + 1
  }
  out
}

///|
pub fn ValidationReport::to_lines(self : ValidationReport) -> Array[String] {
  let lines = Array::new()
  if self.is_ok() {
    lines.push("ok")
  } else {
    let mut i = 0
    while i < self.issues.length() {
      lines.push(self.issues[i].to_line())
      i = i + 1
    }
  }
  lines
}

///|
fn validate_schema_field_rules(
  form : MultipartForm,
  schema : FormSchema,
  issues : Array[ValidationIssue],
) -> Unit {
  let mut i = 0
  while i < schema.field_rules.length() {
    validate_one_field_rule(form, schema.field_rules[i], issues)
    i = i + 1
  }
}

///|
fn validate_one_field_rule(
  form : MultipartForm,
  rule : FieldRule,
  issues : Array[ValidationIssue],
) -> Unit {
  let values = form.field_values(rule.name)
  if rule.required && values.length() == 0 {
    issues.push(
      validation_issue(
        "schema_missing_field",
        "required field is missing: " + rule.name,
        field_name=rule.name,
      ),
    )
  }
  if rule.max_values >= 0 && values.length() > rule.max_values {
    issues.push(
      validation_issue(
        "schema_too_many_field_values",
        "field has too many values: " + rule.name,
        field_name=rule.name,
      ),
    )
  }
  let mut i = 0
  while i < values.length() {
    if !rule.allow_empty && values[i] == "" {
      issues.push(
        validation_issue(
          "schema_empty_field",
          "empty field is not allowed: " + rule.name,
          field_name=rule.name,
        ),
      )
    }
    if rule.max_length >= 0 && values[i].length() > rule.max_length {
      issues.push(
        validation_issue(
          "schema_field_too_large",
          "field exceeds max_length: " + rule.name,
          field_name=rule.name,
        ),
      )
    }
    i = i + 1
  }
}

///|
fn validate_schema_file_rules(
  form : MultipartForm,
  schema : FormSchema,
  issues : Array[ValidationIssue],
) -> Unit {
  let mut i = 0
  while i < schema.file_rules.length() {
    validate_one_file_rule(form, schema.file_rules[i], issues)
    i = i + 1
  }
}

///|
fn validate_one_file_rule(
  form : MultipartForm,
  rule : FileRule,
  issues : Array[ValidationIssue],
) -> Unit {
  let files = form.files(rule.name)
  if rule.required && files.length() == 0 {
    issues.push(
      validation_issue(
        "schema_missing_file",
        "required file is missing: " + rule.name,
        field_name=rule.name,
      ),
    )
  }
  if rule.max_files >= 0 && files.length() > rule.max_files {
    issues.push(
      validation_issue(
        "schema_too_many_files",
        "file field has too many files: " + rule.name,
        field_name=rule.name,
      ),
    )
  }
  let mut i = 0
  while i < files.length() {
    validate_one_schema_file(files[i], i, rule, issues)
    i = i + 1
  }
}

///|
fn validate_one_schema_file(
  part : FormPart,
  index : Int,
  rule : FileRule,
  issues : Array[ValidationIssue],
) -> Unit {
  if !rule.allow_empty && part.body == "" {
    issues.push(
      validation_issue(
        "schema_empty_file",
        "empty file is not allowed: " + rule.name,
        part_index=index,
        field_name=rule.name,
      ),
    )
  }
  if rule.max_length >= 0 && part.body.length() > rule.max_length {
    issues.push(
      validation_issue(
        "schema_file_too_large",
        "file exceeds max_length: " + rule.name,
        part_index=index,
        field_name=rule.name,
      ),
    )
  }
  if rule.allowed_content_types.length() > 0 &&
    !schema_content_type_allowed(part.content_type, rule.allowed_content_types) {
    issues.push(
      validation_issue(
        "schema_content_type_not_allowed",
        "file content type is not allowed: " + rule.name,
        part_index=index,
        field_name=rule.name,
      ),
    )
  }
  if rule.allowed_extensions.length() > 0 &&
    !schema_extension_allowed(part.filename, rule.allowed_extensions) {
    issues.push(
      validation_issue(
        "schema_extension_not_allowed",
        "file extension is not allowed: " + rule.name,
        part_index=index,
        field_name=rule.name,
      ),
    )
  }
}

///|
fn validate_unknown_text_fields(
  form : MultipartForm,
  schema : FormSchema,
  issues : Array[ValidationIssue],
) -> Unit {
  if schema.allow_unknown_fields {
    return
  }
  let names = form.field_names()
  let mut i = 0
  while i < names.length() {
    if field_rule_for(schema, names[i]) is None {
      issues.push(
        validation_issue(
          "schema_unknown_field",
          "unknown field is not allowed: " + names[i],
          field_name=names[i],
        ),
      )
    }
    i = i + 1
  }
}

///|
fn validate_unknown_file_fields(
  form : MultipartForm,
  schema : FormSchema,
  issues : Array[ValidationIssue],
) -> Unit {
  if schema.allow_unknown_files {
    return
  }
  let names = form.file_field_names()
  let mut i = 0
  while i < names.length() {
    if file_rule_for(schema, names[i]) is None {
      issues.push(
        validation_issue(
          "schema_unknown_file",
          "unknown file is not allowed: " + names[i],
          field_name=names[i],
        ),
      )
    }
    i = i + 1
  }
}

///|
fn field_rule_for(schema : FormSchema, name : String) -> FieldRule? {
  let mut i = 0
  while i < schema.field_rules.length() {
    if schema.field_rules[i].name == name {
      return Some(schema.field_rules[i])
    }
    i = i + 1
  }
  None
}

///|
fn file_rule_for(schema : FormSchema, name : String) -> FileRule? {
  let mut i = 0
  while i < schema.file_rules.length() {
    if schema.file_rules[i].name == name {
      return Some(schema.file_rules[i])
    }
    i = i + 1
  }
  None
}

///|
fn schema_content_type_allowed(
  content_type : String?,
  allowed : Array[String],
) -> Bool {
  match content_type {
    Some(value) => string_array_contains_case_insensitive(allowed, value)
    None => false
  }
}

///|
fn schema_extension_allowed(
  filename : String?,
  allowed : Array[String],
) -> Bool {
  match filename {
    Some(value) =>
      match filename_extension(value) {
        Some(ext) => string_array_contains_case_insensitive(allowed, ext)
        None => false
      }
    None => false
  }
}

///|
fn string_array_contains_case_insensitive(
  values : Array[String],
  target : String,
) -> Bool {
  let lower = to_lower_ascii(target)
  let mut i = 0
  while i < values.length() {
    if to_lower_ascii(values[i]) == lower {
      return true
    }
    i = i + 1
  }
  false
}

///|
fn normalize_rule_extension(extension : String) -> String {
  let trimmed = trim_ascii(extension)
  if trimmed.has_prefix(".") {
    to_lower_ascii(slice_from(trimmed, 1))
  } else {
    to_lower_ascii(trimmed)
  }
}

///|
fn upsert_field_rule(rules : Array[FieldRule], rule : FieldRule) -> Unit {
  let mut i = 0
  while i < rules.length() {
    if rules[i].name == rule.name {
      rules[i] = rule
      return
    }
    i = i + 1
  }
  rules.push(rule)
}

///|
fn upsert_file_rule(rules : Array[FileRule], rule : FileRule) -> Unit {
  let mut i = 0
  while i < rules.length() {
    if rules[i].name == rule.name {
      rules[i] = rule
      return
    }
    i = i + 1
  }
  rules.push(rule)
}

///|
fn clone_field_rules(rules : Array[FieldRule]) -> Array[FieldRule] {
  let out = Array::new()
  let mut i = 0
  while i < rules.length() {
    out.push(rules[i])
    i = i + 1
  }
  out
}

///|
fn clone_file_rules(rules : Array[FileRule]) -> Array[FileRule] {
  let out = Array::new()
  let mut i = 0
  while i < rules.length() {
    out.push(rules[i])
    i = i + 1
  }
  out
}