///|
/// Structural or behavioral policy change.
pub(all) enum ChangeKind {
  GroupAdded
  GroupRemoved
  RuleAdded
  RuleRemoved
  SitemapAdded
  SitemapRemoved
  HostAdded
  HostRemoved
  DelayChanged
  AccessBecameAllowed
  AccessBecameDenied
} derive(Eq, Debug)

///|
pub fn ChangeKind::label(self : ChangeKind) -> String {
  match self {
    GroupAdded => "group-added"
    GroupRemoved => "group-removed"
    RuleAdded => "rule-added"
    RuleRemoved => "rule-removed"
    SitemapAdded => "sitemap-added"
    SitemapRemoved => "sitemap-removed"
    HostAdded => "host-added"
    HostRemoved => "host-removed"
    DelayChanged => "delay-changed"
    AccessBecameAllowed => "access-became-allowed"
    AccessBecameDenied => "access-became-denied"
  }
}

///|
pub fn ChangeKind::is_behavioral(self : ChangeKind) -> Bool {
  self == AccessBecameAllowed || self == AccessBecameDenied
}

///|
/// One source-aware difference between two policies.
pub struct PolicyChange {
  kind : ChangeKind
  subject : String
  before : String
  after : String
  line_before : Int
  line_after : Int
} derive(Eq, Debug)

///|
pub fn PolicyChange::kind(self : PolicyChange) -> ChangeKind {
  self.kind
}

///|
pub fn PolicyChange::subject(self : PolicyChange) -> String {
  self.subject
}

///|
pub fn PolicyChange::before(self : PolicyChange) -> String {
  self.before
}

///|
pub fn PolicyChange::after(self : PolicyChange) -> String {
  self.after
}

///|
pub fn PolicyChange::line_before(self : PolicyChange) -> Int {
  self.line_before
}

///|
pub fn PolicyChange::line_after(self : PolicyChange) -> Int {
  self.line_after
}

///|
pub fn PolicyChange::summary(self : PolicyChange) -> String {
  let transition = if self.before.length() > 0 && self.after.length() > 0 {
    "`\{self.before}` -> `\{self.after}`"
  } else if self.after.length() > 0 {
    "`\{self.after}`"
  } else {
    "`\{self.before}`"
  }
  "\{self.kind.label()}: \{self.subject} \{transition}"
}

///|
/// Complete structural and optional behavioral difference.
pub struct PolicyDiff {
  changes : Array[PolicyChange]
  structural_count : Int
  behavioral_count : Int
} derive(Eq, Debug)

///|
pub fn PolicyDiff::changes(self : PolicyDiff) -> Array[PolicyChange] {
  self.changes
}

///|
pub fn PolicyDiff::structural_count(self : PolicyDiff) -> Int {
  self.structural_count
}

///|
pub fn PolicyDiff::behavioral_count(self : PolicyDiff) -> Int {
  self.behavioral_count
}

///|
pub fn PolicyDiff::is_empty(self : PolicyDiff) -> Bool {
  self.changes.length() == 0
}

///|
fn group_key(group : Group) -> String {
  join_strings(group.agents, ",")
}

///|
fn group_rule_key(group : Group, rule : Rule) -> String {
  "\{group_key(group)}|\{rule.kind.label()}|\{rule.normalized_pattern}"
}

///|
fn contains_group(policy : Policy, key : String) -> Bool {
  for group in policy.groups {
    if group_key(group) == key {
      return true
    }
  }
  false
}

///|
fn contains_rule(policy : Policy, key : String) -> Bool {
  for group in policy.groups {
    for rule in group.rules {
      if group_rule_key(group, rule) == key {
        return true
      }
    }
  }
  false
}

///|
fn find_group(policy : Policy, key : String) -> Group? {
  for group in policy.groups {
    if group_key(group) == key {
      return Some(group)
    }
  }
  None
}

///|
fn add_group_changes(
  before : Policy,
  after : Policy,
  output : Array[PolicyChange],
) -> Unit {
  for group in before.groups {
    let key = group_key(group)
    if !contains_group(after, key) {
      output.push({
        kind: GroupRemoved,
        subject: key,
        before: key,
        after: "",
        line_before: group.start_line,
        line_after: 0,
      })
    }
  }
  for group in after.groups {
    let key = group_key(group)
    if !contains_group(before, key) {
      output.push({
        kind: GroupAdded,
        subject: key,
        before: "",
        after: key,
        line_before: 0,
        line_after: group.start_line,
      })
    }
  }
}

