/**
 * AI Job Manager
 *
 * Tracks long-running AI pipeline jobs so the API can return immediately
 * and the frontend polls for results. This avoids 504 Gateway Timeout errors
 * from reverse proxies when pipelines take longer than proxy timeouts allow.
 *
 * Jobs are scoped by companyId and can be listed/queried for the
 * central AI Processing Queue panel.
 *
 * Completion and failure also raise a notification for whoever started the job.
 * That happens here rather than in each route so every AI module gets it from
 * one place — and because these jobs live only in memory, the notification is
 * their sole durable record.
 */

import { getRequestContext } from '../../utils/requestContext';
import { notificationService } from '../notificationService';
import { notifyAiGenerationCompleted } from '../aiGenerationNotifications';

/**
 * AI execution metadata captured from the provider call (AIResult), surfaced on
 * the AI Process record so provider/model/token/duration/cost details are visible.
 */
export interface AiJobMetadata {
  provider?: string;
  model?: string;
  inputTokens?: number | null;
  outputTokens?: number | null;
  totalTokens?: number | null;
  /** Wall-clock time spent on the AI call (ms). */
  latencyMs?: number | null;
  /** Total time from job creation to completion (ms). */
  durationMs?: number | null;
  finishReason?: string | null;
  apiKeyMasked?: string | null;
  /** Estimated cost in USD, when token + pricing data is available. */
  estimatedCost?: number | null;
}

export interface AiJob {
  jobId: string;
  moduleSource: string;
  moduleId?: string; // Frontend-specific module identifier for result routing (e.g., 'business-profile', 'icp-personas')
  companyId?: string;
  /** Who started it — captured from the request so the completion can be notified. */
  userId?: string;
  status: 'processing' | 'completed' | 'failed';
  progress: number; // 0-100
  step?: string; // Human-readable description of current step
  createdAt: number;
  completedAt?: number;
  result?: {
    autoFillData: Record<string, any>;
    source: string;
  };
  error?: string;
  /** AI execution details (provider, model, tokens, duration…) captured on finish. */
  metadata?: AiJobMetadata;
  completedScripts?: string[]; // IDs of scripts already saved to DB during streaming generation
  partialHtml?: string; // Accumulated HTML during streaming generation for progressive preview
}

const jobs = new Map<string, AiJob>();

// Cleanup jobs older than 45 minutes. Must stay well above the frontend's
// MAX_POLL_DURATION_MS (40 min) so that a long-running generation job is
// still in memory when the frontend polls for its status. Website generation
// can take 25-30 min for 5+ pages, and retries can add another 5-10 min.
const JOB_TTL_MS = 45 * 60 * 1000;

