/*
 * Code subagent — handles anything that touches code or the file
 * system: writing, editing, typechecking, running shell commands.
 *
 * Exports `codeAgent` (registered as a coordinator tool in
 * `agent.agency`) plus `codeSysPrompt` / `codeTools`. The Workspace
 * state lives here (not in `shared.agency`) because only the code
 * subagent uses file-system tools. Relative paths resolve against the
 * agent working directory
 * (`setAgentCwd`/`getAgentCwd`), which `agent.agency` pins to the user's
 * cwd at startup.
 */
import { filter } from "std::array"
import { agencyCodingAgent } from "std::agents/agency/coding"
import { agencyExpertAgent } from "std::agents/agency/expert"
import { codingAgent, buildTools as codingAgentTools } from "std::agents/coding"
import { expertAgent } from "std::agents/expert"
import { verifierAgent } from "std::agents/verifier"
import { ExpertGuidance, guidanceOrEmpty, renderGuidance } from "std::agents/lib/expertGuidance"
import { Feedback, feedbackHasErrors, renderFeedback } from "std::agents/lib/feedback"
import { plannerAgent } from "std::agents/planner"
import { setAgentCwd, getAgentCwd } from "std::index"
import { skillsDir } from "std::skills"
import { setMemoryId } from "std::memory"
import { exec } from "std::shell"
import { supervise } from "std::supervise"
import { systemMessage } from "std::thread"

import { withWebSearch, getSearchBackend } from "../lib/search.agency"
import { getSlowModel, getSlowProvider } from "../shared.agency"
import { oracleAgent } from "./oracle.agency"
import {
  reviewWrittenFiles,
 } from "./review.agency"
import {
  brainstormApproaches,
  renderChosen,
  renderAlternatives,
 } from "./brainstorm.agency"
import {
  progressCheck,
  pushSupervisorFrame,
  popSupervisorFrame,
 } from "./supervisor.agency"


static const superpowersSkill = skillsDir(__dirname + "/skills/superpowers", "flat") with approve


export def agencyCli(args: string[]): any {
  """
  Run the `agency` CLI with the given arguments. Returns an object with
  `stdout`, `stderr`, and `exitCode` (same shape as `bash`). Use this for
  agency subcommands such as `parse`, `ast`, and `typecheck` — e.g.
  agencyCli(["ast", "foo.agency"]) or agencyCli(["typecheck",
  "foo.agency"]). Arguments are passed directly to the executable (no
  shell interpretation), so they cannot be chained or redirected. The
  default agent policies auto-approve the read-only agency subcommands;
  other subcommands still prompt for approval. Prefer this over `bash`
  for running agency.

  @param args - CLI arguments, e.g. ["typecheck", "foo.agency"]. The
    first element is the subcommand.
  """
  return exec("agency", args, useAgentCwd: true)
}

/** Read a bundled prompt file, or "" when it cannot be read. The prompts
  ship with the agent, so a missing one means a packaging problem, not a
  runtime condition worth failing the turn over. */
def promptFile(filename: string): string {
  const contents = read(filename) with approve
  if (isFailure(contents)) {
    return ""
  }
  return contents.value
}

export static const codeSysPrompt = promptFile("../prompts/code.md")

// The file-system / shell tools are partially applied with
// `useAgentCwd: true` so that, as agent tools, they resolve relative
// paths against the agent working directory (pinned to the user's cwd at
// startup, changeable via setAgentCwd). The base functions default this
// to false, so non-agent callers are unaffected.
export static const codeTools: any[] = [
  ...codingAgentTools(),
  agencyCli,
  superpowersSkill,
  oracleAgent,
]

// Appended to the system prompt only when a web-search backend is active
static const WEB_SEARCH_NOTE = """
You have **web search**. Use it directly whenever a task needs current or
external information — a fact to verify, a library or API to check, a
dataset or leaderboard to consult. Do NOT ask the user to look things up,
and do NOT decline a task as "needs research / out of scope": search for
what you need and keep going."""

