///|
/// One response sample collected from a route, endpoint, or deployment.
pub struct ResponseSample {
  name : String
  path : String
  document_origin : String
  header_block : String
} derive(Eq, @debug.Debug)

///|
/// Score for one audited response sample.
pub struct SampleScore {
  name : String
  path : String
  grade : String
  points : Int
  max_points : Int
  percent : Int
  high : Int
  warning : Int
} derive(Eq, @debug.Debug)

///|
/// Batch audit across many response samples.
pub struct BatchAudit {
  samples : Array[ResponseSample]
  audits : Array[SecurityAudit]
  scores : Array[SampleScore]
  average_percent : Int
  worst_grade : String
  high_total : Int
  warning_total : Int
} derive(Eq, @debug.Debug)

///|
/// Batch-level CI gate result.
pub struct BatchGate {
  ok : Bool
  required_grade : String
  max_high : Int
  max_warning : Int
  messages : Array[String]
} derive(Eq, @debug.Debug)

///|
/// One batch recommendation tied to a response sample.
pub struct BatchRecommendation {
  sample : String
  path : String
  header : String
  value : String
  priority : String
  reason : String
} derive(Eq, @debug.Debug)

///|
/// Create a response sample.
pub fn response_sample(
  name : String,
  path : String,
  document_origin : String,
  header_block : String,
) -> ResponseSample {
  { name, path, document_origin, header_block }
}

///|
/// Create a response sample from a security header bundle.
pub fn response_sample_from_bundle(
  name : String,
  path : String,
  bundle : SecurityHeaderBundle,
) -> ResponseSample {
  {
    name,
    path,
    document_origin: bundle.document_origin,
    header_block: render_security_bundle(bundle),
  }
}

///|
/// Audit one response sample.
pub fn audit_response_sample(sample : ResponseSample) -> SecurityAudit {
  audit_security_header_block(sample.header_block, sample.document_origin)
}

///|
/// Audit many response samples.
pub fn audit_response_samples(samples : Array[ResponseSample]) -> BatchAudit {
  let audits : Array[SecurityAudit] = []
  let scores : Array[SampleScore] = []
  let mut total_percent = 0
  let mut high_total = 0
  let mut warning_total = 0
  let mut worst_grade = "A"
  for sample in samples {
    let audit = audit_response_sample(sample)
    audits.push(audit)
    let score = sample_score(sample, audit)
    scores.push(score)
    total_percent = total_percent + score.percent
    high_total = high_total + score.high
    warning_total = warning_total + score.warning
    if grade_rank(score.grade) > grade_rank(worst_grade) {
      worst_grade = score.grade
    }
  }
  let average_percent = if scores.length() == 0 {
    0
  } else {
    total_percent / scores.length()
  }
  {
    samples,
    audits,
    scores,
    average_percent,
    worst_grade,
    high_total,
    warning_total,
  }
}

///|
/// Return scores at or below a grade threshold.
pub fn batch_scores_at_or_below(
  batch : BatchAudit,
  grade : String,
) -> Array[SampleScore] {
  let result : Array[SampleScore] = []
  let threshold = grade_rank(grade)
  for score in batch.scores {
    if grade_rank(score.grade) >= threshold {
      result.push(score)
    }
  }
  result
}

///|
/// Return samples with high-severity findings.
pub fn batch_samples_with_high(batch : BatchAudit) -> Array[SampleScore] {
  let result : Array[SampleScore] = []
  for score in batch.scores {
    if score.high > 0 {
      result.push(score)
    }
  }
  result
}

///|
/// Return the best sample score in a batch.
pub fn best_sample_score(batch : BatchAudit) -> SampleScore? {
  let mut best : SampleScore? = None
  for score in batch.scores {
    match best {
      None => best = Some(score)
      Some(current) => if score.percent > current.percent { best = Some(score) }
    }
  }
  best
}

///|
/// Return the weakest sample score in a batch.
pub fn weakest_sample_score(batch : BatchAudit) -> SampleScore? {
  let mut weakest : SampleScore? = None
  for score in batch.scores {
    match weakest {
      None => weakest = Some(score)
      Some(current) =>
        if score.percent < current.percent {
          weakest = Some(score)
        }
    }
  }
  weakest
}

///|
/// Collect recommendations across every response sample.
pub fn batch_recommendations(batch : BatchAudit) -> Array[BatchRecommendation] {
  let recommendations : Array[BatchRecommendation] = []
  let mut index = 0
  for audit in batch.audits {
    let sample = batch.samples[index]
    for item in security_recommendations(audit) {
      recommendations.push({
        sample: sample.name,
        path: sample.path,
        header: item.header,
        value: item.value,
        priority: item.priority,
        reason: item.reason,
      })
    }
    index = index + 1
  }
  recommendations
}

///|
/// Build a batch gate for CI.
pub fn batch_security_gate(
  batch : BatchAudit,
  required_grade : String,
  max_high : Int,
  max_warning : Int,
) -> BatchGate {
  let messages : Array[String] = []
  if grade_rank(batch.worst_grade) > grade_rank(required_grade) {
    messages.push(
      "worst grade " +
      batch.worst_grade +
      " is below required " +
      required_grade.to_upper(),
    )
  }
  if batch.high_total > max_high {
    messages.push(
      "high findings " +
      batch.high_total.to_string() +
      " exceed limit " +
      max_high.to_string(),
    )
  }
  if batch.warning_total > max_warning {
    messages.push(
      "warning findings " +
      batch.warning_total.to_string() +
      " exceed limit " +
      max_warning.to_string(),
    )
  }
  {
    ok: messages.length() == 0,
    required_grade: required_grade.to_upper(),
    max_high,
    max_warning,
    messages,
  }
}

