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

/** @module
  @summary Tools for editing files and managing directories: apply text edits and patches, and create, copy, move, or remove files.
  Tools for editing files and managing directories: apply text edits and
  patches, and create, copy, move, or remove files. Every operation raises an
  approval interrupt before it touches the disk, so nothing is written until a
  handler approves it.

  ```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
}

effect std::edit { dir: string, filename: string, edits: Edit[], before: string, after: string }
effect std::applyPatch { patch: string }
effect std::mkdir { dir: string }
effect std::copy { src: string, dest: string }
effect std::move { src: string, dest: string }
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))`).
 */
export def edit(filename: string, edits: Edit[], dir: string = ".", useAgentCwd: boolean = false): Result {
  """
  Edit a single file by applying one or more text replacements atomically. Each edit's oldText must match a unique, non-overlapping region of the file as it stands when that edit runs (after earlier edits in the same call). If any edit fails, nothing is written.

  Prefer one call with multiple edits over many separate calls. Keep each oldText as small as possible while still matching uniquely. Do not pad with unchanged context to bridge distant changes; split them into separate edits instead.

  @param filename - The file to edit
  @param edits - Text replacements to apply in order; each has oldText, newText, and replaceAll (replace every occurrence when true)
  @param dir - The directory to resolve the filename against
  @param useAgentCwd - When true, resolve a relative path against the agent working directory if one is set
  """
  if (useAgentCwd) {
    dir = applyAgentCwd(dir)
  }
  const preview = _previewEdit(dir, filename, edits)
  return interrupt std::edit("Are you sure you want to apply these edits?", {
    dir: dir,
    filename: filename,
    edits: edits,
    before: preview.before,
    after: preview.after
  })

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

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

export def applyPatch(patch: string, allowedPaths: string[] = []): Result {
  """
  Apply a unified diff to the working tree. Supports file creation (--- /dev/null), line additions, deletions, and context. Fails on a malformed diff or on a context-line mismatch against the current file contents.

  @param patch - The unified diff to apply
  @param allowedPaths - Only allow patches that touch files under these prefixes
  """
  return interrupt std::applyPatch("Are you sure you want to apply this patch?", {
    patch: patch
  })

  destructive {
    return try _applyPatch(patch, allowedPaths)
  }
}

export def mkdir(dir: string, allowedPaths: string[] = [], useAgentCwd: boolean = false): Result {
  """
  Create a directory, including any missing parent directories. Idempotent: succeeds if the directory already exists. Fails if a non-directory entry already occupies the path, or on permission errors.

  @param dir - The directory to create
  @param allowedPaths - Only allow paths starting with these prefixes
  @param useAgentCwd - When true, resolve a relative dir against the agent working directory if one is set
  """
  if (useAgentCwd) {
    dir = applyAgentCwd(dir)
  }
  return interrupt std::mkdir("Are you sure you want to create this directory?", {
    dir: dir
  })

  destructive {
    return try _mkdir(dir, allowedPaths)
  }
}

export def copy(src: string, dest: string, allowedPaths: string[] = [], useAgentCwd: boolean = false): Result {
  """
  Copy a file or directory. Directories are copied recursively. Fails if src does not exist or dest cannot be written.

  @param src - The source path
  @param dest - The destination path
  @param allowedPaths - Only allow paths starting with these prefixes
  @param useAgentCwd - When true, resolve relative src/dest against the agent working directory if one is set
  """
  if (useAgentCwd) {
    src = applyAgentCwd(src)
    dest = applyAgentCwd(dest)
  }
  return interrupt std::copy("Are you sure you want to copy this file or directory?", {
    src: src,
    dest: dest
  })

  destructive {
    return try _copy(src, dest, allowedPaths)
  }
}

export def move(src: string, dest: string, allowedPaths: string[] = [], useAgentCwd: boolean = false): Result {
  """
  Move or rename a file or directory. Falls back to copy+remove if src and dest are on different filesystems. Fails if src does not exist.

  @param src - The source path
  @param dest - The destination path
  @param allowedPaths - Only allow paths starting with these prefixes
  @param useAgentCwd - When true, resolve relative src/dest against the agent working directory if one is set
  """
  if (useAgentCwd) {
    src = applyAgentCwd(src)
    dest = applyAgentCwd(dest)
  }
  return interrupt std::move("Are you sure you want to move this file or directory?", {
    src: src,
    dest: dest
  })

  destructive {
    return try _move(src, dest, allowedPaths)
  }
}

export def remove(target: string, allowedPaths: string[] = [], useAgentCwd: boolean = false): Result {
  """
  Delete a file or directory. Directories are removed recursively. Does not fail if the target does not exist.

  @param target - The path to delete
  @param allowedPaths - Only allow paths starting with these prefixes
  @param useAgentCwd - When true, resolve a relative target against the agent working directory if one is set
  """
  if (useAgentCwd) {
    target = applyAgentCwd(target)
  }
  return interrupt std::remove("Are you sure you want to delete this file or directory? This cannot be undone.", {
    target: target
  })

  destructive {
    return try _remove(target, allowedPaths)
  }
}

