/** @module
  @summary Answers a question from web and Wikipedia sources and synthesizes
  a cited answer, retrying until every claim is grounded.

  The grounding loop is this agent's own contract: a grounded, cited answer
  is what it produces. Checking that answer against a wider task is the
  caller's job, via reviewAgent.
*/
import { feedbackHasErrors, renderFeedback } from "std::agents/lib/feedback"
import { hostedSearchTools, searchTools } from "std::agents/lib/search"
import { llmOptions, withContext } from "std::agents/lib/shared"
import { webTools, SAVE_DRAFT_HINT } from "std::agents/lib/toolkits"
import { reviewAgent } from "std::agents/review"
import { now } from "std::date"
import { agentSkill } from "std::skills"
import { Critique, revise } from "std::strategy"
import { systemMessage, threadIsNew } from "std::thread"

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


static const systemPrompt = """You are a research analyst. Answer the task using your source tools.

- Ground every factual claim in a source and cite it.
- If your tools cannot retrieve the needed information, say so plainly. NEVER fabricate sources or facts.

## Communicating with the user
- Make sure the user is following what you're doing. Use the `whatIAmDoing` tool frequently to tell the user what you're doing.${SAVE_DRAFT_HINT}
"""

static const skills = agentSkill("researcher")

export def buildTools(): any[] {
  """
  Return the researcher's tools: the encyclopedia and fetch tools that need
  no key, a local file read for material the caller points at, and whichever
  search providers the environment has a key for.
  """
  return [
    ...webTools(),
    read.partial(useAgentCwd: true),
    ...searchTools(),
    skills,
    ...communicationTools(),
  ]
}

static const groundingInstruction = "Judge grounding: error=true if a claim is ungrounded or uncited or the task is not fully answered."

def checkGrounding(task: string, startedAt: number, answer: any): Critique {
  // Verification effort scales with the work: the review gets half the
  // time the answer took to produce, clamped between 20s and 2m. A
  // 70-second answer gets a 35-second check instead of a 2-minute one;
  // a long research run keeps the old cap.
  let reviewTime = (now() - startedAt) / 2
  if (reviewTime < 20s) {
    reviewTime = 20s
  }
  if (reviewTime > 2m) {
    reviewTime = 2m
  }
  const reviewed = reviewAgent(
    work: "${answer}",
    task: task,
    context: groundingInstruction,
    maxCost: $1.00,
    maxTime: reviewTime,
  )
  if (!feedbackHasErrors(reviewed)) {
    return {
      accepted: true,
      feedback: ""
    }
  }
  return {
    accepted: false,
    feedback: """
Your answer has gaps:

${renderFeedback(reviewed)}

Retrieve more and fix them.
"""
  }
}

def researchLoop(
  task: string,
  context: string,
  maxAttempts: number,
  model: string,
  provider: string,
  extraTools: any[],
): string {
  const researchTools = buildTools()
  if (threadIsNew()) {
    systemMessage(systemPrompt)
  }
  const question = withContext(task: task, context: context)
  const startedAt = now()
  return revise(maxAttempts: maxAttempts, check: checkGrounding.partial(task: task, startedAt: startedAt)) as critique {

    let msg = critique
    if (critique == "") {
      msg = question
    }

    const answer = llm(
      msg,
      llmOptions(
      model: model,
      provider: provider,
      tools: [...researchTools, ...extraTools, saveDraft],
      hostedTools: hostedSearchTools(model),
    ),
    )
    saveDraft(answer)
    return answer
  }
}

export def researcherAgent(
  task: string,
  context: string = "",
  maxAttempts: number = 2,
  maxCost: number = $50.00,
  maxTime: number = 30m,
  model: string = "",
  provider: string = "",
  session: string = "",
  extraTools: any[] = [],
): Result<string> {
  """
  Answer a research question from web and Wikipedia sources and return a
  cited answer, retrying while a review finds ungrounded claims.

  @param task - The research question
  @param context - Extra material folded into the prompt, or ""
  @param maxAttempts - Answer-and-fix attempts before returning
  @param maxCost - Hard spend cap
  @param maxTime - Hard wall-clock cap
  @param model - Model override, or "" for the ambient model
  @param provider - Provider for the model override
  @param session - Session name to share a thread across calls, or "" for isolated
  @param extraTools - Extra tools to offer the LLM, appended to the built-in set
  """

  return guard(cost: maxCost, time: maxTime, label: "researcherAgent") {
    let answer = ""
    thread(label: "researcherAgent", session: session) {
      answer = researchLoop(
        task: task,
        context: context,
        maxAttempts: maxAttempts,
        model: model,
        provider: provider,
        extraTools: extraTools,
      )
    }
    return answer

    // Salvage the best-so-far answer (a completed attempt, or the model's
    // in-flight saveDraft) if an outer budget guard trips; the assignment
    // above would otherwise drop it.
    finalize {
      return answer
    }
  }
}
