import { Queue, Job } from 'bullmq';
import { MetadataRefreshJob, AudioAnalysisJob, CleanupJob, LibraryScanJob, ReindexTracksJob, ReprocessArtistsJob, AcquisitionJob } from '../types/job.types.js';

const AUDIO_ANALYSIS_JOB_OPTIONS = {
  attempts: 3,
  backoff: { type: 'exponential', delay: 5_000 },
  removeOnComplete: { age: 7 * 24 * 60 * 60, count: 10_000 },
  removeOnFail: { age: 30 * 24 * 60 * 60, count: 10_000 },
} as const;

export interface JobServiceConfig {
  redisUrl: string;
}

export const QUEUE_NAME = 'muzick-queue';

export interface QueueStats {
  waiting: number;
  active: number;
  completed: number;
  failed: number;
  delayed: number;
  paused: number;
}

export interface JobHistoryEntry {
  id: string;
  name: string;
  data: Record<string, unknown>;
  timestamp: number;
  finishedOn?: number;
  failedReason?: string;
  returnvalue?: unknown;
}

export interface ReenrichmentQueuePlan {
  metadataTrackIds: string[];
  artistIds: string[];
  albumIds: string[];
}

export interface EnqueueResult {
  requested: number;
  enqueued: number;
  alreadyQueued: number;
  failed: Array<{ id: string; error: string }>;
}

export interface ReenrichmentQueueResult {
  metadata: EnqueueResult;
  artistImages: EnqueueResult;
  albumCovers: EnqueueResult;
}

export interface EnrichmentQueueDiagnostics {
  pending: Record<string, number>;
  recentFailures: Array<{
    id: string;
    name: string;
    data: Record<string, unknown>;
    failedReason?: string;
    timestamp: number;
    finishedOn?: number;
  }>;
}

export class JobService {
  private queue: Queue;

  constructor(config: JobServiceConfig) {
    this.queue = new Queue(QUEUE_NAME, {
      connection: {
        url: config.redisUrl,
      },
    });
  }

  async enqueueMetadataRefresh(trackId: string, type: 'full' | 'partial') {
    const payload: MetadataRefreshJob = { trackId, refreshType: type };
    await this.queue.add('metadata_refresh', payload);
  }

  async enqueueAudioAnalysis(trackId: string) {
    const payload: AudioAnalysisJob = { trackId };
    await this.queue.add('audio_analysis', payload, {
      jobId: `audio-${trackId}`,
      ...AUDIO_ANALYSIS_JOB_OPTIONS,
    });
  }

  async enqueueCleanup(reason: 'expired' | 'manual', targetFiles: string[]) {
    const payload: CleanupJob = { reason, targetFiles };
    await this.queue.add('cleanup', payload);
  }

  async enqueueLibraryScan(directory: string) {
    const payload: LibraryScanJob = { directory };
    await this.queue.add('scan_library', payload);
  }

  async enqueueReindexTracks() {
    const payload: ReindexTracksJob = {};
    await this.queue.add('reindex_tracks', payload);
  }

  async enqueueReprocessArtists() {
    const payload: ReprocessArtistsJob = { batchSize: 100, offset: 0 };
    await this.queue.add('reprocess_artists', payload);
  }

  async enqueueDiscoveryAcquisition(candidateId: string): Promise<void> {
    const payload: AcquisitionJob = { candidateId };
    const jobId = `acquire-${candidateId}`;
    // BullMQ returns a retained completed/failed job for the same id without
    // executing it. Remove terminal attempts before retrying so a candidate
    // cannot be stranded in `acquiring` for the retention window.
    const existing = await this.queue.getJob(jobId);
    if (existing) {
      const state = await existing.getState();
      if (state === 'completed' || state === 'failed') await existing.remove();
    }
    await this.queue.add('acquire_discovery_candidate', payload, {
      // A candidate may have one active attempt. Failed/completed jobs expire
      // so an operator can explicitly re-evaluate it later.
      jobId,
      removeOnComplete: { age: 86400, count: 1000 },
      removeOnFail: { age: 86400, count: 1000 },
    });
  }

