import {
  _runLineRepl,
  _setSlashPalette,
  _clearScreen,
  _termWidth,
  _interruptBodyWidth,
  _clearHistory,
  _interruptChoice,
  _stickyInterruptAvailable,
 } from "agency-lang/stdlib-lib/cli.js"

import { _isTTY } from "agency-lang/stdlib-lib/system.js"
import {
  _promptsSelect,
  _promptsAutocomplete,
  _promptsText,
  _promptsConfirm,
} from "agency-lang/stdlib-lib/prompts.js"
import { map } from "std::array"

/** @module
  @summary Terminal conversations, menus, text entry, and approval prompts.
  The terminal front end for CLI agents. `repl` is a line-mode REPL driven
  by Node's `readline`: every prompt and reply is a plain line in the
  terminal's scrollback, so you get native search, copy/paste, link
  clicks, line editing, and history for free. The prompts (`select`,
  `autocomplete`, `prompt`, `confirm`, `chooseOption`) ask the user one
  question at a time, and `interruptChoice` is the approval prompt
  `std::policy` uses.

  ```ts
  import { chooseOption } from "std::ui/cli"

  const choice = chooseOption("Next step", "", [
    { key: "continue", label: "Continue working" },
    { key: "stop", label: "Stop here" },
  ])
  print(choice)
  ```
*/

// The active `repl()` session's history-file path, held as an Agency module
// global so it lives in the execution model (serialized, module-isolated)
// rather than captured in a TS closure. `clearHistory()` reads it to know
// which file to clear. The runtime remembers the path, not a TS hook.
let _historyFile: string = ""

/**
Each non-empty submitted line calls `onSubmit`. Returning false exits; a
non-empty string is printed, and a list of interrupts is shown as a turn
without a reply. Other return values are ignored.

The terminal supports line editing, history, slash-command completion, and
a busy indicator. Escape cancels an active turn; Ctrl+D or Ctrl+C exits at
an idle prompt. `/paste` opens a multi-line editor: Enter adds a newline,
Ctrl+D submits, and Ctrl+C or Escape cancels the paste.
*/
@capabilities(terminal)
export def repl(
  status: any,
  onSubmit: any,
  prompt: string = "> ",
  historyFile: string = "",
  historyMax: number = 1000,
  paletteCommands: any = null,
  herdrName: string = "",
) {
  """
  Run a terminal conversation loop, calling onSubmit for each submitted line.

  @param status - Callback returning {left, right, context} strings for the footer, or null
  @param onSubmit - Return false to exit, a string to print, or interrupts that stopped the turn
  @param historyFile - JSON history path; empty disables persistence
  @param historyMax - Maximum number of recent history entries to retain
  @param paletteCommands - Object mapping /commands to descriptions; null uses setSlashPalette
  @param herdrName - Name shown in a Herdr pane; empty disables state reporting
  """
  // Publish the path to the execution-model global so `clearHistory()` can
  // resolve which file to clear from Agency state rather than a TS closure.
  _historyFile = historyFile
  _runLineRepl(
    status,
    onSubmit,
    prompt,
    historyFile,
    historyMax,
    paletteCommands,
    herdrName,
  )
}

/** The map is kept outside the execution model, so call this before any
  checkpoint restore and a resumed session shows the running code's
  commands. */
export def setSlashPalette(paletteCommands: Record<string, string>) {
  """Register the slash-command palette (`/cmd` -> description) for `repl()` calls that pass none."""
  _setSlashPalette(paletteCommands)
}

@capabilities(terminal)
export def clearScreen() {
  """Clear the terminal screen."""
  _clearScreen()
}

export def clearHistory() {
  """
  Clear the active repl session's input recall and saved history file.
  Does nothing outside an interactive repl.
  """
  _clearHistory(_historyFile)
}

@capabilities(terminal)
def termWidth(): number {
  return _termWidth()
}

