import type { GiveawayCreateDashboard, GiveawayDashboard } from '@nexumi/shared';
import type { Giveaway } from '@prisma/client';
import { prisma } from '../prisma';
import { addJobAndAwait, getGiveawayQueue, getGiveawayQueueEvents } from '../queues';

export class GiveawayCreateTimeoutError extends Error {
  constructor() {
    super('The bot did not confirm the giveaway in time. It may still be starting - check the list shortly.');
  }
}

export class GiveawayEndError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'GiveawayEndError';
  }
}

function toDashboard(giveaway: Giveaway): GiveawayDashboard {
  return {
    id: giveaway.id,
    guildId: giveaway.guildId,
    channelId: giveaway.channelId,
    messageId: giveaway.messageId,
    prize: giveaway.prize,
    winnerCount: giveaway.winnerCount,
    endsAt: giveaway.endsAt.toISOString(),
    requiredRoleId: giveaway.requiredRoleId,
    requiredLevel: giveaway.requiredLevel,
    requiredMemberDays: giveaway.requiredMemberDays,
    paused: giveaway.paused,
    endedAt: giveaway.endedAt?.toISOString() ?? null,
    winners: giveaway.winners,
    entrants: giveaway.entrants,
    hostId: giveaway.hostId,
    createdAt: giveaway.createdAt.toISOString()
  };
}

function scheduledEndJobId(giveawayId: string): string {
  return `giveaway-end-${giveawayId}`;
}

async function safeRemoveJob(jobId: string): Promise<void> {
  const job = await getGiveawayQueue().getJob(jobId);
  if (!job) {
    return;
  }
  try {
    await job.remove();
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    if (message.includes('locked') || message.includes('could not be removed')) {
      return;
    }
    throw error;
  }
}

export async function listGiveaways(guildId: string): Promise<GiveawayDashboard[]> {
  const giveaways = await prisma.giveaway.findMany({
    where: { guildId },
    orderBy: [{ endedAt: 'asc' }, { endsAt: 'asc' }],
    take: 50
  });
  return giveaways.map(toDashboard);
}

/**
 * The WebUI has no discord.js Client to post the giveaway announcement
 * message, so creation is delegated to the bot via the same `giveaways`
 * BullMQ queue used by `/giveaway start` (see
 * `apps/bot/src/modules/giveaways/service.ts#runGiveawayCreateJob`). We wait
 * (bounded) for the job to finish so the dashboard can show the created
 * giveaway immediately.
 */
export async function createGiveawayDashboard(
  guildId: string,
  hostId: string,
  input: GiveawayCreateDashboard
): Promise<GiveawayDashboard> {
  await prisma.guild.upsert({ where: { id: guildId }, update: {}, create: { id: guildId } });

  const durationMs = new Date(input.endsAt).getTime() - Date.now();
  if (durationMs <= 0) {
    throw new GiveawayCreateTimeoutError();
  }

  const { confirmed, result } = await addJobAndAwait<{ id: string }>(
    getGiveawayQueue(),
    getGiveawayQueueEvents(),
    'giveawayCreate',
    {
      guildId,
      channelId: input.channelId,
      hostId,
      prize: input.prize,
      winnerCount: input.winnerCount,
      durationMs,
      requiredRoleId: input.requiredRoleId || null,
      requiredLevel: input.requiredLevel ?? null,
      requiredMemberDays: input.requiredMemberDays ?? null
    },
    { removeOnComplete: true, removeOnFail: 25 }
  );

  if (!confirmed || !result) {
    throw new GiveawayCreateTimeoutError();
  }

  const giveaway = await prisma.giveaway.findUnique({ where: { id: result.id } });
  if (!giveaway) {
    throw new GiveawayCreateTimeoutError();
  }
  return toDashboard(giveaway);
}

export async function setGiveawayPaused(
  guildId: string,
  giveawayId: string,
  paused: boolean
): Promise<GiveawayDashboard | null> {
  const existing = await prisma.giveaway.findFirst({ where: { id: giveawayId, guildId } });
  if (!existing || existing.endedAt) {
    return null;
  }

  const { confirmed } = await addJobAndAwait(
    getGiveawayQueue(),
    getGiveawayQueueEvents(),
    'giveawayPause',
    { giveawayId, paused },
    {
      jobId: `giveaway-pause-${giveawayId}-${paused ? '1' : '0'}-${Date.now()}`,
      removeOnComplete: true,
      removeOnFail: 25
    },
    30_000
  );

  const updated = await prisma.giveaway.findUnique({ where: { id: giveawayId } });
  if (!updated || updated.paused !== paused) {
    throw new GiveawayEndError(
      confirmed
        ? 'Giveaway pause job finished without updating pause state'
        : 'Giveaway pause timed out – the bot may still be processing; refresh and retry'
    );
  }
  return toDashboard(updated);
}

/**
 * Ends a giveaway immediately by enqueueing a one-shot `giveawayEnd` job
 * (unique jobId so it never collides with the scheduled/failed timer job)
 * and waiting for the bot to draw winners.
 */
export async function endGiveawayDashboard(guildId: string, giveawayId: string): Promise<GiveawayDashboard | null> {
  const existing = await prisma.giveaway.findFirst({ where: { id: giveawayId, guildId } });
  if (!existing || existing.endedAt) {
    return null;
  }

  await safeRemoveJob(scheduledEndJobId(giveawayId));

  // Unique jobId: reusing `giveaway-end-${id}` fails when a failed/locked
  // timer job still occupies that id in Redis.
  const manualJobId = `giveaway-end-now-${giveawayId}-${Date.now()}`;

  let confirmed = false;
  try {
    const result = await addJobAndAwait(
      getGiveawayQueue(),
      getGiveawayQueueEvents(),
      'giveawayEnd',
      { giveawayId, force: true },
      { jobId: manualJobId, removeOnComplete: true, removeOnFail: 50, delay: 0 },
      30_000
    );
    confirmed = result.confirmed;
  } catch (error) {
    throw new GiveawayEndError(
      error instanceof Error ? error.message : 'Failed to enqueue giveaway end job'
    );
  }

  const updated = await prisma.giveaway.findUnique({ where: { id: giveawayId } });
  if (!updated?.endedAt) {
    throw new GiveawayEndError(
      confirmed
        ? 'Giveaway end job finished without marking the giveaway as ended'
        : 'Giveaway end timed out – the bot may still be processing; refresh and retry'
    );
  }
  return toDashboard(updated);
}

export async function deleteGiveawayDashboard(guildId: string, giveawayId: string): Promise<boolean> {
  const existing = await prisma.giveaway.findFirst({ where: { id: giveawayId, guildId } });
  if (!existing) {
    return false;
  }

  await safeRemoveJob(scheduledEndJobId(giveawayId));

  const { confirmed } = await addJobAndAwait(
    getGiveawayQueue(),
    getGiveawayQueueEvents(),
    'giveawayDelete',
    { giveawayId },
    {
      jobId: `giveaway-delete-${giveawayId}-${Date.now()}`,
      removeOnComplete: true,
      removeOnFail: 25
    },
    30_000
  );

  const remaining = await prisma.giveaway.findUnique({ where: { id: giveawayId } });
  if (remaining) {
    throw new GiveawayEndError(
      confirmed
        ? 'Giveaway delete job finished without deleting the giveaway'
        : 'Giveaway delete timed out – the bot may still be processing; refresh and retry'
    );
  }
  return true;
}