///|
fn add_rule_changes(
  before : Policy,
  after : Policy,
  output : Array[PolicyChange],
) -> Unit {
  for group in before.groups {
    for rule in group.rules {
      let key = group_rule_key(group, rule)
      if !contains_rule(after, key) {
        output.push({
          kind: RuleRemoved,
          subject: group_key(group),
          before: "\{rule.kind.label()} \{rule.pattern}",
          after: "",
          line_before: rule.line,
          line_after: 0,
        })
      }
    }
  }
  for group in after.groups {
    for rule in group.rules {
      let key = group_rule_key(group, rule)
      if !contains_rule(before, key) {
        output.push({
          kind: RuleAdded,
          subject: group_key(group),
          before: "",
          after: "\{rule.kind.label()} \{rule.pattern}",
          line_before: 0,
          line_after: rule.line,
        })
      }
    }
  }
}

///|
fn add_string_set_changes(
  before : Array[String],
  after : Array[String],
  added_kind : ChangeKind,
  removed_kind : ChangeKind,
  subject : String,
  output : Array[PolicyChange],
) -> Unit {
  for value in before {
    if !array_contains_string(after, value) {
      output.push({
        kind: removed_kind,
        subject,
        before: value,
        after: "",
        line_before: 0,
        line_after: 0,
      })
    }
  }
  for value in after {
    if !array_contains_string(before, value) {
      output.push({
        kind: added_kind,
        subject,
        before: "",
        after: value,
        line_before: 0,
        line_after: 0,
      })
    }
  }
}

///|
fn add_delay_changes(
  before : Policy,
  after : Policy,
  output : Array[PolicyChange],
) -> Unit {
  for old_group in before.groups {
    let key = group_key(old_group)
    match find_group(after, key) {
      Some(new_group) =>
        if old_group.crawl_delay_millis != new_group.crawl_delay_millis {
          output.push({
            kind: DelayChanged,
            subject: key,
            before: "\{old_group.crawl_delay_millis}",
            after: "\{new_group.crawl_delay_millis}",
            line_before: old_group.start_line,
            line_after: new_group.start_line,
          })
        }
      None => ()
    }
  }
}

///|
fn add_behavior_changes(
  before : Policy,
  after : Policy,
  probes : Array[AccessProbe],
  output : Array[PolicyChange],
) -> Unit {
  for probe in probes {
    let old_decision = decide(before, probe.agent, probe.path)
    let new_decision = decide(after, probe.agent, probe.path)
    if old_decision.allowed != new_decision.allowed {
      output.push({
        kind: if new_decision.allowed {
          AccessBecameAllowed
        } else {
          AccessBecameDenied
        },
        subject: probe.name,
        before: old_decision.verdict(),
        after: new_decision.verdict(),
        line_before: old_decision.line,
        line_after: new_decision.line,
      })
    }
  }
}

///|
/// Compares policy structure and access behavior on caller-provided probes.
pub fn diff_policies(
  before : Policy,
  after : Policy,
  probes : Array[AccessProbe],
) -> PolicyDiff {
  let changes : Array[PolicyChange] = []
  add_group_changes(before, after, changes)
  add_rule_changes(before, after, changes)
  add_string_set_changes(
    before.sitemaps,
    after.sitemaps,
    SitemapAdded,
    SitemapRemoved,
    "sitemap",
    changes,
  )
  add_string_set_changes(
    before.hosts,
    after.hosts,
    HostAdded,
    HostRemoved,
    "host",
    changes,
  )
  add_delay_changes(before, after, changes)
  add_behavior_changes(before, after, probes, changes)
  let mut structural_count = 0
  let mut behavioral_count = 0
  for change in changes {
    if change.kind.is_behavioral() {
      behavioral_count = behavioral_count + 1
    } else {
      structural_count = structural_count + 1
    }
  }
  { changes, structural_count, behavioral_count }
}

///|
pub fn structural_diff(before : Policy, after : Policy) -> PolicyDiff {
  diff_policies(before, after, [])
}

///|
pub fn render_diff(diff : PolicyDiff) -> String {
  if diff.is_empty() {
    return "no policy changes\n"
  }
  let output = StringBuilder::new()
  output.write_string(
    "structural changes: \{diff.structural_count}, behavioral changes: \{diff.behavioral_count}\n",
  )
  for change in diff.changes {
    output.write_string("- ")
    output.write_string(change.summary())
    output.write_char('\n')
  }
  output.to_string()
}

///|
pub fn behavior_changes(diff : PolicyDiff) -> Array[PolicyChange] {
  let output : Array[PolicyChange] = []
  for change in diff.changes {
    if change.kind.is_behavioral() {
      output.push(change)
    }
  }
  output
}