import {
  ActionRowBuilder,
  ButtonBuilder,
  ButtonStyle,
  EmbedBuilder,
  type Guild,
  type GuildMember,
  type TextChannel
} from 'discord.js';
import { pickRandomWinners, t, tf } from '@nexumi/shared';
import type { Giveaway } from '@prisma/client';
import { ensureGuild } from '../../guild.js';
import { logger } from '../../logger.js';
import { giveawayQueue } from '../../queues.js';
import type { BotContext } from '../../types.js';
import { getGuildLocale } from '../../i18n.js';
import { joinButtonId } from './buttons.js';

export class GiveawayError extends Error {
  constructor(public readonly code: string) {
    super(code);
  }
}

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

/**
 * Removes a scheduled/failed end job if present. Locked (active) jobs cannot be
 * removed — callers must not invoke this from inside the giveawayEnd worker.
 */
export async function cancelGiveawayJob(giveawayId: string): Promise<void> {
  const job = await giveawayQueue.getJob(giveawayEndJobId(giveawayId));
  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')) {
      logger.warn({ giveawayId, error }, 'Giveaway end job is locked; skipping remove');
      return;
    }
    throw error;
  }
}

export async function scheduleGiveawayEnd(giveawayId: string, endsAt: Date): Promise<void> {
  await cancelGiveawayJob(giveawayId);
  const delay = Math.max(0, endsAt.getTime() - Date.now());
  await giveawayQueue.add(
    'giveawayEnd',
    { giveawayId },
    {
      delay,
      jobId: giveawayEndJobId(giveawayId),
      removeOnComplete: true,
      removeOnFail: 50
    }
  );
}

function requirementLines(locale: 'de' | 'en', giveaway: Giveaway): string[] {
  const lines: string[] = [];
  if (giveaway.requiredRoleId) {
    lines.push(tf(locale, 'giveaway.requirement.role', { role: `<@&${giveaway.requiredRoleId}>` }));
  }
  if (giveaway.requiredLevel !== null && giveaway.requiredLevel !== undefined) {
    lines.push(tf(locale, 'giveaway.requirement.level', { level: giveaway.requiredLevel }));
  }
  if (giveaway.requiredMemberDays !== null && giveaway.requiredMemberDays !== undefined) {
    lines.push(
      tf(locale, 'giveaway.requirement.memberDays', { days: giveaway.requiredMemberDays })
    );
  }
  return lines;
}

export function buildGiveawayEmbed(locale: 'de' | 'en', giveaway: Giveaway): EmbedBuilder {
  const ended = giveaway.endedAt !== null;
  const endsAtUnix = Math.floor(giveaway.endsAt.getTime() / 1000);
  // Discord does not parse <t:…> timestamps in embed footers — only in
  // description/fields/content. Keep relative + absolute for clarity.
  const endsAtText = `<t:${endsAtUnix}:R> (<t:${endsAtUnix}:f>)`;

  const embed = new EmbedBuilder()
    .setTitle(t(locale, 'giveaway.embed.title'))
    .setColor(ended ? 0x6b7280 : 0x6366f1)
    .addFields(
      { name: t(locale, 'giveaway.embed.prize'), value: giveaway.prize, inline: true },
      {
        name: t(locale, 'giveaway.embed.winners'),
        value: String(giveaway.winnerCount),
        inline: true
      },
      {
        name: t(locale, 'giveaway.embed.entrants'),
        value: String(giveaway.entrants.length),
        inline: true
      }
    );

  if (ended) {
    embed.setDescription(
      giveaway.winners.length > 0
        ? tf(locale, 'giveaway.embed.endedWinners', {
            winners: giveaway.winners.map((id) => `<@${id}>`).join(', ')
          })
        : t(locale, 'giveaway.embed.endedNoWinners')
    );
    embed.setFooter({ text: t(locale, 'giveaway.embed.ended') });
  } else if (giveaway.paused) {
    embed.setDescription(
      `${t(locale, 'giveaway.embed.paused')}\n\n${tf(locale, 'giveaway.embed.endsAt', {
        time: endsAtText
      })}`
    );
  } else {
    embed.setDescription(
      `${t(locale, 'giveaway.embed.active')}\n\n${tf(locale, 'giveaway.embed.endsAt', {
        time: endsAtText
      })}`
    );
  }

  const requirements = requirementLines(locale, giveaway);
  if (requirements.length > 0) {
    embed.addFields({
      name: t(locale, 'giveaway.embed.requirements'),
      value: requirements.join('\n')
    });
  }

  embed.addFields({
    name: t(locale, 'giveaway.embed.host'),
    value: `<@${giveaway.hostId}>`,
    inline: true
  });

  return embed;
}