  /**
   * Queue a bounded re-enrichment plan. Metadata, artist images and album
   * covers deliberately have separate jobs: metadata resolution can complete
   * even when artwork providers are unavailable, and artwork is deduplicated
   * at its natural artist/album boundary.
   *
   * Existing active/waiting jobs are preserved and reported as alreadyQueued.
   * Completed/failed jobs are removed before retrying, so a request never
   * claims a retry was enqueued when BullMQ retained an old job id.
   */
  async enqueueReenrichment(plan: ReenrichmentQueuePlan): Promise<ReenrichmentQueueResult> {
    return {
      metadata: await this.enqueueMany(
        [...new Set(plan.metadataTrackIds)],
        'metadata_refresh',
        (trackId) => ({ trackId, refreshType: 'full' }),
        (trackId) => `meta-${trackId}`,
      ),
      artistImages: await this.enqueueMany(
        [...new Set(plan.artistIds)],
        'artist_image',
        (artistId) => ({ artistId }),
        (artistId) => `artist-image-${artistId}`,
      ),
      albumCovers: await this.enqueueMany(
        [...new Set(plan.albumIds)],
        'album_cover',
        (albumId) => ({ albumId }),
        (albumId) => `album-cover-${albumId}`,
      ),
    };
  }

  private async enqueueMany(
    ids: string[],
    name: string,
    payloadFor: (id: string) => Record<string, unknown>,
    jobIdFor: (id: string) => string,
  ): Promise<EnqueueResult> {
    const result: EnqueueResult = { requested: ids.length, enqueued: 0, alreadyQueued: 0, failed: [] };
    const keep = {
      removeOnComplete: { age: 86400, count: 10000 },
      removeOnFail: { age: 86400, count: 10000 },
    } as const;

    for (const id of ids) {
      const jobId = jobIdFor(id);
      try {
        const existing = await this.queue.getJob(jobId);
        if (existing) {
          const state = await existing.getState();
          if (state === 'active' || state === 'waiting' || state === 'delayed' || state === 'prioritized') {
            result.alreadyQueued++;
            continue;
          }
          await existing.remove();
        }
        await this.queue.add(name, payloadFor(id), { jobId, ...keep });
        result.enqueued++;
      } catch (err) {
        result.failed.push({ id, error: err instanceof Error ? err.message : String(err) });
      }
    }
    return result;
  }

  async getEnrichmentQueueDiagnostics(limit = 25): Promise<EnrichmentQueueDiagnostics> {
    const relevant = new Set(['metadata_refresh', 'artist_image', 'album_cover']);
    const [waiting, active, delayed, failed] = await Promise.all([
      this.queue.getJobs(['waiting'], 0, 1000),
      this.queue.getJobs(['active'], 0, 1000),
      this.queue.getJobs(['delayed'], 0, 1000),
      this.queue.getJobs(['failed'], 0, Math.max(limit * 4, 100)),
    ]);
    const pending: Record<string, number> = {
      metadata_refresh: 0,
      artist_image: 0,
      album_cover: 0,
    };
    for (const job of [...waiting, ...active, ...delayed]) {
      if (relevant.has(job.name)) pending[job.name]++;
    }

    const recentFailures = failed
      .filter((job) => relevant.has(job.name))
      .sort((a, b) => (b.finishedOn ?? b.timestamp) - (a.finishedOn ?? a.timestamp))
      .slice(0, limit)
      .map((job) => ({
        id: String(job.id),
        name: job.name,
        data: job.data as Record<string, unknown>,
        failedReason: job.failedReason,
        timestamp: job.timestamp,
        finishedOn: job.finishedOn,
      }));

    return { pending, recentFailures };
  }

  async getQueueStats(): Promise<QueueStats> {
    const [waiting, active, completed, failed, delayed] = await Promise.all([
      this.queue.getWaitingCount(),
      this.queue.getActiveCount(),
      this.queue.getCompletedCount(),
      this.queue.getFailedCount(),
      this.queue.getDelayedCount(),
    ]);
    return { waiting, active, completed, failed, delayed, paused: 0 };
  }

  async getJobHistory(limit = 100): Promise<JobHistoryEntry[]> {
    // Get jobs from completed and failed queues (most recent first)
    const [completedJobs, failedJobs] = await Promise.all([
      this.queue.getJobs(['completed'], 0, limit),
      this.queue.getJobs(['failed'], 0, limit),
    ]);

    const allJobs = [...completedJobs, ...failedJobs].map(job => ({
      id: job.id as string,
      name: job.name,
      data: job.data as Record<string, unknown>,
      timestamp: job.timestamp,
      finishedOn: job.finishedOn,
      failedReason: job.failedReason,
      returnvalue: job.returnvalue,
    }));

    // Sort by timestamp descending (most recent first)
    allJobs.sort((a, b) => b.timestamp - a.timestamp);

    return allJobs.slice(0, limit);
  }

  async close() {
    await this.queue.close();
  }
}
