import 'server-only'
import type { ConstructDef } from './constructs'
import { ITEMS_PER_CONSTRUCT } from './constructs'
import { rubricSystemPrompt } from './rubrics'

/**
 * Prompt construction. These are built on the VPS and shipped to the Mac agent
 * in its work payload — the agent never composes its own instructions, so the
 * exact text that produced a score is reproducible from this file plus the
 * version recorded on the grading.
 *
 * Everything the model is asked for comes back as strict JSON on stdout. There
 * is no structured-output API here to lean on, so each prompt states the shape
 * explicitly and the server validates what comes back (see the /api/agent
 * routes) rather than trusting it.
 */

// ── Grading ──────────────────────────────────────────────────────────────────

export interface GradingPromptInput {
  construct: ConstructDef
  /** Item format — a computation control is not scored on reasoning. */
  format?: string
  itemPrompt: string
  itemStimulus: string | null
  rubricCriteria: string
  typedText: string | null
  /** Local filenames the agent has already downloaded into its scratch dir. */
  imageFiles: string[]
}

/**
 * Note what this prompt does NOT contain: no name, no sibling, no prior score,
 * no round number, no baseline/re-test marker (Rule 4). The agent is also run
 * in a scratch directory with tools restricted to Read, so blindness is
 * enforced by construction and not merely by instruction — it has nothing to
 * look up even if it tried.
 */
export function buildGradingPrompt(input: GradingPromptInput): string {
  // A computation control asks for answers and nothing else — its own criteria
  // say so. Scoring it on reasoning invites exactly the disagreement it caused
  // in practice (one pass gave 0 for "no explanation", the other gave full
  // marks for "none required"), and marking a boy down for not writing
  // something he was never asked for is how an automated grader manufactures a
  // deficit that does not exist.
  const answersOnly = input.format === 'COMPUTATION_CONTROL'
  const scored = input.construct.dimensions.filter(
    (d) => !(answersOnly && d.code === 'reasoning_quality'),
  )

  const shape = scored
    .map((d) => `    "${d.code}": {"value": <0-${d.maxValue}>, "justification": "<why>"}`)
    .join(',\n')

  const omission = answersOnly
    ? `\nTHIS ITEM ASKS FOR ANSWERS ONLY. No explanation is required or expected of
him, so reasoning is NOT scored here — omit "reasoning_quality" from your output
entirely rather than guessing at a value. Its absence is correct and expected.\n`
    : ''

  const images = input.imageFiles.length
    ? `\nTHE RESPONSE IS HANDWRITTEN. Use the Read tool on each file below and read the work as written. If a step is genuinely illegible say so in the justification rather than assuming it is wrong. Do not transcribe first and grade the transcription — grade what you see.
${input.imageFiles.map((f) => `  ${f}`).join('\n')}\n`
    : ''

  return `${rubricSystemPrompt(input.construct)}

THE ITEM THE STUDENT WAS GIVEN
${input.itemStimulus ? `\nSource material:\n${input.itemStimulus}\n` : ''}
Question:
${input.itemPrompt}

SCORING CRITERIA FOR THIS ITEM
${input.rubricCriteria}
${omission}${images}
THE STUDENT RESPONSE
${input.typedText ?? '(handwritten — see the image file(s) above)'}

Score each dimension independently and explain each score.

Output STRICT JSON ONLY. No prose before or after, no code fences:
{
${shape},
    "overall_note": "<one or two sentences for the parent: terse, criterion-referenced. NOT feedback for the student.>"
}`
}

// ── Student feedback (Rule 7: separate job, separate audience) ────────────────

export interface FeedbackPromptInput {
  grade: number
  itemPrompt: string
  responseText: string | null
  hasHandwrittenWork: boolean
  graderNote: string
}

