///|
pub fn suite_input(
  domain : String,
  spf_text : String,
  dmarc_text : String,
  dkim_text : String,
  mtasts_text : String,
  tlsrpt_text : String,
  bimi_text : String,
) -> MailSecuritySuiteInput {
  {
    domain,
    spf_text,
    dmarc_text,
    dkim_text,
    mtasts_text,
    tlsrpt_text,
    bimi_text,
  }
}

///|
pub fn parse_suite_input(row : String) -> MailSecuritySuiteInput {
  let cells = row.split("|").to_array()
  suite_input(
    row_cell(cells, 0),
    row_cell(cells, 1),
    row_cell(cells, 2),
    row_cell(cells, 3),
    row_cell(cells, 4).replace_all(old="\\n", new="\n"),
    row_cell(cells, 5),
    row_cell(cells, 6),
  )
}

///|
fn row_cell(cells : Array[StringView], index : Int) -> String {
  if index < cells.length() {
    cells[index].trim().to_owned()
  } else {
    ""
  }
}

///|
pub fn analyze_suite_input(
  input : MailSecuritySuiteInput,
) -> MailSecuritySuiteReport {
  analyze_suite(
    input.domain,
    input.spf_text,
    input.dmarc_text,
    input.dkim_text,
    input.mtasts_text,
    input.tlsrpt_text,
    input.bimi_text,
  )
}

///|
pub fn analyze_suite_inputs(
  inputs : Array[MailSecuritySuiteInput],
) -> BatchSecurityReport {
  let reports = Array::new(capacity=inputs.length())
  for input in inputs {
    reports.push(analyze_suite_input(input))
  }
  batch_from_reports(reports)
}

///|
pub fn analyze_suite_rows(rows : Array[String]) -> BatchSecurityReport {
  let inputs = Array::new(capacity=rows.length())
  for row in rows {
    let trimmed = row.trim().to_owned()
    if trimmed.is_empty() {
      continue
    }
    if trimmed.has_prefix("#") {
      continue
    }
    inputs.push(parse_suite_input(trimmed))
  }
  analyze_suite_inputs(inputs)
}

///|
pub fn batch_from_reports(
  reports : Array[MailSecuritySuiteReport],
) -> BatchSecurityReport {
  let findings = Array::new(capacity=reports.length() + 4)
  let actions = Array::new()
  let domains_checked = reports.length()
  let mut score_total = 0
  let mut lowest_score = 100
  let mut highest_score = 0
  if reports.length() == 0 {
    lowest_score = 0
    findings.push(
      fail(
        "batch.empty", "Batch has no domain rows", "A batch report needs at least one domain input.",
        "", "Provide one or more pipe-separated domain rows.",
      ),
    )
  }
  for report in reports {
    score_total += positive_or_zero(report.score)
    lowest_score = min_int(lowest_score, report.score)
    highest_score = max_int(highest_score, report.score)
    append_batch_actions(actions, report.actions)
    if report.verdict == "risky" {
      findings.push(
        fail(
          "batch.domain-risky",
          "Batch contains a risky domain",
          "At least one domain has hard failures.",
          report.domain,
          "Review that domain's action list before rollout.",
          penalty=8,
        ),
      )
    } else if report.verdict == "monitor" {
      findings.push(
        warn(
          "batch.domain-monitor",
          "Batch contains a monitor domain",
          "At least one domain still has warnings.",
          report.domain,
          "Resolve warnings or document accepted rollout risk.",
          penalty=3,
        ),
      )
    }
  }
  let average_score = if domains_checked == 0 {
    0
  } else {
    score_total / domains_checked
  }
  if domains_checked > 0 {
    findings.push(
      pass(
        "batch.domains",
        "Batch domain rows were analyzed",
        domains_checked.to_string(),
      ),
    )
    findings.push(
      pass(
        "batch.average-score",
        "Batch average score was calculated",
        average_score.to_string(),
      ),
    )
  }
  let summary = summarize_findings(findings)
  {
    reports,
    domains_checked,
    average_score,
    lowest_score,
    highest_score,
    summary,
    actions,
    findings,
  }
}

