/**
 * Durable async extraction job queue.
 *
 * Jobs are stored in a pluggable queue store and processed by a background
 * worker. File-backed storage is the default for single-instance dev/staging
 * deployments; tests can swap in the memory store.
 */

import { mkdir } from 'node:fs/promises';
import { dirname, resolve } from 'node:path';
import {
  FileQueueStore,
  MemoryQueueStore,
  QueueWorker,
  type QueueJob,
  type QueueStore,
} from '@bytelyst/queue';
import {
  sidecarExtract,
  type SidecarExtractRequest,
  type SidecarExtractResponse,
} from '../../lib/python-bridge.js';
import { triggerJobWebhook, type WebhookConfig } from './webhooks.js';

export type JobStatus = 'pending' | 'processing' | 'completed' | 'failed';

export interface ExtractionJobPayload {
  inputs: SidecarExtractRequest[];
  requestId?: string;
  webhookConfig?: WebhookConfig;
}

export interface ExtractionJobResult {
  processingStatus: 'completed' | 'failed';
  results: SidecarExtractResponse[];
  errors: Array<{ index: number; error: string }>;
  progress: { completed: number; total: number };
  completedAt: string;
}

export interface ExtractionJob {
  id: string;
  status: JobStatus;
  inputs: SidecarExtractRequest[];
  results: SidecarExtractResponse[];
  errors: Array<{ index: number; error: string }>;
  progress: { completed: number; total: number };
  createdAt: string;
  completedAt?: string;
  webhookConfig?: WebhookConfig;
  attempts: number;
  lastError?: string;
}

const QUEUE_NAME = 'extraction-jobs';
let queueStore: QueueStore | undefined;
let queueWorker: QueueWorker<ExtractionJobPayload, ExtractionJobResult> | undefined;
let queueInitialized = false;

function getQueueBackend(): 'memory' | 'file' {
  const backend = process.env.EXTRACTION_QUEUE_BACKEND;
  if (backend === 'memory' || backend === 'file') return backend;
  return process.env.NODE_ENV === 'test' ? 'memory' : 'file';
}

function getQueueFilePath(): string {
  return resolve(process.cwd(), process.env.EXTRACTION_QUEUE_FILE || '.data/extraction-jobs.json');
}

async function resolveQueueStore(): Promise<QueueStore> {
  if (queueStore) return queueStore;

  if (getQueueBackend() === 'memory') {
    queueStore = new MemoryQueueStore();
    return queueStore;
  }

  const filePath = getQueueFilePath();
  await mkdir(dirname(filePath), { recursive: true });
  queueStore = new FileQueueStore({ filePath });
  return queueStore;
}

function mapQueueJob(job: QueueJob<ExtractionJobPayload, ExtractionJobResult>): ExtractionJob {
  const total = job.payload.inputs.length;
  const result = job.result;
  const progress = (job.progress as ExtractionJob['progress'] | undefined) ??
    result?.progress ?? {
      completed: 0,
      total,
    };

  return {
    id: job.id,
    status:
      job.status === 'queued'
        ? 'pending'
        : job.status === 'running'
          ? 'processing'
          : job.status === 'succeeded'
            ? (result?.processingStatus ?? 'completed')
            : 'failed',
    inputs: job.payload.inputs,
    results: result?.results ?? [],
    errors: result?.errors ?? [],
    progress,
    createdAt: job.createdAt,
    completedAt: result?.completedAt ?? job.completedAt,
    webhookConfig: job.payload.webhookConfig,
    attempts: job.attempts,
    lastError: job.lastError,
  };
}