export function buildFeedbackPrompt(input: FeedbackPromptInput): string {
  return `You write a short note back to a child about one piece of work they just
did, for a parent-run practice program at home.

Who reads this: the child, directly. Nobody edits it first.

Rules:
- Two to four sentences. No more.
- Name one specific thing they actually did well, quoting their own words or
  method. Generic praise is worse than none — they can tell.
- Name at most one thing to try next time, phrased as a next step rather than a
  correction. If the work was strong it is fine to have nothing here.
- Never mention scores, levels, percentages, standards, or how anyone else did.
- Never imply they are behind, being fixed, or being assessed.
- Write at their grade level, warmly and plainly. No exclamation-mark confetti —
  they are old enough to notice hollow enthusiasm.
- If the work shows a real misunderstanding, be honest about it in a way that
  leaves them wanting to try the next one. Do not paper over it.

Grade ${input.grade}.

The question they were given:
${input.itemPrompt}

What they wrote:
${input.responseText ?? '(they showed their work on paper rather than typing)'}
${input.hasHandwrittenWork ? '\nThey handed in handwritten work.' : ''}

For your context only — an assessor's private note on this response. Do not
quote it, refer to it, or repeat its framing:
${input.graderNote}

Output STRICT JSON ONLY, no prose, no code fences:
{"feedback": "<their note>"}`
}

// ── Item generation ──────────────────────────────────────────────────────────

const GENERATOR_PREAMBLE = `You write custom assessment items for one family's three
sons, at their parent's request. The parent is a technical, involved father; the
boys are strong students and none of them is behind. Your items are a DIAGNOSTIC
INSTRUMENT first and practice second.

WHAT MAKES AN ITEM GOOD HERE

Target constructs, not subjects. Every item names one construct and is written to
produce evidence about that construct specifically.

Ask for reasoning, not just answers. For most of these constructs the reasoning IS
the measured thing, and an answer key alone cannot detect the gap. Written
responses are graded against per-dimension criteria, so constructed response is
cheap and is the default. Prefer it.

The formats that actually produce evidence about mathematical practices:
  - multi-step word problems with NO cue about which operation to use
  - problems containing extra irrelevant information, or missing information the
    student must notice is missing
  - error analysis: "another student got X — explain what they did wrong"
  - critique: "two students disagree — who is right, and why?"
  - estimate first, then compute, then explain the gap
  - modelling: an untidy real situation where the student chooses the
    representation. Their real life is good material — the family runs a quail
    operation, builds things, and keeps a household budget.

Pair a modelling item with a computation-only item on the SAME underlying skill
wherever you can. If the boy gets the computation right and the modelling wrong,
the gap is comprehension and framing rather than math — and no state test can
make that distinction. Give both items the same pair_group.

THE CRITERIA MAY ONLY REQUIRE WHAT THE QUESTION ACTUALLY ASKED

The student never sees rubric_criteria. So anything the criteria demand that the
prompt did not ask for is a mark-down for failing to do something he was never
told to do.

This happened for real: a computation item asked "the volume of a rectangular
prism that is 6 ft long, 3 ft wide, and 1 ft deep", the criteria said "require
cubic units to be named", and a boy who got all four answers right scored zero
on the answer dimension for writing "18" instead of "18 ft3". He was never asked
for units.

So, before you write each rubric_criteria, read your own prompt back and check:
does it ASK for everything the criteria will require? Units, showing work,
"explain why", a specific number of reasons, a particular format — if the
criteria will look for it, the prompt must request it. Otherwise drop it from
the criteria.

THE ANTI-GOAL, WHICH MATTERS MORE THAN ANYTHING ABOVE

Do NOT write computation drill, fact practice, or algorithm repetition as a
target in its own right. These boys are already above standard on computation.
More of it will produce no measurable movement and will read to them as
pointless busywork. Computation appears ONLY as the control half of a matched
pair. If you find yourself writing a set of arithmetic exercises, you have
misunderstood the task.

SIZE AND TONE

Small and targeted beats comprehensive: ${ITEMS_PER_CONSTRUCT.min} to ${ITEMS_PER_CONSTRUCT.max} items per construct, and fewer
if the state provided is thin. These are children taking a test at home and
fatigue destroys the signal — their own testing history contains a result
invalidated by a rushed session.

Nothing student-facing may read as remediation or as a verdict. No item, title or
scenario should imply the student is behind, being fixed, or being compared to
anyone. Write at the boy's grade level in plain, warm language.

Never reproduce a secure or copyrighted test item. Write original items; released
practice material is a format reference only.`