// Appended in one-shot / autonomous mode (`agency agent -p`)
static const ONE_SHOT_NOTE = promptFile("../prompts/oneShot.md")

// Set by `agent.agency` (`setCodeOneShot`) before the first dispatch when the
// run is non-interactive. Gates `ONE_SHOT_NOTE`.
let _oneShot = false

export def setCodeOneShot(oneShot: boolean) {
  _oneShot = oneShot
}

// only add system message the first time
let _first = true

static const ESCALATE_MAX_COST = $20.00
static const ESCALATE_MAX_TIME = 15m

// How often a supervised solve pauses for the supervisor's progress check.
static const SUPERVISE_EVERY = 5m

type TriageResult = {
  path: "simple" | "complex";
  plan: string[]
}

def triage(task: string): TriageResult {
  const prompt = """
Classify this coding task. If it is a single, straightforward change, set path to "simple" and leave plan empty. If it is multi-part or needs decomposition, set path to "complex" and outline the plan in 2 to 4 short bullets.

<task>
${task}
</task>
"""

  const result: TriageResult = llm(prompt)
  return result
}

/** Pure routing decision, testable without the classifier: "planner" or "direct". */
export def routeFor(decision: TriageResult): string {
  return if decision.path == "complex" then "planner" else "direct"
}

def escalate(task: string, reason: string, plan: string): string {
  """
  Switch this task to plan-as-code: writes and runs a purpose-built agent for a
  multi-step task, showing you the plan first. Reach for this ONLY when the task
  genuinely needs a multi-step plan you cannot finish in a few direct edits. Do
  NOT use it for ordinary follow-ups. After it returns, REPORT its result; do not
  redo the work it already did.

  @param reason - why this task needs a multi-step plan.
  @param plan - your outline of the steps.
  """
  const planned = supervisedPlan(task: task, reason: reason, plan: plan)
  if (isFailure(planned)) {
    return "Planner stopped: ${planned.error}"
  }
  return planned.value
}

/* The expert consult runs once and sets the whole direction, so it gets the
   slow model slot. The solve loop stays on the ambient one. */
def consultExpert(task: string, agencyTask: boolean): Result<ExpertGuidance> {
  if (agencyTask) {
    return agencyExpertAgent(
      task: task,
      maxCost: ESCALATE_MAX_COST,
      maxTime: 5m,
      model: getSlowModel(),
      provider: getSlowProvider(),
    )
  }
  return expertAgent(
    task: task,
    maxCost: ESCALATE_MAX_COST,
    maxTime: 5m,
    model: getSlowModel(),
    provider: getSlowProvider(),
  )
}

/* agencyCodingAgent returns source rather than writing files, so its result
   is handed back as the reply for the user to place. */
def solve(task: string, context: string, agencyTask: boolean): Result<string> {
  if (agencyTask) {
    const written = agencyCodingAgent(
      task: task,
      context: context,
      maxCost: ESCALATE_MAX_COST,
      maxTime: ESCALATE_MAX_TIME,
    )
    if (isFailure(written)) {
      return failure("could not write valid Agency code: ${written.error.problems}")
    }
    return success(written.value)
  }
  return codingAgent(
    task: task,
    context: context,
    maxCost: ESCALATE_MAX_COST,
    maxTime: ESCALATE_MAX_TIME,
  )
}

/* The supervised block cannot hand a Result straight out: a stop mid-run
   salvages the worker's saveDraft draft in place of the block's return
   value, so the block normalizes to this plain shape and the flattener
   sorts out what actually came back. */
type SolveOutcome = {
  ok: boolean;
  summary: string
}

/** Turn what supervise hands back into the solve Result the callers expect.
  Three shapes arrive here: a failure (the supervisor stopped a draftless
  run, or maxTime passed), a SolveOutcome (the block ran to completion), or
  a salvaged draft (the supervisor stopped the run after the worker saved
  one — drafts are strings, so anything without a `summary` field is one).
  Exported for tests. */
