///|
pub(all) enum ValueKind {
  KindAny
  KindNull
  KindBool
  KindInt
  KindText
  KindList
  KindRecord
} derive(Eq, @debug.Debug)

///|
pub(all) struct FieldSpec {
  path : String
  kind : ValueKind
  required : Bool
  min_int : Int?
  max_int : Int?
  min_len : Int?
  max_len : Int?
} derive(Eq, @debug.Debug)

///|
pub(all) struct SchemaIssue {
  path : String
  code : String
  message : String
} derive(Eq, @debug.Debug)

///|
pub(all) struct SchemaReport {
  name : String
  valid : Bool
  issues : Array[SchemaIssue]
} derive(Eq, @debug.Debug)

///|
pub(all) struct EventSchema {
  name : String
  topic : TopicPattern?
  fields : Array[FieldSpec]
} derive(Eq, @debug.Debug)

///|
pub fn field_spec(
  path : StringView,
  kind : ValueKind,
  required? : Bool = true,
  min_int? : Int,
  max_int? : Int,
  min_len? : Int,
  max_len? : Int,
) -> FieldSpec {
  { path: path.to_owned(), kind, required, min_int, max_int, min_len, max_len }
}

///|
pub fn event_schema(
  name : StringView,
  topic? : StringView = "",
  fields? : ArrayView[FieldSpec] = [],
) -> Result[EventSchema, EventRailError] {
  let parsed = if topic == "" {
    None
  } else {
    match topic_pattern(topic) {
      Ok(value) => Some(value)
      Err(err) => return Err(err)
    }
  }
  Ok({ name: name.to_owned(), topic: parsed, fields: fields.to_owned() })
}

///|
pub fn EventSchema::validate(
  self : EventSchema,
  event : Envelope,
) -> SchemaReport {
  let issues : Array[SchemaIssue] = []
  match self.topic {
    Some(pattern) =>
      match pattern.matches_topic(event.topic) {
        Ok(true) => ()
        Ok(false) =>
          issues.push(
            schema_issue(
              "$topic",
              "topic.mismatch",
              "topic \{event.topic} does not match \{pattern.raw}",
            ),
          )
        Err(err) =>
          issues.push(schema_issue("$topic", "topic.invalid", err.message()))
      }
    None => ()
  }
  for spec in self.fields {
    validate_field_spec(spec, event.payload, issues)
  }
  { name: self.name, valid: issues.length() == 0, issues }
}

///|
pub fn SchemaReport::summary(self : SchemaReport) -> String {
  if self.valid {
    "schema=\{self.name} valid"
  } else {
    "schema=\{self.name} invalid issues=\{self.issues.length()}"
  }
}

///|
pub fn SchemaReport::to_handler_result(self : SchemaReport) -> HandlerResult {
  if self.valid {
    HandlerAck(self.summary())
  } else {
    HandlerDrop(self.issue_lines().join("; "))
  }
}

///|
pub fn SchemaReport::issue_lines(self : SchemaReport) -> Array[String] {
  self.issues.map(issue => "\{issue.path}:\{issue.code}:\{issue.message}")
}

///|
pub fn SchemaIssue::to_wire(self : SchemaIssue) -> String {
  "path=\{escape_wire_text(self.path)};code=\{escape_wire_text(self.code)};message=\{escape_wire_text(self.message)}"
}

///|
fn validate_field_spec(
  spec : FieldSpec,
  payload : EventValue,
  issues : Array[SchemaIssue],
) -> Unit {
  match payload.get_path(spec.path) {
    None =>
      if spec.required {
        issues.push(
          schema_issue(
            spec.path,
            "field.required",
            "required field \{spec.path} is missing",
          ),
        )
      }
    Some(value) =>
      if !kind_matches(spec.kind, value) {
        issues.push(
          schema_issue(
            spec.path,
            "field.kind",
            "field \{spec.path} expected \{spec.kind.to_wire()}",
          ),
        )
      } else {
        validate_field_bounds(spec, value, issues)
      }
  }
}

///|
fn validate_field_bounds(
  spec : FieldSpec,
  value : EventValue,
  issues : Array[SchemaIssue],
) -> Unit {
  match value {
    VInt(actual) => {
      match spec.min_int {
        Some(minimum) if actual < minimum =>
          issues.push(
            schema_issue(
              spec.path,
              "int.min",
              "field \{spec.path} expected >= \{minimum} but got \{actual}",
            ),
          )
        _ => ()
      }
      match spec.max_int {
        Some(maximum) if actual > maximum =>
          issues.push(
            schema_issue(
              spec.path,
              "int.max",
              "field \{spec.path} expected <= \{maximum} but got \{actual}",
            ),
          )
        _ => ()
      }
    }
    VText(actual) => validate_length_bounds(spec, actual.length(), issues)
    VList(values) => validate_length_bounds(spec, values.length(), issues)
    VRecord(fields) => validate_length_bounds(spec, fields.length(), issues)
    VNull | VBool(_) => ()
  }
}

///|
fn validate_length_bounds(
  spec : FieldSpec,
  actual : Int,
  issues : Array[SchemaIssue],
) -> Unit {
  match spec.min_len {
    Some(minimum) if actual < minimum =>
      issues.push(
        schema_issue(
          spec.path,
          "len.min",
          "field \{spec.path} length expected >= \{minimum} but got \{actual}",
        ),
      )
    _ => ()
  }
  match spec.max_len {
    Some(maximum) if actual > maximum =>
      issues.push(
        schema_issue(
          spec.path,
          "len.max",
          "field \{spec.path} length expected <= \{maximum} but got \{actual}",
        ),
      )
    _ => ()
  }
}

///|
fn kind_matches(kind : ValueKind, value : EventValue) -> Bool {
  match kind {
    KindAny => true
    KindNull => value is VNull
    KindBool => value is VBool(_)
    KindInt => value is VInt(_)
    KindText => value is VText(_)
    KindList => value is VList(_)
    KindRecord => value is VRecord(_)
  }
}

///|
pub fn ValueKind::to_wire(self : ValueKind) -> String {
  match self {
    KindAny => "any"
    KindNull => "null"
    KindBool => "bool"
    KindInt => "int"
    KindText => "text"
    KindList => "list"
    KindRecord => "record"
  }
}

///|
fn schema_issue(
  path : StringView,
  code : StringView,
  message : StringView,
) -> SchemaIssue {
  { path: path.to_owned(), code: code.to_owned(), message: message.to_owned() }
}