const ITEM_SHAPE = `{
  "title": "<short, encouraging, age-appropriate — the student sees this>",
  "target_plan": "<which constructs this set targets and why, given the state above>",
  "items": [
    {
      "construct_code": "<one of the construct codes supplied above>",
      "format": "MULTIPLE_CHOICE|CONSTRUCTED_RESPONSE|ESTIMATE_THEN_JUSTIFY|ERROR_ANALYSIS|CRITIQUE|MODELING|COMPUTATION_CONTROL|REVISE_PARAGRAPH|SOURCE_RESPONSE",
      "stimulus": "<passage/scenario/data, or null. Two paired items MUST share byte-identical stimulus text.>",
      "prompt": "<the question, exactly as the student will read it>",
      "choices": [{"key": "A", "text": "..."}] ,
      "answer_key": "<e.g. \\"B\\", MULTIPLE_CHOICE only, otherwise null>",
      "rubric_criteria": "<what earns credit, as CRITERIA — never a model answer. The grader sees this instead of an ideal response, because children express correct reasoning in unexpected ways and a model answer makes the grader pattern-match phrasing.>",
      "rationale": "<why this item, for this boy, right now, referencing his current state. Shown to the parent on the review screen, never to the student.>",
      "pair_group": "<shared string for a matched pair, or null>",
      "addresses_question_id": "<the question_id from UNRESOLVED QUESTIONS that this item re-tests, or null>"
    }
  ]
}`

export function buildGenerationPrompt(snapshot: {
  grade: number
  course_note: string | null
  constructs: unknown
  previously_used_items: string[]
  unaddressed_questions: unknown[]
}): string {
  return `${GENERATOR_PREAMBLE}

Write the next set for a Grade ${snapshot.grade} student.${
    snapshot.course_note ? ` Current math course: ${snapshot.course_note}.` : ''
  }

CONSTRUCTS YOU MAY TARGET — you may not invent others
${JSON.stringify(snapshot.constructs, null, 2)}

HOW TO CHOOSE
Any construct with no baseline yet is a candidate for one, and a baseline set
should be broad enough to locate him rather than narrow enough to confirm a
guess. Where evidence exists, target the dimension the evidence says is weakest —
and read the dimensions separately. A boy scoring well on "answer" and poorly on
"reasoning" needs items that force justification, not harder arithmetic. A boy
scoring poorly on "strategy" needs unfamiliar problem shapes. Those are different
sets. Where the evidence is thin (few items observed, or items excluded for
grader disagreement), say so in your target_plan and prefer gathering more
evidence on the same construct over moving to a new one.

Read "items_given_up_on" carefully. Those are items he abandoned and asked to
have explained, so they carry no score — a construct with a low count of scored
items but a high count of concessions looks deceptively fine in the numbers and
is in fact where he is struggling most. Weight it accordingly, and when you
target such a construct, come in slightly easier: the goal is to get him
producing work again, not to confirm he finds it hard.

Target at most two constructs in one set.

ITEMS HE HAS ALREADY BEEN GIVEN — do not repeat or lightly reword any of these
${
  snapshot.previously_used_items.length
    ? snapshot.previously_used_items.join('\n')
    : '(none yet — this is his first set)'
}

UNRESOLVED QUESTIONS — the sharpest thing you have, and the reason this set exists
${
  snapshot.unaddressed_questions.length
    ? `${JSON.stringify(snapshot.unaddressed_questions, null, 2)}

Each of these is a moment where he was stuck and said so in his own words —
either mid-problem asking for a hint, or after giving up and being shown how it
worked. Read "kind" to tell them apart: a hint request is the sharper signal,
because he was part-way through and named the exact step he could not make.

Either way this is far more specific than any score: it names the idea he could
not get hold of.

For EVERY question above, include one item in this set that re-tests that same
underlying idea, and set "addresses_question_id" on it to that question_id.

Two rules about how:

  - Put it in a GENUINELY DIFFERENT context. If he asked why you divide by three
    when a bag lasts three weeks, do not write another feed-bag problem — write
    something where the same per-unit reasoning is needed and nothing else looks
    familiar. Recognising the wrapper is not understanding the idea, and an item
    that lets him pattern-match tells you nothing about whether the explanation
    landed.

  - Do not signpost it. He must not be able to tell that this item is the
    follow-up to his question, or he will approach it differently. No "remember
    when", no echoing his phrasing back at him.

Pitch these at a level he can actually reach. The point is to find out whether he
now understands the idea, not to prove it is still hard — and a boy who asked for
help and then met a harder version of the same thing learns not to ask.

If a question is vague or you cannot tell what idea sits behind it, say so in
target_plan and leave addresses_question_id null rather than guessing.`
    : '(none — he has not asked anything that is still open)'
}

Output STRICT JSON ONLY. No prose before or after, no code fences:
${ITEM_SHAPE}`
}