export function buildGiveawayComponents(
  locale: 'de' | 'en',
  giveaway: Giveaway
): ActionRowBuilder<ButtonBuilder>[] {
  if (giveaway.endedAt) {
    return [];
  }
  return [
    new ActionRowBuilder<ButtonBuilder>().addComponents(
      new ButtonBuilder()
        .setCustomId(joinButtonId(giveaway.id))
        .setLabel(t(locale, 'giveaway.button.join'))
        .setStyle(ButtonStyle.Primary)
        .setDisabled(giveaway.paused)
    )
  ];
}

async function updateGiveawayMessage(
  context: BotContext,
  giveaway: Giveaway,
  locale: 'de' | 'en'
): Promise<void> {
  if (!giveaway.messageId) {
    return;
  }
  try {
    const channel = (await context.client.channels.fetch(giveaway.channelId)) as TextChannel;
    const message = await channel.messages.fetch(giveaway.messageId);
    await message.edit({
      embeds: [buildGiveawayEmbed(locale, giveaway)],
      components: buildGiveawayComponents(locale, giveaway)
    });
  } catch (error) {
    logger.warn({ error, giveawayId: giveaway.id }, 'Failed to update giveaway message');
  }
}

export async function checkGiveawayRequirements(
  context: BotContext,
  guild: Guild,
  member: GuildMember,
  giveaway: Giveaway
): Promise<string | null> {
  if (giveaway.requiredRoleId && !member.roles.cache.has(giveaway.requiredRoleId)) {
    return 'missing_role';
  }

  if (giveaway.requiredLevel !== null && giveaway.requiredLevel !== undefined) {
    const levelRow = await context.prisma.memberLevel.findUnique({
      where: { guildId_userId: { guildId: guild.id, userId: member.id } }
    });
    const level = levelRow?.level ?? 0;
    if (level < giveaway.requiredLevel) {
      return 'insufficient_level';
    }
  }

  if (giveaway.requiredMemberDays !== null && giveaway.requiredMemberDays !== undefined) {
    const joinedAt = member.joinedAt;
    const days = joinedAt
      ? Math.floor((Date.now() - joinedAt.getTime()) / 86_400_000)
      : 0;
    if (days < giveaway.requiredMemberDays) {
      return 'insufficient_member_days';
    }
  }

  return null;
}

export async function startGiveaway(
  context: BotContext,
  params: {
    guildId: string;
    channelId: string;
    hostId: string;
    prize: string;
    winnerCount: number;
    durationMs: number;
    requiredRoleId?: string | null;
    requiredLevel?: number | null;
    requiredMemberDays?: number | null;
  },
  locale: 'de' | 'en'
): Promise<Giveaway> {
  await ensureGuild(context.prisma, params.guildId);
  const endsAt = new Date(Date.now() + params.durationMs);

  const giveaway = await context.prisma.giveaway.create({
    data: {
      guildId: params.guildId,
      channelId: params.channelId,
      prize: params.prize,
      winnerCount: params.winnerCount,
      endsAt,
      requiredRoleId: params.requiredRoleId ?? null,
      requiredLevel: params.requiredLevel ?? null,
      requiredMemberDays: params.requiredMemberDays ?? null,
      hostId: params.hostId
    }
  });

  const channel = (await context.client.channels.fetch(params.channelId)) as TextChannel;
  const message = await channel.send({
    embeds: [buildGiveawayEmbed(locale, giveaway)],
    components: buildGiveawayComponents(locale, giveaway)
  });

  const updated = await context.prisma.giveaway.update({
    where: { id: giveaway.id },
    data: { messageId: message.id }
  });

  await scheduleGiveawayEnd(updated.id, updated.endsAt);
  return updated;
}

export async function joinGiveaway(
  context: BotContext,
  giveawayId: string,
  userId: string,
  locale: 'de' | 'en'
): Promise<Giveaway> {
  const giveaway = await context.prisma.giveaway.findUnique({ where: { id: giveawayId } });
  if (!giveaway || giveaway.endedAt) {
    throw new GiveawayError('not_found');
  }
  if (giveaway.paused) {
    throw new GiveawayError('paused');
  }
  if (giveaway.entrants.includes(userId)) {
    throw new GiveawayError('already_joined');
  }

  const guild = await context.client.guilds.fetch(giveaway.guildId);
  const member = await guild.members.fetch(userId).catch(() => null);
  if (!member) {
    throw new GiveawayError('member_not_found');
  }

  const requirementError = await checkGiveawayRequirements(context, guild, member, giveaway);
  if (requirementError) {
    throw new GiveawayError(requirementError);
  }

  const updated = await context.prisma.giveaway.update({
    where: { id: giveawayId },
    data: { entrants: { push: userId } }
  });

  await updateGiveawayMessage(context, updated, locale);
  return updated;
}

