///|
pub(all) enum OperationAuditKind {
  UnsafeOperationPath
  DuplicateOperationSource
  DuplicateOperationDestination
  QuarantineItemMissing
  OriginalPathOccupied
} derive(Eq, Debug, ToJson, FromJson)

///|
pub(all) struct OperationAuditIssue {
  kind : OperationAuditKind
  source : String
  destination : String
  message : String
} derive(Eq, Debug, ToJson, FromJson)

///|
pub(all) struct OperationAudit {
  restorable_items : Int
  blocked_items : Int
  ignored_items : Int
  issues : Array[OperationAuditIssue]
} derive(Eq, Debug, ToJson, FromJson)

///|
fn operation_path_is_safe(path : String) -> Bool {
  path != "" &&
  normalize_relative_path(path) == path &&
  !path.has_prefix("/") &&
  !path.has_prefix("\\") &&
  !path.contains(":")
}

///|
pub async fn audit_operation(operation : OperationManifest) -> OperationAudit {
  let issues : Array[OperationAuditIssue] = []
  let sources : Map[String, Bool] = Map([])
  let destinations : Map[String, Bool] = Map([])
  let blocked : Map[String, Bool] = Map([])
  let mut ignored = 0
  for item in operation.items {
    if item.status != Moved {
      ignored += 1
      continue
    }
    if !operation_path_is_safe(item.source) ||
      !operation_path_is_safe(item.destination) {
      issues.push(OperationAuditIssue::{
        kind: UnsafeOperationPath,
        source: item.source,
        destination: item.destination,
        message: "Operation paths must remain normalized and relative.",
      })
      blocked[item.source] = true
    }
    if sources.get(item.source) is Some(_) {
      issues.push(OperationAuditIssue::{
        kind: DuplicateOperationSource,
        source: item.source,
        destination: item.destination,
        message: "The operation source appears more than once.",
      })
      blocked[item.source] = true
    } else {
      sources[item.source] = true
    }
    if destinations.get(item.destination) is Some(_) {
      issues.push(OperationAuditIssue::{
        kind: DuplicateOperationDestination,
        source: item.source,
        destination: item.destination,
        message: "The quarantine destination appears more than once.",
      })
      blocked[item.source] = true
    } else {
      destinations[item.destination] = true
    }
    let quarantined = native_join(operation.quarantine_root, item.destination)
    let original = native_join(operation.root, item.source)
    if !@fs.exists(quarantined) {
      issues.push(OperationAuditIssue::{
        kind: QuarantineItemMissing,
        source: item.source,
        destination: item.destination,
        message: "The item is no longer present in quarantine.",
      })
      blocked[item.source] = true
    }
    if @fs.exists(original) {
      issues.push(OperationAuditIssue::{
        kind: OriginalPathOccupied,
        source: item.source,
        destination: item.destination,
        message: "The original path is occupied and cannot be restored safely.",
      })
      blocked[item.source] = true
    }
  }
  let moved = operation.items.filter(fn(item) { item.status == Moved }).length()
  OperationAudit::{
    restorable_items: moved - blocked.length(),
    blocked_items: blocked.length(),
    ignored_items: ignored,
    issues,
  }
}

///|
pub fn render_operation_audit(audit : OperationAudit) -> String {
  let lines : Array[String] = [
    "MoonDisk operation audit",
    "Restorable items: " + audit.restorable_items.to_string(),
    "Blocked items: " + audit.blocked_items.to_string(),
    "Ignored items: " + audit.ignored_items.to_string(),
  ]
  if audit.issues.is_empty() {
    lines.push("Result: every moved item is ready for restoration.")
  } else {
    lines.push("")
    for issue in audit.issues {
      lines.push("- " + issue.source + ": " + issue.message)
    }
  }
  lines.join("\n")
}

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