import { ls, glob, grep, exec } from "std::shell"
import { Json } from "std::validation"

import {
  _compile,
  _compileFile,
  _typecheck,
  _typecheckFile,
  _getEffects,
  _describe,
  _parseAST,
  _writeAST,
  _format,
  _formatFile,
  _walkAST,
  _getNodesOfType,
  _filterImports,
  _subprocessDepth,
 } from "agency-lang/stdlib-lib/agency.js"
import {
  _loadTemplate,
  _holesOf,
  _fill,
  _parseExpr,
  _parseStatements,
  _toSource,
  _combine,
} from "agency-lang/stdlib-lib/template.js"
import { VERSION } from "agency-lang/stdlib-lib/version.js"

/** @module
  @summary Tools for compiling, type-checking, running, formatting, and inspecting Agency programs from Agency code.
  Tools for compiling, type-checking, running, formatting, and inspecting
  Agency programs from Agency code. Compile and run source in a sandboxed
  subprocess, type-check or format it, and walk its AST to find imports,
  functions, or nodes.

  ```ts
  import { compile, run } from "std::agency"

  node main() {
    const program = compile("export node main() { return 42 }")
    const result = run(program, "main")
    print(result)
  }
  ```
*/

// `std::read`/`std::write` are the shared file-access effects (also raised by
// `std::read`/`std::write` in `std::index`), so their reads and writes here stay
// governed by the same file policy. The canonical payload is `{ dir, filename }`.
effect std::read {
  dir: string;
  filename: string
}
effect std::write {
  dir: string;
  filename: string
}
effect std::run {
  moduleId: string;
  node: string;
  args: Record<string, any>;
  limits: { wallClock: number; memory: number; ipcPayload: number; stdout: number; maxCost: number | null };
  cwd: string;
  logFile: string;
  depth: number
}

export type CompiledProgram = {
  moduleId: string
}

export type SourceLocation = {
  line: number;
  col: number;
  start: number;
  end: number
}

export type TypeCheckDiagnostic = {
  /** Stable AG#### diagnostic code. Suppress one line with
  `// @tc-ignore AG####`, or match on it instead of parsing the message. */
  code: string;
  severity: string;
  message: string;
  loc?: SourceLocation;
  /** Structured payload of the diagnostic (the values rendered into the
  message, e.g. the expected and actual type strings; counts and positions
  are numbers). */
  params: Record<string, string | number>
}

export type TypeCheckReport = {
  errors: TypeCheckDiagnostic[];
  warnings: TypeCheckDiagnostic[]
}

export idempotent def compile(source: string): Result {
  """
  Compile Agency source code. Returns a CompiledProgram on success, or a failure with compilation errors. Only standard library (`std::`) imports are allowed in the compiled code.

  @param source - Agency source code as a string
  """
  return try _compile(source)
}

/** Runs agent-generated Agency code in a child process.
Any interrupts and guards defined in the parent process will
apply to the child process. Any callbacks in scope will also apply.
Exceeding a resource limit kills the subprocess and returns a
limit_exceeded failure. Exceeding maxCost kills the subprocess and
returns a limit_exceeded failure, like the other limits.

For `maxDepth`, if an ancestor process has a lower maxDepth,
the lower value is used. For example, if a parent process has maxDepth=3
and a child process has maxDepth=5, maxDepth=3 is used.
 */
