import { WriteMode } from "std::index"


/** @module
@summary Represent and execute plans made by safeBash.

Use `safeBash` from `std::safeBash` to run commands. This module holds the
types that describe a plan: the approvals it asks for and what runs after
they are granted. The code that runs a plan lives in `std::safeBash` and is
not exported, because it skips the approvals the plan already raised.

```ts
import { outcomeOf, planFor } from "std::safeBash"

node main() {
  const plan = planFor("git status", "/project")
  print(outcomeOf(plan))
}
```

A plan pairs the required approvals with an execution. Inspect the plan to
test classification without executing its commands.
*/

/** A required approval, with a payload checked for its effect. */
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 }
  | { name: "std::grep"; payload: GrepPayload }
  | { name: "std::read"; payload: ReadPayload }
  | { name: "std::ls"; payload: LsPayload }
  | { name: "std::glob"; payload: GlobPayload }
  | { name: "std::exec"; payload: ExecPayload }

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

/**
Build the write approval from the same payload used by `writeExecution`.
*/
export def writeEffect(write: WritePayload): Effect {
  """Build a write approval descriptor without raising it."""
  return {
    name: "std::write",
    payload: write
  }
}

export def writeExecution(write: WritePayload): WriteExec {
  """Build a file-write execution plan without running it."""
  return {
    kind: "write",
    filename: write.filename,
    dir: write.dir,
    content: write.content,
    mode: write.mode
  }
}

/** The fields the grep tool's own std::grep interrupt carries. */
export type GrepPayload = {
  pattern: string;
  dir: string;
  file: string | null;
  flags: string;
  maxResults: number;
  ignoreCase: boolean;
  wholeWord: boolean;
  filesOnly: boolean;
  invert: boolean
}

export def grepEffect(payload: GrepPayload): Effect {
  """Build a grep approval descriptor without raising it."""
  return {
    name: "std::grep",
    payload: payload
  }
}

/** The fields the read tool's own std::read interrupt is keyed on: the
 real parent of the file, and its name. */
export type ReadPayload = {
  dir: string;
  filename: string
}

export def readEffect(payload: ReadPayload): Effect {
  """Build a file-read approval descriptor without raising it."""
  return {
    name: "std::read",
    payload: payload
  }
}

/** The fields the ls tool's own std::ls interrupt is keyed on. On the
 bash path `maxResults` is 0, meaning no cap: real ls, tree, and du list
 everything. */
export type LsPayload = {
  dir: string;
  recursive: boolean;
  maxResults: number
}

export def lsEffect(payload: LsPayload): Effect {
  """Build a directory-listing approval descriptor without raising it."""
  return {
    name: "std::ls",
    payload: payload
  }
}

/** The fields the glob tool's own std::glob interrupt is keyed on. On the
 bash path `maxResults` is 0, meaning no cap: real find lists
 everything. */
export type GlobPayload = {
  pattern: string;
  dir: string;
  maxResults: number
}

export def globEffect(payload: GlobPayload): Effect {
  """Build a file-search approval descriptor without raising it."""
  return {
    name: "std::glob",
    payload: payload
  }
}

/** The fields the exec tool's own std::exec interrupt carries. From
 safeBash, `cwd` is safeBash's resolved working directory, `timeout` is
 0 and `stdin` is empty, which is how safeBash runs the line, and an
 argument may be an unexpanded glob or `~` that bash expands. */
export type ExecPayload = {
  command: string;
  args: string[];
  subcommand: string;
  cwd: string;
  timeout: number;
  stdin: string
}

export def execEffect(payload: ExecPayload): Effect {
  """Build a command-execution approval descriptor without raising it."""
  return {
    name: "std::exec",
    payload: payload
  }
}

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

// 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.
/** What happens if every effect in the plan is approved. */
export type Execution = BashExec | EchoExec | WriteExec | AgencyExec | RefuseExec | GrepExec | ReadExec | ChainExec

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

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

/** An `agency <subcommand> <file>` line, run through std::agency's cli(). */
export type AgencyExec = {
  kind: "agency";
  args: string[]
}

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

/** Whether grep prints file names: `-H` gives "always", `-h` gives
 "never", neither gives "default". */
export type GrepFileNames = "default" | "always" | "never"

/** What a grep command said about printing. The rules that turn this
 into grep's output live in `_renderGrepOutput`
 (lib/stdlib/grepDialect.ts), whose type of the same name must keep
 these fields. */
export type GrepRender = {
  /** `-n` was given. */
  lineNumbers: boolean;
  fileNames: GrepFileNames;
  /** The path as bash hands it to grep, or "" when none was written. */
  argument: string;
  /** The path names one file, not a directory. */
  oneFile: boolean
}

/** A lone grep, run through the grep tool's own search with no shell.
 The search reads only what `payload` says, so the approval means what
 the grep tool's approval means. */
export type GrepExec = {
  kind: "grep";
  payload: GrepPayload;
  render: GrepRender
}

/**
Build the search execution from the same payload used by `grepEffect`.
*/
export def grepExecution(payload: GrepPayload, render: GrepRender): GrepExec {
  """Build a grep execution plan without running the search."""
  return {
    kind: "grep",
    payload: payload,
    render: render
  }
}

/** How the shell-free path prints a file: whole, numbered (cat -n), the
 first `lines` lines (head), or the last `lines` lines (tail). The rules
 live in `_renderReadView` (lib/stdlib/readDialect.ts), whose type of
 the same name must keep these fields. */
export type ReadView =
  | { kind: "whole" }
  | { kind: "numbered" }
  | { kind: "head"; lines: number }
  | { kind: "tail"; lines: number }

/** A lone cat of one or more files, or a head or tail of one, run
 through the read tool's own read with no shell. The read covers only
 what `payloads` names, so the approval means what the read tool's
 approval means. */
export type ReadExec = {
  kind: "read";
  payloads: ReadPayload[];
  view: ReadView
}

/** How a chain step follows the one before it: `;` always runs it, `&&`
 only after a success, `||` only after a failure. */
export type Join = "seq" | "and" | "or"

/** The executions a chain may hold: every in-process kind, and nothing
 that spawns a shell. */
export type StepExec = EchoExec | WriteExec | GrepExec | ReadExec

export type ChainStep = {
  join: Join;
  execution: StepExec
}

/** Several in-process commands joined by `;`, `&&`, or `||`, run left to
 right with bash's rule for each join. */
export type ChainExec = {
  kind: "chain";
  steps: ChainStep[]
}

/**
Build file reads from the same payloads used by `readEffect`.
*/
export def readExecution(payloads: ReadPayload[], view: ReadView): ReadExec {
  """Build a file-read execution plan without reading files."""
  return {
    kind: "read",
    payloads: payloads,
    view: view
  }
}

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

/** Why a plan asks the broad std::bash question. `detail` is the parser
 message for `unparseable`, the first unrecognized command's message for
 `unrecognized`, the render failure for `unrenderable`, and "" for
 `noEffects`. */
export type BroadReason = {
  kind: "unparseable" | "unrecognized" | "noEffects" | "unrenderable";
  detail: string
}

/** The whole decision about one call to safeBash, made before anything runs. */
export type Plan = {
  effects: Effect[];
  execution: Execution;
  /** Why the plan asks the broad std::bash question, or null when it does not. */
  broadReason: BroadReason | null;
  /** The command word of every simple command planFor decided over, in
    order; "" for an assignment-only command; [] when unparseable. */
  verbs: string[]
}
