import { Feedback, failOpenFeedback } from "std::agents/lib/feedback"
import {
  llmOptions,
  withContext,
  docsOnlyHandler,
 } from "std::agents/lib/shared"
import {
  communicationTools,
  readOnlyFileTools,
  writableFileTools,
  SAVE_DRAFT_HINT,
  ASK_USER_HINT,
 } from "std::agents/lib/toolkits"
import { guarded } from "std::decorators"
import { agentSkill } from "std::skills"
import { evalValue } from "std::statelog"
import { systemMessage, threadIsNew } from "std::thread"

/** @module
@summary Review prose for readability using built-in or caller-supplied guidelines.

Pass prose or a request naming files. The reviewer reports findings and
can apply edits when the request explicitly asks for them. Separate passes
look for unnecessary passages, verbal tics, and readability problems.

By default, a failed review returns no findings. Pass `failOpen: false` to
receive failures. A budget stop may still return saved partial findings.
*/

optimize static const systemPrompt = """You review prose for readability and for whether it reliably communicates the facts this reader needs. Spelling and grammar checkers run separately; your role is the judgment layer — find prose that is technically correct but that will mislead, slow, or fail this reader.

Start by identifying the intended reader and the decisions they must make after reading. Judge every finding against that reader's needs. Keep that analysis to yourself; do not report it as a finding.

Principles — name the principle each finding violates and treat these as concrete, checkable patterns:
- Garden-path sentences: quote the misleading opening, demonstrate the plausible misreading (what a reader will initially parse it as vs the intended meaning), and give a rewrite that cannot be misread.
- Passive voice: flag where hiding the actor slows comprehension; suggest an active rewrite when naming who does what will help.
- Too many concepts in one sentence: show how to split it and give concrete split rewrites.
- Em-dash overuse: suggest a comma, conjunction, or split where em-dashes are jarring.
- Unexplained jargon and shorthand: quote the term and either define it briefly for this reader or say it must be defined; do not invent new domain facts beyond what context supports. A term the intended reader uses daily is not jargon: "parse", "typecheck", and "stdlib" are plain words to a contributor.
- Buried lead: show when the sentence the reader came for sits under unneeded background and provide a version that puts the lead first.

Two things are found by separate passes; do not report them. Cuts: text this reader does not need, which the cuts pass names. Do not reword a passage that should be cut. Verbal tics: filler words, flourishes, clever names, capitals for emphasis.

How to review, stepwise:
1. Identify the intended reader and the concrete decisions or checks they must be able to make after reading.
2. For sentence-level issues: use error=true only when the text will cause a measurable misreading, loss of thread, or a missed requirement — i.e., you can act out the misreading, and a reader who knows the subject would take a wrong action because of it. A sentence that could be more precise is not an error. Example: "When several complete passes scored the same question, the most recent one wins." A reader might want to know how "most recent" is decided, but they will not act wrongly for lack of it, so it is not an error, and a rewrite that names a timestamp would invent a fact. Leave it alone. Quote the exact passage, explain the misread, and give a concrete, faithful rewrite that preserves the original facts. If the rewrite needs a fact the text does not state, the passage is not an error: report it as advisory, only if the assignment supplies the fact, and say which fact you added.
3. Use error=false for advisory polish: tightening, active-voice rewrites, stylistic improvements, or helpful clarifications that do not block understanding. Be conservative: reserve error=true for real failures.
4. When the caller supplies writing guidelines, those outrank this catalog; apply both and note any conflicts.
5. Keep each finding brief: quote the passage, name the violated principle, and give one concrete rewrite.
6. Do not invent errors or facts. Rewrites should preserve the original facts; if you must add a fact to make the text actionable, mark that rewrite advisory and call out the added fact.
7. A clean verdict is a fine outcome. Never pad findings with generic advice. Always offer one concrete rewrite for each substantive finding.

Keep each finding short, factual, and actionable: quote the passage, name the principle, and give one concrete fix. ${ASK_USER_HINT}${SAVE_DRAFT_HINT}"""