export def run(
  compiled: CompiledProgram,
  node: string,
  args: Record<string, any> = {},
  wallClock: number = 60s,
  memory: number = 512mb,
  ipcPayload: number = 100mb,
  stdout: number = 1mb,
  logFile: string = "",
  cwd: string = "",
  maxDepth: number = 5,
  maxCost: number | null = null,
): Result {
  """
  Execute a compiled Agency program in a subprocess and return the node's result.

  @param compiled - A compiled Agency program
  @param node - Which exported node to run
  @param args - Arguments to pass to the node
  @param wallClock - Max wall-clock time in MILLISECONDS before SIGKILL (default 60000 = 60s, max 1h). Pass null for the default.
  @param memory - Max V8 heap size in BYTES (default 536870912 = 512mb, max 4gb). Pass null for the default.
  @param ipcPayload - Max single IPC message size in BYTES (default 104857600 = 100mb, max 1gb). Pass null for the default.
  @param stdout - Max combined stdout+stderr output in BYTES (default 1048576 = 1mb, max 100mb). Pass null for the default.
  @param logFile - Optional statelog JSONL file path for this subprocess run
  @param cwd - Optional working directory for this subprocess run
  @param maxDepth - Max subprocess nesting depth (default 5, hard ceiling 10).
  @param maxCost - Max subprocess LLM spend in dollars (e.g. $0.50). null = no cost limit.
  """
  let configOverrides = null
  if (logFile != "") {
    configOverrides = {
      observability: true,
      log: {
        logFile: logFile
      }
    }
  }
  return interrupt std::run("Running agent-generated code in subprocess", {
    moduleId: compiled.moduleId,
    node: node,
    args: args,
    limits: {
      wallClock: wallClock,
      memory: memory,
      ipcPayload: ipcPayload,
      stdout: stdout,
      maxCost: maxCost
    },
    cwd: cwd,
    logFile: logFile,
    depth: _subprocessDepth() + 1
  })

  // guard() disables a dimension on a NEGATIVE value (null for BOTH
  // dimensions throws), so a null maxCost maps to the disable value and
  // one guarded call site serves both cases. Safe since #513: block
  // frames revive correctly across a nested subprocess pause.
  const NO_COST_CAP = -1.0
  const cap = if maxCost != null then maxCost else NO_COST_CAP
  const guarded = guard(cost: cap) {
    return try _run(
      compiled,
      node,
      args,
      wallClock,
      memory,
      ipcPayload,
      stdout,
      configOverrides,
      cwd,
      maxDepth,
    )
  }
  // guard() runs the block through a try-style call, which passes a
  // returned Result through unchanged — so `guarded` IS the child's
  // Result on success (no double wrapping), and a guardFailure only
  // when the cost guard itself tripped.
  if (guarded is failure(err)) {
    if (err.type == "guardFailure") {
      // Present cost caps in the same limit_exceeded shape as the other
      // run() limits (shape mirrors makeLimitFailure in lib/runtime/ipc.ts
      // — keep the two in sync).
      return failure(
        {
        reason: "limit_exceeded",
        limit: "cost",
        threshold: maxCost,
        value: err.actualCost,
        message: "Subprocess exceeded cost limit of ${maxCost} (used ${err.actualCost})"
      },
      )
    }
  }
  return guarded
}

/** Just like `run`, any interrupts and guards defined in the parent process
will apply to the child process. Any callbacks in scope will also apply.
Exceeding a resource limit kills the subprocess and returns a `limit_exceeded` failure.
*/
export def runFile(
  dir: string,
  filename: string,
  node: string,
  args: Record<string, any> = {},
  wallClock: number = 60s,
  memory: number = 512mb,
  ipcPayload: number = 100mb,
  stdout: number = 1mb,
  maxCost: number | null = null,
): Result {
  """
  Compile and execute an Agency file in a subprocess and return the node's result.
  Only standard-library (`std::`) imports are allowed in the file.

  @param dir - The directory containing the file
  @param filename - The agency file to compile and run
  @param node - Which node to run
  @param args - Arguments to pass to the node
  @param wallClock - Max wall-clock time in MILLISECONDS before SIGKILL (default 60000 = 60s, max 1h). Pass null for the default.
  @param memory - Max V8 heap size in BYTES (default 536870912 = 512mb, max 4gb). Pass null for the default.
  @param ipcPayload - Max single IPC message size in BYTES (default 104857600 = 100mb, max 1gb). Pass null for the default.
  @param stdout - Max combined stdout+stderr output in BYTES (default 1048576 = 1mb, max 100mb). Pass null for the default.
  @param maxCost - Max subprocess LLM spend in dollars (e.g. $0.50). null = no cost limit.
  """
  const compileResult = try _compileFile(dir, filename)
  if (isFailure(compileResult)) {
    return compileResult
  }
  return run(
    compiled: compileResult.value,
    node: node,
    args: args,
    wallClock: wallClock,
    memory: memory,
    ipcPayload: ipcPayload,
    stdout: stdout,
    maxCost: maxCost,
  )
}

