import { _callback } from "agency-lang/stdlib-lib/callback.js"
import {
  _print,
  _printJSON,
  _read,
  _write,
  _writeBinary,
  _readBinary,
  _range,
  _input,
  _sleep,
  _pairsOf as _pairsOfImpl,
 } from "agency-lang/stdlib-lib/builtins.js"
import { _resolve } from "agency-lang/stdlib-lib/path.js"
import { _prepareContainedPath } from "agency-lang/stdlib-lib/fs.js"
import { _formatDuration } from "agency-lang/stdlib-lib/date.js"
import {
  syntaxHighlight as _syntaxHighlight,
 } from "agency-lang/stdlib-lib/syntax.js"
import {
  _saveDraft,
  _pushGuard,
  _runGuarded,
  _popGuard,
 } from "agency-lang/stdlib-lib/thread.js"

/** @module
@summary Printing, input, file access, and collection helpers available without imports.
The always-available prelude: printing, input, file I/O, and the array
helpers. Every `.agency` file auto-imports these, so you can call them
without an import.

```ts
const names = ["Ada", "Grace"]
print(names)
print(range(3))
```
*/

// Payload types for the interrupt effects raised by this module.
// `std::read`/`std::write` are shared file-access effects, also raised by
// `std::agency`. Their canonical payload is `{ dir, filename }`, the file
// identity a policy keys on. Individual raise sites may add per-call extras
// like content, mode, and offset.
@alwaysUnder(dir)
effect std::read {
  dir: string;
  filename: string
}
@alwaysUnder(dir)
effect std::write {
  dir: string;
  filename: string
}
@alwaysUnder(dir)
effect std::readImage {
  dir: string;
  filename: string
}

@alwaysUnder(dir)
effect std::readBinary {
  dir: string;
  filename: string
}

@alwaysUnder(dir)
effect std::writeBinary {
  dir: string;
  filename: string;
  mode: WriteMode
}

/** How an existing file is handled on write:
  "overwrite" replaces it, "append" adds to it, "create-only" fails if it
  already exists. */
export type WriteMode = "overwrite" | "append" | "create-only"

/** What a decorator learns about the function it wraps. The `name` is
  the definition's original name. Its `args` hold the parameters by name,
  with block parameters left out. The `id` is the function's opaque runtime
  identity, shared by partial, renamed, and preapproved copies. It is not
  a source path or an identifier guaranteed to stay the same across builds.
  The `inToolCall` flag records whether the invocation began inside a
  tool call made by `llm()` in this run, including calls from that tool's
  body or branches. Subprocess and MCP calls do not inherit this context.
  Use these fields for reporting. Direct callers and edited checkpoints
  can supply their values, so do not use them to grant permissions or skip
  approvals. See the Decorators guide page. */
export type Call = { name: string, args: object, id: string, inToolCall: boolean }

@capabilities(terminal)
export def print(...messages: any[]) {
  """
  Print a message to the console.
  """
  _print(...messages)
  return "Message printed."
}

// The agent's working directory: a branch-scoped override that
// path-taking stdlib tools resolve relative paths against. Empty string
// means "unset", so tools keep their default base. Distinct from `cwd()`
// (the run host's working directory) on purpose: an agent sets this to point the file
// and shell tools at the user's directory without changing the process.
// Lives here in `std::index` (not `std::system`) because `index` is the
// auto-imported barrel: every stdlib module already imports it, so the
// path wrappers can reach `applyAgentCwd` without an explicit import that
// would create a circular dependency back into `index`.
let _agentCwd = ""

/**
  Set the agent working directory. Path-taking tools (read, write, edit,
  ls, glob, grep, exec, bash, ...) can resolve relative paths against
  the agent working directory if you pass in `useAgentCwd: true` to them.

  This is useful if you're building a coding agent,
  to set the current working directory for the agent.
*/
export def setAgentCwd(dir: string) {
  """
  Set the base for relative paths in tools called with useAgentCwd: true.

  @param dir - The absolute directory to use as the agent working directory
  """
  _agentCwd = dir
}

export def getAgentCwd(): string {
  """
  Return the agent working directory, or an empty string if none is set.
  """
  return _agentCwd
}

