///|
pub(all) struct RepairFeatureUsage {
  feature : String
  uses : Int
  first_rank : Int
  total_cost : Int
  values : Array[Value]
  common_to_all : Bool
  single_target_value : Bool
} derive(Debug, Eq)

///|
pub(all) struct RepairPortfolio {
  policy_name : String
  baseline : Truth
  target : Truth
  repair_count : Int
  cheapest_cost : Int
  most_expensive_cost : Int
  total_changes : Int
  average_changes : Int
  consensus_features : Array[String]
  optional_features : Array[String]
  usages : Array[RepairFeatureUsage]
} derive(Debug, Eq)

///|
pub fn Plan::portfolio(self : Plan) -> RepairPortfolio {
  let usages : Array[RepairFeatureUsage] = []
  let mut cheapest_cost = -1
  let mut most_expensive_cost = -1
  let mut total_changes = 0
  for repair_index, repair in self.repairs {
    if cheapest_cost < 0 || repair.cost < cheapest_cost {
      cheapest_cost = repair.cost
    }
    if repair.cost > most_expensive_cost {
      most_expensive_cost = repair.cost
    }
    total_changes = total_changes + repair.changes.length()
    for change in repair.changes {
      record_feature_usage(usages, change, repair_index)
    }
  }
  let consensus_features : Array[String] = []
  let optional_features : Array[String] = []
  for index, item in usages {
    let common_to_all = item.uses == self.repairs.length() &&
      self.repairs.length() > 0
    let single_target_value = item.values.length() <= 1
    usages[index] = {
      feature: item.feature,
      uses: item.uses,
      first_rank: item.first_rank,
      total_cost: item.total_cost,
      values: item.values,
      common_to_all,
      single_target_value,
    }
    if common_to_all {
      consensus_features.push(item.feature)
    } else {
      optional_features.push(item.feature)
    }
  }
  usages.sort_by(compare_feature_usage)
  {
    policy_name: self.policy_name,
    baseline: self.baseline,
    target: self.target,
    repair_count: self.repairs.length(),
    cheapest_cost,
    most_expensive_cost,
    total_changes,
    average_changes: if self.repairs.is_empty() {
      0
    } else {
      total_changes / self.repairs.length()
    },
    consensus_features,
    optional_features,
    usages,
  }
}

///|
fn record_feature_usage(
  usages : Array[RepairFeatureUsage],
  change : Change,
  repair_index : Int,
) -> Unit {
  let existing = find_usage_index(usages, change.feature)
  match existing {
    None =>
      usages.push({
        feature: change.feature,
        uses: 1,
        first_rank: repair_index + 1,
        total_cost: change.cost,
        values: [change.to],
        common_to_all: false,
        single_target_value: true,
      })
    Some(index) => {
      let item = usages[index]
      let values = item.values.copy()
      if !values.contains(change.to) {
        values.push(change.to)
      }
      usages[index] = {
        feature: item.feature,
        uses: item.uses + 1,
        first_rank: item.first_rank,
        total_cost: item.total_cost + change.cost,
        values,
        common_to_all: item.common_to_all,
        single_target_value: item.single_target_value,
      }
    }
  }
}

///|
fn find_usage_index(
  usages : Array[RepairFeatureUsage],
  feature : String,
) -> Int? {
  for index, item in usages {
    if item.feature == feature {
      return Some(index)
    }
  }
  None
}

///|
fn compare_feature_usage(
  left : RepairFeatureUsage,
  right : RepairFeatureUsage,
) -> Int {
  let by_uses = right.uses.compare(left.uses)
  if by_uses != 0 {
    return by_uses
  }
  let by_cost = right.total_cost.compare(left.total_cost)
  if by_cost != 0 {
    return by_cost
  }
  left.feature.compare(right.feature)
}

///|
pub fn RepairPortfolio::has_consensus(self : RepairPortfolio) -> Bool {
  !self.consensus_features.is_empty()
}

///|
pub fn RepairPortfolio::feature_usage(
  self : RepairPortfolio,
  feature : String,
) -> RepairFeatureUsage? {
  for item in self.usages {
    if item.feature == feature {
      return Some(item)
    }
  }
  None
}

///|
pub fn RepairFeatureUsage::values_text(self : RepairFeatureUsage) -> String {
  if self.values.is_empty() {
    return "none"
  }
  self.values.map(value => value.render()).join(", ")
}

///|
pub fn RepairFeatureUsage::average_cost(self : RepairFeatureUsage) -> Int {
  if self.uses == 0 {
    0
  } else {
    self.total_cost / self.uses
  }
}