async function filterEligibleEntrants(
  context: BotContext,
  guild: Guild,
  giveaway: Giveaway
): Promise<string[]> {
  const eligible: string[] = [];
  for (const userId of giveaway.entrants) {
    const member = await guild.members.fetch(userId).catch(() => null);
    if (!member) {
      continue;
    }
    const error = await checkGiveawayRequirements(context, guild, member, giveaway);
    if (!error) {
      eligible.push(userId);
    }
  }
  return eligible;
}

async function dmWinners(
  context: BotContext,
  giveaway: Giveaway,
  winnerIds: string[],
  locale: 'de' | 'en'
): Promise<void> {
  for (const userId of winnerIds) {
    try {
      const user = await context.client.users.fetch(userId);
      await user.send(
        tf(locale, 'giveaway.dm.won', {
          prize: giveaway.prize,
          guild: (await context.client.guilds.fetch(giveaway.guildId)).name
        })
      );
    } catch (error) {
      logger.warn({ error, userId, giveawayId: giveaway.id }, 'Failed to DM giveaway winner');
    }
  }
}

/**
 * Draws winners and marks the giveaway ended. Does **not** cancel BullMQ jobs —
 * the slash-command / dashboard paths cancel the delayed job first; the
 * `giveawayEnd` worker must never remove its own locked job (that threw and
 * left giveaways stuck with the join button still visible).
 *
 * Uses a conditional update so concurrent ends (timer + slash + recover)
 * cannot each write different winners.
 */
export async function endGiveaway(context: BotContext, giveawayId: string): Promise<Giveaway> {
  const giveaway = await context.prisma.giveaway.findUnique({ where: { id: giveawayId } });
  if (!giveaway) {
    throw new GiveawayError('not_found');
  }
  if (giveaway.endedAt) {
    throw new GiveawayError('already_ended');
  }

  const locale = await getGuildLocale(context.prisma, giveaway.guildId);
  const guild = await context.client.guilds.fetch(giveaway.guildId);
  const eligible = await filterEligibleEntrants(context, guild, giveaway);
  const winners = pickRandomWinners(eligible, giveaway.winnerCount);
  const endedAt = new Date();

  const claimed = await context.prisma.giveaway.updateMany({
    where: { id: giveawayId, endedAt: null },
    data: {
      endedAt,
      winners,
      paused: false
    }
  });

  if (claimed.count === 0) {
    const current = await context.prisma.giveaway.findUnique({ where: { id: giveawayId } });
    if (!current) {
      throw new GiveawayError('not_found');
    }
    throw new GiveawayError('already_ended');
  }

  const updated = await context.prisma.giveaway.findUniqueOrThrow({ where: { id: giveawayId } });

  await updateGiveawayMessage(context, updated, locale);
  if (winners.length > 0) {
    await dmWinners(context, updated, winners, locale);
  }
  return updated;
}

/**
 * Ends giveaways whose `endsAt` has passed but `endedAt` is still null
 * (e.g. after a failed/locked BullMQ job). Skips paused giveaways.
 */
export async function recoverOverdueGiveaways(context: BotContext): Promise<number> {
  const overdue = await context.prisma.giveaway.findMany({
    where: {
      endedAt: null,
      paused: false,
      endsAt: { lte: new Date() }
    },
    take: 100,
    orderBy: { endsAt: 'asc' }
  });

  let recovered = 0;
  for (const giveaway of overdue) {
    try {
      await cancelGiveawayJob(giveaway.id);
      await endGiveaway(context, giveaway.id);
      recovered += 1;
    } catch (error) {
      if (error instanceof GiveawayError && error.code === 'already_ended') {
        continue;
      }
      logger.error({ error, giveawayId: giveaway.id }, 'Failed to recover overdue giveaway');
    }
  }

  if (recovered > 0) {
    logger.info({ recovered }, 'Recovered overdue giveaways');
  }
  return recovered;
}

