///|
fn budget_status_text(status : BudgetStatus) -> String {
  match status {
    BudgetPassed => "passed"
    BudgetExceeded => "exceeded"
    BudgetUnavailable => "unavailable"
  }
}

///|
fn budget_scope() -> String {
  "All sizes are bytes. Equality passes; growth compares current - baseline. " +
  "file.gzip measures the entire file as one gzip stream with the current bikallem/compress defaults, not server transfer cost. " +
  "code.raw includes its section header and count encoding. package.raw sums local function total_size under the existing symbol attribution; " +
  "it is not total source-package cost or predicted deletion savings. Unknown attribution makes a package rule unavailable."
}

///|
fn budget_number(value : Int?) -> String {
  match value {
    Some(n) => n.to_string()
    None => "-"
  }
}

///|
fn budget_artifact_json(artifact : BudgetArtifact, path : String?) -> Json {
  Json::object({
    "path": path_optional_string(path),
    "sha256": Json::string(artifact.sha256),
    "raw_bytes": int_json(artifact.raw_bytes),
    "code_bytes": int_json(artifact.code_bytes),
    "whole_gzip_bytes": match artifact.gzip {
      Some(result) => gzip_json(result)
      None => Json::null()
    },
    "gzip_error": match artifact.gzip {
      Some(result) => gzip_error_json(result)
      None => Json::null()
    },
    "attribution": match artifact.coverage {
      None => Json::null()
      Some(c) =>
        Json::object({
          "functions": int_json(c.functions),
          "function_bytes": int_json(c.function_bytes),
          "unattributed_functions": int_json(c.unattributed_functions),
          "unattributed_bytes": int_json(c.unattributed_bytes),
          "complete_within_symbol_model": Json::boolean(
            c.unattributed_functions == 0,
          ),
        })
    },
  })
}

///|
fn budget_bool(value : Bool?) -> Json {
  match value {
    Some(value) => Json::boolean(value)
    None => Json::null()
  }
}

///|
pub fn render_budget_json(
  report : BudgetReport,
  current_path : String,
  baseline_path? : String,
) -> String {
  Json::object({
    "format": Json::string("moonsize-budget"),
    "schema_version": int_json(1),
    "config_version": int_json(report.config_version),
    "status": Json::string(budget_status_text(report.status)),
    "exit_code": int_json(report.exit_code),
    "current": budget_artifact_json(report.current, Some(current_path)),
    "baseline": match report.baseline {
      Some(side) => budget_artifact_json(side, baseline_path)
      None => Json::null()
    },
    "compression": Json::string(
      "whole file, one gzip stream; bikallem/compress current defaults; independent streams are never added",
    ),
    "scope": Json::string(budget_scope()),
    "rules": Json::array(
      report.rules.map(fn(rule) {
        Json::object({
          "id": Json::string(rule.id),
          "metric": Json::string(rule.metric),
          "package": path_optional_string(rule.package_name),
          "mode": Json::string(rule.mode),
          "current": optional_index_json(rule.current),
          "baseline": optional_index_json(rule.baseline),
          "delta": optional_index_json(rule.delta),
          "max_bytes": int_json(rule.max_bytes),
          "status": Json::string(budget_status_text(rule.status)),
          "reason": Json::string(rule.reason),
          "current_package_present": budget_bool(rule.current_package_present),
          "baseline_package_present": budget_bool(rule.baseline_package_present),
        })
      }),
    ),
  }).stringify(indent=2) +
  "\n"
}

///|
/// Escape untrusted table content without allowing HTML, Markdown or line injection.
fn budget_md(text : String) -> String {
  html_escape(text)
  .replace(old="|", new="|")
  .replace(old="`", new="`")
  .replace(old="\\", new="\")
  .replace(old="\r", new="
")
  .replace(old="\n", new="
")
  .replace(old="*", new="*")
  .replace(old="_", new="_")
  .replace(old="[", new="[")
  .replace(old="]", new="]")
}

///|
pub fn render_budget_markdown(
  report : BudgetReport,
  current_path : String,
  baseline_path? : String,
) -> String {
  let out = StringBuilder()
  out.write_string(
    "## Size budget: " + budget_status_text(report.status) + "\n\n",
  )
  out.write_string(
    "| Rule | Metric / package | Mode | Current B | Baseline B | Delta B | Limit B | Status | Reason |\n",
  )
  out.write_string(
    "| --- | --- | --- | ---: | ---: | ---: | ---: | --- | --- |\n",
  )
  for r in report.rules {
    let label = r.metric +
      (match r.package_name {
        Some(name) => " / " + name
        None => ""
      })
    out.write_string(
      "| " +
      budget_md(r.id) +
      " | " +
      budget_md(label) +
      " | " +
      r.mode +
      " | " +
      budget_number(r.current) +
      " | " +
      budget_number(r.baseline) +
      " | " +
      budget_number(r.delta) +
      " | " +
      r.max_bytes.to_string() +
      " | " +
      budget_status_text(r.status) +
      " | " +
      budget_md(r.reason) +
      " |\n",
    )
  }
  out.write_string(
    "\nCurrent: " +
    budget_md(current_path) +
    "; SHA-256: " +
    report.current.sha256 +
    "\n",
  )
  if report.baseline is Some(side) {
    out.write_string(
      "\nBaseline: " +
      budget_md(baseline_path.unwrap_or("(path not supplied)")) +
      "; SHA-256: " +
      side.sha256 +
      "\n",
    )
  }
  for
    (label, side) in [
      ("Current", Some(report.current)),
      ("Baseline", report.baseline),
    ] {
    if side is Some(artifact) && artifact.coverage is Some(c) {
      out.write_string(
        "\n" +
        label +
        " attribution: " +
        c.unattributed_functions.to_string() +
        " / " +
        c.functions.to_string() +
        " local functions unassigned (" +
        c.unattributed_bytes.to_string() +
        " B). Package presence is observed, not proof of complete coverage.\n",
      )
    }
  }
  out.write_string(
    "\nExit code: " +
    report.exit_code.to_string() +
    ".\n\n" +
    budget_scope() +
    "\n",
  )
  out.to_string()
}

///|
pub fn render_budget(report : BudgetReport) -> String {
  let out = StringBuilder()
  out.write_string(
    "SIZE BUDGET\n\n  Values in bytes; delta = current - baseline.\n",
  )
  out.write_string(
    "  RULE / METRIC / MODE / CURRENT / BASELINE / DELTA / LIMIT / STATUS\n",
  )
  for r in report.rules {
    out.write_string(
      "  " +
      Json::string(r.id).stringify() +
      " / " +
      r.metric +
      " / " +
      r.mode +
      " / " +
      budget_number(r.current) +
      " / " +
      budget_number(r.baseline) +
      " / " +
      budget_number(r.delta) +
      " / " +
      r.max_bytes.to_string() +
      " / " +
      budget_status_text(r.status) +
      "\n",
    )
    if r.package_name is Some(name) {
      out.write_string("    package: " + Json::string(name).stringify() + "\n")
    }
    out.write_string("    " + Json::string(r.reason).stringify() + "\n")
  }
  out.write_string(
    "\nOverall: " +
    budget_status_text(report.status) +
    "; exit code " +
    report.exit_code.to_string() +
    "\n" +
    budget_scope() +
    "\n",
  )
  out.to_string()
}