/**
File and shell tools call this when their `useAgentCwd` option is true.
*/
export def applyAgentCwd(dir: string): string {
  """
  Resolve a relative path against the agent working directory. Absolute paths
  and paths with no agent working directory set pass through unchanged.
  """
  const base = getAgentCwd()
  if (base == "") {
    return dir
  }
  return _resolve([base, dir])
}

@capabilities(terminal)
export def printJSON(obj: any, highlight: boolean = false) {
  """
  Print an object as formatted JSON to the console.

  @param highlight - Whether to syntax-highlight the output
  """
  if (highlight) {
    _print(_syntaxHighlight(JSON.stringify(obj, null, 2), "json"))
  } else {
    _print(JSON.stringify(obj, null, 2))
  }
  return "JSON printed."
}

/** Ctrl-C, race-loser, or time-guard abort releases a blocked input
prompt, which surfaces as an AgencyCancelledError. */
@capabilities(terminal)
export def input(prompt: string): string {
  """
  Prompt the user for input and return their response.
  """
  return _input(prompt)
}

/** Use unit literals for clarity: sleep(1s), sleep(500ms), sleep(2m).
A long sleep wakes immediately on Ctrl-C, race-loser, or time-guard abort. */
export def sleep(ms: number) {
  """
  Pause execution for the given duration in milliseconds.
  """
  _sleep(ms)
}

/**
Use this to retain a partial answer when a guard stops unfinished work.
The last saved value wins and must match the enclosing scope's return type.
Drafts belong to their function, node, or guard block; a caller can pass a
callee's draft onward with `return callee()`, or save a draft of its own.
Calling at module scope is an error. Without a guard, saving a draft does
not affect the normal return value.
*/
export def saveDraft(value: any) {
  """
  Save the current scope's best answer so far for recovery if a guard stops it.
  Call again as the answer improves.

  @param value - A partial result matching the enclosing scope's return type
  """
  _saveDraft(value)
}

@capabilities(fileRead)
export idempotent def read(
  filename: string,
  dir: string = ".",
  offset: number = 0,
  limit: number = 0,
  useAgentCwd: boolean = false,
): Result raises <std::read> {
  """
  Read the contents of a file and return it as a string.

  @param filename - Relative path inside dir; absolute paths, ~, escapes, and symlink components are refused. To reach another directory, pass it as dir.
  @param dir - Base directory for filename
  @param offset - 1-indexed line to start at (0 means start of file)
  @param limit - Maximum number of lines to return (0 means read to end of file)
  @param useAgentCwd - Resolve a relative dir against the agent working directory
  """
  if (useAgentCwd) {
    dir = applyAgentCwd(dir)
  }
  const prepared = try _prepareContainedPath(dir, filename, "read")
  if (isFailure(prepared)) {
    return prepared
  }
  return interrupt std::read("Are you sure you want to read this file?", {
    dir: prepared.value.dir,
    filename: prepared.value.filename,
    offset: offset,
    limit: limit
  })
  return try _read(prepared.value.dir, prepared.value.filename, offset, limit)
}

@capabilities(fileRead, fileWrite)
export def write(
  filename: string,
  content: string,
  dir: string = ".",
  mode: WriteMode = "overwrite",
  useAgentCwd: boolean = false,
): Result raises <std::write> {
  """
  Write content to a file.

  @param filename - Relative path inside dir; absolute paths, ~, escapes, and symlink components are refused. To reach another directory, pass it as dir.
  @param dir - Base directory for filename
  @param mode - How to handle an existing file
  @param useAgentCwd - Resolve a relative dir against the agent working directory
  """
  if (useAgentCwd) {
    dir = applyAgentCwd(dir)
  }
  const prepared = try _prepareContainedPath(dir, filename, "write")
  if (isFailure(prepared)) {
    return prepared
  }
  return interrupt std::write("Are you sure you want to write to this file?", {
    dir: prepared.value.dir,
    filename: prepared.value.filename,
    content: content,
    mode: mode
  })
  destructive {
    return try _write(prepared.value.dir, prepared.value.filename, content, mode)
  }
}

