///|
pub(all) enum AuditSeverity {
  AuditInfo
  AuditWarning
  AuditError
} derive(Eq, Debug, ToJson, FromJson)

///|
pub(all) enum SnapshotAuditKind {
  InvalidRoot
  InvalidCreationTime
  InvalidEntryPath
  DuplicateEntryPath
  CaseInsensitiveCollision
  NegativeEntrySize
  NonFileHasSize
  MissingParentEntry
  NonDirectoryParent
  EntryOrderMismatch
  InvalidErrorPath
} derive(Eq, Debug, ToJson, FromJson)

///|
pub(all) struct SnapshotAuditIssue {
  kind : SnapshotAuditKind
  severity : AuditSeverity
  path : String
  message : String
} derive(Eq, Debug, ToJson, FromJson)

///|
pub(all) struct SnapshotAudit {
  issues : Array[SnapshotAuditIssue]
  errors : Int
  warnings : Int
  information : Int
} derive(Eq, Debug, ToJson, FromJson)

///|
fn audit_add(
  issues : Array[SnapshotAuditIssue],
  kind : SnapshotAuditKind,
  severity : AuditSeverity,
  path : String,
  message : String,
) -> Unit {
  issues.push(SnapshotAuditIssue::{ kind, severity, path, message, })
}

///|
fn audit_entry_paths(
  snapshot : Snapshot,
  issues : Array[SnapshotAuditIssue],
) -> Unit {
  let exact : Map[String, EntryKind] = Map([])
  let folded : Map[String, String] = Map([])
  let mut previous = ""
  let mut first = true
  for entry in snapshot.entries {
    let normalized = normalize_relative_path(entry.path)
    if entry.path == "" ||
      normalized != entry.path ||
      entry.path.has_prefix("/") ||
      entry.path.contains(":") {
      audit_add(
        issues,
        InvalidEntryPath,
        AuditError,
        entry.path,
        "Entry paths must be non-empty normalized paths relative to the scan root.",
      )
    }
    if exact.get(entry.path) is Some(_) {
      audit_add(
        issues,
        DuplicateEntryPath,
        AuditError,
        entry.path,
        "The snapshot contains this exact path more than once.",
      )
    } else {
      exact[entry.path] = entry.kind
    }
    let lower = entry.path.to_lower()
    match folded.get(lower) {
      Some(existing) if existing != entry.path =>
        audit_add(
          issues,
          CaseInsensitiveCollision,
          AuditWarning,
          entry.path,
          "This path differs only by letter case from " + existing + ".",
        )
      None => folded[lower] = entry.path
      _ => ()
    }
    if !first && entry.path < previous {
      audit_add(
        issues,
        EntryOrderMismatch,
        AuditInfo,
        entry.path,
        "Entries are not in deterministic path order.",
      )
    }
    previous = entry.path
    first = false
    if entry.size < 0L {
      audit_add(
        issues,
        NegativeEntrySize,
        AuditError,
        entry.path,
        "Entry size cannot be negative.",
      )
    } else if !entry.is_file() && entry.size != 0L {
      audit_add(
        issues,
        NonFileHasSize,
        AuditWarning,
        entry.path,
        "Directory, symbolic-link, and other entries should store zero logical bytes.",
      )
    }
  }
  for entry in snapshot.entries {
    let parent = entry.parent()
    if parent == "" {
      continue
    }
    match exact.get(parent) {
      None =>
        audit_add(
          issues,
          MissingParentEntry,
          AuditInfo,
          entry.path,
          "The parent directory " + parent + " is not recorded in the snapshot.",
        )
      Some(Directory) => ()
      Some(_) =>
        audit_add(
          issues,
          NonDirectoryParent,
          AuditError,
          entry.path,
          "The recorded parent " + parent + " is not a directory.",
        )
    }
  }
}

///|
pub fn audit_snapshot(snapshot : Snapshot) -> SnapshotAudit {
  let issues : Array[SnapshotAuditIssue] = []
  if snapshot.root.trim().is_empty() {
    audit_add(
      issues,
      InvalidRoot,
      AuditError,
      "",
      "Snapshot root cannot be empty.",
    )
  }
  if snapshot.created_at < 0L {
    audit_add(
      issues,
      InvalidCreationTime,
      AuditError,
      "",
      "Snapshot creation time cannot be negative.",
    )
  }
  audit_entry_paths(snapshot, issues)
  for error in snapshot.errors {
    if error.path != normalize_relative_path(error.path) {
      audit_add(
        issues,
        InvalidErrorPath,
        AuditWarning,
        error.path,
        "Scan error paths should use normalized relative separators.",
      )
    }
  }
  let mut errors = 0
  let mut warnings = 0
  let mut information = 0
  for issue in issues {
    match issue.severity {
      AuditError => errors += 1
      AuditWarning => warnings += 1
      AuditInfo => information += 1
    }
  }
  SnapshotAudit::{ issues, errors, warnings, information, }
}

///|
pub fn SnapshotAudit::is_usable(self : SnapshotAudit) -> Bool {
  self.errors == 0
}

///|
pub fn audit_severity_name(severity : AuditSeverity) -> String {
  match severity {
    AuditInfo => "info"
    AuditWarning => "warning"
    AuditError => "error"
  }
}

///|
pub fn render_snapshot_audit(audit : SnapshotAudit) -> String {
  let lines : Array[String] = [
    "MoonDisk snapshot audit",
    "Errors: " + audit.errors.to_string(),
    "Warnings: " + audit.warnings.to_string(),
    "Information: " + audit.information.to_string(),
  ]
  if audit.issues.is_empty() {
    lines.push("Result: snapshot structure is consistent.")
  } else {
    if audit.is_usable() {
      lines.push("Result: snapshot structure is consistent.")
    } else {
      lines.push("Result: snapshot has structural errors.")
    }
    lines.push("")
    for issue in audit.issues {
      let location = if issue.path == "" { "" } else { " [" + issue.path + "]" }
      lines.push(
        "[" +
        audit_severity_name(issue.severity) +
        "] " +
        issue.message +
        location,
      )
    }
  }
  lines.join("\n")
}

///|
pub fn snapshot_audit_to_json(audit : SnapshotAudit) -> String {
  ToJson::to_json(audit).stringify(indent=2)
}