import { getAgentCwd } from "std::index"
import {
  _gitRun, _gitIsRepo, assertPathsContained, assertAllNotRestricted, assertBranchAllowed,
  statusArgs, logArgs, diffArgs, showArgs, branchListArgs,
  remoteListArgs, blameArgs, stashListArgs,
  addArgs, commitArgs, checkoutArgs, switchArgs,
  branchCreateArgs, branchDeleteArgs, stashPushArgs, stashPopArgs, restoreArgs,
  parseStatus, parseLog, parseDiff, parseBranchList,
  parseRemoteList, parseBlame, parseStashList,
 } from "agency-lang/stdlib-lib/git.js"

/** @module
  @summary Typed, safe git tools for agents: each tool builds its own git command, so the model never supplies a raw flag.
  Typed, safe git tools for agents. Each tool builds its own git command
  internally, so the model never supplies a raw flag. Read tools (status, log,
  diff, ...) raise effects an agent policy can auto-approve. Write tools (add,
  commit, checkout, ...) prompt for approval. Tighten any tool before handing it
  to an agent with `.partial()`, e.g.
  `gitAdd.partial(all: false, allowedPaths: ["src/"])` or
  `gitBranchDelete.partial(force: false, protectedBranches: ["main"])`.

  Every tool takes an optional `cwd`, the repo to operate on. It defaults to the
  agent working directory (see `setAgentCwd`). Pass an absolute path to target a
  different repo.

  ```ts
  import { gitStatus, gitCommit } from "std::git"

  node main() {
    const status = gitStatus()             // read: auto-approvable
    print(status.branch)
    gitCommit("Update docs") with approve  // write: prompts for approval
  }
  ```
*/

// Result types are declared natively in Agency (not imported from the TS
// module): Agency does not parse TypeScript, so a TS-imported type is an opaque
// name with no accessible fields — `status.entries` would not typecheck. These
// mirror the shapes the TS parsers (parseStatus, parseLog, ...) return, so
// `return parseStatus(...)` bridges cleanly, exactly as `bash` returns a
// natively-declared `ExecResult` from a TS `_bash` impl. Keep these in sync
// with `lib/stdlib/gitCore.ts`.

/** git porcelain change codes: "." unmodified, "M" modified, "A" added,
  "D" deleted, "R" renamed, "C" copied, "U" unmerged, "T" type-changed,
  "?" untracked, "!" ignored. */
export type ChangeCode = "." | "M" | "A" | "D" | "R" | "C" | "U" | "T" | "?" | "!"
export type FileStatus = {
  path: string;
  index: ChangeCode;
  worktree: ChangeCode;
  renamedFrom?: string
}
export type GitStatus = {
  branch: string;
  upstream: string;
  ahead: number;
  behind: number;
  entries: FileStatus[]
}
export type GitCommit = {
  sha: string;
  author: string;
  email: string;
  date: string;
  subject: string;
  body: string
}
export type GitLog = { commits: GitCommit[] }
export type FileDiff = {
  path: string;
  status: ChangeCode;
  additions: number | null;
  deletions: number | null
}
export type GitDiff = { files: FileDiff[]; patch: string }
export type GitBranch = {
  name: string;
  current: boolean;
  upstream: string;
  sha: string
}
export type BlameLine = {
  sha: string;
  author: string;
  line: number;
  content: string
}
export type GitRemote = { name: string; url: string; direction: "fetch" | "push" }
export type GitStash = { ref: string; description: string }

// Positional values (refs/paths/branches) may not start with "-". Agency uses an
// explicit `cwd` as given, never joining it to the agent working directory.

// Read effects
effect std::git::status     { cwd: string }
effect std::git::log        { cwd: string, ref: string, path: string }
effect std::git::diff       { cwd: string, ref: string, ref2: string, staged: boolean, path: string }
effect std::git::show       { cwd: string, ref: string }
effect std::git::branchList { cwd: string }
effect std::git::remoteList { cwd: string }
effect std::git::blame      { cwd: string, path: string }
effect std::git::stashList  { cwd: string }
// Write effects
effect std::git::add          { cwd: string, paths: string[], all: boolean }
effect std::git::commit       { cwd: string, message: string }
effect std::git::checkout     { cwd: string, target: string, force: boolean }
effect std::git::switch       { cwd: string, branch: string, create: boolean }
effect std::git::branchCreate { cwd: string, branch: string }
effect std::git::branchDelete { cwd: string, branch: string, force: boolean }
effect std::git::stashPush    { cwd: string, message: string }
effect std::git::stashPop     { cwd: string }
effect std::git::restore      { cwd: string, paths: string[], staged: boolean }