// Reject out-of-range limit values with a message the model can act on.
// Ceilings mirror LIMIT_CEILINGS in lib/runtime/ipc.ts (keep in sync).
def checkRunCodeLimits(
  wallClock: number,
  memory: number,
  ipcPayload: number,
  stdout: number,
): string {
  const checks = [
    {
    name: "wallClock",
    value: wallClock,
    max: 3600s,
    unit: "milliseconds"
  },
    {
    name: "memory",
    value: memory,
    max: 4096mb,
    unit: "bytes"
  },
    {
    name: "ipcPayload",
    value: ipcPayload,
    max: 1024mb,
    unit: "bytes"
  },
    {
    name: "stdout",
    value: stdout,
    max: 100mb,
    unit: "bytes"
  },
  ]
  for (check in checks) {
    if (check.value <= 0) {
      return "${check.name} must be a positive number of ${check.unit} (got ${check.value})."
    }
    if (check.value > check.max) {
      return "${check.name} is too large: ${check.value} ${check.unit} exceeds the maximum of ${check.max} ${check.unit}."
    }
  }
  return ""
}

/** Just like `run`, any interrupts and guards defined in the parent process
will apply to the child process. Any callbacks in scope will also apply.
Exceeding a resource limit kills the subprocess and returns a `limit_exceeded` failure.

Designed for LLM tool use: compile() returns a CompiledProgram the model
would have to echo back into run() verbatim (it will not — see the
compile→run "CompiledProgram has no code" failure mode). runCode takes
the source directly, so nothing large round-trips through the model.

Left unmarked (neither destructive nor idempotent): it runs arbitrary
code whose danger depends on that code, so its failures reach a
tool-calling model as the neutral, re-callable tier. Each attempt
re-raises std::run and the child's own effects re-prompt.
*/
export def runCode(
  source: string,
  node: string = "main",
  args: Record<string, any> = {},
  wallClock: number = 60s,
  memory: number = 512mb,
  ipcPayload: number = 100mb,
  stdout: number = 1mb,
  maxCost: number | null = null,
  cwd: string = "",
): Result {
  """
  Compile Agency source code and execute one of its nodes in a subprocess,
  returning the value the node returned. Prefer this over separate
  compile() and run() calls. Only standard-library (`std::`) imports are
  allowed in the source. Compile errors are returned as a failure without
  running anything; fix the source and call again.

  @param source - Agency source code as a string
  @param node - Which exported node to run (default "main")
  @param args - Arguments to pass to the node
  @param wallClock - Max wall-clock time in MILLISECONDS before SIGKILL (default 60000 = 60s, max 3600000 = 1h). Pass null for the default.
  @param memory - Max V8 heap size in BYTES (default 536870912 = 512mb, max 4294967296 = 4gb). Pass null for the default.
  @param ipcPayload - Max single IPC message size in BYTES (default 104857600 = 100mb, max 1073741824 = 1gb). Pass null for the default.
  @param stdout - Max combined stdout+stderr output in BYTES (default 1048576 = 1mb, max 104857600 = 100mb). Pass null for the default.
  @param maxCost - Max subprocess LLM spend in dollars (e.g. 0.50). null = no cost limit.
  @param cwd - Working directory for the subprocess. Empty inherits the caller's process cwd (which may be the package dir, not where you want files); pass the agent working directory so the generated program's file writes land there.
  """
  const limitError = checkRunCodeLimits(wallClock, memory, ipcPayload, stdout)
  if (limitError != "") {
    return failure(limitError)
  }
  const compileResult = try _compile(source)
  if (compileResult is failure(compileErr)) {
    return compileResult
  }
  const result = run(
    compiled: compileResult.value,
    node: node,
    args: args,
    wallClock: wallClock,
    memory: memory,
    ipcPayload: ipcPayload,
    stdout: stdout,
    maxCost: maxCost,
    cwd: cwd,
  )
  // run() succeeds with a subprocess envelope ({ data, tokens, ... }).
  // runCode is built for LLM tool use, where the envelope reads as noise
  // (token stats first, the actual result buried in .data) — return just
  // the node's value. Callers who want the envelope use run().
  if (result is success(envelope)) {
    return success(envelope.data)
  }
  return result
}