@capabilities(fileRead, fileWrite)
export def writeBinary(
  filename: string,
  base64: string,
  dir: string = ".",
  mode: WriteMode = "overwrite",
  useAgentCwd: boolean = false,
): Result raises <std::writeBinary> {
  """
  Decode base64 data and write the bytes to a file.

  @param filename - Relative path inside dir; absolute paths, ~, escapes, and symlink components are refused. To reach another directory, pass it as dir.
  @param dir - Base directory for filename
  @param mode - How to handle an existing file
  @param useAgentCwd - Resolve a relative dir against the agent working directory
  """
  if (useAgentCwd) {
    dir = applyAgentCwd(dir)
  }
  const prepared = try _prepareContainedPath(dir, filename, "write")
  if (isFailure(prepared)) {
    return prepared
  }
  return interrupt std::writeBinary("Are you sure you want to write this file?", {
    dir: prepared.value.dir,
    filename: prepared.value.filename,
    mode: mode
  })
  destructive {
    return try _writeBinary(prepared.value.dir, prepared.value.filename, base64, mode)
  }
}

@capabilities(fileRead)
export idempotent def readBinary(
  filename: string,
  dir: string = ".",
  useAgentCwd: boolean = false,
): Result raises <std::readBinary> {
  """
  Read a binary file as a base64-encoded string.

  @param filename - Relative path inside dir; absolute paths, ~, escapes, and symlink components are refused. To reach another directory, pass it as dir.
  @param dir - Base directory for filename
  @param useAgentCwd - Resolve a relative dir against the agent working directory
  """
  if (useAgentCwd) {
    dir = applyAgentCwd(dir)
  }
  const prepared = try _prepareContainedPath(dir, filename, "read")
  if (isFailure(prepared)) {
    return prepared
  }
  return interrupt std::readBinary("Are you sure you want to read this file?", {
    dir: prepared.value.dir,
    filename: prepared.value.filename
  })
  return try _readBinary(prepared.value.dir, prepared.value.filename)
}

export def range(start: number, end: number = -1): number[] {
  """
  Generate an array of numbers. With one argument, counts from 0 to start-1;
  with two, from start to end-1.

  @param start - The count with one argument, or the starting number with two
  @param end - The exclusive end number (omit to count up from 0)
  """
  if (end == -1) {
    return _range(start)
  }
  return _range(start, end)
}

// --- Array helpers (auto-imported) ---------------------------------------
// These helpers are auto-imported through std::index. std::array
// re-exports them for callers using explicit imports.

export def map(arr: any[], func: (any) -> any): any[] {
  """
  Map a function over an array, returning a new array of results.

  @param func - Called with each element
  """
  const newArr = []
  for (item in arr) {
    newArr.push(func(item))
  }
  return newArr
}

export def _pairsOf(src: any): any[] {
  """
  Internal: the lowering target for two-binder list comprehensions - use
  a comprehension, not this function. Returns pairs in binder order,
  matching the `for` loop: arrays give [item, index], objects give
  [key, value], and anything else gives an empty array.

  @param src - An array, an object, or anything else
  """
  return _pairsOfImpl(src)
}

export def mapWithIndex(arr: any[], func: (any, any) -> any): any[] {
  """
  Apply a function to each item of an array along with its index, and
  return a new array of the results.

  @param func - Called with the item and its zero-based index
  """
  return map(_pairsOf(arr)) as pair { return func(pair[0], pair[1]) }
}

export def filter(arr: any[], func: (any) -> any): any[] {
  """
  Return a new array containing only the elements for which the function returns true.

  @param func - Called with each element; return true to keep it
  """
  const result = []
  for (item in arr) {
    if (func(item)) {
      result.push(item)
    }
  }
  return result
}

export def exclude(arr: any[], func: (any) -> any): any[] {
  """
  Return a new array excluding elements for which the function returns true.

  @param func - Called with each element; return true to exclude it
  """
  const result = []
  for (item in arr) {
    if (func(item) == false) {
      result.push(item)
    }
  }
  return result
}