/** Read-only git effects, auto-approvable. */
export effectSet GitRead = <std::git::status, std::git::log, std::git::diff, std::git::show, std::git::branchList, std::git::remoteList, std::git::blame, std::git::stashList>
/** Mutating git effects, should prompt. */
export effectSet GitWrite = <std::git::add, std::git::commit, std::git::checkout, std::git::switch, std::git::branchCreate, std::git::branchDelete, std::git::stashPush, std::git::stashPop, std::git::restore>
/** All git effects. */
export effectSet Git = <GitRead, GitWrite>

// Resolve the repo directory. Use an explicit cwd as given, otherwise fall
// back to the agent working directory. `_gitRun` rejects an empty/relative
// result, so "no repo directory" fails loudly instead of targeting the
// process cwd.
def repoDir(cwd: string): string {
  if (cwd != "") {
    return cwd
  }
  return getAgentCwd()
}

// ---- Reads -----------------------------------------------------------------

export idempotent def gitIsRepo(cwd: string = ""): boolean raises <std::git::status> {
  """
  True when `cwd` is inside a git work tree. Never fails: a directory that is not
  a git repository (or does not exist) returns false, so callers can guard on it
  before running other git tools.
  @param cwd - The directory to check. Defaults to the agent working directory.
  """
  const dir = repoDir(cwd)
  return interrupt std::git::status("Check whether this is a git repository", { cwd: dir })
  return _gitIsRepo(dir)
}

export idempotent def gitStatus(cwd: string = ""): GitStatus raises <std::git::status> {
  """
  Show the working-tree status: current branch, ahead/behind counts, and
  changed files.
  @param cwd - The git repo directory. Defaults to the agent working directory. Pass an absolute path to target a different repo.
  """
  const dir = repoDir(cwd)
  return interrupt std::git::status("Show git status", { cwd: dir })
  return parseStatus(_gitRun(dir, statusArgs()))
}

/** `allowedPaths` is a developer guardrail: bind a prefix list via `.partial()`
    to constrain which paths the model may query. */
export idempotent def gitLog(
  n: number = 20, oneline: boolean = false, path: string = "",
  ref: string = "", author: string = "", allowedPaths: string[] = [],
  cwd: string = "",
): GitLog raises <std::git::log> {
  """
  Show commit history as structured commits.
  @param n - Max number of commits (default 20).
  @param oneline - Omit commit bodies.
  @param path - Limit to commits touching this path (may not start with "-").
  @param ref - Start from this revision (e.g. HEAD~5, a branch, a sha).
  @param author - Filter by author substring.
  @param allowedPaths - If non-empty, path must fall under one of these prefixes.
  @param cwd - The git repo directory. Defaults to the agent working directory. Pass an absolute path to target a different repo.
  """
  const dir = repoDir(cwd)
  if (path != "") {
    assertPathsContained([path], allowedPaths, dir)
  }
  return interrupt std::git::log("Show git log", { cwd: dir, ref: ref, path: path })
  return parseLog(_gitRun(dir, logArgs({ count: n, oneline: oneline, path: path, ref: ref, author: author })))
}

/** `allowedPaths` is a developer guardrail: bind a prefix list via `.partial()`
    to constrain which paths the model may diff. */
export idempotent def gitDiff(
  ref: string = "", ref2: string = "", staged: boolean = false,
  path: string = "", allowedPaths: string[] = [], cwd: string = "",
): GitDiff raises <std::git::diff> {
  """
  Show a diff as a structured per-file summary plus the raw unified patch.
  @param ref - Compare against this revision (default: working tree vs index).
  @param ref2 - Optional second revision to diff ref..ref2.
  @param staged - Diff the index (staged changes) instead of the working tree.
  @param path - Limit the diff to this path (may not start with "-").
  @param allowedPaths - If non-empty, path must fall under one of these prefixes.
  @param cwd - The git repo directory. Defaults to the agent working directory. Pass an absolute path to target a different repo.
  """
  const dir = repoDir(cwd)
  if (path != "") {
    assertPathsContained([path], allowedPaths, dir)
  }
  return interrupt std::git::diff("Show git diff", { cwd: dir, ref: ref, ref2: ref2, staged: staged, path: path })
  return parseDiff(_gitRun(dir, diffArgs({ ref: ref, ref2: ref2, staged: staged, path: path })))
}

