///|
pub(all) struct ImpactNote {
  severity : String
  category : String
  message : String
  owner : String
  line : Int
} derive(Eq, Debug)

///|
pub(all) struct ChangeImpact {
  score : Int
  level : String
  changed_owners : Int
  added : Int
  removed : Int
  ttl_changed : Int
  notes : Array[ImpactNote]
} derive(Debug)

///|
fn impact_note(
  notes : Array[ImpactNote],
  severity : String,
  category : String,
  message : String,
  change : ZoneChange,
) -> Unit {
  notes.push({
    severity,
    category,
    message,
    owner: change.owner,
    line: change.line,
  })
}

///|
fn change_type_weight(change : ZoneChange, origin : String) -> Int {
  if change.record_type == "SOA" {
    return 0
  }
  if change.record_type == "NS" && change.owner == origin {
    return 4
  }
  if change.record_type == "MX" && change.owner == origin {
    return 3
  }
  if change.record_type == "CAA" {
    return 3
  }
  if change.record_type == "CNAME" {
    return 2
  }
  if change.record_type == "A" || change.record_type == "AAAA" {
    return 2
  }
  1
}

///|
fn inspect_one_change(
  change : ZoneChange,
  origin : String,
  notes : Array[ImpactNote],
) -> Int {
  if change.kind == "ttl-changed" {
    match (change.before_ttl, change.after_ttl) {
      (Some(before), Some(after)) =>
        if after < before {
          impact_note(
            notes,
            "note",
            "ttl",
            "TTL decreased from \{before} to \{after}; caches may refresh sooner",
            change,
          )
        } else {
          impact_note(
            notes,
            "note",
            "ttl",
            "TTL increased from \{before} to \{after}; rollback may take longer",
            change,
          )
        }
      _ => ()
    }
    return 1
  }
  let weight = change_type_weight(change, origin)
  if change.record_type == "NS" && change.owner == origin {
    impact_note(
      notes, "high", "delegation", "apex nameserver changed; review authoritative service and glue",
      change,
    )
  } else if change.record_type == "MX" {
    impact_note(
      notes, "high", "mail", "mail routing changed; review target and priority",
      change,
    )
  } else if change.record_type == "CAA" {
    impact_note(
      notes, "high", "certificate", "CAA changed; review certificate issuance policy",
      change,
    )
  } else if change.record_type == "CNAME" {
    impact_note(
      notes, "medium", "alias", "alias destination changed; review dependent names",
      change,
    )
  } else if change.record_type == "A" || change.record_type == "AAAA" {
    impact_note(
      notes, "medium", "address", "address record changed; review service endpoint",
      change,
    )
  }
  if change.kind == "removed" {
    impact_note(
      notes, "medium", "removal", "record removed; check consumers and cache lifetime",
      change,
    )
    return weight + 1
  }
  weight
}

///|
/// A transparent review priority score, not a prediction of DNS availability.
/// SOA serial changes alone are ignored in this score.
pub fn assess_impact(diff : ZoneDiff, origin : String) -> ChangeImpact {
  let notes : Array[ImpactNote] = []
  let owners : Map[String, Bool] = Map([])
  let mut score = 0
  let mut added = 0
  let mut removed = 0
  let mut ttl_changed = 0
  for change in diff.changes {
    if change.record_type == "SOA" {
      continue
    }
    owners.set(change.owner, true)
    match change.kind {
      "added" => added += 1
      "removed" => removed += 1
      "ttl-changed" => ttl_changed += 1
      _ => ()
    }
    score += inspect_one_change(change, origin, notes)
  }
  for item in diff.diagnostics {
    if item.severity == "error" {
      score += 4
      notes.push({
        severity: "high",
        category: "serial",
        message: item.message,
        owner: item.owner,
        line: item.line,
      })
    }
  }
  let level = if score >= 8 {
    "high"
  } else if score >= 3 {
    "medium"
  } else {
    "low"
  }
  {
    score,
    level,
    changed_owners: owners.length(),
    added,
    removed,
    ttl_changed,
    notes,
  }
}

///|
pub fn render_impact_text(impact : ChangeImpact) -> String {
  let out = StringBuilder()
  out.write_string("Review priority: \{impact.level} (score \{impact.score})\n")
  out.write_string(
    "Owners: \{impact.changed_owners}, added: \{impact.added}, removed: \{impact.removed}, TTL changed: \{impact.ttl_changed}\n",
  )
  for note in impact.notes {
    out.write_string(
      "[\{note.severity}] \{note.category}: \{note.message} (\{note.owner})\n",
    )
  }
  out.to_string()
}

///|
pub fn render_impact_json(impact : ChangeImpact) -> String {
  let out = StringBuilder()
  out.write_string("{\"level\":")
  out.write_string(json_escaped(impact.level))
  out.write_string(",\"score\":\{impact.score}")
  out.write_string(",\"changed_owners\":\{impact.changed_owners}")
  out.write_string(",\"added\":\{impact.added}")
  out.write_string(",\"removed\":\{impact.removed}")
  out.write_string(",\"ttl_changed\":\{impact.ttl_changed}")
  out.write_string(",\"notes\":[")
  for index in 0.. 0 {
      out.write_char(',')
    }
    let note = impact.notes[index]
    out.write_char('{')
    write_json_field(out, "severity", note.severity, false)
    write_json_field(out, "category", note.category, true)
    write_json_field(out, "message", note.message, true)
    write_json_field(out, "owner", note.owner, true)
    write_json_number(out, "line", note.line, true)
    out.write_char('}')
  }
  out.write_string("]}")
  out.to_string()
}