export def hline(char: string = "─", width: number = null): string {
  """Return a horizontal rule: `char` repeated `width` times (terminal
  width when `width` is omitted)."""
  let _width = width || termWidth()
  return char.repeat(_width)
}

/**
A row in a choice prompt: the `key` is what the prompt returns, the
`label` is the text shown to the user.
*/
export type ChoiceItem = {
  key: string;
  label: string
}

@capabilities(terminal)
export def pushMessage(message: string) {
  """
  Print a message to the terminal.
  """
  print(message)
}

/**
Requires stdout to be a terminal. Ctrl+C exits the process. There is no
non-TTY fallback, so a scripted caller must route around it
(`chooseOption` does).
*/
@capabilities(terminal)
export def select(
  message: string,
  items: ChoiceItem[],
  allowFreeText: boolean = false,
  hint: string = "",
): Result<string> {
  """
  Ask the user to choose with arrow keys in a terminal. Returns the selected
  key, free text when enabled, or a failure if cancelled.

  @param allowFreeText - Add a choice that opens a text-entry prompt
  @param hint - Dim hint shown after the question
  """
  return _promptsSelect(message, items, allowFreeText, hint)
}

/**
Requires stdout to be a terminal. Ctrl+C exits the process.
*/
@capabilities(terminal)
export def autocomplete(
  message: string,
  items: ChoiceItem[],
  allowFreeText: boolean = false,
  hint: string = "",
  cancelOnEscape: boolean = false,
): Result<string> {
  """
  Ask the user to choose in a terminal, filtering keys by case-insensitive
  substring. Returns the selected key, free text when enabled, or a failure
  if cancelled.

  @param allowFreeText - Offer the typed text when no key matches
  @param hint - Dim hint shown after the question
  @param cancelOnEscape - Cancel the current request instead of returning a cancellation failure
  """
  return _promptsAutocomplete(message, items, allowFreeText, hint, cancelOnEscape)
}

/**
Requires stdout to be a terminal. Ctrl+C exits the process.

An empty string is a valid answer; check the Result tag rather than truthiness.
Bind `validate` via `.partial()` before handing `prompt` to an LLM as
a tool so the LLM cannot override the constraint.
*/
@capabilities(terminal)
export def prompt(
  message: string,
  initial: string = "",
  hint: string = "",
  validate: any = null,
): Result<string> {
  """
  Ask the user for a line of text in a terminal, or return a cancellation failure.

  @param initial - Prefilled answer
  @param hint - Dim hint shown after the question
  @param validate - Callback returning true to accept, or an error message to ask again
  """
  return _promptsText(message, initial, hint, validate)
}

/**
Requires stdout to be a terminal. Ctrl+C exits the process.
*/
@capabilities(terminal)
export def confirm(message: string, initial: boolean = false): Result<boolean> {
  """
  Ask a yes/no question in a terminal. Returns success(true), success(false),
  or a cancellation failure; check the Result tag before reading the answer.
  """
  return _promptsConfirm(message, initial)
}

def indent(str: string): string {
  if (str == "" || str == null || str == undefined) {
    return ""
  }
  if (typeof str != "string") {
    // Defensive guard against non-string input; don't attempt to
    // split/indent it. Coerce to a JSON string so the declared
    // `string` return type is upheld and the rendered prompt still
    // shows *something* useful instead of crashing.
    print(
      "Warning: indent() expected a string but got",
      typeof str,
      "- rendering without indentation",
      JSON.stringify(str),
    )
    return JSON.stringify(str)
  }
  const lines = str.split("\n")
  const res = map(lines, \line -> "    ${line}")
  return res.join("\n")
}