// The cuts pass: one question of every part of the text, does this reader
// need it. Returns cuts only, never rewrites.
optimize static const cutsPrompt = """You read a piece of prose and decide what the reader does not need. You report cuts only. Rewording, misreadings, and verbal tics are handled by other passes.

Start from the task the text was written for. Name the reader and the decisions or checks they must be able to make after reading. Then go through the text one paragraph at a time, and within a paragraph one sentence at a time, and ask of each: does this reader need it to make those decisions? Material the reader does not need is a cut. Common cuts:
- Background the reader already has, or does not need for the decision.
- The writer's own process: what they tried, checked, or thought before arriving at the fact.
- A second statement of a fact the text already states.
- Detail that serves a different reader (an implementer's note in a user doc, a user's note in a design doc).
- Restating the assignment back to the reader.
- Every value of an option or every member of a set, where knowing the option or set exists is enough for the reader.
- Praise, reassurance, and sentences that comment on the text itself.
- A note the writer left about a decision they made while working, when the decision has no lasting consequence for the reader. A paragraph saying that something did not need to be done, and why, records the writer's checking; the reader will never act on it. The reasoning behind it does not make it needed.
- A whole piece that gives the reader nothing they need. Then say so and recommend removing it; do not list its sentences. Ask this of the whole piece first, before going sentence by sentence.

How to report:
- Do not report the reader or the decisions; they are for your judgment only. Report cuts.
- One finding per cut. Quote the passage exactly, then say why this reader does not need it, in terms of the decision they are making.
- Say "cut". Say "move to <where>" only when the task or the text names a place the passage belongs; do not invent a destination. Do not offer a shorter version of the passage; a cut is a cut.
- Do not add facts and do not suggest new material.
- Set error to true only when the whole piece, or a whole section, should go. Every other cut is advisory: error false.
- Text the reader needs gets no finding. A text with nothing to cut gets an empty list.

If the text names files instead of holding the prose, read each file and review its contents."""

def askForCuts(
  text: string,
  task: string,
  guidelines: string,
  context: string,
  model: string,
  provider: string,
): Feedback[] {
  let guidelinesSection = ""
  if (guidelines != "") {
    guidelinesSection = """The caller's own writing guidelines. They outrank the rules above:
<guidelines>
${guidelines}
</guidelines>

"""
  }
  if (threadIsNew()) {
    systemMessage(cutsPrompt)
  }
  return llm(
    """
${guidelinesSection}The text was written for this task:
<task>
${withContext(task: task, context: context)}
</task>

<text>
${text}
</text>

Return a list of feedback items.
""",
    llmOptions(
      model: model,
      provider: provider,
      tools: readOnlyFileTools(),
      validationRetries: 1,
    ),
  )
}

def findCuts(
  text: string,
  task: string,
  guidelines: string,
  context: string,
  model: string,
  provider: string,
): Feedback[] {
  """
  Find passages the intended reader does not need and return cut findings.
  """
  let found: Feedback[] = []
  thread(label: "cuts") {
    found = askForCuts(
      text: text,
      task: task,
      guidelines: guidelines,
      context: context,
      model: model,
      provider: provider,
    )
  }
  return found
}

def cutsOrNothing(
  text: string,
  task: string,
  guidelines: string,
  context: string,
  model: string,
  provider: string,
): Feedback[] {
  """
  Find unnecessary passages, returning no findings when that pass fails.
  """
  const found = try findCuts(
    text: text,
    task: task,
    guidelines: guidelines,
    context: context,
    model: model,
    provider: provider,
  )
  return match(found) {
    success(items) => items
    failure(_) => []
  }
}

