import { _multiedit, _previewEdit, _applyPatch, _patchFiles, _mkdir, _copy, _move, _remove, _prepareContainedPath, _realTarget } from "agency-lang/stdlib-lib/fs.js"
import { bash, glob, ls, grep } from "std::shell"
import { applyAgentCwd } from "std::index"

/** @module
  @summary Edit files and create, copy, move, or remove filesystem entries.
  Apply text replacements or unified patches, and manage files and directories.
  Mutations require approval through a handler or an interactive response.

  ```ts
  import { edit } from "std::fs"

  node main() {
    const result = edit("notes.txt", [
      { oldText: "draft", newText: "final", replaceAll: true },
    ]) with approve
  }
  ```
*/

type Edit = {
  oldText: string;
  newText: string;
  replaceAll: boolean
}

type EditResult = {
  replacements: number;
  path: string;
  edits: number
}

@alwaysUnder(dir)
effect std::edit { dir: string, filename: string, edits: Edit[], before: string, after: string }
effect std::applyPatch { patch: string; files: string[] }
@alwaysUnder(dir)
effect std::mkdir { dir: string }
@alwaysUnder(src, dest)
effect std::copy { src: string, dest: string }
@alwaysUnder(src, dest)
effect std::move { src: string, dest: string }
@alwaysUnder(target)
effect std::remove { target: string }

/**
  The `std::edit` interrupt carries the full `before` and `after` file contents
  in its data, so a handler can render a diff itself. A handler receives the
  whole interrupt object, so the contents are at `data.data.before` /
  `data.data.after` (e.g. `print(diff(data.data.before, data.data.after, color: true))`).
*/
@capabilities(fileRead, fileWrite)
export def edit(filename: string, edits: Edit[], dir: string = ".", useAgentCwd: boolean = false): Result raises <std::edit> {
  """
  Apply text replacements in order, writing only if every edit succeeds.
  Prefer one call with multiple edits over many separate calls. Each oldText
  must match uniquely unless replaceAll is true. Use separate edits in the
  same call for distant changes, with just enough oldText to identify each match.

  @param filename - Relative path within dir, without upward traversal or symlink components. To reach another directory, pass it as dir.
  @param edits - Replacements to apply in order; replaceAll replaces every occurrence.
  @param dir - Directory containing the file.
  @param useAgentCwd - Resolve a relative dir against the agent working directory when set.
  """
  if (useAgentCwd) {
    dir = applyAgentCwd(dir)
  }
  // Prepared BEFORE the preview: the preview, the interrupt payload, and
  // the eventual write must all refer to the same spelling.
  const prepared = try _prepareContainedPath(dir, filename, "write")
  if (isFailure(prepared)) {
    return prepared
  }
  const preview = _previewEdit(prepared.value.dir, prepared.value.filename, edits)
  return interrupt std::edit("Are you sure you want to apply these edits?", {
    dir: prepared.value.dir,
    filename: prepared.value.filename,
    edits: edits,
    before: preview.before,
    after: preview.after
  })

  destructive {
    return try _multiedit(prepared.value.dir, prepared.value.filename, edits)
  }
}

type PatchResult = {
  applied: number;
  files: string[]
}

@capabilities(fileRead, fileWrite)
export def applyPatch(patch: string, allowedPaths: string[] = []): Result raises <std::applyPatch> {
  """
  Apply a unified diff to files, including file creation.
  Fails on malformed patches or mismatched context.

  @param allowedPaths - Restrict affected files to these directories, or leave empty for no restriction.
  """
  const files = try _patchFiles(patch)
  if (isFailure(files)) {
    return files
  }
  return interrupt std::applyPatch("Are you sure you want to apply this patch?", {
    patch: patch,
    files: files.value
  })

  destructive {
    return try _applyPatch(patch, allowedPaths, files.value)
  }
}

@capabilities(fileRead, fileWrite)
export def mkdir(dir: string, allowedPaths: string[] = [], useAgentCwd: boolean = false): Result raises <std::mkdir> {
  """
  Create a directory and any missing parents. Succeeds if it already exists.

  @param allowedPaths - Restrict creation to these directories, or leave empty for no restriction.
  @param useAgentCwd - Resolve a relative dir against the agent working directory when set.
  """
  if (useAgentCwd) {
    dir = applyAgentCwd(dir)
  }
  const target = try _realTarget(dir)
  if (isFailure(target)) {
    return target
  }
  return interrupt std::mkdir("Are you sure you want to create this directory?", {
    dir: target.value
  })

  destructive {
    return try _mkdir(target.value, allowedPaths)
  }
}

@capabilities(fileRead, fileWrite)
export def copy(src: string, dest: string, allowedPaths: string[] = [], useAgentCwd: boolean = false): Result raises <std::copy> {
  """
  Copy a file or directory recursively.

  @param allowedPaths - Restrict paths to these directories, or leave empty for no restriction.
  @param useAgentCwd - Resolve relative paths against the agent working directory when set.
  """
  if (useAgentCwd) {
    src = applyAgentCwd(src)
    dest = applyAgentCwd(dest)
  }
  const from = try _realTarget(src)
  if (isFailure(from)) {
    return from
  }
  const to = try _realTarget(dest)
  if (isFailure(to)) {
    return to
  }
  return interrupt std::copy("Are you sure you want to copy this file or directory?", {
    src: from.value,
    dest: to.value
  })

  destructive {
    return try _copy(from.value, to.value, allowedPaths)
  }
}

@capabilities(fileRead, fileWrite)
export def move(src: string, dest: string, allowedPaths: string[] = [], useAgentCwd: boolean = false): Result raises <std::move> {
  """
  Move or rename a file or directory.

  @param allowedPaths - Restrict paths to these directories, or leave empty for no restriction.
  @param useAgentCwd - Resolve relative paths against the agent working directory when set.
  """
  if (useAgentCwd) {
    src = applyAgentCwd(src)
    dest = applyAgentCwd(dest)
  }
  const from = try _realTarget(src)
  if (isFailure(from)) {
    return from
  }
  const to = try _realTarget(dest)
  if (isFailure(to)) {
    return to
  }
  return interrupt std::move("Are you sure you want to move this file or directory?", {
    src: from.value,
    dest: to.value
  })

  destructive {
    return try _move(from.value, to.value, allowedPaths)
  }
}

@capabilities(fileRead, fileWrite)
export def remove(target: string, allowedPaths: string[] = [], useAgentCwd: boolean = false): Result raises <std::remove> {
  """
  Delete a file or directory recursively. Succeeds if the target is missing.

  @param allowedPaths - Restrict paths to these directories, or leave empty for no restriction.
  @param useAgentCwd - Resolve relative paths against the agent working directory when set.
  """
  if (useAgentCwd) {
    target = applyAgentCwd(target)
  }
  const real = try _realTarget(target)
  if (isFailure(real)) {
    return real
  }
  return interrupt std::remove("Are you sure you want to delete this file or directory? This cannot be undone.", {
    target: real.value
  })

  destructive {
    return try _remove(real.value, allowedPaths)
  }
}