/** Unlike some of the other functions in this module,
`typecheck` does not restrict imports to the standard library only.
Relative imports (./foo.agency) cannot be resolved from a source string.
*/
export idempotent def typecheck(source: string): Result<TypeCheckReport> {
  """
  Type-check Agency source code.

  @param source - Agency source code as a string
  """
  return try _typecheck(source)
}

/** Per-exported-symbol effect lists, keyed by node/function name. */
export type EffectsByExport = Record<string, string[]>

export idempotent def getEffects(source: string): Result<EffectsByExport> {
  """
  Map each exported node and function in the source to the list of
  interrupt effects it can raise, transitively. Bare `interrupt(...)`
  sites appear as the sentinel "unknown", so the envelope never
  silently under-reports. Use this to show or check what a program can
  do before running it.

  Not visible to this: code generated by a compile-time splice, a
  function received as a parameter and then called, and a function
  reference stored in a variable before being passed on. An empty list
  means nothing risky was found, not that nothing risky exists.

  @param source - Agency source code as a string
  """
  return try _getEffects(source)
}

/** What describe reports for one exported symbol. `signature` is the
printed declaration line without the export keyword or tool markers;
`docstring` is the symbol's docstring (defs and nodes) or doc comment
(types); `effects` uses the same names and "unknown" sentinel as
getEffects, and is empty for types and consts. Re-exported symbols carry
the module path they came through in `reexportedFrom` (the outermost hop
when re-exports chain); when that module cannot be read from a source
string (relative paths), the entry has kind "reexport" and its effects
are ["unknown"] rather than silently missing. */
export type ExportInfo = {
  name: string,
  kind: "def" | "node" | "type" | "const" | "reexport",
  signature: string,
  docstring: string | null,
  effects: string[],
  destructive: boolean,
  idempotent: boolean,
  reexportedFrom: string | null
}

export type ModuleInfo = {
  description: string | null,
  exports: ExportInfo[]
}

/** The reify primitive: exports-as-data, so generators can be shaped by
what a module contains instead of hand-maintained lists. Underscore-prefixed
exports are omitted, matching the `agency doc` rule — they are lowering
targets, not caller surface. `description` is the module doc comment's
one-line summary, extracted the same way `agency doc` extracts it. */
export idempotent def describe(source: string): Result<ModuleInfo> {
  """
  Describe what an Agency module exports: each exported function, node, type, const, and re-export, in source order, with its signature, docstring, transitive effect list, and destructive/idempotent markers. Re-exports from std:: modules resolve to full entries; re-exports from relative paths cannot be read from a source string and come back with effects ["unknown"]. Use this instead of reading source when generating code shaped by a module - for example, one handler per effect its tools can raise.

  @param source - Agency source code as a string
  """
  return try _describe(source)
}


type FilterImportsResult = {
  source: string;
  filtered: boolean
}

export idempotent def typecheckFile(dir: string, filename: string): Result {
  """
  Type-check an Agency file on disk. The file is read from dir/filename,
  with relative imports inside it resolved against the file's directory.

  @param dir - The directory containing the file
  @param filename - The agency file to type-check
  """
  return interrupt std::read("Are you sure you want to read this file?", {
    dir: dir,
    filename: filename
  })
  return try _typecheckFile(dir, filename)
}

/** A parsed Agency program, the value `parseAST` returns on success.
  `type` is always "agencyProgram". `nodes` holds the top-level
  declarations (imports, functions, graph nodes, type aliases, ...). Each
  is an object with a `type` discriminant field. Its remaining fields
  vary by node type, so nodes stay untyped. */
export type AST = {
  type: "agencyProgram";
  nodes: any[];
  docComment?: any
}

export idempotent def parseAST(source: string): Result<AST> {
  """
  Parse Agency source code into an abstract syntax tree.

  @param source - Agency source code as a string
  """
  return try _parseAST(source)
}