async function processExtractionJob(
  job: QueueJob<ExtractionJobPayload, ExtractionJobResult>,
  context: {
    patch: (patch: Partial<QueueJob<ExtractionJobPayload, ExtractionJobResult>>) => Promise<void>;
    heartbeat: () => Promise<void>;
  }
): Promise<ExtractionJobResult> {
  const results: SidecarExtractResponse[] = [];
  const errors: Array<{ index: number; error: string }> = [];
  const total = job.payload.inputs.length;

  await context.patch({
    progress: { completed: 0, total },
  });

  for (let i = 0; i < total; i++) {
    try {
      const result = await sidecarExtract(job.payload.inputs[i], job.payload.requestId);
      results.push(result);
    } catch (err) {
      const message = err instanceof Error ? err.message : 'Unknown error';
      errors.push({ index: i, error: message });
      results.push({
        extractions: [],
        metadata: { model_id: 'error', duration_ms: 0, char_count: 0 },
      });
    }

    await context.patch({
      progress: { completed: i + 1, total },
    });
    await context.heartbeat();
  }

  const completedAt = new Date().toISOString();
  const result: ExtractionJobResult = {
    processingStatus: errors.length === total ? 'failed' : 'completed',
    results,
    errors,
    progress: { completed: total, total },
    completedAt,
  };

  if (job.payload.webhookConfig) {
    const view: ExtractionJob = {
      ...mapQueueJob({
        ...job,
        status: 'succeeded',
        result,
        completedAt,
      }),
      completedAt,
    };
    await triggerJobWebhook(view, job.payload.webhookConfig).catch(() => {
      // Webhook failures are non-fatal to job processing.
    });
  }

  return result;
}

export async function initJobQueue(
  log: { info: (...args: unknown[]) => void } = { info: () => {} }
): Promise<void> {
  if (queueInitialized) return;

  const store = await resolveQueueStore();
  queueWorker = new QueueWorker<ExtractionJobPayload, ExtractionJobResult>({
    queueName: QUEUE_NAME,
    store,
    handler: processExtractionJob,
    pollIntervalMs: parseInt(process.env.EXTRACTION_QUEUE_POLL_MS || '100', 10),
    leaseMs: parseInt(process.env.EXTRACTION_QUEUE_LEASE_MS || '30000', 10),
  });
  queueWorker.start();
  queueInitialized = true;
  log.info({ backend: getQueueBackend() }, '[extract/jobs] queue worker started');
}

export async function shutdownJobQueue(): Promise<void> {
  if (queueWorker) {
    await queueWorker.stop();
  }
  queueWorker = undefined;
  queueInitialized = false;
}

export async function createJob(
  inputs: SidecarExtractRequest[],
  requestId?: string,
  webhookConfig?: WebhookConfig
): Promise<ExtractionJob> {
  await initJobQueue();
  const store = await resolveQueueStore();
  const job = await store.enqueue<ExtractionJobPayload>(QUEUE_NAME, {
    type: 'extract.batch',
    payload: {
      inputs,
      requestId,
      webhookConfig,
    },
    progress: {
      completed: 0,
      total: inputs.length,
    },
    productId: process.env.DEFAULT_PRODUCT_ID,
  });
  return mapQueueJob(job as QueueJob<ExtractionJobPayload, ExtractionJobResult>);
}

export async function getJob(jobId: string): Promise<ExtractionJob | undefined> {
  const store = await resolveQueueStore();
  const job = await store.get<ExtractionJobPayload, ExtractionJobResult>(QUEUE_NAME, jobId);
  return job ? mapQueueJob(job) : undefined;
}

export async function listJobs(limit = 50): Promise<ExtractionJob[]> {
  const store = await resolveQueueStore();
  const jobs = await store.list<ExtractionJobPayload, ExtractionJobResult>(QUEUE_NAME, { limit });
  return jobs.map(mapQueueJob);
}

export async function resetJobStore(): Promise<void> {
  await shutdownJobQueue();
  if (queueStore) {
    await queueStore.clear(QUEUE_NAME);
  }
}

export async function setJobStoreForTesting(store?: QueueStore): Promise<void> {
  await shutdownJobQueue();
  queueStore = store;
}