// ── Draft review (the second agent) ─────────────────────────────────────────

export interface ReviewPromptInput {
  grade: number
  courseNote: string | null
  targetPlan: string | null
  constructs: { code: string; label: string; description: string; rationale: string }[]
  items: {
    position: number
    constructCode: string
    format: string
    stimulus: string | null
    prompt: string
    choices: { key: string; text: string }[] | null
    answerKey: string | null
    rubricCriteria: string
    pairedWith: number | null
  }[]
  /** Everything this boy has already been given, for the repeat check. */
  previouslyUsedItems: string[]
}

/**
 * The reviewer is a second, independent Claude session whose only job is to
 * catch the first one doing something stupid. It is given the rules verbatim
 * and asked to check the draft against them — not to write items, not to
 * improve prose, and not to second-guess a defensible judgement call.
 *
 * It is deliberately told what NOT to flag. A reviewer that objects to
 * everything is the same as no reviewer, because Eric will stop reading it.
 */
export function buildReviewPrompt(input: ReviewPromptInput): string {
  return `You are reviewing a draft assessment set that another model just wrote, before
a parent sees it. You did not write it and you have no stake in it. Your only
job is to catch mistakes that would waste an administration or produce a false
measurement.

WHY THIS MATTERS: a bad item does not merely look wrong. The boy's answers feed
a per-construct state model that decides what he is given next, so a bad item
writes a false reading into the record and the following set gets aimed at a gap
that was never there. That is the failure you are guarding against.

THE RULES THIS SET MUST FOLLOW

1. THE ANTI-GOAL, and the most important thing you check. Computation drill,
   fact practice and algorithm repetition are forbidden as targets in their own
   right — this student is already above standard on computation, so more of it
   measures nothing and reads to him as busywork. Computation may appear ONLY as
   the matched control half of a pair, where its job is to prove he can do the
   arithmetic so that failing the modelling half means the gap is framing rather
   than math. A "modelling" item that is really an arithmetic exercise with a
   story pasted on top is the exact mistake to catch. So is a control item that
   does not actually match its partner's underlying skill.

2. CRITERIA, NOT A MODEL ANSWER. rubric_criteria is shown to the grader instead
   of an ideal response, precisely so the grader judges reasoning rather than
   pattern-matching phrasing. Criteria that quote or paraphrase an expected
   answer will cause a child who is right in unexpected words to be marked wrong.

3. THE CRITERIA MAY ONLY REQUIRE WHAT THE PROMPT ASKED FOR. The student never
   sees rubric_criteria. If the criteria penalise something the question did not
   request — units, showing work, an explanation, a set number of reasons, a
   particular format — then a boy who did exactly what was asked loses marks for
   it. Read the prompt and the criteria against each other every time; this is
   the check most worth your attention, and a real set shipped with "require
   cubic units to be named" against a prompt that only said "find each answer".
   Either is fixable, so say which: the prompt should ask for it, or the criteria
   should stop requiring it.

4. GRADE LEVEL. Items must be answerable by a student at the stated grade
   without knowledge he would not yet have. Flag anything that needs a later
   course. Do NOT flag an item merely for being hard — difficulty is the point.

5. NO REPEATS. An item that repeats or lightly rewords something he has already
   been given produces no new evidence. The prior items are listed below.

6. TONE. Nothing may read as remediation, as a verdict, or as a comparison to
   anyone. These are children and the material is not framed to them as a test
   of whether they are behind.

7. SOUNDNESS. The item must be answerable as written: no missing information it
   does not intend to be missing, no contradictory numbers, no multiple-choice
   key that is wrong or has two defensible answers. (Deliberately withheld
   information the student is meant to NOTICE is missing is correct design, not
   an error — do not flag it.)

WHAT NOT TO FLAG
Do not flag stylistic preferences, an item you would have written differently,
an unusual but valid approach, or difficulty on its own. Do not rewrite items.
If you flag everything, the parent stops reading you and the review is worthless.
Most items in a good set should come back KEEP.

VERDICTS
- "DROP"  — breaks rule 1, 2, 3, 5 or 7. The item is withheld from the boy
            automatically, so use it only when you are confident.
- "FLAG"  — worth the parent's eye but not a clear violation. Stays in the set.
- "KEEP"  — no concern.

THE STUDENT: Grade ${input.grade}${input.courseNote ? `, currently taking ${input.courseNote}` : ''}.

CONSTRUCTS THIS SET MAY TARGET
${input.constructs.map((c) => `- ${c.code} (${c.label}): ${c.description}\n  Why it exists: ${c.rationale}`).join('\n')}

THE GENERATOR'S STATED PLAN
${input.targetPlan ?? '(none given)'}

ITEMS HE HAS ALREADY BEEN GIVEN (rule 4)
${input.previouslyUsedItems.length ? input.previouslyUsedItems.join('\n') : '(none — this is his first set)'}

THE DRAFT
${JSON.stringify(input.items, null, 2)}

Output STRICT JSON ONLY, no prose, no code fences:
{
  "summary": "<two or three sentences for the parent: does this set do what its plan claims, and is there anything about it as a WHOLE he should know? Say plainly if it is fine.>",
  "items": [
    {"position": <the item's number, exactly as given above>, "verdict": "KEEP|FLAG|DROP", "note": "<one or two sentences. Required for FLAG and DROP; empty string for KEEP. Name the rule and quote the specific text. When you refer to another item, use its number as given above.>"}
  ]
}
Return one entry for every item, in position order.`
}