/** Output is canonical formatter output (the same style as `pnpm run fmt`):
  the formatter preserves comments (they live in the AST as nodes) but
  normalizes whitespace and formatting. */
export def writeAST(
  ast: AST,
  dir: string,
  filename: string,
  overwrite: boolean = true,
): Result {
  """
  Format an AST as Agency source and write it to dir/filename. Absolute paths and .. segments cannot escape dir. Symlinks on existing files are followed and re-checked.

  @param ast - The AST to write (typically a parsed Agency AST)
  @param dir - The sandbox directory
  @param filename - The agency file to write, resolved relative to dir
  @param overwrite - If false, fail when the file already exists (default true)
  """
  return interrupt std::write("Are you sure you want to write this file?", {
    dir: dir,
    filename: filename,
    overwrite: overwrite
  })
  return try _writeAST(ast, dir, filename, overwrite)
}

export idempotent def format(source: string): Result {
  """
  Format Agency source code with the standard Agency formatter.

  @param source - Agency source code as a string
  """
  return try _format(source)
}

/** `Code` is `AST` plus a fragment kind: a value can hold a whole program
  (what `loadTemplate` and `parseAST` produce), a statement list, or a
  single expression. The kind is what lets an expression-sized fragment
  fill an expression hole. */
export type Code = {
  type: "agencyProgram";
  kind?: "program" | "statements" | "expr";
  nodes: any[];
  docComment?: any
}

export type HoleInfo = {
  name: string,
  sort: "expr" | "statements" | "identifier" | "decl",
  splice: boolean,
  type: string | null,
  origin: string | null
}

export def loadTemplate(dir: string, filename: string): Result<Code> {
  """
  Load an Agency file containing holes as a template.

  @param dir - The sandbox directory
  @param filename - The template file, resolved relative to dir
  """
  return interrupt std::read("Can I read this template?", {
    dir: dir,
    filename: filename
  })
  return try _loadTemplate(dir, filename)
}

export idempotent def holesOf(template: Code): HoleInfo[] {
  """
  The unfilled holes in a template, in the order they appear. Each entry has the hole's name, its sort (what category of thing fills it), whether it is a splice, and its type when one is known. origin names the fill this hole most recently arrived through when it came in via a grafted fragment (best-effort; null for holes written directly in the template).

  @param template - A template loaded with loadTemplate
  """
  return _holesOf(template)
}

export idempotent def fill(template: Code, values: Record<string, Json | Code>): Result<Code> {
  """
  Fill holes in a template. Plain values become literals and are never parsed; Code values are grafted as trees. Filling some holes and not others returns a template with the rest still in it.

  @param template - A template loaded with loadTemplate
  @param values - A record mapping hole names to values
  """
  return try _fill(template, values)
}

export idempotent def combine(codes: Code[]): Result<Code> {
  """
  Merge several Code fragments into one, in order. Use this to build one
  fragment from a loop, for example one function per item in a list.
  Fragments of the same kind merge into that kind. Expressions merge into
  a statement list. A whole-program fragment cannot merge with loose
  statements or expressions.

  @param codes - The fragments to merge, in order
  """
  return try _combine(codes)
}

export idempotent def toSource(code: Code): string {
  """
  Print a Code value back to Agency source, including any unfilled holes.

  @param code - A template or filled program
  """
  return _toSource(code)
}

export idempotent def parseExpr(source: string): Result<Code> {
  """
  Parse a single Agency expression into a Code fragment that can fill an expr hole. Fails on anything other than exactly one expression.

  @param source - Agency source for one expression
  """
  return try _parseExpr(source)
}

export idempotent def parseStatements(source: string): Result<Code> {
  """
  Parse a list of Agency statements into a Code fragment that can fill a statements hole.

  @param source - Agency source for one or more statements
  """
  return try _parseStatements(source)
}

/** Read and write happen inside the same interrupt, so approving it approves both.
  If the file is already formatted, no write occurs and its mtime is preserved. */
export idempotent def formatFile(dir: string, filename: string): Result {
  """
  Format an Agency file in place using the standard Agency formatter.

  @param dir - The directory containing the file
  @param filename - The agency file to format
  """
  return interrupt std::write("Are you sure you want to format this file in place?", {
    dir: dir,
    filename: filename
  })
  return try _formatFile(dir, filename)
}