export def flattenSolveOutcome(captured: Result<any>): Result<string> {
  if (isFailure(captured)) {
    return failure("${captured.error}")
  }
  const value = captured.value
  if (value.summary == null) {
    return success(
      "The run was stopped early by the supervisor. Its last saved draft:\n\n${value}",
    )
  }
  if (!value.ok) {
    return failure(value.summary)
  }
  return success(value.summary)
}

def supervisedRun(
  task: string,
  alternatives: string,
  work: () -> Result<string>,
): Result<string> {
  pushSupervisorFrame(alternatives: alternatives)
  const captured = supervise(
    every: SUPERVISE_EVERY,
    maxTime: ESCALATE_MAX_TIME,
    check: progressCheck.partial(task: task),
  ) {
    const worked = work()
    if (isFailure(worked)) {
      return {
        ok: false,
        summary: "${worked.error}"
      }
    }
    return {
      ok: true,
      summary: worked.value
    }
  }
  popSupervisorFrame()
  return flattenSolveOutcome(captured)
}

def supervisedSolve(
  task: string,
  context: string,
  agencyTask: boolean,
  alternatives: string = "",
): Result<string> {
  return supervisedRun(
    task: task,
    alternatives: alternatives,
    work: \-> solve(task: task, context: context, agencyTask: agencyTask),
  )
}

/* The planner escalation, supervised the same way as a solve: it hands
   plannerAgent the largest budget in the agent and previously ran it
   with nobody watching. Draft deltas fail open here — the planner's
   generated program runs in a subprocess, so its drafts live in the
   child and the delta reads "none" in the parent; the verifier and
   check history still carry the evidence. */
def supervisedPlan(task: string, reason: string, plan: string): Result<string> {
  return supervisedRun(
    task: task,
    alternatives: "",
    work: \-> plannerAgent(
      task,
      context: reason,
      plan: plan,
      maxCost: ESCALATE_MAX_COST,
      maxTime: ESCALATE_MAX_TIME,
    ),
  )
}

export def codeAgent(userMsg: string, agencyTask: boolean = false) {
  """
  This agent can help with all coding-related tasks,
  such as reading, writing, or editing code. It can also
  respond to questions about the Agency language,
  such as questions related to the Agency syntax or CLI tool.

  @param userMsg - a message describing what help is needed.
  @param agencyTask - true when the code in question is Agency code, which
    routes to the agents that know the language. Leave it false for every
    other language.
  """
  const originalUserMsg = userMsg
  const decision = triage(userMsg)
  if (routeFor(decision) == "planner") {
    return escalateToExpert(task: userMsg, agencyTask: agencyTask)
  }
  if (_oneShot) {
    return solveUnattended(task: userMsg, agencyTask: agencyTask)
  }
  return converse(userMsg: userMsg, originalUserMsg: originalUserMsg, agencyTask: agencyTask)
}

/* A multi-part task acquires domain expertise first, brainstorms several
   genuinely different directions and commits to one, solves with both in
   context, then verifies against the expert's checklist and gets one round
   to fix what failed. The set-aside directions travel to the supervisor,
   so a mid-run redirect can name a specific alternative when the chosen
   path is not working. `escalate`, the mid-flight tool, still uses
   plannerAgent. */