export def find(arr: any[], func: (any) -> any): any {
  """
  Return the first element for which the function returns true, or null if none match.

  @param func - Called with each element
  """
  for (item in arr) {
    if (func(item)) {
      return item
    }
  }
  return null
}

export def findIndex(arr: any[], func: (any) -> any): number {
  """
  Return the index of the first element for which the function returns true, or -1 if none match.

  @param func - Called with each element
  """
  for (item, index in arr) {
    if (func(item)) {
      return index
    }
  }
  return -1
}

export def reduce(arr: any[], initial: any, func: (any, any) -> any): any {
  """
  Reduce an array to a single value by applying a function to an accumulator and each element.

  @param initial - The initial accumulator value
  @param func - The reducer function receiving (accumulator, element)
  """
  let acc = initial
  for (item in arr) {
    acc = func(acc, item)
  }
  return acc
}

export def flatMap(arr: any[], func: (any) -> any): any[] {
  """
  Map a function over an array and flatten the results by one level.

  @param func - Called with each element
  """
  const result = []
  for (item in arr) {
    const mapped = func(item)
    for (sub in mapped) {
      result.push(sub)
    }
  }
  return result
}

export def flatten(arr: any[]): any[] {
  """
  Flatten an array of arrays by one level.
  """
  const result = []
  for (item in arr) {
    for (sub in item) {
      result.push(sub)
    }
  }
  return result
}

export def every(arr: any[], func: (any) -> any): boolean {
  """
  Return true if the function returns true for every element in the array.

  @param func - Called with each element
  """
  for (item in arr) {
    if (func(item) == false) {
      return false
    }
  }
  return true
}

export def some(arr: any[], func: (any) -> any): boolean {
  """
  Return true if the function returns true for at least one element in the array.

  @param func - Called with each element
  """
  for (item in arr) {
    if (func(item)) {
      return true
    }
  }
  return false
}

export def count(arr: any[], func: (any) -> any): number {
  """
  Count the number of elements in the array for which the function returns true.

  @param func - Called with each element
  """
  let n = 0
  for (item in arr) {
    if (func(item)) {
      n = n + 1
    }
  }
  return n
}

export def sortBy(arr: any[], func: (any) -> any): any[] {
  """
  Return a new array sorted by the values returned by the function, in ascending order.

  @param func - Returns the sort key for each element
  """
  const result = []
  for (item in arr) {
    result.push(item)
  }
  let i = 0
  while (i < result.length) {
    let j = i + 1
    while (j < result.length) {
      if (func(result[j]) < func(result[i])) {
        const temp = result[i]
        result[i] = result[j]
        result[j] = temp
      }
      j = j + 1
    }
    i = i + 1
  }
  return result
}

export def unique(arr: any[], func: (any) -> any): any[] {
  """
  Return a new array with duplicate elements removed, using the function to determine the identity of each element.

  @param func - Returns the identity key for each element
  """
  const result = []
  const seen = []
  for (item in arr) {
    const key = func(item)
    let found = false
    for (s in seen) {
      if (s == key) {
        found = true
      }
    }
    if (found == false) {
      result.push(item)
      seen.push(key)
    }
  }
  return result
}

export def groupBy(arr: any[], func: (any) -> any): any {
  """
  Group elements of an array by the value returned by the function. Returns an object where keys are group names and values are arrays of elements.

  @param func - Returns the group key for each element
  """
  // Null-prototype object so a user-supplied key like "__proto__" or
  // "constructor" is stored as a plain own property instead of hitting
  // the Object prototype (which would otherwise crash `.push` or mutate
  // shared state).
  const groups = Object.create(null)
  for (item in arr) {
    const key = func(item)
    groups[key] ??= []
    groups[key].push(item)
  }
  return groups
}

export def callback(name: string, fn: any) {
  """
  Register a callback for a lifecycle event. A callback registered inside a
  function or node is removed when that returns. One registered at the top
  level stays active for the whole run.

  @param name - The callback hook name, e.g. "onNodeStart", "onFunctionStart", "onLLMCallEnd"
  @param fn - A function that receives the event data
  """
  _callback(name, fn)
}

