///|
/// A deterministic context/platform probe matrix for shortcut reachability.
///
/// Conflict reports explain that two declarations overlap; this report answers
/// the complementary release question: for a concrete context and platform,
/// which binding actually wins, and which declarations are never selected?
pub(all) enum ReachabilityStatus {
  Reachable
  Shadowed
  Ambiguous
  Unavailable
  Disabled
} derive(Eq, @debug.Debug)

///|
pub(all) struct ReachabilityCell {
  binding_id : String
  context : String
  platform : String
  status : ReachabilityStatus
  selected_id : String
  message : String
} derive(Eq, @debug.Debug)

///|
pub(all) struct ReachabilityReport {
  keymap_name : String
  contexts : Array[String]
  platforms : Array[String]
  cells : Array[ReachabilityCell]
  binding_count : Int
  reachable_count : Int
  shadowed_count : Int
  ambiguous_count : Int
  unavailable_count : Int
  disabled_count : Int
} derive(Eq, @debug.Debug)

///|
fn reachability_status_name(status : ReachabilityStatus) -> String {
  match status {
    Reachable => "reachable"
    Shadowed => "shadowed"
    Ambiguous => "ambiguous"
    Unavailable => "unavailable"
    Disabled => "disabled"
  }
}

///|
fn add_unique(values : Array[String], value : String) -> Unit {
  if value.length() > 0 && !array_contains(values, value) {
    values.push(value)
  }
}

///|
fn sorted_unique(values : Array[String]) -> Array[String] {
  let result : Array[String] = []
  for value in values {
    add_unique(result, value)
  }
  result.sort()
  result
}

///|
fn sorted_platforms(values : Array[String]) -> Array[String] {
  let cleaned : Array[String] = []
  for value in values {
    add_unique(cleaned, lower_ascii(trim_ascii(value)))
  }
  sorted_unique(cleaned)
}

///|
fn reachability_cell(
  binding : Binding,
  context : String,
  platform : String,
  result : DispatchResult,
) -> ReachabilityCell {
  if !binding.enabled {
    return {
      binding_id: binding.id,
      context,
      platform,
      status: Disabled,
      selected_id: "",
      message: "binding is disabled and retained only for migration",
    }
  }
  let selected_id = if result.binding_ids.length() > 0 {
    result.binding_ids[0]
  } else {
    ""
  }
  let includes = array_contains(result.binding_ids, binding.id)
  if result.status == DispatchStatus::Ambiguous && includes {
    {
      binding_id: binding.id,
      context,
      platform,
      status: ReachabilityStatus::Ambiguous,
      selected_id,
      message: "binding ties with another equally specific declaration",
    }
  } else if result.status == DispatchStatus::Matched &&
    includes &&
    selected_id == binding.id {
    {
      binding_id: binding.id,
      context,
      platform,
      status: ReachabilityStatus::Reachable,
      selected_id,
      message: "binding is selected by the dispatcher",
    }
  } else if result.status == DispatchStatus::Matched && includes {
    {
      binding_id: binding.id,
      context,
      platform,
      status: ReachabilityStatus::Shadowed,
      selected_id,
      message: "another binding wins by priority or context specificity",
    }
  } else {
    {
      binding_id: binding.id,
      context,
      platform,
      status: ReachabilityStatus::Unavailable,
      selected_id,
      message: "binding is not active in this context and platform",
    }
  }
}

