///|
pub(all) struct SketchRecommendation {
  severity : String
  code : String
  title : String
  detail : String
  action : String
} derive(Eq, Debug)

///|
pub fn recommendations_from_gate(
  report : SketchGateReport,
) -> Array[SketchRecommendation] {
  let items : Array[SketchRecommendation] = Array::new()
  if report.issues.length() == 0 {
    items.push(
      sketch_recommendation(
        "info", "gate-clean", "Quality gate passed", "The compared streams stayed inside the configured policy.",
        "Keep this policy in CI and tighten thresholds when the data becomes stable.",
      ),
    )
  } else {
    for issue in report.issues {
      items.push(recommendation_from_gate_issue(issue))
    }
  }
  if report.drift.candidate.bloom.fill_ratio > 0.70 {
    items.push(
      sketch_recommendation(
        "warning", "bloom-saturated", "Bloom filter is getting saturated", "The candidate bloom filter has a high bit fill ratio.",
        "Increase the bit array size or use more selective keys.",
      ),
    )
  }
  items
}

///|
pub fn recommendations_from_windowed(
  report : WindowedStreamReport,
) -> Array[SketchRecommendation] {
  let items : Array[SketchRecommendation] = Array::new()
  if report.windows.length() == 0 {
    items.push(
      sketch_recommendation(
        "warning", "empty-windowed-stream", "No windows were parsed", "The input did not contain any non-empty event window.",
        "Check whether the input uses spaces, commas, `|`, or `;` as separators.",
      ),
    )
    return items
  }
  if report.max_event_spike > 0 {
    items.push(
      sketch_recommendation(
        "info",
        "event-spike",
        "Window traffic changed",
        "At least one adjacent window increased by " +
        report.max_event_spike.to_string() +
        " events.",
        "Inspect the Top-K timeline to see whether the increase is concentrated in a few keys.",
      ),
    )
  }
  if report.min_similarity < 0.40 {
    items.push(
      sketch_recommendation(
        "warning",
        "low-window-similarity",
        "Top-K composition shifted",
        "The minimum adjacent-window MinHash similarity is " +
        sketch_double_text(report.min_similarity) +
        ".",
        "Treat this as a drift candidate and compare the raw examples for the affected window.",
      ),
    )
  }
  for snapshot in report.windows {
    if snapshot.bloom_false_positive_rate > 0.20 {
      items.push(
        sketch_recommendation(
          "warning",
          "window-bloom-fpr",
          "Bloom filter may be too small",
          snapshot.label +
          " has estimated false positive rate " +
          sketch_double_text(snapshot.bloom_false_positive_rate) +
          ".",
          "Use a larger bloom size for this event volume.",
        ),
      )
    }
  }
  if items.length() == 0 {
    items.push(
      sketch_recommendation(
        "info", "windowed-stable", "Windowed stream looks stable", "No strong spike, similarity drop, or bloom saturation was detected.",
        "Use the JSON output as a compact regression fixture.",
      ),
    )
  }
  items
}

///|
pub fn recommendations_from_weighted(
  summary : WeightedStreamSummary,
) -> Array[SketchRecommendation] {
  let items : Array[SketchRecommendation] = Array::new()
  if summary.event_rows == 0 {
    items.push(
      sketch_recommendation(
        "warning", "empty-weighted-stream", "No weighted events were parsed", "The input has no valid `key:weight` or `key=weight` token.",
        "Use tokens such as `login:12 checkout:3`.",
      ),
    )
    return items
  }
  if summary.total_weight > summary.event_rows * 20 {
    items.push(
      sketch_recommendation(
        "info",
        "heavy-weighted-updates",
        "Weights dominate row count",
        "The stream contains " +
        summary.event_rows.to_string() +
        " rows but " +
        summary.total_weight.to_string() +
        " total updates.",
        "Prefer weighted updates instead of expanding rows in application code.",
      ),
    )
  }
  if summary.topk.length() > 0 {
    let top = summary.topk[0]
    if top.count * 2 >= summary.total_weight {
      items.push(
        sketch_recommendation(
          "warning",
          "single-key-dominates",
          "One key dominates the weighted stream",
          top.key +
          " contributes " +
          top.count.to_string() +
          " out of " +
          summary.total_weight.to_string() +
          " weighted updates.",
          "Check whether this is expected traffic or a data-quality issue.",
        ),
      )
    }
  }
  if items.length() == 0 {
    items.push(
      sketch_recommendation(
        "info", "weighted-balanced", "Weighted stream is balanced", "No single key dominates and the row-to-weight ratio is moderate.",
        "Use the Count-Min table as a compact monitoring snapshot.",
      ),
    )
  }
  items
}

///|
pub fn recommendations_markdown(items : Array[SketchRecommendation]) -> String {
  let out = StringBuilder()
  out.write_string("# Sketch Recommendations\n\n")
  out.write_string(
    "| severity | code | title | action |\n| --- | --- | --- | --- |\n",
  )
  for item in items {
    out.write_string(
      "| " +
      item.severity +
      " | " +
      item.code +
      " | " +
      item.title +
      " | " +
      item.action +
      " |\n",
    )
  }
  out.to_string()
}

///|
pub fn recommendations_json(items : Array[SketchRecommendation]) -> String {
  let out = StringBuilder()
  out.write_string("[")
  for i in 0.. 0 {
      out.write_string(",")
    }
    let item = items[i]
    out.write_string(
      "{\"severity\":\"" +
      sketch_escape_json(item.severity) +
      "\",\"code\":\"" +
      sketch_escape_json(item.code) +
      "\",\"title\":\"" +
      sketch_escape_json(item.title) +
      "\",\"detail\":\"" +
      sketch_escape_json(item.detail) +
      "\",\"action\":\"" +
      sketch_escape_json(item.action) +
      "\"}",
    )
  }
  out.write_string("]")
  out.to_string()
}

///|
pub fn sketch_recommendation(
  severity : String,
  code : String,
  title : String,
  detail : String,
  action : String,
) -> SketchRecommendation {
  { severity, code, title, detail, action }
}

///|
fn recommendation_from_gate_issue(
  issue : SketchGateIssue,
) -> SketchRecommendation {
  let action = match issue.rule {
    "max_event_delta" =>
      "Check ingestion volume and decide whether the baseline should be updated."
    "max_unique_delta" =>
      "Inspect new key sources and look for accidental high-cardinality values."
    "min_top_overlap" =>
      "Compare Top-K tables and verify whether dominant traffic shifted."
    "min_minhash_similarity" =>
      "Sample both streams and inspect whether the population changed."
    "max_bloom_false_positive_rate" =>
      "Increase bloom filter size or lower the number of inserted keys per filter."
    _ => "Review the metric and add a project-specific rule if needed."
  }
  sketch_recommendation(
    issue.severity,
    issue.rule,
    "Gate issue: " + issue.metric,
    issue.message,
    action,
  )
}