/**
 * Identity-matching worker.
 *
 * Consumes the 'identity-matching' queue. For each photo with new
 * (match_status='pending') faces:
 *   - Load all candidate identities (with centroid_embedding) for the device.
 *   - For each face: pick the nearest centroid by cosine similarity.
 *     If sim >= MATCH_THRESHOLD, assign the face to that identity.
 *     Else, create a brand new identity whose centroid is this face's
 *     embedding and assign the face to it.
 *   - Trigger a centroid recompute for every identity that gained a face.
 */

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

import {
  assignFaceToIdentity,
  listFacesForPhoto,
} from '@/entities/face';
import {
  createIdentity,
  listIdentitiesWithCentroid,
} from '@/entities/identity';
import { getDb } from '@/shared/db';
import { logger } from '@/shared/logger';
import { createQueue, createWorker } from '@/shared/queues';
import { cosineSimilarity } from '@/shared/vector';

const MATCH_QUEUE_NAME = 'identity-matching';
const CENTROID_QUEUE_NAME = 'identity-centroid';
const MATCH_THRESHOLD = 0.5;

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

export interface CentroidRecomputeJob {
  readonly identityId: string;
}

let matchQueue: Queue<IdentityMatchingJob> | null = null;
let centroidQueue: Queue<CentroidRecomputeJob> | null = null;

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

export function getCentroidQueue(): Queue<CentroidRecomputeJob> {
  if (!centroidQueue) centroidQueue = createQueue<CentroidRecomputeJob>(CENTROID_QUEUE_NAME);
  return centroidQueue;
}

export async function enqueueCentroidJob(identityId: string): Promise<void> {
  try {
    await getCentroidQueue().add('recompute', { identityId }, { jobId: `centroid-${identityId}`, delay: 5_000 });
  } catch (err) {
    throw new Error(`enqueueCentroidJob failed: ${err instanceof Error ? err.message : String(err)}`);
  }
}

async function processOne(job: IdentityMatchingJob): Promise<void> {
  try {
    const pool = getDb();
    const faces = (await listFacesForPhoto(pool, job.photoId)).filter((f) => f.matchStatus === 'pending');
    if (faces.length === 0) return;

    const candidates = [...(await listIdentitiesWithCentroid(pool, job.deviceId))];

    const touched = new Set<string>();

    for (const face of faces) {
      let bestId: string | null = null;
      let bestSim = -1;
      for (const ident of candidates) {
        if (!ident.centroidEmbedding) continue;
        const sim = cosineSimilarity(face.embedding, ident.centroidEmbedding);
        if (sim > bestSim) {
          bestSim = sim;
          bestId = ident.id;
        }
      }

      if (bestId && bestSim >= MATCH_THRESHOLD) {
        await assignFaceToIdentity(pool, face.id, bestId);
        touched.add(bestId);
      } else {
        const newId = randomUUIDv7();
        await createIdentity(pool, newId, job.deviceId);
        await assignFaceToIdentity(pool, face.id, newId);
        touched.add(newId);
        // Add to in-memory candidate list so the next face in this photo
        // can match this brand-new identity (e.g. group photo of unknown people).
        candidates.push({
          id: newId,
          deviceId: job.deviceId,
          name: null,
          faceCount: 1,
          centroidEmbedding: face.embedding,
          centroidUpdatedAt: null,
          createdAt: new Date().toISOString(),
          updatedAt: new Date().toISOString(),
        });
      }
    }

    for (const id of touched) {
      await enqueueCentroidJob(id);
    }

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

let started = false;

export function startMatchWorker(): void {
  if (started) return;
  started = true;
  getMatchQueue();
  getCentroidQueue();
  createWorker<IdentityMatchingJob>(MATCH_QUEUE_NAME, async (job) => {
    await processOne(job.data);
  });
}