///|
/// Render a batch audit summary.
pub fn render_batch_audit(batch : BatchAudit) -> String {
  let lines : Array[String] = []
  lines.push("permscope batch security audit")
  lines.push("samples=" + batch.samples.length().to_string())
  lines.push("average_percent=" + batch.average_percent.to_string())
  lines.push("worst_grade=" + batch.worst_grade)
  lines.push("high_total=" + batch.high_total.to_string())
  lines.push("warning_total=" + batch.warning_total.to_string())
  for score in batch.scores {
    lines.push(render_sample_score(score))
  }
  lines.join("\n")
}

///|
/// Render one sample score.
pub fn render_sample_score(score : SampleScore) -> String {
  score.name +
  " " +
  score.path +
  " grade=" +
  score.grade +
  " score=" +
  score.points.to_string() +
  "/" +
  score.max_points.to_string() +
  " percent=" +
  score.percent.to_string() +
  " high=" +
  score.high.to_string() +
  " warning=" +
  score.warning.to_string()
}

///|
/// Render batch recommendations.
pub fn render_batch_recommendations(
  recommendations : Array[BatchRecommendation],
) -> String {
  if recommendations.length() == 0 {
    return "permscope batch recommendations: no changes"
  }
  let lines : Array[String] = ["permscope batch recommendations:"]
  for item in recommendations {
    lines.push(
      item.priority +
      " " +
      item.sample +
      " " +
      item.path +
      " " +
      item.header +
      " -> " +
      item.value +
      " - " +
      item.reason,
    )
  }
  lines.join("\n")
}

///|
/// Render a batch gate result.
pub fn render_batch_gate(gate : BatchGate) -> String {
  let lines : Array[String] = []
  if gate.ok {
    lines.push("permscope batch gate: pass")
  } else {
    lines.push("permscope batch gate: fail")
  }
  lines.push("required_grade=" + gate.required_grade)
  lines.push("max_high=" + gate.max_high.to_string())
  lines.push("max_warning=" + gate.max_warning.to_string())
  for message in gate.messages {
    lines.push("message=" + message)
  }
  lines.join("\n")
}

///|
/// Render a Markdown batch report.
pub fn render_batch_markdown(batch : BatchAudit) -> String {
  let lines : Array[String] = []
  lines.push("# permscope batch security audit")
  lines.push("")
  lines.push("- Samples: " + batch.samples.length().to_string())
  lines.push("- Average percent: " + batch.average_percent.to_string())
  lines.push("- Worst grade: " + batch.worst_grade)
  lines.push("- High findings: " + batch.high_total.to_string())
  lines.push("- Warning findings: " + batch.warning_total.to_string())
  lines.push("")
  lines.push("| Sample | Path | Grade | Percent | High | Warning |")
  lines.push("| --- | --- | --- | ---: | ---: | ---: |")
  for score in batch.scores {
    lines.push(
      "| " +
      score.name +
      " | " +
      score.path +
      " | " +
      score.grade +
      " | " +
      score.percent.to_string() +
      " | " +
      score.high.to_string() +
      " | " +
      score.warning.to_string() +
      " |",
    )
  }
  lines.join("\n")
}

///|
/// Render a JSON-like batch report.
pub fn render_batch_json(batch : BatchAudit) -> String {
  let lines : Array[String] = []
  lines.push("{")
  lines.push("  \"samples\": " + batch.samples.length().to_string() + ",")
  lines.push(
    "  \"average_percent\": " + batch.average_percent.to_string() + ",",
  )
  lines.push("  \"worst_grade\": \"" + batch.worst_grade + "\",")
  lines.push("  \"scores\": [")
  let mut index = 0
  for score in batch.scores {
    index = index + 1
    lines.push("    {")
    lines.push("      \"name\": \"" + batch_json_escape(score.name) + "\",")
    lines.push("      \"path\": \"" + batch_json_escape(score.path) + "\",")
    lines.push("      \"grade\": \"" + score.grade + "\",")
    lines.push("      \"percent\": " + score.percent.to_string())
    if index == batch.scores.length() {
      lines.push("    }")
    } else {
      lines.push("    },")
    }
  }
  lines.push("  ]")
  lines.push("}")
  lines.join("\n")
}

///|
fn sample_score(sample : ResponseSample, audit : SecurityAudit) -> SampleScore {
  {
    name: sample.name,
    path: sample.path,
    grade: audit.score.grade,
    points: audit.score.points,
    max_points: audit.score.max_points,
    percent: audit.score.percent,
    high: audit.score.high,
    warning: audit.score.warning,
  }
}

///|
fn batch_json_escape(value : String) -> String {
  let parts : Array[String] = []
  for ch in value {
    match ch {
      '"' => parts.push("\\\"")
      '\\' => parts.push("\\\\")
      '\n' => parts.push("\\n")
      '\r' => parts.push("\\r")
      '\t' => parts.push("\\t")
      _ => parts.push(ch.to_string())
    }
  }
  parts.join("")
}