import 'server-only'
import { db } from './db'
import { CONSTRUCTS_BY_CODE, STUDENT_CONSTRUCTS } from './constructs'
import { buildReviewPrompt } from './prompts'

/**
 * The second agent. Reviews a generated draft before Eric sees it, and before
 * any boy can. DROP verdicts are acted on automatically — the item is withheld —
 * but nothing is deleted and every reason stays visible on the review screen,
 * so Eric can restore anything the reviewer got wrong.
 */

export async function buildReviewPayload(setId: string) {
  const set = await db.itemSet.findUniqueOrThrow({
    where: { id: setId },
    include: {
      student: true,
      generationRun: true,
      items: {
        orderBy: { position: 'asc' },
        include: { item: true },
      },
    },
  })

  const allowed = STUDENT_CONSTRUCTS[set.student.slug] ?? []
  // 1-based throughout the reviewer's view, so the item numbers in its notes
  // match what Eric sees on the review screen. Sending raw 0-based positions
  // made every note point one item off target.
  const positionOf = new Map(set.items.map((si) => [si.item.id, si.position + 1]))

  // Items from *earlier* sets only — the draft under review is not its own
  // repeat evidence.
  const priorItems = await db.item.findMany({
    where: {
      studentId: set.studentId,
      setItems: { none: { setId } },
    },
    select: { constructCode: true, prompt: true, format: true },
    orderBy: { createdAt: 'desc' },
    take: 250,
  })

  return {
    setId,
    prompt: buildReviewPrompt({
      grade: set.student.grade,
      courseNote: set.student.courseNote,
      targetPlan: (set.generationRun?.targetPlan as { plan?: string } | null)?.plan ?? null,
      constructs: allowed.map((code) => {
        const def = CONSTRUCTS_BY_CODE[code]
        return {
          code,
          label: def?.label ?? code,
          description: def?.description ?? '',
          rationale: def?.rationale ?? '',
        }
      }),
      items: set.items.map((si) => ({
        position: si.position + 1,
        constructCode: si.item.constructCode,
        format: si.item.format,
        stimulus: si.item.stimulus,
        prompt: si.item.prompt,
        choices: si.item.choices as { key: string; text: string }[] | null,
        answerKey: si.item.answerKey,
        rubricCriteria: si.item.rubricCriteria,
        pairedWith:
          si.item.pairedItemId !== null
            ? (positionOf.get(si.item.pairedItemId) ?? null)
            : null,
      })),
      previouslyUsedItems: priorItems.map(
        (i) => `[${i.constructCode}/${i.format}] ${i.prompt}`,
      ),
    }),
  }
}

export interface ReviewVerdictIn {
  position?: unknown
  verdict?: unknown
  note?: unknown
}

/** KEEP < FLAG < DROP — merging two passes always keeps the harsher verdict. */
const SEVERITY: Record<string, number> = { KEEP: 0, FLAG: 1, DROP: 2 }

/**
 * Record a reviewer pass, MERGING with anything already there.
 *
 * Two independent passes are run over every generated set, because the reviewer
 * is not deterministic: run it twice on the same draft and it surfaces largely
 * different findings (observed on a real set — five findings, then six entirely
 * different ones). One pass is a spot check; two is coverage.
 *
 * Merging rules: the harsher verdict wins, so a DROP from either pass withholds
 * the item; notes from both are kept, attributed, because a second reader's
 * reason for the same verdict is often the more useful one.
 */
export async function persistReview(opts: {
  setId: string
  summary: string
  verdicts: ReviewVerdictIn[]
  /** 1 or 2. Pass 2 merges into pass 1 rather than replacing it. */
  pass?: number
}): Promise<{ kept: number; flagged: number; dropped: number }> {
  const set = await db.itemSet.findUniqueOrThrow({
    where: { id: opts.setId },
    include: { items: true },
  })

  let kept = 0
  let flagged = 0
  let dropped = 0

  for (const raw of opts.verdicts) {
    // The reviewer speaks in 1-based item numbers (see buildReviewPayload);
    // storage is 0-based.
    const position = Number(raw.position) - 1
    const verdict = String(raw.verdict ?? '').toUpperCase()
    if (!Number.isInteger(position)) continue
    if (!['KEEP', 'FLAG', 'DROP'].includes(verdict)) continue

    const si = set.items.find((x) => x.position === position)
    if (!si) continue

    const note = typeof raw.note === 'string' ? raw.note.trim() : ''
    const pass = opts.pass ?? 1

    const existing = pass > 1 ? si.reviewVerdict : null
    const winner =
      existing && SEVERITY[existing] >= SEVERITY[verdict]
        ? existing
        : (verdict as 'KEEP' | 'FLAG' | 'DROP')

    // Keep both readers' reasoning when both had something to say.
    let mergedNote: string | null = note || null
    if (pass > 1 && si.reviewNote && note && !si.reviewNote.includes(note)) {
      mergedNote = `Reviewer 1: ${si.reviewNote}\n\nReviewer 2: ${note}`
    } else if (pass > 1 && si.reviewNote && !note) {
      mergedNote = si.reviewNote
    }

    await db.setItem.update({
      where: { id: si.id },
      data: {
        reviewVerdict: winner,
        reviewNote: mergedNote,
        droppedByReviewer: winner === 'DROP',
      },
    })

    if (winner === 'DROP') dropped++
    else if (winner === 'FLAG') flagged++
    else kept++
  }

  // Only move a draft into Eric's queue. A set that is already released (or
  // being worked on) keeps its status: pulling it back mid-sitting would take
  // work away from a boy who is part-way through it. Dropped items are withheld
  // either way, so a live set is still protected from a bad item.
  const preRelease = ['DRAFT', 'AWAITING_REVIEW', 'PENDING_REVIEW'].includes(set.status)
  const summary =
    (opts.pass ?? 1) > 1 && set.reviewSummary
      ? `${set.reviewSummary}\n\n— Second reviewer —\n${opts.summary}`
      : opts.summary

  await db.itemSet.update({
    where: { id: opts.setId },
    data: {
      reviewedAt: new Date(),
      reviewSummary: summary.slice(0, 8000),
      ...(preRelease ? { status: 'PENDING_REVIEW' as const } : {}),
    },
  })

  return { kept, flagged, dropped }
}