// Block has `= null` so the parser accepts trailing optional params;
// the desugared construct call always supplies it.
/*
Time semantics are compute-time: the clock only ticks while a Runner is
actively executing inside the guarded scope. Time spent paused on an
interrupt (e.g. waiting for user input) does not count. On resume the
timer is re-armed with the remaining budget.

Nested guards are independent. An inner trip does not trip an outer
guard. Across fork/race branches, cost guards are SHARED (every branch
charges the same counter in real time), while time guards are cloned
per branch: each branch gets the parent's remaining budget as its own
countdown, a branch's input-wait pauses only its own clock, and the
parent's clock advances by the longest branch's working time at the
join. `thread`/`subthread` isolate message history but not cost or
abort plumbing, so a guard sees every LLM call inside them.

Limitations: a tool whose body is a JS function (not Agency code) cannot
be aborted mid-execution. It runs to completion in the background, and
its result is discarded.
*/
export def _guard(
  cost: number | null = null,
  time: number | null = null,
  label: string | null = null,
  block: () -> any = null,
): Result raises <std::guard> {
  """
  Internal: the lowering target of the `guard(...) { }` construct — use
  the construct, not this function. Runs `block` under the given cost
  and/or time limits and returns a `Result` (see the guards guide).
  """
  const ids = _pushGuard(cost, time, label)
  const result = _runGuarded(ids, block)
  _popGuard(ids)
  return result
}

// The message for a budget stop: "its time budget of 30m ran out".
def _describeTrip(trip: any): string {
  if (trip.dimension == "time") {
    return "its time budget of ${_formatDuration(trip.limit)} ran out"
  }
  return "its cost budget of $${trip.limit} ran out"
}

// Append the budget note to a string answer. Any other value, such as a
// Feedback list or a record, cannot carry a sentence and is returned as is.
def _withBudgetNote(value: any, name: string, trip: any): any {
  if (!(value is string)) {
    return value
  }
  return "${value}\n\n[${name}: the ${trip.dimension} budget ran out, so this answer may be unfinished or unchecked. It is the best answer so far.]"
}

/* Internal: what a budgeted agent call reports, given the Result its
  guard returned and the guard's trip interrupt data (`null` when the guard
  did not trip). The rule is written once here, for the `@guarded`
  decorator in `std::decorators`:

  1. No trip: the guard's Result, except that a finished `""` is a failure,
     "it returned no answer".
  2. A trip, and a value reached the guard: that value as a success. It
     is the best answer the agent had, whether a finished answer or a
     draft it saved along the way. A string answer gets one line saying
     the budget ran out, so it may be unfinished or unchecked.
  3. A trip, and nothing reached the guard: a failure naming the budget
     ("its time budget of 30m ran out"), with `{ dimension, limit, draft }`
     in its data. `draft` is the trip's `draftValue`: the innermost draft
     saved anywhere on the stack when the guard tripped, of whatever type
     the frame that saved it returns, or `null`.

  With `keepDraft` false, rule 2 does not apply: every trip is a failure,
  and `draft` is the value that reached the guard when one did. That is for
  a caller that must not take a partial result for a finished one, such as
  a review cut short, whose findings cannot be told from a clean review's.

  See docs/dev/agents/subagent-budgets.md. */
export def _budgetOutcome(
  name: string,
  guarded: Result,
  trip: any,
  keepDraft: boolean = true,
): Result {
  """
  Internal: turn a budgeted agent's guard Result and trip into the Result
  its caller reports. Use the @guarded decorator, not this function.
  """
  if (trip == null) {
    if (isSuccess(guarded) && guarded.value == "") {
      return failure("it returned no answer")
    }
    return guarded
  }
  const reached = isSuccess(guarded) && guarded.value != "" && guarded.value != null
  if (reached && keepDraft) {
    return success(_withBudgetNote(guarded.value, name, trip))
  }
  let draft = trip.draftValue
  if (reached) {
    draft = guarded.value
  }
  // Built from the trip, never from `guarded`: a guard whose finalize
  // returned an empty local hands back success(null), which has no error.
  return failure(_describeTrip(trip), {
    dimension: trip.dimension,
    limit: trip.limit,
    draft: draft
  })
}