/**
- Iteration is pre-order (a node is visited before its children)
- The visit list is fixed upfront: nodes the visitor adds during
  the walk are not visited. Replacing a child reference still visits the old subtree.
*/
export def walkAST(
  ast: AST,
  visitor: (node: any, ancestors: any[]) -> any,
): AST {
  """
  Walk every node in a deep-cloned copy of the AST, invoking the visitor
  with each (node, ancestors) pair, and return the modified clone.
  The visitor may mutate nodes in place. This will not modify the original tree.
  The ancestors array lists every enclosing node from the root outward, excluding the node itself.

  @param ast - The AST to walk
  @param visitor - Called once per node as visitor(node, ancestors). Mutate node in place, return value is ignored.
  """
  const result = _walkAST(ast)
  for (visit in result.visits) {
    visitor(visit.node, visit.ancestors)
  }
  return result.clone
}

export idempotent def getNodesOfType(
  source: string,
  types: string[],
): Result<any[]> {
  """
  Parse Agency source code and return every AST node whose `type` field matches any of the provided types.

  @param source - Agency source code
  @param types - List of AST type strings to match (e.g. ["function", "graphNode"])
  """
  return try _getNodesOfType(source, types)
}

export idempotent def getImports(source: string): Result<any[]> {
  """
  Return every import statement in the source (i.e. `import { x } from "..."`).

  @param source - Agency source code
  """
  return getNodesOfType(source, ["importStatement"])
}

export idempotent def getFunctions(source: string): Result<any[]> {
  """
  Return every function definition (`def foo(...) { ... }`) in the source.

  @param source - Agency source code
  """
  return getNodesOfType(source, ["function"])
}

export idempotent def getGraphNodes(source: string): Result<any[]> {
  """
  Return every graph node definition (`node main() { ... }`) in the source.

  @param source - Agency source code
  """
  return getNodesOfType(source, ["graphNode"])
}

/**
  Parse Agency source, drop imports that fail the policy, and return the resulting source plus a flag indicating whether anything was dropped.

  Imports are classified by `kind`:
  - "stdlib" — `std::*` (e.g. `std::shell`)
  - "pkg"    — `pkg::*` (e.g. `pkg::wikipedia`)
  - "local"  — relative or absolute file paths (e.g. `./util.agency`)
  - "node"   — bare specifiers resolved by Node (e.g. `fs`, `child_process`)

  Policy:
  - `allowedPackages` / `excludedPackages` are glob patterns (picomatch syntax) matched against the raw import path string.
  - `allowKinds` / `excludeKinds` accept the kind strings above.
  - Exclude rules always win: if a path matches anything in `excludedPackages` or `excludeKinds`, it is dropped.
  - When all four lists are empty, every import is allowed (default-allow).
  - When at least one allow list is non-empty, an import must match an allowed kind OR an allowed package glob (union across the two axes). Note that allowKinds=["stdlib"] is still a restriction even with the package lists empty, only stdlib passes.

  We format the source with the Agency formatter before returning it.
*/
export idempotent def filterImports(
  source: string,
  allowedPackages: string[] = [],
  excludedPackages: string[] = [],
  allowKinds: string[] = [],
  excludeKinds: string[] = [],
): Result<{ source: string; filtered: boolean }> {
  """
  Filter imports in Agency source code according to the given policy.
  Returns the filtered source and a boolean indicating whether any imports were dropped.

  @param source - Agency source code
  @param allowedPackages - Glob patterns; matched imports are allowed (subject to excludes)
  @param excludedPackages - Glob patterns; matched imports are dropped
  @param allowKinds - Kind strings ("stdlib" | "pkg" | "local" | "node") to allow
  @param excludeKinds - Kind strings to drop
  """
  return try _filterImports(
    source,
    allowedPackages,
    excludedPackages,
    allowKinds,
    excludeKinds,
  )
}

export idempotent def getVersion(): string {
  """
  Get the current version of the Agency standard library.
  """
  return VERSION
}