function generateJobId(): string {
  return `job_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
}

function cleanupExpiredJobs(): void {
  const now = Date.now();
  for (const [id, job] of jobs) {
    if (now - job.createdAt > JOB_TTL_MS) {
      jobs.delete(id);
    }
  }
}

export function createJob(moduleSource: string, companyId?: string, moduleId?: string): AiJob {
  cleanupExpiredJobs();

  // Attribute the job to whoever started it, read from the ambient request
  // context so the 136 call sites do not each have to pass a userId.
  const context = getRequestContext();

  const job: AiJob = {
    jobId: generateJobId(),
    moduleSource,
    moduleId,
    companyId: companyId ?? context?.organizationId ?? undefined,
    userId: context?.userId,
    status: 'processing',
    progress: 0,
    createdAt: Date.now(),
  };
  jobs.set(job.jobId, job);
  return job;
}

/**
 * Modules where one job is one deliberate, user-initiated artifact rather than
 * an item in a bulk run — so every completion must surface on its own.
 *
 * The default `groupKey` collapses repeats of the same module into a single
 * unread entry for an hour (see below). For Executive CV that is wrong: each
 * run is a whole CV the user explicitly asked for, so generating a second CV
 * while the first notification was still unread was silently absorbed into it
 * and nothing new appeared — the reported "sometimes it notifies, sometimes it
 * doesn't". Keying on the jobId gives these modules one entry per run while
 * still de-duplicating a repeated completion of the SAME job.
 */
const UNGROUPED_JOB_MODULES = new Set(['executive-cv']);

/**
 * Tell the person who started a job that it finished.
 *
 * Fire-and-forget: a notification failure must never affect the generation, and
 * a job started outside a request (no known user) simply isn't notified.
 */
function notifyJobOutcome(job: AiJob, outcome: 'completed' | 'failed'): void {
  if (!job.userId) return;

  const module = job.moduleId || job.moduleSource;
  const label = moduleLabel(module);

  void notificationService.notifyUser(job.userId, {
    type: outcome === 'completed' ? 'ai.generation.completed' : 'ai.generation.failed',
    title: outcome === 'completed' ? `${label} generation completed` : `${label} generation failed`,
    message:
      outcome === 'completed'
        ? `Your ${label} content is ready to review.`
        : job.error || 'The generation did not finish. Please try again.',
    module,
    organizationId: job.companyId ?? null,
    entityType: 'ai-job',
    entityId: job.jobId,
    actorUserId: job.userId,
    // One entry per module per outcome per hour — a bulk run of 40 items must
    // not produce 40 separate notifications. Single-artifact modules opt out
    // above and are keyed per job instead.
    groupKey: UNGROUPED_JOB_MODULES.has(module)
      ? `ai.${outcome}:${module}:${job.companyId || 'none'}:${job.jobId}`
      : `ai.${outcome}:${module}:${job.companyId || 'none'}`,
    // A successful generation already has a dedicated completion email —
    // notifyAiGenerationCompleted, sent from completeJob below and gated on the
    // same AI generation → Email switch. Without narrowing the channel here the
    // user would get two emails for one generation: the generic notification
    // copy and the proper one. A failure has no dedicated email, so it keeps
    // both channels.
    channels: outcome === 'completed' ? ['in_app'] : undefined,
  });
}

/** 'blog-content-os' → 'Blog Content Os'. Good enough for a notification title. */
function moduleLabel(module: string): string {
  return module
    .split(/[-_]/)
    .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
    .join(' ');
}

/**
 * Attach (or merge) AI execution metadata onto a job so the AI Process record
 * shows provider/model/token/duration details. Safe to call before completeJob
 * or failJob; merges so a partial capture on failure is preserved.
 */
export function setJobMetadata(jobId: string, metadata: AiJobMetadata): void {
  const job = jobs.get(jobId);
  if (!job) return;
  job.metadata = { ...(job.metadata || {}), ...metadata };
}

export function updateJobProgress(jobId: string, progress: number, step?: string): void {
  const job = jobs.get(jobId);
  if (job) {
    job.progress = Math.min(100, Math.max(0, progress));
    if (step) job.step = step;
  }
}

export function completeJob(jobId: string, autoFillData: Record<string, any>, source: string): void {
  const job = jobs.get(jobId);
  if (!job) return;

  // Guard against a second completion so the success email is sent exactly once.
  // Must be evaluated BEFORE the status is written, otherwise it always matches
  // and every completion returns early.
  if (job.status === 'completed') return;
  // Only a job still in flight may complete. This rejects a second completion
  // (so each job notifies exactly once) and a completion arriving after the job
  // was already marked failed (so a failed generation can never produce a
  // success email).
  //
  // The check MUST come before the status assignment below. A merge previously
  // left two copies of the completion body in this function: the first copy set
  // `status = 'completed'` and then the guard ran, so it matched on every first
  // call and returned — making the email call at the bottom dead code for every
  // AI module in the app. The in-app notification sat in the first copy and so
  // kept working, which is why the two diverged silently.
  //
  // A later merge reintroduced the duplication in a different form: the body
  // below ended up with TWO notifyJobOutcome calls, so every completion raised
  // two identical dashboard notifications. Keep this function to exactly one
  // notify call and one email call.
  if (job.status !== 'processing') return;

  job.status = 'completed';
  job.progress = 100;
  job.completedAt = Date.now();
  job.result = { autoFillData, source };

  // In-app notification to whoever started the job. Exactly ONE call — a second
  // copy of this line has now been reintroduced by a merge twice, and each time
  // it made every completed generation raise two identical dashboard
  // notifications. Asserted against in aiJobManager.completeJob.test.ts; if that
  // spec starts reporting "Expected 1, received 2", a third copy is back.
  // In-app notification to whoever started the job. EXACTLY ONE call: a second
  // copy of this line has now been re-introduced by merge twice, and each time
  // it made every completed generation raise two identical dashboard
  // notifications. Asserted against in aiJobManager.completeJob.test.ts — if
  // that test fails with a count of 2, a merge has duplicated this line again.
  notifyJobOutcome(job, 'completed');

  // Fire-and-forget completion email. Runs asynchronously in the background —
  // never blocks completion, and never throws (the notifier swallows all its own
  // errors). Only reached on success: failed and cancelled generations go
  // through failJob and are never notified.
  notifyAiGenerationCompleted({
    jobId: job.jobId,
    moduleSource: job.moduleSource,
    moduleId: job.moduleId,
    companyId: job.companyId,
    userId: job.userId,
    autoFillData,
    completedAt: job.completedAt,
  });
}

export function failJob(jobId: string, error: string): void {
  const job = jobs.get(jobId);
  if (job) {
    job.status = 'failed';
    job.completedAt = Date.now();
    job.error = error;
    notifyJobOutcome(job, 'failed');
  }
}

export function getJob(jobId: string): AiJob | undefined {
  return jobs.get(jobId);
}

// ============================================
// LISTING / QUERY FUNCTIONS
// ============================================

/** Returns all non-expired jobs */
export function getAllJobs(): AiJob[] {
  cleanupExpiredJobs();
  return Array.from(jobs.values());
}

/** Returns all non-expired jobs for a specific company */
export function getJobsByCompany(companyId: string): AiJob[] {
  cleanupExpiredJobs();
  return Array.from(jobs.values()).filter(
    (job) => job.companyId === companyId
  );
}

/** Returns lightweight counts for a company (for badge indicators) */
export function getJobCountByCompany(companyId: string): { processing: number; completed: number; failed: number } {
  cleanupExpiredJobs();
  let processing = 0;
  let completed = 0;
  let failed = 0;
  for (const job of jobs.values()) {
    if (job.companyId !== companyId) continue;
    if (job.status === 'processing') processing++;
    else if (job.status === 'completed') completed++;
    else if (job.status === 'failed') failed++;
  }
  return { processing, completed, failed };
}

/**
 * Add a script ID to the job's completedScripts array.
 * Used by the streaming generation endpoint to track which scripts have been saved.
 */
export function addJobCompletedScript(jobId: string, scriptId: string): void {
  const job = jobs.get(jobId);
  if (job) {
    if (!job.completedScripts) job.completedScripts = [];
    job.completedScripts.push(scriptId);
  }
}

/**
 * Update the partial HTML content of a job for progressive preview.
 * As the AI streams HTML content, this is called with the latest accumulated
 * partial HTML so the frontend can show sections appearing in real-time.
 */
export function updateJobPartialHtml(jobId: string, html: string): void {
  const job = jobs.get(jobId);
  if (job) {
    job.partialHtml = html;
  }
}