export idempotent def gitShow(ref: string = "HEAD", cwd: string = ""): GitDiff raises <std::git::show> {
  """
  Show a commit as a structured per-file summary plus the raw patch. Line
  counts are approximate for merge commits (combined diffs are not counted).
  @param ref - The revision to show (default HEAD).
  @param cwd - The git repo directory. Defaults to the agent working directory. Pass an absolute path to target a different repo.
  """
  const dir = repoDir(cwd)
  return interrupt std::git::show("Show git commit", { cwd: dir, ref: ref })
  return parseDiff(_gitRun(dir, showArgs({ ref: ref })))
}

export idempotent def gitBranchList(cwd: string = ""): GitBranch[] raises <std::git::branchList> {
  """
  List local branches with their current-marker, upstream, and sha.
  @param cwd - The git repo directory. Defaults to the agent working directory. Pass an absolute path to target a different repo.
  """
  const dir = repoDir(cwd)
  return interrupt std::git::branchList("List git branches", { cwd: dir })
  return parseBranchList(_gitRun(dir, branchListArgs()))
}

export idempotent def gitRemoteList(cwd: string = ""): GitRemote[] raises <std::git::remoteList> {
  """
  List configured remotes with their fetch/push URLs.
  @param cwd - The git repo directory. Defaults to the agent working directory. Pass an absolute path to target a different repo.
  """
  const dir = repoDir(cwd)
  return interrupt std::git::remoteList("List git remotes", { cwd: dir })
  return parseRemoteList(_gitRun(dir, remoteListArgs()))
}

export idempotent def gitBlame(path: string, ref: string = "", cwd: string = ""): BlameLine[] raises <std::git::blame> {
  """
  Show line-by-line authorship for a file.
  @param path - The file to blame (may not start with "-").
  @param ref - Optional revision to blame at.
  @param cwd - The git repo directory. Defaults to the agent working directory. Pass an absolute path to target a different repo.
  """
  const dir = repoDir(cwd)
  return interrupt std::git::blame("Show git blame", { cwd: dir, path: path })
  return parseBlame(_gitRun(dir, blameArgs({ path: path, ref: ref })))
}

export idempotent def gitStashList(cwd: string = ""): GitStash[] raises <std::git::stashList> {
  """
  List stashes with their ref and description.
  @param cwd - The git repo directory. Defaults to the agent working directory. Pass an absolute path to target a different repo.
  """
  const dir = repoDir(cwd)
  return interrupt std::git::stashList("List git stashes", { cwd: dir })
  return parseStashList(_gitRun(dir, stashListArgs()))
}

// ---- Writes ----------------------------------------------------------------

/** `all` and `allowedPaths` are developer guardrails: bind `all: false` and/or
    a prefix list for `allowedPaths` via `.partial()` before handing this to an
    agent. */
export def gitAdd(paths: string[] = [], all: boolean = false, allowedPaths: string[] = [], cwd: string = ""): string raises <std::git::add> {
  """
  Stage changes for commit.
  @param paths - Files to stage (may not start with "-").
  @param all - Stage every change in the repo (git add -A).
  @param allowedPaths - If non-empty, each path must fall under one of these prefixes.
  @param cwd - The git repo directory. Defaults to the agent working directory. Pass an absolute path to target a different repo.
  """
  const dir = repoDir(cwd)
  assertAllNotRestricted(all, allowedPaths)
  assertPathsContained(paths, allowedPaths, dir)
  return interrupt std::git::add("Stage changes", { cwd: dir, paths: paths, all: all })
  destructive {
    _gitRun(dir, addArgs({ paths: paths, all: all }))
    return "Staged changes"
  }
}

export def gitCommit(message: string, cwd: string = ""): string raises <std::git::commit> {
  """
  Create a commit from the staged changes.
  @param message - The commit message.
  @param cwd - The git repo directory. Defaults to the agent working directory. Pass an absolute path to target a different repo.
  """
  const dir = repoDir(cwd)
  return interrupt std::git::commit("Create commit: ${message}", { cwd: dir, message: message })
  destructive {
    return _gitRun(dir, commitArgs({ message: message }))
  }
}

/** Bind `force: false` via `.partial()` to forbid discarding local changes. */
export def gitCheckout(target: string, force: boolean = false, cwd: string = ""): string raises <std::git::checkout> {
  """
  Check out a branch, commit, or path.
  @param target - The branch/commit/path (may not start with "-").
  @param force - Discard local changes while checking out (git checkout --force).
  @param cwd - The git repo directory. Defaults to the agent working directory. Pass an absolute path to target a different repo.
  """
  const dir = repoDir(cwd)
  let message = "Checkout ${target}"
  if (force) {
    message = "Force checkout ${target} (DISCARDS local changes, cannot be undone)"
  }
  return interrupt std::git::checkout(message, { cwd: dir, target: target, force: force })
  destructive {
    return _gitRun(dir, checkoutArgs({ target: target, force: force }))
  }
}

