///|
struct ContractConfig {
  name : String
  version : String
  fields : Array[FieldConfig]
  rules : Array[RuleConfig]
} derive(FromJson, Debug)

///|
struct FieldConfig {
  name : String
  kind : String?
  required : Bool?
  nullable : Bool?
  allowed_values : Array[String]?
  min_int : Int?
  max_int : Int?
  pattern : String?
  description : String?
} derive(FromJson, Debug)

///|
struct RuleConfig {
  kind : String
  fields : Array[String]?
  field : String?
  allowed_values : Array[String]?
  min_percent : Int?
  min : Int?
  max : Int?
  left : String?
  op : String?
  right : String?
  pattern : String?
  min_length : Int?
  max_length : Int?
  condition_field : String?
  condition_value : String?
  severity : String?
  message : String?
} derive(FromJson, Debug)

///|
pub fn parse_contract_json(source : String) -> Contract {
  let parsed : Json = @json.parse(source) catch {
    _ => abort("invalid contract json")
  }
  let config : ContractConfig = @json.from_json(parsed) catch {
    _ => abort("contract json does not match expected schema")
  }
  {
    name: config.name,
    version: config.version,
    fields: config.fields.map(field_config_to_spec),
    rules: config.rules.map(rule_config_to_rule),
  }
}

///|
fn field_config_to_spec(config : FieldConfig) -> FieldSpec {
  {
    name: config.name,
    kind: parse_field_type(option_or(config.kind, "string")),
    required: option_or_bool(config.required, false),
    nullable: option_or_bool(config.nullable, false),
    allowed_values: option_or_array(config.allowed_values),
    min_int: config.min_int,
    max_int: config.max_int,
    pattern: config.pattern,
    description: option_or(config.description, ""),
  }
}

///|
fn rule_config_to_rule(config : RuleConfig) -> Rule {
  let severity = parse_severity(option_or(config.severity, "error"))
  let message = option_or(config.message, config.kind)
  match config.kind {
    "unique" =>
      Unique(
        fields=expect_string_array(config.fields, "fields"),
        severity~,
        message~,
      )
    "completeness" =>
      Completeness(
        field=expect_string(config.field, "field"),
        min_percent=expect_int(config.min_percent, "min_percent"),
        severity~,
        message~,
      )
    "enum" =>
      Enum(
        field=expect_string(config.field, "field"),
        allowed_values=expect_string_array(
          config.allowed_values,
          "allowed_values",
        ),
        severity~,
        message~,
      )
    "int_range" =>
      IntRange(
        field=expect_string(config.field, "field"),
        min=expect_int(config.min, "min"),
        max=expect_int(config.max, "max"),
        severity~,
        message~,
      )
    "compare_ints" =>
      CompareInts(
        left=expect_string(config.left, "left"),
        op=parse_comparison_op(expect_string(config.op, "op")),
        right=expect_string(config.right, "right"),
        severity~,
        message~,
      )
    "pattern_match" =>
      PatternMatch(
        field=expect_string(config.field, "field"),
        pattern=expect_string(config.pattern, "pattern"),
        severity~,
        message~,
      )
    "string_length" =>
      StringLength(
        field=expect_string(config.field, "field"),
        min=expect_int(config.min_length, "min_length"),
        max=expect_int(config.max_length, "max_length"),
        severity~,
        message~,
      )
    "distinct_count" =>
      DistinctCount(
        field=expect_string(config.field, "field"),
        min=expect_int(config.min, "min"),
        max=expect_int(config.max, "max"),
        severity~,
        message~,
      )
    "required_if" =>
      RequiredIf(
        condition_field=expect_string(config.condition_field, "condition_field"),
        condition_value=expect_string(config.condition_value, "condition_value"),
        field=expect_string(config.field, "field"),
        severity~,
        message~,
      )
    "row_count" => RowCount(min=config.min, max=config.max, severity~, message~)
    _ => abort("unsupported rule kind: \{config.kind}")
  }
}

///|
fn option_or(value : String?, fallback : String) -> String {
  match value {
    Some(text) => text
    None => fallback
  }
}

///|
fn option_or_bool(value : Bool?, fallback : Bool) -> Bool {
  match value {
    Some(flag) => flag
    None => fallback
  }
}

///|
fn option_or_array(value : Array[String]?) -> Array[String] {
  match value {
    Some(values) => values
    None => []
  }
}

///|
fn expect_string(value : String?, label : String) -> String {
  match value {
    Some(text) => text
    None => abort("missing rule property: \{label}")
  }
}

///|
fn expect_string_array(value : Array[String]?, label : String) -> Array[String] {
  match value {
    Some(values) => values
    None => abort("missing rule property: \{label}")
  }
}

///|
fn expect_int(value : Int?, label : String) -> Int {
  match value {
    Some(number) => number
    None => abort("missing rule property: \{label}")
  }
}

///|
fn parse_field_type(value : String) -> FieldType {
  match value.to_lower() {
    "string" => String
    "int" => Int
    "bool" => Bool
    "date" => Date
    _ => abort("unsupported field type: \{value}")
  }
}

///|
fn parse_severity(value : String) -> Severity {
  match value.to_lower() {
    "error" => Error
    "warning" => Warning
    _ => abort("unsupported severity: \{value}")
  }
}

///|
fn parse_comparison_op(value : String) -> ComparisonOp {
  match value {
    ">=" => GreaterOrEqual
    ">" => GreaterThan
    "<=" => LessOrEqual
    "<" => LessThan
    "==" => Equal
    _ => abort("unsupported comparison op: \{value}")
  }
}