/**
In a terminal this is an arrow-key picker. Otherwise it prints the
title and body and reads a key with `input()`, which is what scripted
and headless runs see. Both branches re-ask after a cancelled choice unless `allowCancel` is true.
In a terminal, Ctrl+C exits the process regardless of `allowCancel`.
std::policy uses it for its approve/reject menus.
*/
@capabilities(terminal)
export def chooseOption(
  title: string,
  body: string,
  items: ChoiceItem[],
  allowFreeText: boolean = false,
  allowCancel: boolean = false,
): string {
  """
  Ask the user to select a key, using a terminal picker or a plain input prompt.
  Invalid answers are requested again.

  @param body - Additional context shown above the choices; empty omits it
  @param allowFreeText - Accept a non-empty answer that matches no item key
  @param allowCancel - In a terminal, let Escape cancel the request instead of asking again
  """
  // Non-TTY fallback: `autocomplete` throws when stdout isn't a TTY
  // (e.g. piped output, agency-js tests that script answers through
  // the `inputOverride` invocation option). Route through a plain
  // `input()` loop in that case so tests can mock answers and the
  // "must answer" contract is still preserved.
  if (!_isTTY()) {
    if (title != "") {
      print(title)
    }
    if (body != "") {
      print(body)
    }
    const validKeys = map(items, _entryKey)
    while (true) {
      const answer = input("> ").trim()
      if (validKeys.includes(answer)) {
        return answer
      }
      if (allowFreeText && answer != "") {
        return answer
      }
      print("(answer required)")
    }
    // unreachable
    return ""
  }
  // Print the title/body banner ourselves, then delegate to
  // `autocomplete`. We loop on `failure("cancelled")` to preserve
  // chooseOption's "must answer" contract. Safety-critical callers
  // like std::policy depend on Escape never letting the user skip the
  // prompt. The non-TTY throw lives inside `autocomplete()` (in TS);
  // we don't need to repeat it here.
  print("\n")
  print(color.cyan(hline()))
  if (title != "") {
    print(color.cyan(title), "\n")
  }
  if (body != "") {
    print(indent(body))
  }
  print("\n")
  let cancelHint = ""
  if (allowCancel) {
    cancelHint = "Esc to cancel"
  }
  while (true) {
    // With `allowCancel`, autocomplete raises a cancellation on Escape
    // (it never returns failure("cancelled") here), so the re-prompt loop
    // below only runs for the must-answer callers.
    const result = autocomplete(
      message: "Do you want to continue?",
      items: items,
      allowFreeText: allowFreeText,
      hint: cancelHint,
      cancelOnEscape: allowCancel,
    )
    if (!isFailure(result)) {
      return result.value
    }
    // Only re-prompt on an explicit user-cancel; anything else
    // (e.g. "requires a TTY") gets surfaced via throw so chooseOption
    // doesn't loop forever in non-interactive environments.
    if (result.error != "cancelled") {
      throw(result.error)
    }
    print("(answer required)")
  }
  // unreachable; satisfies the type checker
  return ""
}

def _entryKey(entry: any): string {
  return entry.key
}

@capabilities(terminal)
export def interruptBodyWidth(): number {
  """
  The width, in columns, that the approval prompt wraps its body to. Render a table at this width so the prompt does not wrap it a second time.
  """
  return _interruptBodyWidth()
}

/**
During a line-mode `repl`, the prompt stays at the bottom of the terminal
while tool output appears above it. Outside that session, this delegates
to `chooseOption`, including its plain-input fallback.
*/
@capabilities(terminal)
export def interruptChoice(
  title: string,
  body: string,
  items: any[],
  allowFreeText: boolean = false,
  allowCancel: boolean = false,
  expectsValue: boolean = false,
): string {
  """
  Ask for an approval choice or a free-text answer, using the active terminal
  conversation when available.

  @param body - Additional context shown below the heading
  @param items - Choices with key and label fields
  @param allowFreeText - Accept an answer outside the listed choices
  @param allowCancel - Let Escape cancel the current request in a terminal
  @param expectsValue - Treat free text as an answer value instead of a rejection reason
  """
  if (_stickyInterruptAvailable()) {
    return _interruptChoice(title, body, items, allowFreeText, allowCancel, expectsValue)
  }
  return chooseOption(title, body, items, allowFreeText: allowFreeText, allowCancel: allowCancel)
}