// The tics pass.
static const ticsPrompt = """You read prose and report verbal tics: phrases a language model reaches for that a careful editor cuts. Report nothing else. Readability, structure, and facts are reviewed by a separate pass.

The rule behind every entry: state the fact. Do not decorate it, rank it, or warn the reader about it.

Sentence shapes:

1. Contrastive binary: "not X, but Y", "neither X nor Y", "X, never Y".
   Bad: "Judge the code as Agency, never as JavaScript."
   Good: "Judge the code as Agency. Do not judge it as JavaScript."

2. Stakes-raising: a sentence that tells the reader how much the last sentence matters.
   Bad: "Facts about Agency. Get these wrong and the review is wrong."
   Good: "Facts about Agency:"

3. Aphoristic ender: a short punchy clause that closes a paragraph with a flourish. "And no more." "Full stop." "The rest is noise." "That is the whole point."

4. Colon reveal: a setup, a colon, then the point delivered as a twist.
   Bad: "The song and dance is all in one step you skipped: the parser parses literals, but the model returns text."
   Good: "The parser parses string literals. The model returns text. Converting between them is where the extra code comes from."

5. Mirrored clauses: two halves built on the same frame for effect.
   Bad: "Keep this to yourself; do not report it as a finding."
   Good: "Do not report this analysis as a finding."

6. Semicolon joins and "but" joins that leave the reader unsure which half is the instruction.
   Bad: "Rebuilding it with `failure(e)` loses nothing but is not an error."
   Good: "Return the original failure unchanged. Rebuilding it with `failure(e)` changes the source of the error message."

7. Meta-signposting: announcing the structure instead of using it. "Three things." "Here's the thing." "A quick word on how this is organized." Use a heading or a numbered list and start.

8. The reframe: "Better posed:", "Put differently:", "In other words". Say it once.

9. Negative-list prohibitions as rhythm: "No checklist, no summary line, no praise."
   Good: "Omit checklists, summary lines, and praise for correct code."

10. Shouting with capitals for emphasis: "ONLY", "NEVER", "ALL". Emphasis is a plain word in a short sentence.

11. Naming a scheme cleverly, or writing about code as if it were a person: "the reject arm has the last word", "the command also answers to", "one document, one fence". Say what the code does, and let the checks state the rule.

12. Turning a verb into a noun to save words: "costs no parse". Say "does not parse the file again".

13. Narrating a correction: "I said it before checking." Give the corrected fact, or "That was wrong."

Words and phrases:

14. "load-bearing", "seam", "surface" (as a verb), "footgun", "trap", "the flagship X". Say what the thing does.

15. "the key insight", "the real fix", "the actual problem", "the root cause". Describe the problem. The reader decides what is key.

16. "genuinely", "honestly", "the honest answer", "to be fair", "frankly". Delete the word.

17. "worth noting", "it is worth stating plainly", "note that". Delete the phrase and keep the fact.

18. "You're absolutely right", "Great question", "Fair question". Answer the question.

19. "essentially", "basically", "simply", "just", "fundamentally", "structurally". Delete.

20. "say so and I will." Delete.

21. Treating an inanimate object as an actor, especially paired with "answered": "the file answered the question", The work is a research agent's answer to the question".

How to report:
- One finding per passage. Quote the passage, name the entry by number and name, and give the plain replacement or say to delete it.
- Report a repeated word once, listing each occurrence in the quote.
- A colon that introduces a list, a code block, or an example is not a colon reveal. A lowercase "only", a short sentence, and a plain "note" as a noun are not tics. Report only the patterns above.
- When a sentence carries no fact of its own (a signpost, a flourish, a list of prohibitions the checks already state), say to delete it. Rewrite only a sentence that carries a fact.
- Clean text gets an empty list.
- Every finding is advisory: set error to false.

If the text names files instead of holding the prose, read each file and review its contents."""

def askForTics(
  text: string,
  task: string,
  guidelines: string,
  context: string,
  model: string,
  provider: string,
): Feedback[] {
  let guidelinesSection = ""
  if (guidelines != "") {
    guidelinesSection = """The caller's own writing guidelines. They outrank the rules above:
<guidelines>
${guidelines}
</guidelines>

"""
  }
  if (threadIsNew()) {
    systemMessage(ticsPrompt)
  }
  return llm(
    """
${guidelinesSection}The text was written for this task:
<task>
${withContext(task: task, context: context)}
</task>

<text>
${text}
</text>

Return a list of feedback items.
""",
    llmOptions(
      model: model,
      provider: provider,
      tools: readOnlyFileTools(),
      validationRetries: 1,
    ),
  )
}

def findVerbalTics(
  text: string,
  task: string,
  guidelines: string,
  context: string,
  model: string,
  provider: string,
): Feedback[] {
  """
  Find verbal tics and return advisory findings.
  """
  let found: Feedback[] = []
  thread(label: "verbalTics") {
    found = askForTics(
      text: text,
      task: task,
      guidelines: guidelines,
      context: context,
      model: model,
      provider: provider,
    )
  }
  return [{
    error: false,
    feedback: item.feedback
  } for item in found]
}

def verbalTicsOrNothing(
  text: string,
  task: string,
  guidelines: string,
  context: string,
  model: string,
  provider: string,
): Feedback[] {
  """
  Find verbal tics, returning no findings when that pass fails.
  """
  const found = try findVerbalTics(
    text: text,
    task: task,
    guidelines: guidelines,
    context: context,
    model: model,
    provider: provider,
  )
  return match(found) {
    success(items) => items
    failure(_) => []
  }
}

static const skills = agentSkill("writing/review")

export def buildTools(): any[] {
  """
  Return the writing reviewer's file editing, skill, and communication tools.
  """
  return [...writableFileTools(), skills, ...communicationTools()]
}

def judgeText(
  text: string,
  task: string,
  guidelines: string,
  context: string,
  model: string,
  provider: string,
  extraTools: any[],
): Feedback[] {
  if (threadIsNew()) {
    systemMessage(systemPrompt)
  }
  let guidelinesSection = ""
  if (guidelines != "") {
    guidelinesSection = """The caller's own writing guidelines. They outrank the built-in catalog:
<guidelines>
${guidelines}
</guidelines>

"""
  }
  const prompt = """
${guidelinesSection}The text was written for this task:
<task>
${withContext(task: task, context: context)}
</task>

<text>
${text}
</text>

If the text names files instead of holding the prose, read each file and
review its contents. Do not write to or edit any file unless the text
explicitly asks you to apply the fixes; when it does, apply them with the
edit tool, one finding at a time, and still return the findings you applied.
Return a list of feedback items.
"""
  // Return position so a model saveDraft propagates as partial findings.
  return llm(
    prompt,
    llmOptions(
      model: model,
      provider: provider,
      tools: buildTools().concat(extraTools).concat([saveDraft]),
    ),
  )
}