// ── Teaching explanations (shown when a boy concedes) ───────────────────────

export interface ExplanationPromptInput {
  grade: number
  items: {
    position: number
    stimulus: string | null
    prompt: string
    choices: { key: string; text: string }[] | null
    answerKey: string | null
    rubricCriteria: string
  }[]
}

/**
 * Written when the set is generated and stored, so it appears the instant a boy
 * asks. He only ever sees this after giving up on that item, and he has already
 * written down what he tried — so this is a teaching moment, not a spoiler.
 */
export function buildExplanationPrompt(input: ExplanationPromptInput): string {
  return `Write a teaching explanation for each problem below. A Grade ${input.grade} student
reads it at the moment he gives up on that problem, having just written down
what he tried.

He is not in trouble and he is not behind. He asked to understand this. Write to
someone who wanted to know.

WHAT EACH EXPLANATION MUST DO

Start where he is stuck, not at the beginning of the topic. The first sentence
should name the decision the problem turns on — the thing that is genuinely hard
about it — rather than restating the question back at him.

Walk the whole way through, showing the actual numbers from this problem. Do not
skip the step you consider obvious; it is usually the one that lost him.

Say WHY each step, not just what. "Divide by 3 because the bag has to cover three
weeks and we want one week" teaches; "divide 23.60 by 3" does not.

Name the trap. Most of these problems have a specific wrong turn — a number that
looks relevant and isn't, an operation the wording seems to suggest but doesn't,
a remainder that has to be rounded a particular way because of what it means.
Say plainly what that trap is and how to spot it next time.

End with the answer stated plainly, so there is no ambiguity about what it was.

HOW TO WRITE IT

Plain language at his grade level. Short paragraphs. You may use a few lines of
worked arithmetic where they help, but this is prose that teaches, not a
worked-solutions key.

Never say or imply he should have got it, that it was easy, or that most students
find this simple. Never compare him to anyone.

Six to twelve sentences for most problems. Longer only if the problem genuinely
has several parts.

THE PROBLEMS
${JSON.stringify(input.items, null, 2)}

Output STRICT JSON ONLY, no prose, no code fences:
{"explanations": [{"position": <the item's number as given above>, "explanation": "<the explanation>"}]}
Return one entry for every problem.`
}

// ── A boy's follow-up question ──────────────────────────────────────────────

export interface QuestionPromptInput {
  grade: number
  itemPrompt: string
  itemStimulus: string | null
  explanation: string
  /** What he wrote before giving up. */
  attempt: string | null
  /** Earlier questions and answers on this same item, oldest first. */
  history: { question: string; answer: string }[]
  question: string
}

