/** @module
  What safeBash decides, and the three ways it can carry that out.

  A `Plan` is the whole decision about one call, made before anything
  happens: which interrupts to raise, and what to run if they are all
  approved. Deciding and doing are separate steps so the deciding half can
  be tested by handing in a string and looking at the plan.

  The executors call the NON-RAISING internals (`_bash`, `_write`) rather
  than the `bash` and `write` tools, because those raise their own
  interrupts and the caller has already raised a narrower one. See the
  comment on `runBash`.
*/
import { WriteMode } from "std::index"

import { _write } from "agency-lang/stdlib-lib/builtins.js"
import { _bash } from "agency-lang/stdlib-lib/shell.js"

/** How much of a command's output to keep before truncating. */
export static const MAX_STDOUT_LEN = 2000

/** One interrupt a command needs raised before it may run.
 *
 * A discriminated union rather than a bag of `any`: the effect set is
 * closed, so each payload can be typed, and typing them is what makes the
 * raise sites check that every payload satisfies its effect's contract. */
export type Effect =
  | { name: "std::bash" }
  | { name: "std::write"; payload: WritePayload }
  | { name: "std::git::status"; payload: { cwd: string } }
  | { name: "std::git::log"; payload: { cwd: string; ref: string; path: string } }
  | { name: "std::git::diff"; payload: GitDiffPayload }

export type WritePayload = {
  dir: string;
  filename: string;
  content: string;
  mode: WriteMode
}

export type GitDiffPayload = {
  cwd: string;
  ref: string;
  ref2: string;
  staged: boolean;
  path: string
}

/** What happens if every effect in the plan is approved.
 *
 * One variant per way a plan can be carried out, each carrying only the
 * fields that way needs. The dispatch in `safeBash` matches over this
 * union exhaustively, so adding a variant is a type error at the dispatch
 * rather than a silent fall-through. */
export type Execution = BashExec | EchoExec | WriteExec | RefuseExec

export type BashExec = {
  kind: "bash";
  command: string
}

export type EchoExec = {
  kind: "echo";
  content: string
}

export type WriteExec = {
  kind: "write";
  filename: string;
  dir: string;
  content: string;
  mode: WriteMode
}

export type RefuseExec = {
  kind: "refuse";
  reason: string
}

/** The whole decision about one call to safeBash, made before anything runs. */
export type Plan = {
  effects: Effect[];
  execution: Execution
}

export def runBash(command: string, cwd: string): Result<string> {
  """
  Run a command string through bash and return what it printed.

  On success the value is bash's stdout, raw — nothing added, nothing
  stripped. stderr is discarded on success and reported on failure: two
  captured pipes cannot interleave the way a terminal does, so this is a
  policy either way, and discarding on success keeps the value exactly
  equal to bash's stdout.

  A non-zero exit is a failure, which `&&` and `||` require. The known
  cost: `grep` exits 1 when it finds nothing and `diff` exits 1 when files
  differ, and neither is an error.
  """
  // `_bash`, NOT `bash`. The `bash` tool raises `std::bash` itself, so
  // calling it here would ask the broad question again — after the human
  // already answered a narrower one — and the entire feature would be a
  // no-op.
  //
  // Bypassing a tool's interrupt is normally forbidden: handlers are
  // safety infrastructure. It is correct here for one specific reason,
  // and only while that reason holds: the narrow interrupt REPLACES the
  // broad one, and the only path into this function runs through a
  // `raiseAll` that raised every effect in the plan and returned success.
  // Every plan raises at least one effect, so no classification bug can
  // turn into an ungated shell call.
  const attempt = try _bash(command, cwd, 0, "", {})
  if (attempt is failure(why)) {
    return failure("`${command}` was not run: ${why}")
  }
  // `try` WRAPS the call in a Result, so the ExecResult is inside
  // `.value`. Reading `attempt.stdout` directly is undefined.
  const result: any = attempt.value
  const out = truncate(result.stdout)
  if (result.exitCode != 0) {
    return failure(
      JSON.stringify(
      {
      command: command,
      exitCode: result.exitCode,
      stderr: truncate(result.stderr),
      stdout: out
    },
    ),
    )
  }
  return success(out)
}

export def truncate(text: string): string {
  """
  Cap output length. Raw output with no bound is a context-window hazard,
  and a visible loss of fidelity beats an invisible one.
  """
  if (text.length <= MAX_STDOUT_LEN) {
    return text
  }
  return text[:MAX_STDOUT_LEN] + "\n[truncated]"
}

export def runWrite(exec: WriteExec): Result<string> {
  """
  Perform the write a redirected echo asked for. Returns the empty string,
  which is what bash's stdout for `echo hi > f` is.

  `_write`, NOT `write`, for the same reason `runBash` uses `_bash`: the
  `write` tool raises its own `std::write`, and the caller already raised
  one carrying the content.
  """
  // `destructive { }` because `write()` wraps `_write` in one, and that
  // marker feeds the tool-loop retry rules. Calling `_write` directly
  // without it would give this write a different retry classification
  // from the tool it replaces.
  destructive {
    const written = try _write(exec.dir, exec.filename, exec.content, exec.mode)
    if (written is failure(why)) {
      return failure(why)
    }
  }
  return success("")
}
