/**
 * Sweep stuck Interview & Media Prep generations
 *
 * Session rows are written to Mongo with status 'generating' before the AI
 * calls start. The generation route reconciles that row on every in-process
 * terminal path, but a hard process death — crash, redeploy, OOM — runs no
 * catch block, and the in-memory job map that knew about the row dies with it.
 * The row is then stranded at 'generating' forever: it is database state, so it
 * survives refreshes and restarts and nothing else will ever resolve it.
 *
 * This runs once at startup and marks those orphans failed so the dashboard
 * stops showing a loader for work that no longer exists.
 *
 * Only rows older than STALE_AFTER_MS are touched. A younger 'generating' row
 * could still be legitimately in flight on another instance, and killing live
 * work would be worse than the stall this fixes. The cutoff matches
 * aiJobManager's JOB_TTL_MS: past that the server has already discarded the
 * job, so no generation can still complete.
 *
 * Rows with no updatedAt at all are always swept. That field was previously
 * dropped by strict mode, so its absence means the row was written by code
 * predating this deploy — it cannot belong to a running generation.
 */

import { getModels } from '../models';

/** Matches JOB_TTL_MS in services/aiContext/aiJobManager.ts. */
const STALE_AFTER_MS = 30 * 60 * 1000;

const STUCK_MESSAGE =
  'Generation was interrupted (the server restarted or the job was lost). Please try again.';

export async function sweepStuckInterviewSessions(): Promise<number> {
  const { InterviewMediaPrep } = getModels();
  const cutoff = new Date(Date.now() - STALE_AFTER_MS).toISOString();
  const now = new Date().toISOString();

  const result = await InterviewMediaPrep.updateMany(
    { 'sessions.status': 'generating' },
    {
      $set: {
        'sessions.$[stuck].status': 'failed',
        'sessions.$[stuck].generationError': STUCK_MESSAGE,
        'sessions.$[stuck].updatedAt': now,
      },
    },
    {
      // `$not: { $gte }` also matches rows where updatedAt is absent, which a
      // plain `$lt` would skip. Every arrayFilters term must be prefixed with
      // the `stuck` identifier, so a top-level $or is not an option here.
      arrayFilters: [
        {
          'stuck.status': 'generating',
          'stuck.updatedAt': { $not: { $gte: cutoff } },
        },
      ],
    }
  );

  return result.modifiedCount ?? 0;
}