def escalateToExpert(task: string, agencyTask: boolean): string {
  const guidance = guidanceOrEmpty(consultExpert(task: task, agencyTask: agencyTask))
  const ideas = brainstormApproaches(task: task, guidance: renderGuidance(guidance))
  const alternatives = renderAlternatives(ideas)
  // Join only the non-empty parts: both renderings return "" on their
  // fail-open paths, and a bare "\n" would defeat withContext's
  // empty-context gate — the solver must run untouched when there is
  // genuinely no context.
  const contextParts = filter(
    [renderGuidance(guidance), renderChosen(ideas)],
    \part -> part != "",
  )
  const solved = supervisedSolve(
    task: task,
    context: contextParts.join("\n"),
    agencyTask: agencyTask,
    alternatives: alternatives,
  )
  let summary = ""
  if (isFailure(solved)) {
    summary = "Coding agent stopped: ${solved.error}"
  } else {
    summary = solved.value
    // Milestone draft: a completed solve is worth keeping even if the
    // verify or fix round below is cut short by an outer budget guard —
    // salvage-on-abort then returns this instead of nothing.
    saveDraft(summary)
  }
  const checked = verifierAgent(
    task: task,
    criteria: guidance.checklist,
    maxCost: $5.00,
    maxTime: 5m,
  )
  if (feedbackHasErrors(checked)) {
    const fixMsg = """
${task}

A strict review found these problems:

${renderFeedback(checked)}

Fix each problem exactly.
"""
    const fixed = supervisedSolve(task: fixMsg, context: "", agencyTask: agencyTask, alternatives: alternatives)
    if (!isFailure(fixed)) {
      summary = fixed.value
      saveDraft(summary)
    }
  }
  return summary
}

/* `agency agent -p`: nobody is here to answer a question and there is no next
   turn, which is what the stdlib coding agent already does. The rules for
   working that way are the whole of ONE_SHOT_NOTE, so they travel as context
   rather than as a second copy of the loop. */
def solveUnattended(task: string, agencyTask: boolean): string {
  const solved = supervisedSolve(task: task, context: ONE_SHOT_NOTE, agencyTask: agencyTask)
  if (isFailure(solved)) {
    return "The task did not complete: ${solved.error}"
  }
  // Milestone draft: the verify and fix rounds below can be cut short by
  // an outer budget guard; the completed solve must survive that.
  saveDraft(solved.value)
  const checked = verifierAgent(task: task, maxCost: $5.00, maxTime: 5m)
  if (!feedbackHasErrors(checked)) {
    return solved.value
  }
  const fixMsg = """
${task}

A fresh review found these gaps:

${renderFeedback(checked)}

Fix each one; your work will be re-checked.
"""
  const fixed = supervisedSolve(task: fixMsg, context: ONE_SHOT_NOTE, agencyTask: agencyTask)
  if (isFailure(fixed)) {
    return solved.value
  }
  saveDraft(fixed.value)
  return fixed.value
}

/* The interactive turn, which is the part that is genuinely this agent's own:
   a conversation with the user, in their thread, formatted for the REPL. */
def converse(
  userMsg: string,
  originalUserMsg: string,
  agencyTask: boolean,
): string {
  const tools = [...codeTools]
  tools.push(escalate.partial(task: userMsg))
  let message = userMsg
  let attemptsLeft = 3
  let reply = ""
  thread(label: "code", session: "code") {
    setMemoryId("code")
    if (_first) {
      let sys = codeSysPrompt
      if (getSearchBackend() != "off") {
        sys = "${sys}\n\n${WEB_SEARCH_NOTE}"
      }
      systemMessage(sys)
      _first = false
    }
    let done = false
    while (!done && attemptsLeft > 0) {
      attemptsLeft = attemptsLeft - 1
      reply = llm(message, withWebSearch({ memory: true, tools: tools }))
      // Reviewing the .agency files just written only means something on the
      // Agency path; elsewhere there are none and this is a no-op.
      let feedback: Result<Feedback[]> = success([])
      if (agencyTask) {
        feedback = reviewWrittenFiles() with approve
      }
      if (feedbackHasErrors(feedback)) {
        message = """
        The last code had errors:

        ${renderFeedback(feedback)}

        Please fix the errors.
        """
      } else {
        done = true
      }
    }
    const formatPrompt = """
   Please format this response so it correctly addresses the user's question. Remove any extra text such as "this code now typechecks correctly" or "this code doesn't have any errors now".

   User message:
   ```
   ${originalUserMsg}
   ```

   Code agent response:
   ```
   ${reply}
   ```
   """
    reply = llm(formatPrompt)
  }
  return reply
}
