import 'server-only'
import { db } from './db'
import { formatDuration, summarisePacing } from './timing'

/**
 * The results email.
 *
 * Composed here rather than on the Mac so the presentation rules live with the
 * data and stay versioned: per-dimension only (never a total), n shown beside
 * every number, disagreements surfaced, and pacing stated. The agent only sends
 * what this produces.
 */

export interface Report {
  subject: string
  body: string
}

export async function buildResultsReport(setId: string): Promise<Report | null> {
  const set = await db.itemSet.findUnique({
    where: { id: setId },
    include: {
      student: true,
      items: {
        orderBy: { position: 'asc' },
        include: {
          item: { include: { construct: { include: { dimensions: true } } } },
          response: {
            include: {
              gradings: {
                include: { scores: { include: { dimension: true } } },
                orderBy: [{ gradedBy: 'asc' }, { pass: 'asc' }],
              },
            },
          },
        },
      },
    },
  })
  if (!set) return null

  const responses = set.items.map((si) => si.response).filter((r) => r !== null)
  if (!responses.length) return null

  // Per construct, per dimension: mean of accepted scores, normalised. Flagged
  // responses are excluded and counted, exactly as the state model does.
  const byConstruct = new Map<
    string,
    {
      label: string
      dims: Map<string, { label: string; values: number[]; max: number }>
      flagged: number
      conceded: number
      hinted: number
      items: number
    }
  >()

  for (const si of set.items) {
    if (!si.response) continue
    const construct = si.item.construct
    if (!byConstruct.has(construct.code)) {
      byConstruct.set(construct.code, {
        label: construct.label,
        dims: new Map(),
        flagged: 0,
        conceded: 0,
        hinted: 0,
        items: 0,
      })
    }
    const entry = byConstruct.get(construct.code)!
    entry.items++
    if (si.response.conceded) {
      entry.conceded++
      continue
    }
    if (si.response.flagged) {
      entry.flagged++
      continue
    }
    if (si.response.hintsUsed > 0) entry.hinted++
    const human = si.response.gradings.find((g) => g.gradedBy === 'HUMAN')
    const ai = si.response.gradings.filter((g) => g.gradedBy === 'AI')
    for (const dim of construct.dimensions) {
      const pick = human
        ? [human.scores.find((s) => s.dimensionId === dim.id)?.value]
        : ai.map((g) => g.scores.find((s) => s.dimensionId === dim.id)?.value)
      const values = pick.filter((v): v is number => typeof v === 'number')
      if (!values.length) continue
      if (!entry.dims.has(dim.code)) {
        entry.dims.set(dim.code, { label: dim.label, values: [], max: dim.maxValue })
      }
      entry.dims.get(dim.code)!.values.push(
        values.reduce((a, b) => a + b, 0) / values.length / dim.maxValue,
      )
    }
  }

  const lines: string[] = []
  lines.push(`${set.student.name} finished "${set.title}" (round ${set.round}).`)
  lines.push('')

  for (const [, c] of byConstruct) {
    lines.push(`${c.label} — ${c.items} item${c.items === 1 ? '' : 's'}`)
    const scored = [...c.dims.entries()].map(([, d]) => ({
      label: d.label,
      pct: Math.round((d.values.reduce((a, b) => a + b, 0) / d.values.length) * 100),
      n: d.values.length,
    }))
    const weakest = scored.length
      ? scored.reduce((a, b) => (b.pct < a.pct ? b : a))
      : null
    for (const d of scored) {
      const marker = weakest && d.label === weakest.label && scored.length > 1 ? '   <- weakest' : ''
      lines.push(`  ${d.label.padEnd(14)} ${String(d.pct).padStart(3)}%   n=${d.n}${marker}`)
    }
    if (!scored.length) lines.push('  (nothing scored yet)')
    if (c.hinted) {
      lines.push(
        `  ${c.hinted} of the scored items needed a hint first — those ARE in the percentages above.`,
      )
    }
    if (c.conceded) {
      lines.push(
        `  ${c.conceded} of ${c.items} given up on — he asked to see how those worked. Not scored above; this is usually the more useful number.`,
      )
    }
    if (c.flagged) {
      lines.push(`  ${c.flagged} response${c.flagged === 1 ? '' : 's'} held back — the two graders disagreed, so they need your score`)
    }
    lines.push('')
  }

  const flaggedTotal = responses.filter((r) => r.flagged).length
  if (flaggedTotal) {
    lines.push(
      `${flaggedTotal} response${flaggedTotal === 1 ? '' : 's'} need your score. A high rate here means the rubric is underspecified, not that he is inconsistent.`,
    )
    lines.push('')
  }

  const pacing = summarisePacing(
    set.items
      .filter((si) => si.response)
      .map((si) => ({ timeSpentMs: si.response!.timeSpentMs, format: si.item.format })),
    set.startedAt,
    set.completedAt,
  )
  lines.push(
    `Took ${formatDuration(pacing.totalMs)}${
      pacing.medianMsPerItem ? `, median ${formatDuration(pacing.medianMsPerItem)} per item` : ''
    }.`,
  )
  if (pacing.warning) lines.push(pacing.warning)
  else if (pacing.itemsTimed) lines.push('Nothing looks rushed.')
  lines.push('')

  const ungraded = responses.filter((r) => r.gradings.length === 0).length
  if (ungraded) lines.push(`${ungraded} response(s) could not be graded — check the dashboard.`)

  lines.push('Read the evidence: https://learn.etpics.com/parent')
  lines.push('')
  lines.push(
    'Scores are stamped uncalibrated until grade-level anchors are added (Rule 3), so read the per-item justifications before drawing a conclusion.',
  )

  return {
    subject: `${set.student.name} finished "${set.title}" — results in`,
    body: lines.join('\n'),
  }
}