export function buildQuestionPrompt(input: QuestionPromptInput): string {
  const history = input.history.length
    ? `\nWHAT HE HAS ALREADY ASKED ABOUT THIS PROBLEM, AND WHAT HE WAS TOLD
${input.history.map((h) => `He asked: ${h.question}\nHe was told: ${h.answer}`).join('\n\n')}\n`
    : ''

  return `A Grade ${input.grade} student is asking a follow-up question about a problem he
gave up on and has just had explained to him. Answer him directly.

RULES
- Answer the question he actually asked. Do not restate the whole explanation.
- Two to six sentences unless he asked something that genuinely needs more.
- Stay on this problem and the maths or reading behind it. If he asks about
  something else entirely, answer briefly and kindly and bring it back.
- Plain language at his grade level. No jargon he has not met.
- If his question reveals a misunderstanding underneath the one he asked about,
  address that too — that is usually the more useful answer.
- Never imply the question was silly, obvious, or already answered.
- If you genuinely cannot tell what he is asking, say so plainly and ask him one
  short question back.

THE PROBLEM HE WAS WORKING ON
${input.itemStimulus ? `${input.itemStimulus}\n\n` : ''}${input.itemPrompt}

WHAT HE TRIED BEFORE GIVING UP
${input.attempt || '(he did not say)'}

THE EXPLANATION HE WAS SHOWN
${input.explanation}
${history}
HIS QUESTION
${input.question}

Output STRICT JSON ONLY, no prose, no code fences:
{"answer": "<your answer to him>"}`
}

// ── Hints ───────────────────────────────────────────────────────────────────

/** Three, then the only honest thing left is the full explanation. */
export const MAX_HINTS = 3

export interface HintPromptInput {
  grade: number
  itemPrompt: string
  itemStimulus: string | null
  rubricCriteria: string
  /** Hints already given on this item, oldest first. */
  previousHints: { question: string; hint: string }[]
  question: string
  hintNumber: number
}

/**
 * A hint is a rung, not a shortcut. The boy is still going to answer this item
 * and it is still going to be scored, so a hint that hands over the answer does
 * not help him — it destroys the measurement and teaches him that asking is a
 * way to be given things.
 *
 * The constraint that does the work is the last one: give him the NEXT step to
 * think about, never the step itself worked out.
 */
export function buildHintPrompt(input: HintPromptInput): string {
  const prior = input.previousHints.length
    ? `\nHINTS HE HAS ALREADY HAD ON THIS PROBLEM
${input.previousHints
  .map((h, i) => `${i + 1}. He asked: ${h.question}\n   You said: ${h.hint}`)
  .join('\n')}

Do not repeat these. He is still stuck after them, so find the different thing
that is in his way.\n`
    : ''

  const lastCall =
    input.hintNumber >= MAX_HINTS
      ? `\nThis is the last hint he can have on this problem. After it his only
option is to give up and be shown the full worked explanation. Make it the most
useful one you can — but it is still a hint, and it still must not contain the
answer. Do not compensate for being last by giving more away.\n`
      : ''

  return `A Grade ${input.grade} student is part-way through a problem, is stuck, and has
asked for a hint. He is going to finish the problem himself and his answer will
be scored, so your job is to get him moving again — not to do it for him.

WHAT A HINT IS
One step forward from where he actually is. Point at the thing he has not
noticed, or ask him the question he has not asked himself. Then stop.

WHAT A HINT IS NOT
- Never the answer, or any part of the final numeric answer.
- Never a worked step. "Divide 27 by 1.5" is not a hint, it is the step. "You
  know how much soil you need — now look at how soil is sold" is a hint.
- Never the whole method laid out, even in outline.
- Never more than one step ahead. If he needs three more moves, give him the
  first one only.

HOW TO WRITE IT
- One to three sentences. Short.
- Answer the specific thing he said he was stuck on. If he has already done part
  of the work correctly, say so first in a few words — he often does not know
  which part was right, and that alone unsticks people.
- Plain language at his grade level.
- If what he wrote reveals a wrong turn earlier than the step he is asking
  about, point at the wrong turn instead. Answering the wrong question politely
  is worse than useless here.
- Never say the problem is easy, and never imply he should have seen it.
- If you genuinely cannot tell what he is stuck on, ask him one short question
  back rather than guessing.

THE PROBLEM HE IS WORKING ON
${input.itemStimulus ? `${input.itemStimulus}\n\n` : ''}${input.itemPrompt}

WHAT EARNS CREDIT ON THIS PROBLEM — for your understanding only. He must not be
able to reconstruct the answer from your hint, and this tells you what to
protect.
${input.rubricCriteria}
${prior}${lastCall}
WHAT HE SAID HE IS STUCK ON
${input.question}

Output STRICT JSON ONLY, no prose, no code fences:
{"hint": "<your hint>"}`
}

