///|
pub(all) enum OperationStatus {
  Moved
  Restored
  Failed
  Skipped
} derive(Eq, Debug, ToJson, FromJson)

///|
pub(all) struct OperationItem {
  source : String
  destination : String
  size : Int64
  status : OperationStatus
  message : String
} derive(Eq, Debug, ToJson, FromJson)

///|
pub(all) struct OperationManifest {
  format_version : Int
  root : String
  quarantine_root : String
  created_at : Int64
  items : Array[OperationItem]
} derive(Eq, Debug, ToJson, FromJson)

///|
fn native_join(base : String, relative : String) -> String {
  if relative == "" {
    base
  } else {
    base + "/" + relative
  }
}

///|
async fn ensure_relative_parent(base : String, relative : String) -> Unit {
  let parent = parent_path(relative)
  if parent == "" {
    return
  }
  let mut current = base
  for part in parent.split("/") {
    current = current + "/" + part.to_owned()
    if !@fs.exists(current) {
      @fs.mkdir(current)
    }
  }
}

///|
pub async fn apply_cleanup_plan(plan : CleanupPlan) -> OperationManifest {
  if !@fs.exists(plan.quarantine_root) {
    @fs.mkdir(plan.quarantine_root, recursive=true)
  }
  let results : Array[OperationItem] = []
  for item in plan.items {
    let source = native_join(plan.root, item.source)
    let destination = native_join(plan.quarantine_root, item.destination)
    if !@fs.exists(source) {
      results.push(OperationItem::{
        source: item.source,
        destination: item.destination,
        size: item.expected_size,
        status: Skipped,
        message: "Source no longer exists.",
      })
      continue
    }
    if @fs.exists(destination) {
      results.push(OperationItem::{
        source: item.source,
        destination: item.destination,
        size: item.expected_size,
        status: Skipped,
        message: "Quarantine destination already exists.",
      })
      continue
    }
    try {
      ensure_relative_parent(plan.quarantine_root, item.destination)
      @fs.rename(source, destination, replace=false)
      results.push(OperationItem::{
        source: item.source,
        destination: item.destination,
        size: item.expected_size,
        status: Moved,
        message: "Moved to quarantine.",
      })
    } catch {
      error =>
        results.push(OperationItem::{
          source: item.source,
          destination: item.destination,
          size: item.expected_size,
          status: Failed,
          message: @debug.to_string(error),
        })
    }
  }
  OperationManifest::{
    format_version: 1,
    root: plan.root,
    quarantine_root: plan.quarantine_root,
    created_at: plan.created_at,
    items: results,
  }
}

///|
pub async fn restore_operation(
  operation : OperationManifest,
) -> OperationManifest {
  let results : Array[OperationItem] = []
  let mut index = operation.items.length() - 1
  while index >= 0 {
    let item = operation.items[index]
    if item.status != Moved {
      results.push({
        ..item,
        status: Skipped,
        message: "Item was not moved by this operation.",
      })
      index -= 1
      continue
    }
    let source = native_join(operation.quarantine_root, item.destination)
    let destination = native_join(operation.root, item.source)
    if !@fs.exists(source) || @fs.exists(destination) {
      let message = if !@fs.exists(source) {
        "Quarantined source no longer exists."
      } else {
        "Original destination already exists."
      }
      results.push({ ..item, status: Skipped, message, })
      index -= 1
      continue
    }
    try {
      ensure_relative_parent(operation.root, item.source)
      @fs.rename(source, destination, replace=false)
      results.push({
        ..item,
        status: Restored,
        message: "Restored to original path.",
      })
    } catch {
      error =>
        results.push({
          ..item,
          status: Failed,
          message: @debug.to_string(error),
        })
    }
    index -= 1
  }
  { ..operation, items: results, }
}

///|
pub fn operation_to_json(operation : OperationManifest) -> String {
  ToJson::to_json(operation).stringify(indent=2)
}

///|
pub fn operation_from_json(source : String) -> OperationManifest raise {
  let operation : OperationManifest = @json.from_json(@json.parse(source))
  if operation.format_version != 1 {
    raise Failure("Unsupported operation manifest version")
  }
  operation
}

///|
pub async fn write_operation(
  path : String,
  operation : OperationManifest,
) -> Unit {
  @fs.write_file(path, operation_to_json(operation))
}

///|
pub async fn read_operation(path : String) -> OperationManifest {
  operation_from_json(@fs.read_file(path).text())
}

///|
pub fn OperationManifest::moved_count(self : OperationManifest) -> Int {
  self.items.filter(fn(item) { item.status == Moved }).length()
}

///|
pub fn OperationManifest::restored_count(self : OperationManifest) -> Int {
  self.items.filter(fn(item) { item.status == Restored }).length()
}

///|
pub fn OperationManifest::failed_count(self : OperationManifest) -> Int {
  self.items.filter(fn(item) { item.status == Failed }).length()
}