/**
 * Photo classification worker.
 *
 * Consumes the 'photo-classification' queue. For each image photo:
 *   - Read the original blob via storage helper.
 *   - Base64-encode.
 *   - Call imajin-semantic /detect with a default tag set (config-driven).
 *   - Call imajin-moderator /scan in parallel.
 *   - Persist top tags into photos.semantic_tags + photos.category and set
 *     classification_status='completed'.
 *
 * If either model service fails, we mark the photo failed (BullMQ retries).
 * Videos and audio are skipped.
 */

import type { Queue } from 'bullmq';

import type { PhotoMediaKind } from '@/entities/photo';
import { getDb, pgRun } from '@/shared/db';
import { logger } from '@/shared/logger';
import { classifyWithModerator, classifyWithSiglip } from '@/shared/imajin-classifier';
import { createQueue, createWorker } from '@/shared/queues';

import { readBlob } from './storage';

export const CLASSIFY_QUEUE_NAME = 'photo-classification';

const DEFAULT_TAGS: readonly string[] = [
  'a photo of a person',
  'a photo of a group of people',
  'a selfie',
  'a portrait',
  'a landscape',
  'a beach',
  'mountains',
  'a city street',
  'food',
  'a meal at a restaurant',
  'a pet dog',
  'a pet cat',
  'a screenshot',
  'a document',
  'a sunset',
  'a building',
  'a vehicle',
];

const TAG_SCORE_THRESHOLD = 0.25;
const TOP_N_TAGS = 5;

export interface ClassifyJob {
  readonly photoId: string;
  readonly storageKey: string;
  readonly mediaKind: PhotoMediaKind;
}

let classifyQueue: Queue<ClassifyJob> | null = null;

export function getClassifyQueue(): Queue<ClassifyJob> {
  if (!classifyQueue) classifyQueue = createQueue<ClassifyJob>(CLASSIFY_QUEUE_NAME);
  return classifyQueue;
}

export async function enqueueClassifyJob(job: ClassifyJob): Promise<void> {
  try {
    await getClassifyQueue().add('classify', job, { jobId: `classify-${job.photoId}` });
  } catch (err) {
    throw new Error(`enqueueClassifyJob failed: ${err instanceof Error ? err.message : String(err)}`);
  }
}

async function setStatus(
  photoId: string,
  status: 'pending' | 'processing' | 'completed' | 'failed',
  fields: { category?: string | null; semanticTags?: readonly string[] | null } = {},
): Promise<void> {
  try {
    const pool = getDb();
    await pgRun(
      pool,
      `UPDATE macsync.photos
         SET classification_status = $1,
             category              = COALESCE($2, category),
             semantic_tags         = COALESCE($3, semantic_tags),
             updated_at            = now()
         WHERE id = $4`,
      [status, fields.category ?? null, fields.semanticTags ?? null, photoId],
    );
  } catch (err) {
    throw new Error(`classify-worker setStatus failed: ${err instanceof Error ? err.message : String(err)}`);
  }
}

async function processOne(job: ClassifyJob): Promise<void> {
  try {
    if (job.mediaKind !== 'image' && job.mediaKind !== 'live_photo') {
      await setStatus(job.photoId, 'completed');
      return;
    }
    await setStatus(job.photoId, 'processing');

    const buf = await readBlob(job.storageKey);
    const b64 = buf.toString('base64');

    const [siglip, mod] = await Promise.all([
      classifyWithSiglip(b64, DEFAULT_TAGS),
      classifyWithModerator(b64).catch((err) => {
        logger.warn('classify-worker: moderator failed, continuing', {
          photoId: job.photoId,
          err: err instanceof Error ? err.message : String(err),
        });
        return null;
      }),
    ]);

    const scored = Object.entries(siglip.scores ?? {})
      .filter(([, v]) => typeof v === 'number' && v >= TAG_SCORE_THRESHOLD)
      .sort((a, b) => b[1] - a[1])
      .slice(0, TOP_N_TAGS);
    const tags = scored.map(([k]) => k);
    const category = siglip.top_label ?? scored[0]?.[0] ?? null;

    await setStatus(job.photoId, 'completed', { category, semanticTags: tags });
    logger.debug('classify-worker: completed', {
      photoId: job.photoId,
      tags: tags.length,
      safe: mod?.safe,
    });
  } catch (err) {
    logger.warn('classify-worker: job error', {
      photoId: job.photoId,
      err: err instanceof Error ? err.message : String(err),
    });
    try {
      await setStatus(job.photoId, 'failed');
    } catch (_) {
      // already inside the catch; swallow secondary error so the original surfaces
    }
    throw err;
  }
}

let started = false;

export function startClassifyWorker(): void {
  if (started) return;
  started = true;
  getClassifyQueue();
  createWorker<ClassifyJob>(CLASSIFY_QUEUE_NAME, async (job) => {
    await processOne(job.data);
  });
}
