/**
 * Face-extraction worker.
 *
 * Consumes the 'face-extraction' BullMQ queue. For each photo:
 *   1. Build a URL imajin-identity can fetch (MEDIA_FETCH_BASE_URL + storageKey).
 *   2. Call POST /imajin-identity/embed/from-url.
 *   3. Insert one macsync.faces row per detected face (bbox, embedding, conf).
 *   4. Update photos.face_extraction_status + face_count.
 *   5. If any faces detected, enqueue a job onto the 'identity-matching' queue.
 *
 * Videos and audio are skipped (face_extraction_status='skipped').
 */

import { randomUUIDv7 } from 'bun';
import type { Queue } from 'bullmq';

import { loadConfig } from '@/app/config';
import { insertFace } from '@/entities/face';
import type { PhotoMediaKind } from '@/entities/photo';
import { getDb, pgRun } from '@/shared/db';
import { logger } from '@/shared/logger';
import { embedFromUrl } from '@/shared/imajin-identity';
import { createQueue, createWorker } from '@/shared/queues';

import { headBlob } from './storage';

export const FACE_QUEUE_NAME = 'face-extraction';
export const MATCH_QUEUE_NAME = 'identity-matching';

export interface FaceExtractionJob {
  readonly photoId: string;
  readonly deviceId: string;
  readonly storageKey: string;
  readonly mediaKind: PhotoMediaKind;
  readonly mimeType: string | null;
}

export interface IdentityMatchingJob {
  readonly photoId: string;
  readonly deviceId: string;
}

let faceQueue: Queue<FaceExtractionJob> | null = null;
let matchQueue: Queue<IdentityMatchingJob> | null = null;

export function getFaceQueue(): Queue<FaceExtractionJob> {
  if (!faceQueue) faceQueue = createQueue<FaceExtractionJob>(FACE_QUEUE_NAME);
  return faceQueue;
}

export function getMatchQueue(): Queue<IdentityMatchingJob> {
  if (!matchQueue) matchQueue = createQueue<IdentityMatchingJob>(MATCH_QUEUE_NAME);
  return matchQueue;
}

export async function enqueueFaceJob(job: FaceExtractionJob): Promise<void> {
  try {
    await getFaceQueue().add('extract', job, { jobId: `face-${job.photoId}` });
  } catch (err) {
    throw new Error(`enqueueFaceJob failed: ${err instanceof Error ? err.message : String(err)}`);
  }
}

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

function buildFetchUrl(storageKey: string): string {
  const { MEDIA_FETCH_BASE_URL, MINIO_ENDPOINT, MINIO_BUCKET } = loadConfig();
  if (MEDIA_FETCH_BASE_URL) {
    const base = MEDIA_FETCH_BASE_URL.endsWith('/') ? MEDIA_FETCH_BASE_URL : `${MEDIA_FETCH_BASE_URL}/`;
    return `${base}${storageKey}`;
  }
  if (MINIO_ENDPOINT) {
    return `${MINIO_ENDPOINT}/${MINIO_BUCKET}/${storageKey}`;
  }
  throw new Error('cannot build fetch URL: MEDIA_FETCH_BASE_URL or MINIO_ENDPOINT must be set');
}

async function setFaceStatus(
  photoId: string,
  status: 'pending' | 'processing' | 'completed' | 'failed' | 'skipped' | 'no_face',
  faceCount?: number,
): Promise<void> {
  try {
    const pool = getDb();
    if (faceCount !== undefined) {
      await pgRun(
        pool,
        `UPDATE macsync.photos
           SET face_extraction_status = $1, face_count = $2, updated_at = now()
           WHERE id = $3`,
        [status, faceCount, photoId],
      );
    } else {
      await pgRun(
        pool,
        `UPDATE macsync.photos SET face_extraction_status = $1, updated_at = now() WHERE id = $2`,
        [status, photoId],
      );
    }
  } catch (err) {
    throw new Error(`setFaceStatus failed (photo=${photoId}): ${err instanceof Error ? err.message : String(err)}`);
  }
}

async function processOne(job: FaceExtractionJob): Promise<void> {
  try {
    if (job.mediaKind !== 'image' && job.mediaKind !== 'live_photo') {
      await setFaceStatus(job.photoId, 'skipped', 0);
      return;
    }

    await setFaceStatus(job.photoId, 'processing');

    if (!(await headBlob(job.storageKey))) {
      await setFaceStatus(job.photoId, 'failed');
      throw new Error(`face-worker: storage_key ${job.storageKey} not present yet`);
    }

    const imageUrl = buildFetchUrl(job.storageKey);
    const result = await embedFromUrl({ imageUrl, extractAll: true });

    if (!result.success || result.faces.length === 0) {
      await setFaceStatus(job.photoId, 'no_face', 0);
      return;
    }

    const pool = getDb();
    for (const face of result.faces) {
      const id = randomUUIDv7();
      const [x, y, w, h] = face.bbox;
      await insertFace(pool, id, {
        photoId: job.photoId,
        bbox: { x, y, w, h },
        embedding: face.embedding,
        confidence: face.confidence,
      });
    }
    await setFaceStatus(job.photoId, 'completed', result.faces.length);

    await enqueueMatchJob({ photoId: job.photoId, deviceId: job.deviceId });

    logger.debug('face-worker: completed', { photoId: job.photoId, faces: result.faces.length });
  } catch (err) {
    logger.warn('face-worker: job error', {
      photoId: job.photoId,
      err: err instanceof Error ? err.message : String(err),
    });
    throw err;
  }
}

let started = false;

export function startFaceWorker(): void {
  if (started) return;
  started = true;
  getFaceQueue();
  getMatchQueue();
  createWorker<FaceExtractionJob>(FACE_QUEUE_NAME, async (job) => {
    await processOne(job.data);
  });
}
