///|
pub(all) struct Document {
  path : String
  content : String
} derive(Eq, Debug)

///|
pub fn Document::new(path : String, content : String) -> Document {
  { path, content }
}

///|
pub(all) struct DocumentFinding {
  path : String
  finding : Finding
  start : SourceLocation
  end : SourceLocation
  fingerprint : String
} derive(Eq, Debug)

///|
pub(all) struct DetectorCount {
  detector_id : String
  count : Int
} derive(Eq, Debug)

///|
pub(all) struct BatchSummary {
  documents : Int
  bytes : Int
  findings : Int
  critical : Int
  high : Int
  medium : Int
  low : Int
  info : Int
  by_detector : Array[DetectorCount]
} derive(Eq, Debug)

///|
pub(all) struct BatchResult {
  findings : Array[DocumentFinding]
  summary : BatchSummary
} derive(Eq, Debug)

///|
fn fingerprint_for(path : String, finding : Finding) -> String {
  let identity = path +
    "\u0000" +
    finding.detector_id +
    "\u0000" +
    finding.span.start.to_string() +
    "\u0000" +
    finding.span.end.to_string()
  fnv1a(identity).to_string(radix=16)
}

///|
fn to_document_finding(
  document : Document,
  finding : Finding,
) -> DocumentFinding {
  {
    path: document.path,
    finding,
    start: locate(document.content, finding.span.start),
    end: locate(document.content, finding.span.end),
    fingerprint: fingerprint_for(document.path, finding),
  }
}

///|
fn compare_document_findings(
  left : DocumentFinding,
  right : DocumentFinding,
) -> Int {
  let path_order = left.path.compare(right.path)
  if path_order != 0 {
    path_order
  } else if left.finding.span.start != right.finding.span.start {
    left.finding.span.start.compare(right.finding.span.start)
  } else {
    left.finding.detector_id.compare(right.finding.detector_id)
  }
}

///|
fn detector_counts(findings : Array[DocumentFinding]) -> Array[DetectorCount] {
  let counts : Map[String, Int] = Map([])
  for item in findings {
    let current = match counts.get(item.finding.detector_id) {
      Some(value) => value
      None => 0
    }
    counts[item.finding.detector_id] = current + 1
  }
  let out : Array[DetectorCount] = []
  counts.each(fn(key, value) { out.push({ detector_id: key, count: value }) })
  out.sort_by(fn(left, right) {
    if left.count != right.count {
      right.count.compare(left.count)
    } else {
      left.detector_id.compare(right.detector_id)
    }
  })
  out
}

///|
fn batch_summary(
  documents : Array[Document],
  findings : Array[DocumentFinding],
) -> BatchSummary {
  let mut bytes = 0
  for document in documents {
    bytes = bytes + document.content.to_array().length()
  }
  let plain_findings = findings.map(fn(item) { item.finding })
  let severity = summarize(plain_findings)
  {
    documents: documents.length(),
    bytes,
    findings: findings.length(),
    critical: severity.critical,
    high: severity.high,
    medium: severity.medium,
    low: severity.low,
    info: severity.info,
    by_detector: detector_counts(findings),
  }
}

///|
pub fn scan_documents(
  documents : Array[Document],
  scanner? : Scanner = Scanner::new(),
  allowlist? : Array[AllowRule] = [],
) -> BatchResult {
  let findings : Array[DocumentFinding] = []
  for document in documents {
    let raw = scanner.find(document.content)
    let allowed = apply_allowlist(document.content, raw, allowlist)
    for finding in allowed {
      findings.push(to_document_finding(document, finding))
    }
  }
  findings.sort_by(compare_document_findings)
  { findings, summary: batch_summary(documents, findings) }
}

///|
pub fn filter_new_findings(
  findings : Array[DocumentFinding],
  known_fingerprints : Array[String],
) -> Array[DocumentFinding] {
  findings.filter(fn(item) {
    !known_fingerprints.any(fn(known) { known == item.fingerprint })
  })
}

///|
fn sarif_level(severity : Severity) -> String {
  match severity {
    Critical | High => "error"
    Medium => "warning"
    Low | Info => "note"
  }
}

///|
fn sarif_rule_id(detector_id : String) -> String {
  "LUNASIEVE/" + detector_id
}

///|
fn write_sarif_result(out : StringBuilder, item : DocumentFinding) -> Unit {
  out.write_string("{\"ruleId\":\"")
  out.write_string(json_escape(sarif_rule_id(item.finding.detector_id)))
  out.write_string("\",\"level\":\"")
  out.write_string(sarif_level(item.finding.severity))
  out.write_string("\",\"message\":{\"text\":\"")
  out.write_string(json_escape(item.finding.message))
  out.write_string(
    "\"},\"partialFingerprints\":{\"primaryLocationLineHash\":\"",
  )
  out.write_string(item.fingerprint)
  out.write_string(
    "\"},\"locations\":[{\"physicalLocation\":{\"artifactLocation\":{\"uri\":\"",
  )
  out.write_string(json_escape(item.path))
  out.write_string("\"},\"region\":{\"startLine\":")
  out.write_string(item.start.line.to_string())
  out.write_string(",\"startColumn\":")
  out.write_string(item.start.column.to_string())
  out.write_string(",\"endLine\":")
  out.write_string(item.end.line.to_string())
  out.write_string(",\"endColumn\":")
  out.write_string(item.end.column.to_string())
  out.write_string("}}}]}")
}

///|
pub fn batch_to_sarif(result : BatchResult) -> String {
  let out = StringBuilder::new()
  out.write_string(
    "{\"version\":\"2.1.0\",\"$schema\":\"https://json.schemastore.org/sarif-2.1.0.json\",\"runs\":[{",
  )
  out.write_string(
    "\"tool\":{\"driver\":{\"name\":\"LunaSieve\",\"informationUri\":\"https://github.com/sujy123456/my\"}},",
  )
  out.write_string("\"results\":[")
  for i = 0; i < result.findings.length(); i = i + 1 {
    if i > 0 {
      out.write_char(',')
    }
    write_sarif_result(out, result.findings[i])
  }
  out.write_string("]}]}")
  out.to_string()
}

///|
pub fn batch_to_text(result : BatchResult) -> String {
  let out = StringBuilder::new()
  for item in result.findings {
    out.write_string(item.path)
    out.write_char(':')
    out.write_string(item.start.line.to_string())
    out.write_char(':')
    out.write_string(item.start.column.to_string())
    out.write_string(" [")
    out.write_string(item.finding.severity.name())
    out.write_string("] ")
    out.write_string(item.finding.detector_id)
    out.write_string(" - ")
    out.write_string(item.finding.message)
    out.write_char('\n')
  }
  out.write_string("scanned ")
  out.write_string(result.summary.documents.to_string())
  out.write_string(" document(s), found ")
  out.write_string(result.summary.findings.to_string())
  out.write_string(" issue(s)\n")
  out.to_string()
}