///|
/// Probe every declaration against the requested contexts and platforms.
///
/// Empty probe arrays mean all declared contexts and the `all` platform. The
/// resulting arrays are sorted and deduplicated so the report fingerprint can
/// be compared in CI without depending on caller order.
pub fn reachability_matrix(
  keymap : Keymap,
  contexts? : Array[String] = [],
  platforms? : Array[String] = [],
) -> ReachabilityReport {
  let context_values = if contexts.length() == 0 {
    let declared : Array[String] = []
    for context in keymap.contexts {
      add_unique(declared, context.name)
    }
    sorted_unique(declared)
  } else {
    sorted_unique(contexts)
  }
  let platform_values = if platforms.length() == 0 {
    ["all"]
  } else {
    sorted_platforms(platforms)
  }
  let cells : Array[ReachabilityCell] = []
  let mut reachable = 0
  let mut shadowed = 0
  let mut ambiguous = 0
  let mut unavailable = 0
  let mut disabled = 0
  for binding in keymap.bindings {
    for context in context_values {
      for platform in platform_values {
        let cell = reachability_cell(
          binding,
          context,
          platform,
          dispatch(keymap, binding.keys.raw, context~, platform~),
        )
        match cell.status {
          ReachabilityStatus::Reachable => reachable += 1
          ReachabilityStatus::Shadowed => shadowed += 1
          ReachabilityStatus::Ambiguous => ambiguous += 1
          ReachabilityStatus::Unavailable => unavailable += 1
          ReachabilityStatus::Disabled => disabled += 1
        }
        cells.push(cell)
      }
    }
  }
  {
    keymap_name: keymap.name,
    contexts: context_values,
    platforms: platform_values,
    cells,
    binding_count: keymap.bindings.length(),
    reachable_count: reachable,
    shadowed_count: shadowed,
    ambiguous_count: ambiguous,
    unavailable_count: unavailable,
    disabled_count: disabled,
  }
}

///|
pub fn ReachabilityReport::summary(self : ReachabilityReport) -> String {
  "reachability=" +
  self.keymap_name +
  " bindings=" +
  self.binding_count.to_string() +
  " reachable=" +
  self.reachable_count.to_string() +
  " shadowed=" +
  self.shadowed_count.to_string() +
  " ambiguous=" +
  self.ambiguous_count.to_string() +
  " unavailable=" +
  self.unavailable_count.to_string() +
  " disabled=" +
  self.disabled_count.to_string()
}

///|
pub fn reachability_to_markdown(report : ReachabilityReport) -> String {
  let lines : Array[String] = [
    "## Reachability matrix",
    "",
    report.summary(),
    "",
    "| Binding | Context | Platform | Status | Selected | Explanation |",
    "| --- | --- | --- | --- | --- | --- |",
  ]
  for cell in report.cells {
    lines.push(
      "| `" +
      cell.binding_id +
      "` | `" +
      cell.context +
      "` | `" +
      cell.platform +
      "` | " +
      reachability_status_name(cell.status) +
      " | `" +
      cell.selected_id +
      "` | " +
      cell.message +
      " |",
    )
  }
  lines.join("\n")
}

///|
pub fn reachability_to_json(report : ReachabilityReport) -> String {
  let rows : Array[String] = []
  for cell in report.cells {
    rows.push(
      "{\"binding\":" +
      json_string(cell.binding_id) +
      ",\"context\":" +
      json_string(cell.context) +
      ",\"platform\":" +
      json_string(cell.platform) +
      ",\"status\":" +
      json_string(reachability_status_name(cell.status)) +
      ",\"selected\":" +
      json_string(cell.selected_id) +
      ",\"message\":" +
      json_string(cell.message) +
      "}",
    )
  }
  "{\"keymap\":" +
  json_string(report.keymap_name) +
  ",\"contexts\":[" +
  report.contexts.map(item => json_string(item)).join(",") +
  "],\"platforms\":[" +
  report.platforms.map(item => json_string(item)).join(",") +
  "],\"bindings\":" +
  report.binding_count.to_string() +
  ",\"reachable\":" +
  report.reachable_count.to_string() +
  ",\"shadowed\":" +
  report.shadowed_count.to_string() +
  ",\"ambiguous\":" +
  report.ambiguous_count.to_string() +
  ",\"unavailable\":" +
  report.unavailable_count.to_string() +
  ",\"disabled\":" +
  report.disabled_count.to_string() +
  ",\"cells\":[" +
  rows.join(",") +
  "]}"
}