///|
fn context_path_key(path : String) -> String {
  if @platform.is_windows() {
    @platform.separators(path).to_lower()
  } else {
    path
  }
}

///|
// Compact original-line coverage: merge overlapping/adjacent fragment lines.
// Detailed fragment line ranges remain available from the compression package.
fn context_ranges(ranges : Array[@compression.LineRange]) -> Array[Json] {
  let result : Array[Json] = []
  let mut first = 0
  let mut last = 0
  for range in ranges {
    if first == 0 {
      first = range.start_line
      last = range.end_line
    } else if range.start_line <= last + 1 {
      if range.end_line > last {
        last = range.end_line
      }
    } else {
      result.push(Json([first, last]))
      first = range.start_line
      last = range.end_line
    }
  }
  if first > 0 {
    result.push(Json([first, last]))
  }
  result
}

///|
priv struct GitContext {
  sections : Array[@document.Section]
  metadata : Json
  revision : String?
}

///|
// Git's '-' marker is not part of a deleted source line. Mask just the patch
// framing byte, preserving UTF-8 offsets and line numbers for diagnostics.
fn diff_scan_text(text : String) -> String {
  let out = StringBuilder()
  let mut line_start = true
  for c in text {
    out.write_char(if line_start && (c == '+' || c == '-') { ' ' } else { c })
    line_start = c == '\n' || c == '\r'
  }
  out.to_string()
}

///|
fn context_safe(
  label : String,
  text : String,
  options : Options,
  skipped : Array[Skipped],
  findings : Array[Finding],
  diff~ : Bool = false,
) -> Bool raise {
  if options.security_check {
    let scan_text = if diff { diff_scan_text(text) } else { text }
    let hits = @security.scan(label, scan_text, 0) catch {
      _ => raise ScanError
    }
    if !hits.is_empty() {
      findings.append(hits)
      skipped.push({ path: label, reason: "SecurityFinding", })
      return false
    }
  }
  if options.style is Xml && !@output.xml_representable(text) {
    skipped.push({ path: label, reason: "UnrepresentableText", })
    return false
  }
  true
}

///|
async fn git_context(
  root : String,
  options : Options,
  selection_options : @selection.SelectionOptions,
  excluded : Set[String],
  skipped : Array[Skipped],
  findings : Array[Finding],
) -> GitContext {
  if !options.include_diffs && !options.include_logs {
    return { sections: [], metadata: Json::null(), revision: None, }
  }
  let filter = @selection.path_filter(
    root,
    selection_options,
    file_paths=options.file_paths,
  )
  let accept_diff = async fn(paths : Array[String]) -> Bool {
    if paths.is_empty() {
      return false
    }
    for path in paths {
      if excluded.contains(context_path_key(path)) || !filter.allows(path) {
        return false
      }
    }
    true
  }
  let snapshot = @git.collect(
    root,
    accept_diff~,
    diffs=options.include_diffs,
    logs=if options.include_logs { options.include_logs_count } else { 0 },
    max_bytes=options.max_git_bytes,
  ) catch {
    err => {
      if @async.is_cancellation_error(err) {
        raise err
      }
      raise GitError
    }
  }
  let sections : Array[@document.Section] = []
  let diffs : Array[Json] = []
  let commits : Array[Json] = []
  for index, patch in snapshot.diffs {
    let label = "git:diff:" + index.to_string()
    if !context_safe(label, patch.text, options, skipped, findings, diff=true) {
      continue
    }
    let title = "Git " +
      patch.kind +
      " diff: " +
      patch.paths.map(p => Json(p).stringify()).join(", ")
    if options.style is Xml && !@output.xml_representable(title) {
      continue
    }
    sections.push({ kind: "git_diff", title, text: patch.text, })
    diffs.push({ "kind": patch.kind, "paths": patch.paths })
  }
  for commit in snapshot.commits {
    let paths : Array[String] = []
    for path in commit.files {
      if !excluded.contains(context_path_key(path)) &&
        filter.allows(path) &&
        (options.style != Xml || @output.xml_representable(path)) {
        paths.push(path)
      }
    }
    if paths.is_empty() {
      continue
    }
    let text = "Commit: " +
      commit.id +
      "\nDate: " +
      commit.date +
      "\nFiles:\n" +
      paths.map(p => "  " + Json(p).stringify()).join("\n") +
      "\n\n" +
      commit.message
    if !context_safe(
        "git:commit:" + commit.id,
        text,
        options,
        skipped,
        findings,
      ) {
      continue
    }
    sections.push({ kind: "git_log", title: "Git commit " + commit.id, text, })
    commits.push({ "id": commit.id, "date": commit.date, "files": paths })
  }
  {
    sections,
    metadata: { "diffs": diffs, "commits": commits },
    revision: snapshot.revision,
  }
}