///|
fn append_batch_actions(
  target : Array[ActionItem],
  source : Array[ActionItem],
) -> Unit {
  for action in source {
    target.push(action)
  }
}

///|
pub fn BatchSecurityReport::risky_count(self : BatchSecurityReport) -> Int {
  let mut count = 0
  for report in self.reports {
    if report.verdict == "risky" {
      count += 1
    }
  }
  count
}

///|
pub fn BatchSecurityReport::monitor_count(self : BatchSecurityReport) -> Int {
  let mut count = 0
  for report in self.reports {
    if report.verdict == "monitor" {
      count += 1
    }
  }
  count
}

///|
pub fn BatchSecurityReport::protected_count(self : BatchSecurityReport) -> Int {
  let mut count = 0
  for report in self.reports {
    if report.verdict == "protected" {
      count += 1
    }
  }
  count
}

///|
pub fn BatchSecurityReport::has_failures(self : BatchSecurityReport) -> Bool {
  self.summary.fail_count > 0 || self.risky_count() > 0
}

///|
pub fn BatchSecurityReport::to_markdown(self : BatchSecurityReport) -> String {
  let out = StringBuilder()
  out.write_string("# MailShield Batch Report\n\n")
  out.write_string("- Domains checked: ")
  out.write_string(self.domains_checked.to_string())
  out.write_string("\n")
  out.write_string("- Average score: ")
  out.write_string(self.average_score.to_string())
  out.write_string("\n")
  out.write_string("- Lowest score: ")
  out.write_string(self.lowest_score.to_string())
  out.write_string("\n")
  out.write_string("- Highest score: ")
  out.write_string(self.highest_score.to_string())
  out.write_string("\n")
  out.write_string("- Protected: ")
  out.write_string(self.protected_count().to_string())
  out.write_string("\n")
  out.write_string("- Monitor: ")
  out.write_string(self.monitor_count().to_string())
  out.write_string("\n")
  out.write_string("- Risky: ")
  out.write_string(self.risky_count().to_string())
  out.write_string("\n\n")
  out.write_string("## Domains\n\n")
  for report in self.reports {
    out.write_string("- ")
    out.write_string(report.domain)
    out.write_string(": ")
    out.write_string(report.verdict)
    out.write_string(" (")
    out.write_string(report.score.to_string())
    out.write_string("/100, ")
    out.write_string(report.coverage_label())
    out.write_string(")\n")
  }
  out.write_string("\n## Action Items\n\n")
  if self.actions.length() == 0 {
    out.write_string("- No action items.\n")
  } else {
    for action in self.actions {
      out.write_string(action.to_markdown())
      out.write_string("\n")
    }
  }
  out.to_string()
}

///|
pub fn BatchSecurityReport::to_json(self : BatchSecurityReport) -> String {
  let out = StringBuilder()
  out.write_string("{")
  append_json_int(out, "domains_checked", self.domains_checked)
  out.write_string(",")
  append_json_int(out, "average_score", self.average_score)
  out.write_string(",")
  append_json_int(out, "lowest_score", self.lowest_score)
  out.write_string(",")
  append_json_int(out, "highest_score", self.highest_score)
  out.write_string(",")
  append_json_int(out, "protected", self.protected_count())
  out.write_string(",")
  append_json_int(out, "monitor", self.monitor_count())
  out.write_string(",")
  append_json_int(out, "risky", self.risky_count())
  out.write_string(",\"domains\":[")
  for i in 0.. 0 {
      out.write_string(",")
    }
    let report = self.reports[i]
    out.write_string("{")
    append_json_string(out, "domain", report.domain)
    out.write_string(",")
    append_json_string(out, "verdict", report.verdict)
    out.write_string(",")
    append_json_int(out, "score", report.score)
    out.write_string(",")
    append_json_string(out, "coverage", report.coverage_label())
    out.write_string("}")
  }
  out.write_string("]}")
  out.to_string()
}