///|
pub(all) struct RuleSpec {
  id : String
  label : String
  kind : PhiKind
  pattern : String
  placeholder : String
  confidence : Int
  contextual : Bool
} derive(Debug, Eq)

///|
pub(all) struct RulePatch {
  id : String
  enabled : Bool?
  confidence : Int?
  placeholder : String?
  label : String?
} derive(Debug, Eq)

///|
pub fn rule_spec(
  id~ : String,
  label~ : String,
  kind~ : PhiKind,
  pattern~ : String,
  placeholder~ : String,
  confidence~ : Int,
  contextual? : Bool = false,
) -> RuleSpec {
  { id, label, kind, pattern, placeholder, confidence, contextual }
}

///|
pub fn rule_from_spec(spec : RuleSpec) -> Rule {
  rule(
    spec.id,
    spec.label,
    spec.kind,
    spec.pattern,
    spec.placeholder,
    spec.confidence,
    contextual=spec.contextual,
  )
}

///|
pub fn rule_spec_from_rule(item : Rule) -> RuleSpec {
  {
    id: item.id,
    label: item.label,
    kind: item.kind,
    pattern: item.pattern,
    placeholder: item.placeholder,
    confidence: item.confidence,
    contextual: item.contextual,
  }
}

///|
pub fn rule_patch(
  id~ : String,
  enabled? : Bool,
  confidence? : Int,
  placeholder? : String,
  label? : String,
) -> RulePatch {
  { id, enabled, confidence, placeholder, label }
}

///|
pub fn apply_rule_patch(item : Rule, patch : RulePatch) -> Rule {
  {
    ..item,
    enabled: match patch.enabled {
      Some(value) => value
      None => item.enabled
    },
    confidence: match patch.confidence {
      Some(value) => value
      None => item.confidence
    },
    placeholder: match patch.placeholder {
      Some(value) => value
      None => item.placeholder
    },
    label: match patch.label {
      Some(value) => value
      None => item.label
    },
  }
}

///|
pub fn apply_rule_patches(
  rules : Array[Rule],
  patches : Array[RulePatch],
) -> Array[Rule] {
  rules.map(fn(item) {
    for patch in patches {
      if patch.id == item.id {
        return apply_rule_patch(item, patch)
      }
    }
    item
  })
}

///|
pub fn custom_rule(
  id : String,
  label : String,
  kind : PhiKind,
  pattern : String,
  placeholder : String,
  confidence : Int,
) -> Rule {
  rule(id, label, kind, pattern, placeholder, confidence)
}

///|
pub fn contextual_rule(
  id : String,
  label : String,
  kind : PhiKind,
  pattern : String,
  placeholder : String,
  confidence : Int,
) -> Rule {
  rule(id, label, kind, pattern, placeholder, confidence, contextual=true)
}

///|
pub fn normalize_rule_confidence(item : Rule) -> Rule {
  let confidence = if item.confidence < 0 {
    0
  } else if item.confidence > 100 {
    100
  } else {
    item.confidence
  }
  { ..item, confidence, }
}

///|
pub fn normalize_rules(rules : Array[Rule]) -> Array[Rule] {
  rules.map(normalize_rule_confidence)
}

///|
pub fn deduplicate_rules(rules : Array[Rule]) -> Array[Rule] {
  let result = []
  let seen : Map[String, Unit] = Map([])
  for item in rules {
    if !seen.contains(item.id) {
      seen[item.id] = ()
      result.push(item)
    }
  }
  result
}

///|
pub fn compose_rules(
  base : Array[Rule],
  additions : Array[Rule],
) -> Array[Rule] {
  let combined = base.copy()
  combined.append(additions)
  deduplicate_rules(combined)
}

///|
pub fn replace_rule_by_id(
  rules : Array[Rule],
  replacement : Rule,
) -> Array[Rule] {
  let result = rules.map(fn(item) {
    if item.id == replacement.id {
      replacement
    } else {
      item
    }
  })
  if !result.any(fn(item) { item.id == replacement.id }) {
    result.push(replacement)
  }
  result
}

///|
pub fn remove_rules_by_id(
  rules : Array[Rule],
  ids : Array[String],
) -> Array[Rule] {
  rules.filter(fn(item) { !ids.contains(item.id) })
}

///|
pub fn disable_rules_by_kind(
  rules : Array[Rule],
  kind : PhiKind,
) -> Array[Rule] {
  rules.map(fn(item) {
    if item.kind == kind {
      { ..item, enabled: false }
    } else {
      item
    }
  })
}

///|
pub fn enable_rules_above(rules : Array[Rule], confidence : Int) -> Array[Rule] {
  rules.map(fn(item) {
    if item.confidence >= confidence {
      { ..item, enabled: true }
    } else {
      item
    }
  })
}

///|
pub fn rules_to_tsv(rules : Array[Rule]) -> String {
  let lines = [
    "id\tlabel\tkind\tconfidence\tenabled\tcontextual\tpattern\tplaceholder",
  ]
  for item in rules {
    lines.push(
      [
        item.id,
        item.label,
        phi_kind_name(item.kind),
        "\{item.confidence}",
        "\{item.enabled}",
        "\{item.contextual}",
        item.pattern,
        item.placeholder,
      ].join("\t"),
    )
  }
  lines.join("\n")
}

///|
pub fn rules_checksum(rules : Array[Rule]) -> String {
  stable_hash(rules_to_tsv(rules))
}

///|
pub fn rule_diff(left : Array[Rule], right : Array[Rule]) -> Array[String] {
  let changes = []
  for item in left {
    let mut found = false
    for candidate in right {
      if candidate.id == item.id {
        found = true
        if candidate.pattern != item.pattern {
          changes.push("pattern:\{item.id}")
        }
        if candidate.confidence != item.confidence {
          changes.push("confidence:\{item.id}")
        }
        if candidate.enabled != item.enabled {
          changes.push("enabled:\{item.id}")
        }
      }
    }
    if !found {
      changes.push("removed:\{item.id}")
    }
  }
  for item in right {
    if !left.any(fn(candidate) { candidate.id == item.id }) {
      changes.push("added:\{item.id}")
    }
  }
  changes
}

///|
pub fn rule_pack_with_patches(
  pack : RulePack,
  patches : Array[RulePatch],
) -> RulePack {
  { ..pack, rules: apply_rule_patches(pack.rules, patches) }
}

///|
pub fn pack_round_trip_text(pack : RulePack) -> Bool {
  !pack_to_text(pack).is_empty() && !pack_checksum(pack).is_empty()
}