export def writingReviewAgent(
  text: string,
  task: string = "",
  guidelines: string = "",
  context: string = "",
  maxCost: number = $5.00,
  maxTime: number = 10m,
  model: string = "",
  provider: string = "",
  session: string = "",
  extraTools: any[] = [],
  failOpen: boolean = true,
): Result<Feedback[]> {
  """
  Review prose for readability and return findings, applying edits only when requested.

  @param text - Prose or a request naming files, explicitly asking for edits if wanted.
  @param task - The intended reader and what the text must communicate.
  @param guidelines - Writing guidelines that override conflicting built-in review rules.
  @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 for the main review, or "" for an isolated thread.
  @param extraTools - Tools added to the main review only.
  @param failOpen - False returns review failures, while saved partial findings can still succeed.
  """
  const captured = writingReviewUnderBudget(
    text: text,
    task: task,
    guidelines: guidelines,
    context: context,
    model: model,
    provider: provider,
    session: session,
    extraTools: extraTools,
    maxCost: maxCost,
    maxTime: maxTime,
  )
  if (failOpen) {
    return failOpenFeedback(captured)
  }
  return captured
}

// The guarded half of writingReviewAgent, a def of its own because the
// exported def decides what a failed review returns. The cuts and tics
// passes run first, in parallel, each with its own short prompt, so the
// main prompt carries misreadings only. The main pass runs after them and
// on its own: a draft saved inside a parallel arm never reaches the guard,
// and a trip inside one skips this def's finalize. Here the assignment
// binds the findings the model saved into `items`, and the finalize hands
// them up when the budget runs out.
@guarded(cost: maxCost, time: maxTime, label: "writingReviewAgent")
def writingReviewUnderBudget(
  text: string,
  task: string,
  guidelines: string,
  context: string,
  model: string,
  provider: string,
  session: string,
  extraTools: any[],
  maxCost: number,
  maxTime: number,
): Result<Feedback[]> {
  parallel {
    let cuts = cutsOrNothing(
      text: text,
      task: task,
      guidelines: guidelines,
      context: context,
      model: model,
      provider: provider,
    )
    let tics = verbalTicsOrNothing(
      text: text,
      task: task,
      guidelines: guidelines,
      context: context,
      model: model,
      provider: provider,
    )
  }
  let items: Feedback[] = []
  thread(label: "writingReviewAgent", session: session) {
    items = judgeText(
      text: text,
      task: task,
      guidelines: guidelines,
      context: context,
      model: model,
      provider: provider,
      extraTools: extraTools,
    )
  }
  return [...items, ...cuts, ...tics]

  // The budget ran out during the main pass: the findings the model saved,
  // with the side passes' findings. Null when it saved none, so the guard
  // reports the stop.
  finalize {
    if (items == null) {
      return null
    }
    return [...items, ...cuts, ...tics]
  }
}

/** What an eval hands the reviewer: the task the text was written for, and
  the file holding that text, seeded into the working directory by the
  test's `files/`. The shape the `evals/writing-review` suite uses as its
  input. */
export type WritingReviewEvalInput = {
  assignment: string;
  sourceFile: string
}

// Any other reviewer scored on the same suite supplies a node with this
// signature. Effects are decided at this boundary. Reading the seeded input
// file is this node's own decision (`with approve`). The reviewer runs under
// `docsOnlyHandler`, which lets it read the packaged docs and rejects every
// other interrupt, so a budget trip stays a failure and the reviewer's
// fail-open result comes back instead of an interrupt escaping the entry
// point.
/** Run this module through an eval suite with `agency eval run stdlib/agents/writing/review.agency:evalMain --suite <dir>`. */
node evalMain(input: WritingReviewEvalInput): Feedback[] {
  evalValue(input)
  const source = read(input.sourceFile) with approve
  if (source is failure(readErr)) {
    return [
      {
        error: true,
        feedback: "could not read ${input.sourceFile}: ${readErr}"
      },
    ]
  }
  let reviewed: Result<Feedback[]> = failure("the reviewer never ran")
  handle {
    reviewed = writingReviewAgent(text: source.value, task: input.assignment)
  } with docsOnlyHandler
  if (reviewed is failure(err)) {
    return [{
      error: true,
      feedback: "review agent failed: ${err}"
    }]
  }
  return reviewed.value
}