export async function rerollGiveaway(
  context: BotContext,
  giveawayId: string,
  locale: 'de' | 'en'
): Promise<Giveaway> {
  const giveaway = await context.prisma.giveaway.findUnique({ where: { id: giveawayId } });
  if (!giveaway || !giveaway.endedAt) {
    throw new GiveawayError('not_ended');
  }

  const guild = await context.client.guilds.fetch(giveaway.guildId);
  const eligible = (await filterEligibleEntrants(context, guild, giveaway)).filter(
    (id) => !giveaway.winners.includes(id)
  );
  const newWinners = pickRandomWinners(eligible, giveaway.winnerCount);

  const updated = await context.prisma.giveaway.update({
    where: { id: giveawayId },
    data: { winners: newWinners }
  });

  await updateGiveawayMessage(context, updated, locale);
  if (newWinners.length > 0) {
    await dmWinners(context, updated, newWinners, locale);
  }
  return updated;
}

export async function pauseGiveaway(
  context: BotContext,
  giveawayId: string,
  locale: 'de' | 'en',
  forcePaused?: boolean
): Promise<Giveaway> {
  const giveaway = await context.prisma.giveaway.findUnique({ where: { id: giveawayId } });
  if (!giveaway || giveaway.endedAt) {
    throw new GiveawayError('not_found');
  }

  const nextPaused = forcePaused ?? !giveaway.paused;
  if (nextPaused === giveaway.paused) {
    await updateGiveawayMessage(context, giveaway, locale);
    return giveaway;
  }

  if (nextPaused) {
    // Stop the end timer while paused; remaining wall-clock is preserved via endsAt.
    await cancelGiveawayJob(giveawayId);
  }

  const updated = await context.prisma.giveaway.update({
    where: { id: giveawayId },
    data: { paused: nextPaused }
  });

  if (!nextPaused) {
    // Resume: re-schedule with delay based on remaining time until endsAt
    // (delay 0 if endsAt already passed while paused → ends immediately).
    await scheduleGiveawayEnd(updated.id, updated.endsAt);
  }

  await updateGiveawayMessage(context, updated, locale);
  return updated;
}

/**
 * Dashboard entry points — same Discord side-effects as slash commands.
 */
export async function runGiveawayPauseJob(
  context: BotContext,
  giveawayId: string,
  paused: boolean
): Promise<{ id: string; paused: boolean }> {
  const giveaway = await context.prisma.giveaway.findUnique({ where: { id: giveawayId } });
  if (!giveaway) {
    throw new GiveawayError('not_found');
  }
  const locale = await getGuildLocale(context.prisma, giveaway.guildId);
  const updated = await pauseGiveaway(context, giveawayId, locale, paused);
  return { id: updated.id, paused: updated.paused };
}

export async function runGiveawayDeleteJob(context: BotContext, giveawayId: string): Promise<void> {
  await deleteGiveaway(context, giveawayId);
}

export async function deleteGiveaway(context: BotContext, giveawayId: string): Promise<void> {
  const giveaway = await context.prisma.giveaway.findUnique({ where: { id: giveawayId } });
  if (!giveaway) {
    throw new GiveawayError('not_found');
  }

  await cancelGiveawayJob(giveawayId);

  if (giveaway.messageId) {
    try {
      const channel = (await context.client.channels.fetch(giveaway.channelId)) as TextChannel;
      const message = await channel.messages.fetch(giveaway.messageId);
      await message.delete();
    } catch {
      // Message may already be gone
    }
  }

  await context.prisma.giveaway.delete({ where: { id: giveawayId } });
}

/**
 * Entry point for the `giveawayCreate` BullMQ job, enqueued by the WebUI
 * dashboard (which has no discord.js Client to post the giveaway message
 * itself). Resolves the guild's locale and delegates to {@link startGiveaway}
 * so dashboard-created giveaways behave identically to `/giveaway start`.
 */
export async function runGiveawayCreateJob(
  context: BotContext,
  params: {
    guildId: string;
    channelId: string;
    hostId: string;
    prize: string;
    winnerCount: number;
    durationMs: number;
    requiredRoleId?: string | null;
    requiredLevel?: number | null;
    requiredMemberDays?: number | null;
  }
): Promise<{ id: string }> {
  const locale = await getGuildLocale(context.prisma, params.guildId);
  const giveaway = await startGiveaway(context, params, locale);
  return { id: giveaway.id };
}

export async function listActiveGiveaways(
  context: BotContext,
  guildId: string
): Promise<Giveaway[]> {
  return context.prisma.giveaway.findMany({
    where: { guildId, endedAt: null },
    orderBy: { endsAt: 'asc' }
  });
}
