import 'server-only'
import { db } from './db'
import { triggerAgent } from './agent-trigger'

/**
 * Session lifecycle. One in-flight session at a time, per eorganize: two passes
 * racing the same responses would double-write gradings and corrupt the
 * disagreement metric that Rule 5 depends on.
 */

export type SessionKind = 'GENERATE' | 'GRADE' | 'ANSWER'

interface StartOpts {
  kind: SessionKind
  /** GENERATE: which line of learning to write for. */
  track?: 'ACADEMIC' | 'SPIRITUAL'
  studentId?: string
  setId?: string
  responseId?: string
  /** "manual" (a button) or "cron" (the 3-week sweep). */
  trigger?: string
}

/**
 * A session actually occupying the mini: either running, or sent and awaiting
 * pickup. A QUEUED session (never triggered) is not in flight — it is waiting
 * its turn.
 */
export async function inflightSession() {
  return db.agentSession.findFirst({
    where: {
      OR: [
        { status: 'RUNNING' },
        { status: 'PENDING', triggeredAt: { not: null } },
      ],
    },
    orderBy: { startedAt: 'desc' },
  })
}

/** Requested while the mini was busy; goes as soon as it frees up. */
export async function queuedSessions() {
  return db.agentSession.findMany({
    where: { status: 'PENDING', triggeredAt: null },
    orderBy: { startedAt: 'asc' },
  })
}

/**
 * Send the next queued request, if the mini is free. Called when a session
 * finishes and by the cron, so a queued job goes within seconds rather than
 * waiting up to an hour for the next tick.
 */
export async function drainQueue(): Promise<string | null> {
  if (await inflightSession()) return null
  const [next] = await queuedSessions()
  if (!next) return null
  try {
    await triggerAgent(next.id)
    await db.agentSession.update({
      where: { id: next.id },
      data: { triggeredAt: new Date() },
    })
    return next.id
  } catch (err) {
    await db.agentSession.update({
      where: { id: next.id },
      data: {
        status: 'FAILED',
        finishedAt: new Date(),
        errorMessage: err instanceof Error ? err.message.slice(0, 500) : 'launch failed',
      },
    })
    return null
  }
}

/**
 * A PENDING session whose SSH landed but which the Mac never checked in on is
 * stuck — the watcher may be down, or the Mac asleep. Treat anything older than
 * this as dead so one bad launch doesn't block the app forever.
 */
const STALE_MINUTES = 30

export async function expireStaleSessions(): Promise<void> {
  const cutoff = new Date(Date.now() - STALE_MINUTES * 60_000)
  await db.agentSession.updateMany({
    // Only ones actually sent. A queued request is not stuck, it is waiting —
    // expiring it would silently throw away something Eric asked for.
    where: {
      status: { in: ['PENDING', 'RUNNING'] },
      triggeredAt: { not: null },
      startedAt: { lt: cutoff },
    },
    data: {
      status: 'FAILED',
      finishedAt: new Date(),
      errorMessage: `No word from the Mac in ${STALE_MINUTES} minutes — is it awake, and is the agent watcher installed?`,
    },
  })
}

export interface StartResult {
  sessionId: string
  /** True when the mini was busy and this is waiting its turn. */
  queued: boolean
}

export async function startSession(opts: StartOpts): Promise<string> {
  return (await requestSession(opts)).sessionId
}

/**
 * Ask for a session. If the mini is busy the request is QUEUED rather than
 * refused — anything Eric asks for should happen, just possibly in a minute.
 */
export async function requestSession(opts: StartOpts): Promise<StartResult> {
  await expireStaleSessions()

  const busy = await inflightSession()

  let queueDepth = 1
  if (opts.kind === 'GRADE') {
    queueDepth = await db.response.count({
      where: {
        gradings: { none: {} },
        // A conceded item has no score to give: he saw the answer.
        conceded: false,
        ...(opts.responseId ? { id: opts.responseId } : {}),
        ...(opts.setId ? { setItem: { setId: opts.setId } } : {}),
      },
    })
    if (queueDepth === 0) throw new Error('Nothing here is waiting to be graded.')
  }
  if (opts.kind === 'ANSWER') {
    queueDepth =
      (await db.studentQuestion.count({ where: { status: 'PENDING' } })) +
      (await db.hintRequest.count({ where: { status: 'PENDING' } }))
    if (queueDepth === 0) throw new Error('No questions are waiting.')
  }

  const session = await db.agentSession.create({
    data: {
      kind: opts.kind,
      studentId: opts.studentId ?? null,
      setId: opts.setId ?? null,
      responseId: opts.responseId ?? null,
      trigger: opts.trigger ?? 'manual',
      track: opts.track ?? 'ACADEMIC',
      queueDepth,
    },
  })

  // Busy → leave it queued; drainQueue sends it the moment the mini frees up.
  if (busy) return { sessionId: session.id, queued: true }

  try {
    await triggerAgent(session.id)
    await db.agentSession.update({
      where: { id: session.id },
      data: { triggeredAt: new Date() },
    })
  } catch (err) {
    await db.agentSession.update({
      where: { id: session.id },
      data: {
        status: 'FAILED',
        finishedAt: new Date(),
        errorMessage: err instanceof Error ? err.message.slice(0, 500) : 'launch failed',
      },
    })
    throw err
  }

  return { sessionId: session.id, queued: false }
}