// ── The spiritual track ─────────────────────────────────────────────────────

const SPIRITUAL_PREAMBLE = `You write questions that help three brothers think about
what they are being taught from the Bible and from their church's ministry
material. Their father asked for this. It is a separate line of learning from
their schoolwork and it is not a test of their faith.

WHAT IS BEING MEASURED, AND WHAT IS NOT

Two things only: whether he has the content right, and whether he understands
what it means in his own words.

You are NOT measuring, and must never write an item that probes: how he lives,
what he personally believes, how sincere or spiritual he is, whether he prays or
reads, or how he compares to his brothers. Never ask him to disclose a private
struggle, confess anything, or rate himself. If an item would only be answerable
by a boy revealing his own spiritual state, it is the wrong item — ask about the
concept instead.

The difference in practice:
  WRONG: "Do you call on the Lord when you are worried? Describe a time."
  RIGHT: "What does it mean to call on the Lord's name? Why do you think the
          Bible tells us to do it out loud rather than only in our heads?"

  WRONG: "How has the Lord been real to you this week?"
  RIGHT: "In the story, what did the disciples notice about Jesus after the
          storm that they had not seen before?"

The second of each pair can be answered well or poorly on its merits. The first
cannot be scored at all without judging the boy, which is not what this is for.

WHAT A GOOD ITEM LOOKS LIKE HERE

Ask for his own words. "Say what this means as if you were telling your younger
brother" produces something you can actually read; "what are the three points of
salvation" produces recitation.

Prefer why over what, once the what is established. A boy can carry correct
vocabulary a long way without it meaning anything yet, and finding that out is
most of the value here.

Use the narrative. Where a concept comes from a story, ask about the story — it
is concrete, memorable, and a child can reason about it.

Honest questions are allowed and welcome. "Some people find this hard to
understand. What do you think is confusing about it?" is a good item: it invites
thinking rather than performance, and it is scoreable on the quality of the
thinking.

COPYRIGHT — this one is not negotiable

The ministry books are in copyright. Write ORIGINAL questions about the concepts.
Do NOT reproduce, paraphrase closely, or quote from Witness Lee's or Watchman
Nee's writings, and do not present any wording as being from them. You may quote
short scripture passages, and where you do, use a public-domain translation
(King James or World English Bible) and name it.

TONE

They are children who are being brought up in this, not converts being examined.
Warm, plain, curious. Nothing that reads as a test of whether they are good
enough, and nothing that would embarrass a boy who got it wrong.`

export function buildSpiritualPrompt(snapshot: {
  grade: number
  constructs: unknown
  topics: { id: string; construct_code: string; label: string; notes: string | null; last_covered: string | null }[]
  previously_used_items: string[]
  unaddressed_questions: unknown[]
}): string {
  return `${SPIRITUAL_PREAMBLE}

Write the next set for a Grade ${snapshot.grade} student.

CONSTRUCTS YOU MAY TARGET — you may not invent others
${JSON.stringify(snapshot.constructs, null, 2)}

TOPICS HIS FATHER WANTS COVERED
${
  snapshot.topics.length
    ? `${JSON.stringify(snapshot.topics, null, 2)}

Choose one or two of these, favouring ones covered longest ago or not at all.
Name the topic ids you used in target_plan. Do not go outside this list — it is
how he steers what they are learning, and wandering off it makes the list
pointless.`
    : '(the list is empty — say so in target_plan and write a broad, gentle set from the constructs above)'
}

Total 8 to 12 items. Fewer if the topics are narrow.

ITEMS HE HAS ALREADY BEEN GIVEN — do not repeat or lightly reword any of these
${
  snapshot.previously_used_items.length
    ? snapshot.previously_used_items.join('\n')
    : '(none yet — this is his first set on this track)'
}

UNRESOLVED QUESTIONS — things he asked about and that nothing has re-tested yet
${
  Array.isArray(snapshot.unaddressed_questions) && snapshot.unaddressed_questions.length
    ? `${JSON.stringify(snapshot.unaddressed_questions, null, 2)}

For each, include one item that comes at the same idea from a different angle,
and set "addresses_question_id" to that question_id. Do not signpost it.`
    : '(none)'
}

Output STRICT JSON ONLY. No prose before or after, no code fences:
${ITEM_SHAPE}`
}
