import { randomUUID } from 'crypto';
import { and, eq } from 'drizzle-orm';

import { db } from '@/lib/db';
import { interests } from '@/lib/db/schema/interests';
import { clients } from '@/lib/db/schema/clients';
import { ValidationError, ForbiddenError } from '@/lib/errors';
import { getQueue } from '@/lib/queue';

// ─── Types ────────────────────────────────────────────────────────────────────

export interface DraftRequest {
  interestId: string;
  clientId: string;
  portId: string;
  context: 'follow_up' | 'introduction' | 'stage_update' | 'general';
  additionalInstructions?: string;
}

export interface DraftResult {
  subject: string;
  body: string;
  generatedAt: Date;
}

// ─── Request draft (enqueues job) ─────────────────────────────────────────────

/**
 * Request an AI-generated email draft.
 *
 * Generates an opaque random jobId rather than relying on BullMQ's default
 * sequential ids - the jobId is the access token for polling, so it must
 * not be enumerable. The job payload also captures the requesting user
 * + port so the poll endpoint can refuse cross-tenant / cross-user reads.
 *
 * The interestId and clientId are validated against portId before enqueue
 * so a port-A caller cannot trigger a draft built from port-B data.
 */
export async function requestEmailDraft(
  userId: string,
  request: DraftRequest,
): Promise<{ jobId: string }> {
  const interest = await db.query.interests.findFirst({
    where: and(eq(interests.id, request.interestId), eq(interests.portId, request.portId)),
  });
  if (!interest) {
    throw new ValidationError('interestId not found in this port');
  }
  const client = await db.query.clients.findFirst({
    where: and(eq(clients.id, request.clientId), eq(clients.portId, request.portId)),
  });
  if (!client) {
    throw new ValidationError('clientId not found in this port');
  }

  const aiQueue = getQueue('ai');
  const jobId = randomUUID();

  await aiQueue.add(
    'generate-email-draft',
    {
      // No PII - only IDs and context parameters
      interestId: request.interestId,
      clientId: request.clientId,
      portId: request.portId,
      context: request.context,
      additionalInstructions: request.additionalInstructions,
      requestedBy: userId,
    },
    { jobId },
  );

  return { jobId };
}

// ─── Poll for result ──────────────────────────────────────────────────────────

/**
 * Get the result of an email draft generation job.
 * Returns null if still processing.
 *
 * Verifies the caller (userId + portId) matches the job's original
 * requester before returning the drafted subject/body. A foreign caller
 * who happens to know the jobId (or stumbles on it) sees null, not the
 * drafted content.
 */
export async function getEmailDraftResult(
  jobId: string,
  caller: { userId: string; portId: string },
): Promise<DraftResult | null> {
  const aiQueue = getQueue('ai');
  const job = await aiQueue.getJob(jobId);

  if (!job) return null;

  const data = job.data as { requestedBy?: string; portId?: string } | undefined | null;
  if (!data || data.requestedBy !== caller.userId || data.portId !== caller.portId) {
    throw new ForbiddenError('Email draft not accessible');
  }

  const state = await job.getState();

  if (state !== 'completed') return null;

  const returnValue = job.returnvalue as
    | { subject: string; body: string; generatedAt: string }
    | undefined
    | null;

  if (!returnValue) return null;

  return {
    subject: returnValue.subject,
    body: returnValue.body,
    generatedAt: new Date(returnValue.generatedAt),
  };
}