///|
pub fn RepairPortfolio::to_markdown(self : RepairPortfolio) -> String {
  let out = StringBuilder::new()
  out.write_string("# Repair portfolio: \{self.policy_name}\n\n")
  out.write_string("- Current outcome: **\{self.baseline.render()}**\n")
  out.write_string("- Target outcome: **\{self.target.render()}**\n")
  out.write_string("- Repairs: \{self.repair_count}\n")
  out.write_string("- Cheapest cost: \{portfolio_cost(self.cheapest_cost)}\n")
  out.write_string(
    "- Most expensive cost: \{portfolio_cost(self.most_expensive_cost)}\n",
  )
  out.write_string("- Total changes: \{self.total_changes}\n")
  out.write_string("- Average changes: \{self.average_changes}\n")
  out.write_string(
    "- Consensus features: \{portfolio_names(self.consensus_features)}\n",
  )
  out.write_string(
    "- Optional features: \{portfolio_names(self.optional_features)}\n\n",
  )
  if self.usages.is_empty() {
    out.write_string("No repair changes were available.\n")
    return out.to_string()
  }
  out.write_string(
    "| feature | uses | first rank | total cost | average cost | values | common | single value |\n",
  )
  out.write_string("| --- | ---: | ---: | ---: | ---: | --- | --- | --- |\n")
  for item in self.usages {
    out.write_string(
      "| \{portfolio_cell(item.feature)} | \{item.uses} | \{item.first_rank} | \{item.total_cost} | \{item.average_cost()} | \{portfolio_cell(item.values_text())} | \{item.common_to_all} | \{item.single_target_value} |\n",
    )
  }
  out.to_string()
}

///|
pub fn RepairPortfolio::to_json(self : RepairPortfolio) -> String {
  let out = StringBuilder::new()
  out.write_string("{")
  out.write_string("\"policy\":\"\{portfolio_json_escape(self.policy_name)}\",")
  out.write_string("\"baseline\":\"\{self.baseline.render()}\",")
  out.write_string("\"target\":\"\{self.target.render()}\",")
  out.write_string("\"repair_count\":\{self.repair_count},")
  out.write_string("\"cheapest_cost\":\{self.cheapest_cost},")
  out.write_string("\"most_expensive_cost\":\{self.most_expensive_cost},")
  out.write_string("\"total_changes\":\{self.total_changes},")
  out.write_string("\"average_changes\":\{self.average_changes},")
  out.write_string("\"consensus_features\":")
  append_string_array(out, self.consensus_features)
  out.write_string(",\"optional_features\":")
  append_string_array(out, self.optional_features)
  out.write_string(",\"usages\":[")
  for index, item in self.usages {
    if index > 0 {
      out.write_char(',')
    }
    out.write_string("{")
    out.write_string("\"feature\":\"\{portfolio_json_escape(item.feature)}\",")
    out.write_string("\"uses\":\{item.uses},")
    out.write_string("\"first_rank\":\{item.first_rank},")
    out.write_string("\"total_cost\":\{item.total_cost},")
    out.write_string("\"average_cost\":\{item.average_cost()},")
    out.write_string("\"common_to_all\":\{item.common_to_all},")
    out.write_string("\"single_target_value\":\{item.single_target_value},")
    out.write_string("\"values\":[")
    for value_index, value in item.values {
      if value_index > 0 {
        out.write_char(',')
      }
      out.write_string("\"\{portfolio_json_escape(value.plain())}\"")
    }
    out.write_string("]}")
  }
  out.write_string("]}")
  out.to_string()
}

///|
fn append_string_array(out : StringBuilder, values : Array[String]) -> Unit {
  out.write_char('[')
  for index, value in values {
    if index > 0 {
      out.write_char(',')
    }
    out.write_string("\"\{portfolio_json_escape(value)}\"")
  }
  out.write_char(']')
}

///|
fn portfolio_cost(value : Int) -> String {
  if value < 0 {
    "none"
  } else {
    value.to_string()
  }
}

///|
fn portfolio_names(values : Array[String]) -> String {
  if values.is_empty() {
    "none"
  } else {
    values.join(", ")
  }
}

///|
fn portfolio_cell(value : String) -> String {
  value.replace_all(old="|", new="\\|").replace_all(old="\n", new=" ")
}

///|
fn portfolio_json_escape(text : String) -> String {
  let out = StringBuilder::new()
  for ch in text {
    match ch {
      '"' => out.write_string("\\\"")
      '\\' => out.write_string("\\\\")
      '\n' => out.write_string("\\n")
      '\r' => out.write_string("\\r")
      '\t' => out.write_string("\\t")
      _ => out.write_char(ch)
    }
  }
  out.to_string()
}