import 'server-only'
import { db } from './db'
import {
  startSession,
  inflightSession,
  expireStaleSessions,
  drainQueue,
  queuedSessions,
} from './agent-sessions'

/**
 * The 3-week clock, and the sweep that keeps things moving.
 *
 * The clock counts from when a boy last FINISHED a set, not from when one was
 * released. A set that is sitting untaken means the next one is not due — if he
 * hasn't done the last one, generating another produces a pile, not a cadence.
 */

export const CADENCE_DAYS = Number(process.env.CADENCE_DAYS || 21)
/** The spiritual track runs on its own clock — change it without touching the other. */
export const SPIRITUAL_CADENCE_DAYS = Number(
  process.env.SPIRITUAL_CADENCE_DAYS || CADENCE_DAYS,
)

export type Track = 'ACADEMIC' | 'SPIRITUAL'

export function cadenceDaysFor(track: Track): number {
  return track === 'SPIRITUAL' ? SPIRITUAL_CADENCE_DAYS : CADENCE_DAYS
}

export type SkipReason =
  | 'set outstanding'
  | 'too soon'
  | 'agent busy'
  | null

export interface StudentCadence {
  studentId: string
  name: string
  track: Track
  /** Null when he has never finished one. */
  lastCompletedAt: Date | null
  daysSince: number | null
  dueNow: boolean
  /** Why nothing will be generated for him right now. */
  blockedBy: SkipReason
  outstandingSetTitle: string | null
}

export async function cadenceForAll(): Promise<StudentCadence[]> {
  const students = await db.student.findMany({ orderBy: { grade: 'desc' } })
  const out: StudentCadence[] = []

  for (const s of students) {
    for (const track of ['ACADEMIC', 'SPIRITUAL'] as const) {
      const lastCompleted = await db.itemSet.findFirst({
        where: { studentId: s.id, track, status: 'COMPLETE' },
        orderBy: { completedAt: 'desc' },
        select: { completedAt: true },
      })
      // Anything on THIS track already written or handed over and unfinished.
      // The two tracks never block each other: he can be mid-way through a
      // maths set and still be due a spiritual one.
      const outstanding = await db.itemSet.findFirst({
        where: {
          studentId: s.id,
          track,
          status: {
            in: ['AWAITING_REVIEW', 'PENDING_REVIEW', 'APPROVED', 'RELEASED', 'IN_PROGRESS'],
          },
        },
        orderBy: { generatedAt: 'desc' },
        select: { title: true },
      })

      const lastCompletedAt = lastCompleted?.completedAt ?? null
      const daysSince = lastCompletedAt
        ? Math.floor((Date.now() - lastCompletedAt.getTime()) / 86_400_000)
        : null

      const dueNow =
        !outstanding && (daysSince === null || daysSince >= cadenceDaysFor(track))

      out.push({
        studentId: s.id,
        name: s.name,
        track,
        lastCompletedAt,
        daysSince,
        dueNow,
        blockedBy: outstanding ? 'set outstanding' : dueNow ? null : 'too soon',
        outstandingSetTitle: outstanding?.title ?? null,
      })
    }
  }
  return out
}

export interface TickResult {
  started: { kind: string; student?: string; setId?: string }[]
  skipped: string[]
}

/**
 * One pass of the scheduler. Started by cron on the VPS.
 *
 * Grading is swept BEFORE generation on purpose: a finished set waiting on
 * grades is holding up an email Eric is expecting, and it is also the input the
 * next generation should be reading. Only one session can be in flight, so each
 * tick does at most one thing and the next tick picks up where this left off.
 */
export async function tick(): Promise<TickResult> {
  await expireStaleSessions()
  const result: TickResult = { started: [], skipped: [] }

  if (await inflightSession()) {
    result.skipped.push('an agent session is already running')
    return result
  }

  // Anything Eric asked for by hand goes before anything the schedule wants.
  const drained = await drainQueue()
  if (drained) {
    result.started.push({ kind: 'QUEUED REQUEST' })
    return result
  }
  const stillQueued = await queuedSessions()
  if (stillQueued.length) {
    result.skipped.push(`${stillQueued.length} request(s) still queued`)
  }

  // 1. Questions first — a boy is sitting there waiting for an answer, and
  //    grading is not waiting for anything.
  const pendingQuestion = await db.studentQuestion.findFirst({
    where: { status: 'PENDING' },
    orderBy: { askedAt: 'asc' },
  })
  const pendingHint = await db.hintRequest.findFirst({
    where: { status: 'PENDING' },
    orderBy: { askedAt: 'asc' },
  })
  if (pendingQuestion || pendingHint) {
    try {
      await startSession({ kind: 'ANSWER', trigger: 'cron' })
      result.started.push({ kind: 'ANSWER' })
      return result
    } catch (err) {
      result.skipped.push(`answer: ${err instanceof Error ? err.message : 'failed'}`)
      return result
    }
  }

  // 2. Ungraded responses in finished sets. Conceded items are excluded — he
  //    saw the answer, so there is nothing to grade and sweeping them would
  //    retry forever.
  const ungraded = await db.response.findFirst({
    where: {
      gradings: { none: {} },
      conceded: false,
      setItem: { set: { status: 'COMPLETE' } },
    },
    include: { setItem: true },
    orderBy: { submittedAt: 'asc' },
  })
  if (ungraded) {
    try {
      await startSession({ kind: 'GRADE', setId: ungraded.setItem.setId, trigger: 'cron' })
      result.started.push({ kind: 'GRADE', setId: ungraded.setItem.setId })
      return result
    } catch (err) {
      result.skipped.push(`grade: ${err instanceof Error ? err.message : 'failed'}`)
      return result
    }
  }

  // 3. Anyone due a new set.
  for (const c of await cadenceForAll()) {
    if (!c.dueNow) {
      result.skipped.push(
        `${c.name} (${c.track.toLowerCase()}): ${
          c.blockedBy === 'set outstanding'
            ? `still has "${c.outstandingSetTitle}" outstanding`
            : `${c.daysSince ?? 0} of ${cadenceDaysFor(c.track)} days`
        }`,
      )
      continue
    }
    try {
      await startSession({
        kind: 'GENERATE',
        studentId: c.studentId,
        track: c.track,
        trigger: 'cron',
      })
      result.started.push({ kind: 'GENERATE', student: `${c.name} (${c.track.toLowerCase()})` })
      return result // one at a time
    } catch (err) {
      result.skipped.push(`${c.name}: ${err instanceof Error ? err.message : 'failed'}`)
      return result
    }
  }

  return result
}
