import { Feedback, failOpenFeedback } from "std::agents/lib/feedback"
import { AGENT_SPILL_OVER, llmOptions } from "std::agents/lib/shared"
import { readOnlyFileTools, readOnlyGitTools, shellTools, agencyCodeTools, SAVE_DRAFT_HINT, ASK_USER_HINT } from "std::agents/lib/toolkits"
import { concurrencyPool } from "std::concurrency"
import { guarded } from "std::decorators"
import { agentSkill } from "std::skills"
import { systemMessage, threadIsNew } from "std::thread"

import { communicationTools } from "./lib/toolkits.agency"

/** @module
@summary Check completed work on disk against measurable task criteria.

The verifier derives criteria, uses tools to check the work, and returns
`Feedback` findings. You can supply known criteria as well. It has shell
execution tools, so the caller's handlers decide which commands may run.
Use `reviewAgent` to judge text without running it.

A stopped run may return saved partial findings. A failed verification
returns no findings, so an empty list alone does not prove the work passed.
*/

static const POOL_SIZE = 4

static const skills = agentSkill("verifier")

// The verifier measures with shell pipelines and reads bytes, because a
// criterion checked by eye is a criterion not checked. It has no write
// tools: a verifier that can fix things is no longer a verifier. Agency code
// is checked with the type checker and test harness tools, since the
// `agency` command line is not available from inside the agent.
export def buildTools(): any[] {
  """
  Return the verifier's file inspection, command execution, and Agency check tools.
  """
  return [
    ...readOnlyFileTools(),
    ...readOnlyGitTools(),
    ...shellTools(),
    ...agencyCodeTools(),
    ...communicationTools(),
    readBinary.partial(useAgentCwd: true),
    skills,
  ]
}

static const systemPrompt = """You verify whether a coding task was completed correctly. You do NOT fix anything. Inspect, COMPUTE, and report.

The grading tests are hidden and NOT in this environment, so you must reconstruct and ACTUALLY RUN every check the task states — never eyeball the artifact.

Method:
1. Enumerate EVERY explicit success criterion in the task. Two kinds: the output contract (exact file path and name, format, JSON keys, field types, units, signed-vs-unsigned, the stated whitespace / trailing-newline requirement) AND every quantitative rule (lengths, ranges, counts, thresholds, temperatures, GC content, percentages, etc.).
2. For EACH criterion, use the `exec` tool to COMPUTE the artifact's actual value and compare it to the requirement. If the task names a specific tool or command to compute a value (e.g. a CLI with exact flags), install if needed and run THAT tool — it is the ground truth; do not approximate it with your own estimate.
   For Agency code there is no `agency` command line here: run a `.test.json` harness with `testFile(dir, filename)` and check a source file with `typecheck`. Do not search for an `agency` executable.
3. A criterion you did NOT computationally verify is itself a gap — report it as one. A near-miss on any numeric constraint is an error: report the actual computed value vs. the requirement (e.g. "Tm difference 6.5°C, limit 5°C").

Never say the artifact "appears" or "should" satisfy something. Run it, measure it, and report a concrete pass/fail per criterion with the numbers.${ASK_USER_HINT}${SAVE_DRAFT_HINT}"""

// The judgment call, split out so its declared `Feedback[]` return type
// flows into `llm` and a model saveDraft propagates as typed partial
// findings when an outer guard trips.
def judgeCriteria(
  prompt: string,
  model: string,
  provider: string,
  extraTools: any[],
): Feedback[] {
  return llm(
    prompt,
    llmOptions(
    model: model,
    provider: provider,
    tools: [...buildTools(), ...extraTools, saveDraft],
    spillToolResultsOver: AGENT_SPILL_OVER,
  ),
  )
}

export def verifierAgent(
  task: string,
  criteria: string[] = [],
  maxCost: number = $20.00,
  maxTime: number = 15m,
  model: string = "",
  provider: string = "",
  session: string = "",
  extraTools: any[] = [],
): Result<Feedback[]> {
  """
  Check work on disk against task criteria with tools and return findings.
  A failed verification returns no findings.

  @param criteria - Known criteria to check alongside those derived from the task.
  @param maxCost - Cost budget in USD.
  @param maxTime - Time budget in milliseconds.
  @param model - Model override, or "" for the ambient model.
  @param provider - Provider for the model override, ignored when model is empty.
  @param session - Name a conversation to reuse across calls, or "" for an isolated thread.
  @param extraTools - Tools available to criteria generation and verification.
  """
  return failOpenFeedback(verifyUnderBudget(
    task: task,
    criteria: criteria,
    model: model,
    provider: provider,
    session: session,
    extraTools: extraTools,
    maxCost: maxCost,
    maxTime: maxTime,
  ))
}

// The guarded half of verifierAgent, a def of its own because the exported
// def post-processes the guard's Result. Return position: when the budget
// runs out, the findings the model saved reach the guard and come back as
// the result.
@guarded(cost: maxCost, time: maxTime, label: "verifierAgent")
def verifyUnderBudget(
  task: string,
  criteria: string[],
  model: string,
  provider: string,
  session: string,
  extraTools: any[],
  maxCost: number,
  maxTime: number,
): Result<Feedback[]> {
  thread(label: "verifierAgent", session: session) {
    if (threadIsNew()) {
      systemMessage(systemPrompt)
    }
    const criteriaString = criteriaAsString(criteria)
    /*       const authoritativeCriteria = """
      Here is some example criteria that has already been provided for this task.
      Use it as a reference, but do not assume it is complete. Don't duplicate any of these,
      come up with new criteria.
      <criteria>
      ${criteriaString}
      </criteria>
    """
    const criteriaBlock = if criteria.length == 0 then "" else authoritativeCriteria
 */
    const generateCriteriaPrompt = """
    Given this task, come up with measurable criteria for success.
    <task>${task}</task>
    """

    const generatedCriteria: string[] = llm(
      generateCriteriaPrompt,
      llmOptions(model: model, provider: provider, tools: extraTools),
    )

    const allCriteria = [...criteria, ...generatedCriteria]

    const prompt = """
      Given this task:
      <task>${task}</task>

      Judge it against this criterion:
      <criterion>${criteriaAsString(allCriteria)}</criterion>

      Verify, don't assume. Use your tools to see what code has been produced, and compare it to the criterion.
      Report a concrete pass/fail. If the criterion involves a number, compute that number.
      A criterion you did not actually compute counts as a failure.
      """

    // A typed helper so the block returns Feedback[]: inlining
    // `return llm(...)` here would infer string.
    return judgeCriteria(
      prompt: prompt,
      model: model,
      provider: provider,
      extraTools: extraTools,
    )
  }
}

def criteriaAsString(criteria: string[]): string {
  if (criteria.length == 0) {
    return ""
  }
  return map(criteria, \criterion -> "- ${criterion}").join("\n")
}