export def gitSwitch(branch: string, create: boolean = false, cwd: string = ""): string raises <std::git::switch> {
  """
  Switch to a branch.
  @param branch - The branch to switch to (may not start with "-").
  @param create - Create the branch first (git switch -c).
  @param cwd - The git repo directory. Defaults to the agent working directory. Pass an absolute path to target a different repo.
  """
  const dir = repoDir(cwd)
  return interrupt std::git::switch("Switch to branch ${branch}", { cwd: dir, branch: branch, create: create })
  destructive {
    return _gitRun(dir, switchArgs({ branch: branch, create: create }))
  }
}

export def gitBranchCreate(branch: string, cwd: string = ""): string raises <std::git::branchCreate> {
  """
  Create a new branch at HEAD (does not switch to it).
  @param branch - The new branch name (may not start with "-").
  @param cwd - The git repo directory. Defaults to the agent working directory. Pass an absolute path to target a different repo.
  """
  const dir = repoDir(cwd)
  return interrupt std::git::branchCreate("Create branch ${branch}", { cwd: dir, branch: branch })
  destructive {
    _gitRun(dir, branchCreateArgs({ branch: branch }))
    return "Created branch ${branch}"
  }
}

/** Guardrails: bind `force: false` and/or a `protectedBranches` list via
    `.partial()` before handing this to an agent. */
export def gitBranchDelete(branch: string, force: boolean = false, protectedBranches: string[] = [], cwd: string = ""): string raises <std::git::branchDelete> {
  """
  Delete a local branch.
  @param branch - The branch to delete (may not start with "-").
  @param force - Delete even if the branch is unmerged (git branch -D).
  @param protectedBranches - Branch names that may never be deleted (e.g. ["main", "master"]).
  @param cwd - The git repo directory. Defaults to the agent working directory. Pass an absolute path to target a different repo.
  """
  const dir = repoDir(cwd)
  assertBranchAllowed(branch, protectedBranches)
  let message = "Delete branch ${branch}"
  if (force) {
    message = "Force-delete branch ${branch} (may discard unmerged commits, cannot be undone)"
  }
  return interrupt std::git::branchDelete(message, { cwd: dir, branch: branch, force: force })
  destructive {
    _gitRun(dir, branchDeleteArgs({ branch: branch, force: force, protectedBranches: protectedBranches }))
    return "Deleted branch ${branch}"
  }
}

export def gitStashPush(message: string = "", cwd: string = ""): string raises <std::git::stashPush> {
  """
  Stash the working-tree changes.
  @param message - Optional stash message.
  @param cwd - The git repo directory. Defaults to the agent working directory. Pass an absolute path to target a different repo.
  """
  const dir = repoDir(cwd)
  return interrupt std::git::stashPush("Stash changes", { cwd: dir, message: message })
  destructive {
    return _gitRun(dir, stashPushArgs({ message: message }))
  }
}

export def gitStashPop(cwd: string = ""): string raises <std::git::stashPop> {
  """
  Apply and drop the most recent stash.
  @param cwd - The git repo directory. Defaults to the agent working directory. Pass an absolute path to target a different repo.
  """
  const dir = repoDir(cwd)
  return interrupt std::git::stashPop("Pop the latest stash", { cwd: dir })
  destructive {
    return _gitRun(dir, stashPopArgs())
  }
}

/** `allowedPaths` is a developer guardrail: bind a prefix list via `.partial()`
    to constrain which paths the model may restore. */
export def gitRestore(paths: string[], staged: boolean = false, allowedPaths: string[] = [], cwd: string = ""): string raises <std::git::restore> {
  """
  Restore files to a previous state.
  @param paths - Files to restore (may not start with "-").
  @param staged - Unstage the files instead of discarding working-tree changes.
  @param allowedPaths - If non-empty, each path must fall under one of these prefixes.
  @param cwd - The git repo directory. Defaults to the agent working directory. Pass an absolute path to target a different repo.
  """
  const dir = repoDir(cwd)
  assertPathsContained(paths, allowedPaths, dir)
  let message = "Discard working-tree changes to ${paths.length} file(s) (cannot be undone)"
  if (staged) {
    message = "Unstage ${paths.length} file(s)"
  }
  return interrupt std::git::restore(message, { cwd: dir, paths: paths, staged: staged })
  destructive {
    _gitRun(dir, restoreArgs({ paths: paths, staged: staged }))
    return "Restored ${paths.length} file(s)"
  }
}
