/** @module
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.
*/
import { _callback } from "agency-lang/stdlib-lib/agency.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 {
  syntaxHighlight as _syntaxHighlight,
 } from "agency-lang/stdlib-lib/syntax.js"
import {
  _saveDraft,
  _pushGuard,
  _runGuarded,
  _popGuard,
 } from "agency-lang/stdlib-lib/thread.js"

// 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.
effect std::read {
  dir: string;
  filename: string
}
effect std::write {
  dir: string;
  filename: string
}
effect std::readImage {
  dir: string;
  filename: string
}

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

export def print(...messages: any[]) {
  """
  Print a message to the console.

  @param messages - The values to print
  """
  _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 OS process cwd) 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 working directory that path-taking tools resolve relative paths against.

  @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
}

export def applyAgentCwd(dir: string): string {
  """Resolve a relative path against the agent working directory (set with
  setAgentCwd). Absolute paths and an unset working directory pass through
  unchanged. read, write, and the other file tools already call this."""
  const base = getAgentCwd()
  if (base == "") {
    return dir
  }
  return _resolve([base, dir])
}

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

  @param obj - The object to print
  @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. */
export def input(prompt: string): string {
  """
  Prompt the user for input and return their response.

  @param prompt - The message to show the user
  """
  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.

  @param ms - The number of milliseconds to pause
  """
  _sleep(ms)
}

export def saveDraft(value: any) {
  """
  Record a best-so-far value for the current function or guarded block. If an
  enclosing `guard(...)` trips before this scope returns, the guard yields the
  last saved draft instead of a failure — an "anytime" result you can always
  fall back to. Call it repeatedly as your result improves; the last value
  wins. With no enclosing guard it is a harmless no-op. Calling it at module
  top level is an error: there is no enclosing scope to save a draft for.

  The value is type-checked against the enclosing scope's return type — a
  function/node body, or a `guard` block (whose return type is inferred from its
  `return`).

  @param value - The best-so-far value. Should match the enclosing scope's return type.
  """
  _saveDraft(value)
}

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

  @param filename - The file to read
  @param dir - The directory to resolve the filename against (defaults to ".")
  @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 relative paths against the agent working directory instead of dir
  """
  if (useAgentCwd) {
    dir = applyAgentCwd(dir)
  }
  return interrupt std::read("Are you sure you want to read this file?", {
    dir: dir,
    filename: filename,
    offset: offset,
    limit: limit
  })
  return try _read(dir, filename, offset, limit)
}

export def write(
  filename: string,
  content: string,
  dir: string = ".",
  mode: WriteMode = "overwrite",
  useAgentCwd: boolean = false,
): Result {
  """
  Write content to a file.

  @param filename - The file to write
  @param content - The content to write
  @param dir - The directory to resolve the filename against (defaults to ".")
  @param mode - How to handle an existing file
  @param useAgentCwd - Resolve relative paths against the agent working directory instead of dir
  """
  if (useAgentCwd) {
    dir = applyAgentCwd(dir)
  }
  return interrupt std::write("Are you sure you want to write to this file?", {
    dir: dir,
    filename: filename,
    content: content,
    mode: mode
  })
  destructive {
    return try _write(dir, filename, content, mode)
  }
}

export def writeBinary(
  filename: string,
  base64: string,
  dir: string = ".",
  mode: WriteMode = "overwrite",
  useAgentCwd: boolean = false,
): Result {
  """
  Write base64-encoded binary data to a file: images, audio, video, PDFs, or any
  binary. Decodes the base64 and writes raw bytes rather than UTF-8 text.

  @param filename - The file to write
  @param base64 - The binary content, base64-encoded
  @param dir - The directory to resolve the filename against (defaults to ".")
  @param mode - How to handle an existing file
  @param useAgentCwd - Resolve relative paths against the agent working directory instead of dir
  """
  if (useAgentCwd) {
    dir = applyAgentCwd(dir)
  }
  return interrupt std::writeBinary("Are you sure you want to write this file?", {
    dir: dir,
    filename: filename,
    mode: mode
  })
  destructive {
    return try _writeBinary(dir, filename, base64, mode)
  }
}

export idempotent def readBinary(
  filename: string,
  dir: string = ".",
  useAgentCwd: boolean = false,
): Result {
  """
  Read a file and return its contents as a Base64-encoded string. Works for any
  binary file: images, audio, video, PDFs.

  @param filename - The file to read
  @param dir - The directory to resolve the filename against (defaults to ".")
  @param useAgentCwd - Resolve relative paths against the agent working directory instead of dir
  """
  if (useAgentCwd) {
    dir = applyAgentCwd(dir)
  }
  return interrupt std::readBinary("Are you sure you want to read this file?", {
    dir: dir,
    filename: filename
  })
  return try _readBinary(dir, 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 collection primitives live in `std::index` so they ride the single
// auto-import prelude line into every file. Agency has no `.map()` method
// syntax, so these free functions are the only way to operate on arrays,
// which is why they belong in the always-available set. `std::array`
// re-exports them for backward compatibility.

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

  @param arr - The array to map over
  @param func - The mapping function
  """
  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 arr - The array to map over
  @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 arr - The array to filter
  @param func - The filter function
  """
  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 arr - The array to filter
  @param func - The exclusion predicate
  """
  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 arr - The array to search
  @param func - The predicate function
  """
  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 arr - The array to search
  @param func - The predicate function
  """
  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 arr - The array to reduce
  @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 arr - The array to map over
  @param func - The mapping function
  """
  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.

  @param arr - The array to flatten
  """
  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 arr - The array to test
  @param func - The predicate function
  """
  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 arr - The array to test
  @param func - The predicate function
  """
  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 arr - The array to count in
  @param func - The predicate function
  """
  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 arr - The array to sort
  @param func - The sort-key function
  """
  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 arr - The array to deduplicate
  @param func - The identity-key function
  """
  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 arr - The array to group
  @param func - The group-key function
  """
  // 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. Memory-layer LLM calls currently bypass cost
 * guards. Cost from inside a fork only propagates to an outer cost guard at
 * fork completion, not mid-flight.
 */
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
}
