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 Inspect repositories and manage local Git changes with typed tools.
  Inspect status, history, and diffs, or stage, commit, and restore local
  changes. Each operation has its own effect for handlers and policies.
  Bind restrictions before handing a tool to an agent, for example:
  `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() with approve
    print(status.branch)
    gitCommit("Update docs") with approve
  }
  ```
*/

// Keep these native result types in sync with lib/stdlib/gitCore.ts.
// Agency cannot inspect the fields of types imported from TypeScript.

/** 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 }

// gitCore rejects leading "-" on positional values before building argv.
// The runner uses an explicit cwd without joining it to the agent directory.

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

/** Effects for reading repository state without changing it. */
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>
/** Effects for operations that change repository state. */
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 -----------------------------------------------------------------

@capabilities(fileRead, subprocess, env)
export idempotent def gitIsRepo(cwd: string = ""): boolean raises <std::git::status> {
  """
  Check whether a directory is inside a Git worktree, returning false for a missing or non-repository directory.

  @param cwd - An absolute directory, or empty to use 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)
}

@capabilities(fileRead, subprocess, env)
export idempotent def gitStatus(cwd: string = ""): GitStatus raises <std::git::status> {
  """
  Inspect staged and unstaged repository changes.
  @param cwd - An absolute repository directory, or empty to use the agent working directory.
  """
  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. */
@capabilities(fileRead, subprocess, env)
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 - The maximum commits to return, or zero or less for all matching commits.
  @param oneline - Omit commit bodies.
  @param path - Limit to commits touching this path (may not start with "-").
  @param ref - Start from this revision, such as HEAD~5 or a SHA, without a leading "-".
  @param author - Filter by author substring.
  @param allowedPaths - If non-empty, path must fall under one of these prefixes.
  @param cwd - An absolute repository directory, or empty to use the agent working directory.
  """
  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. */
@capabilities(fileRead, subprocess, env)
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 - Compare ref against this second revision, without a leading "-".
  @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 - An absolute repository directory, or empty to use the agent working directory.
  """
  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 })))
}

/**
  Line counts are approximate for merge commits because combined diffs
  are not counted by the summary parser.
*/
@capabilities(fileRead, subprocess, env)
export idempotent def gitShow(ref: string = "HEAD", cwd: string = ""): GitDiff raises <std::git::show> {
  """
  Show a commit's per-file changes and unified patch.
  @param ref - A revision such as HEAD or a SHA, without a leading "-".
  @param cwd - An absolute repository directory, or empty to use the agent working directory.
  """
  const dir = repoDir(cwd)
  return interrupt std::git::show("Show git commit", { cwd: dir, ref: ref })
  return parseDiff(_gitRun(dir, showArgs({ ref: ref })))
}

@capabilities(fileRead, subprocess, env)
export idempotent def gitBranchList(cwd: string = ""): GitBranch[] raises <std::git::branchList> {
  """
  List local Git branches.
  @param cwd - An absolute repository directory, or empty to use the agent working directory.
  """
  const dir = repoDir(cwd)
  return interrupt std::git::branchList("List git branches", { cwd: dir })
  return parseBranchList(_gitRun(dir, branchListArgs()))
}

@capabilities(fileRead, subprocess, env)
export idempotent def gitRemoteList(cwd: string = ""): GitRemote[] raises <std::git::remoteList> {
  """
  List configured remotes with their fetch/push URLs.
  @param cwd - An absolute repository directory, or empty to use the agent working directory.
  """
  const dir = repoDir(cwd)
  return interrupt std::git::remoteList("List git remotes", { cwd: dir })
  return parseRemoteList(_gitRun(dir, remoteListArgs()))
}

@capabilities(fileRead, subprocess, env)
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 - A revision without a leading "-", or empty for the working tree.
  @param cwd - An absolute repository directory, or empty to use the agent working directory.
  """
  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 })))
}

@capabilities(fileRead, subprocess, env)
export idempotent def gitStashList(cwd: string = ""): GitStash[] raises <std::git::stashList> {
  """
  List stashes with their ref and description.
  @param cwd - An absolute repository directory, or empty to use the agent working directory.
  """
  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. */
@capabilities(fileRead, subprocess, env)
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 all repository changes, which cannot be combined with allowedPaths.
  @param allowedPaths - If non-empty, each path must fall under one of these prefixes.
  @param cwd - An absolute repository directory, or empty to use the agent working directory.
  """
  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"
  }
}

@capabilities(fileRead, subprocess, env)
export def gitCommit(message: string, cwd: string = ""): string raises <std::git::commit> {
  """
  Create a commit from the staged changes.
  @param cwd - An absolute repository directory, or empty to use the agent working directory.
  """
  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. */
@capabilities(fileRead, subprocess, env)
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 - An absolute repository directory, or empty to use the agent working directory.
  """
  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 }))
  }
}

@capabilities(fileRead, subprocess, env)
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 - An absolute repository directory, or empty to use the agent working directory.
  """
  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 }))
  }
}

@capabilities(fileRead, subprocess, env)
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 - An absolute repository directory, or empty to use the agent working directory.
  """
  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. */
@capabilities(fileRead, subprocess, env)
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 - An absolute repository directory, or empty to use the agent working directory.
  """
  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}"
  }
}

@capabilities(fileRead, subprocess, env)
export def gitStashPush(message: string = "", cwd: string = ""): string raises <std::git::stashPush> {
  """
  Stash the working-tree changes.
  @param cwd - An absolute repository directory, or empty to use the agent working directory.
  """
  const dir = repoDir(cwd)
  return interrupt std::git::stashPush("Stash changes", { cwd: dir, message: message })
  destructive {
    return _gitRun(dir, stashPushArgs({ message: message }))
  }
}

@capabilities(fileRead, subprocess, env)
export def gitStashPop(cwd: string = ""): string raises <std::git::stashPop> {
  """
  Apply and drop the most recent stash.
  @param cwd - An absolute repository directory, or empty to use the agent working directory.
  """
  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. */
@capabilities(fileRead, subprocess, env)
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 - An absolute repository directory, or empty to use the agent working directory.
  """
  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)"
  }
}
