import {
  ActionRowBuilder,
  ButtonBuilder,
  ButtonStyle,
  EmbedBuilder,
  type TextChannel
} from 'discord.js';
import type { PrismaClient, Suggestion } from '@prisma/client';
import { SuggestionStatusSchema, t, tf, type SuggestionStatus } from '@nexumi/shared';
import type { Locale } from '@nexumi/shared';
import { ensureGuild } from '../../guild.js';
import { logger } from '../../logger.js';
import { getGuildLocale } from '../../i18n.js';
import type { BotContext } from '../../types.js';

export const SUGGESTION_VOTE_PREFIX = 'sug:vote:';

export function upvoteButtonId(suggestionId: string): string {
  return `${SUGGESTION_VOTE_PREFIX}${suggestionId}:up`;
}

export function downvoteButtonId(suggestionId: string): string {
  return `${SUGGESTION_VOTE_PREFIX}${suggestionId}:down`;
}

export function isSuggestionVoteButton(customId: string): boolean {
  return customId.startsWith(SUGGESTION_VOTE_PREFIX);
}

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

const STATUS_COLORS: Record<SuggestionStatus, number> = {
  OPEN: 0x6366f1,
  APPROVED: 0x22c55e,
  DENIED: 0xef4444,
  CONSIDERED: 0xeab308,
  IMPLEMENTED: 0x3b82f6
};

export async function getSuggestionConfig(prisma: PrismaClient, guildId: string) {
  await ensureGuild(prisma, guildId);
  let config = await prisma.suggestionConfig.findUnique({ where: { guildId } });
  if (!config) {
    config = await prisma.suggestionConfig.create({ data: { guildId } });
  }
  return config;
}

export async function updateSuggestionConfig(
  prisma: PrismaClient,
  guildId: string,
  data: {
    enabled: boolean;
    openChannelId: string;
    approvedChannelId?: string | null;
    deniedChannelId?: string | null;
    createThread: boolean;
  }
) {
  await ensureGuild(prisma, guildId);
  return prisma.suggestionConfig.upsert({
    where: { guildId },
    update: data,
    create: { guildId, ...data }
  });
}

function statusLabel(locale: Locale, status: SuggestionStatus): string {
  return t(locale, `suggestion.status.${status.toLowerCase()}`);
}

export function buildSuggestionEmbed(
  locale: Locale,
  suggestion: Suggestion,
  authorTag?: string
): EmbedBuilder {
  const status = SuggestionStatusSchema.parse(suggestion.status);
  const embed = new EmbedBuilder()
    .setColor(STATUS_COLORS[status])
    .setTitle(tf(locale, 'suggestion.embed.title', { id: suggestion.id.slice(-8) }))
    .setDescription(suggestion.content.slice(0, 4096))
    .addFields(
      {
        name: t(locale, 'suggestion.embed.status'),
        value: statusLabel(locale, status),
        inline: true
      },
      {
        name: t(locale, 'suggestion.embed.votes'),
        value: `👍 ${suggestion.upvotes} · 👎 ${suggestion.downvotes}`,
        inline: true
      }
    )
    .setFooter({
      text: tf(locale, 'suggestion.embed.author', {
        author: authorTag ?? `<@${suggestion.authorId}>`
      })
    })
    .setTimestamp(suggestion.createdAt);

  if (suggestion.staffReason) {
    embed.addFields({
      name: t(locale, 'suggestion.embed.staffReason'),
      value: suggestion.staffReason.slice(0, 1024),
      inline: false
    });
  }

  return embed;
}

export function buildVoteButtons(suggestion: Suggestion, disabled = false): ActionRowBuilder<ButtonBuilder> {
  return new ActionRowBuilder<ButtonBuilder>().addComponents(
    new ButtonBuilder()
      .setCustomId(upvoteButtonId(suggestion.id))
      .setLabel(`👍 ${suggestion.upvotes}`)
      .setStyle(ButtonStyle.Success)
      .setDisabled(disabled),
    new ButtonBuilder()
      .setCustomId(downvoteButtonId(suggestion.id))
      .setLabel(`👎 ${suggestion.downvotes}`)
      .setStyle(ButtonStyle.Danger)
      .setDisabled(disabled)
  );
}

export async function createSuggestion(
  context: BotContext,
  params: {
    guildId: string;
    authorId: string;
    content: string;
  },
  locale: Locale
): Promise<Suggestion> {
  const config = await getSuggestionConfig(context.prisma, params.guildId);
  if (!config.enabled || !config.openChannelId) {
    throw new SuggestionError('not_configured');
  }

  const channel = (await context.client.channels.fetch(config.openChannelId)) as TextChannel;
  if (!channel?.isTextBased() || channel.isDMBased()) {
    throw new SuggestionError('invalid_channel');
  }

  const suggestion = await context.prisma.suggestion.create({
    data: {
      guildId: params.guildId,
      authorId: params.authorId,
      content: params.content,
      status: 'OPEN',
      channelId: config.openChannelId
    }
  });

  const author = await context.client.users.fetch(params.authorId).catch(() => null);
  const embed = buildSuggestionEmbed(locale, suggestion, author?.tag);
  const components = [buildVoteButtons(suggestion)];

  const message = await channel.send({ embeds: [embed], components });
  let threadId: string | null = null;

  if (config.createThread) {
    const thread = await message
      .startThread({
        name: tf(locale, 'suggestion.threadName', { id: suggestion.id.slice(-8) }),
        autoArchiveDuration: 1440
      })
      .catch((error) => {
        logger.warn({ error, suggestionId: suggestion.id }, 'Failed to create suggestion thread');
        return null;
      });
    threadId = thread?.id ?? null;
  }

  return context.prisma.suggestion.update({
    where: { id: suggestion.id },
    data: {
      messageId: message.id,
      threadId
    }
  });
}

export async function applySuggestionVote(
  context: BotContext,
  suggestionId: string,
  userId: string,
  direction: 'up' | 'down',
  locale: Locale
): Promise<Suggestion> {
  const suggestion = await context.prisma.suggestion.findUnique({ where: { id: suggestionId } });
  if (!suggestion || suggestion.status !== 'OPEN') {
    throw new SuggestionError('not_open');
  }
  if (!suggestion.messageId || !suggestion.channelId) {
    throw new SuggestionError('not_found');
  }

  const upIds = suggestion.voterUpIds.filter((id) => id !== userId);
  const downIds = suggestion.voterDownIds.filter((id) => id !== userId);

  if (direction === 'up') {
    if (suggestion.voterUpIds.includes(userId)) {
      throw new SuggestionError('already_voted');
    }
    upIds.push(userId);
  } else {
    if (suggestion.voterDownIds.includes(userId)) {
      throw new SuggestionError('already_voted');
    }
    downIds.push(userId);
  }

  const upvotes = upIds.length;
  const downvotes = downIds.length;

  const updated = await context.prisma.suggestion.update({
    where: { id: suggestionId },
    data: {
      voterUpIds: upIds,
      voterDownIds: downIds,
      upvotes,
      downvotes
    }
  });

  const channel = (await context.client.channels.fetch(suggestion.channelId)) as TextChannel;
  const message = await channel.messages.fetch(suggestion.messageId).catch(() => null);
  if (message) {
    const author = await context.client.users.fetch(updated.authorId).catch(() => null);
    await message.edit({
      embeds: [buildSuggestionEmbed(locale, updated, author?.tag)],
      components: [buildVoteButtons(updated)]
    });
  }

  return updated;
}

async function notifyAuthor(
  context: BotContext,
  suggestion: Suggestion,
  locale: Locale,
  status: SuggestionStatus,
  reason: string
): Promise<void> {
  const user = await context.client.users.fetch(suggestion.authorId).catch(() => null);
  if (!user) {
    return;
  }

  await user
    .send({
      content: tf(locale, 'suggestion.dm.statusUpdate', {
        status: statusLabel(locale, status),
        reason
      })
    })
    .catch(() => undefined);
}

export async function updateSuggestionStatus(
  context: BotContext,
  params: {
    suggestionId: string;
    guildId: string;
    status: SuggestionStatus;
    reason: string;
  },
  locale: Locale
): Promise<Suggestion> {
  const suggestion = await context.prisma.suggestion.findFirst({
    where: { id: params.suggestionId, guildId: params.guildId }
  });
  if (!suggestion) {
    throw new SuggestionError('not_found');
  }

  const config = await getSuggestionConfig(context.prisma, params.guildId);
  const author = await context.client.users.fetch(suggestion.authorId).catch(() => null);
  const updatedData = {
    status: params.status,
    staffReason: params.reason
  };

  let targetChannelId = suggestion.channelId;
  if (params.status === 'APPROVED' || params.status === 'IMPLEMENTED') {
    targetChannelId = config.approvedChannelId ?? suggestion.channelId;
  } else if (params.status === 'DENIED') {
    targetChannelId = config.deniedChannelId ?? suggestion.channelId;
  }

  const embed = buildSuggestionEmbed(
    locale,
    { ...suggestion, ...updatedData },
    author?.tag
  );
  const components = [buildVoteButtons({ ...suggestion, ...updatedData }, true)];

  let messageId = suggestion.messageId;
  let channelId = suggestion.channelId;

  if (targetChannelId && targetChannelId !== suggestion.channelId) {
    const targetChannel = (await context.client.channels.fetch(targetChannelId)) as TextChannel;
    if (targetChannel?.isTextBased() && !targetChannel.isDMBased()) {
      const newMessage = await targetChannel.send({ embeds: [embed], components });
      messageId = newMessage.id;
      channelId = targetChannelId;

      if (suggestion.messageId && suggestion.channelId) {
        const oldChannel = (await context.client.channels.fetch(suggestion.channelId)) as TextChannel;
        const oldMessage = await oldChannel?.messages.fetch(suggestion.messageId).catch(() => null);
        await oldMessage?.delete().catch(() => undefined);
      }
    } else if (suggestion.messageId && suggestion.channelId) {
      const channel = (await context.client.channels.fetch(suggestion.channelId)) as TextChannel;
      const message = await channel?.messages.fetch(suggestion.messageId).catch(() => null);
      await message?.edit({ embeds: [embed], components });
    }
  } else if (suggestion.messageId && suggestion.channelId) {
    const channel = (await context.client.channels.fetch(suggestion.channelId)) as TextChannel;
    const message = await channel?.messages.fetch(suggestion.messageId).catch(() => null);
    await message?.edit({ embeds: [embed], components });
  }

  const updated = await context.prisma.suggestion.update({
    where: { id: suggestion.id },
    data: {
      ...updatedData,
      messageId,
      channelId
    }
  });

  await notifyAuthor(context, updated, locale, params.status, params.reason);
  return updated;
}

/**
 * Entry point for the `suggestionStatusUpdate` BullMQ job, enqueued by the
 * WebUI dashboard (which has no discord.js Client to move/edit the
 * suggestion embed or DM the author). Resolves the guild's locale and
 * delegates to {@link updateSuggestionStatus} so staff actions performed
 * from the dashboard behave identically to `/suggestion approve|deny|...`.
 */
export async function runSuggestionStatusUpdateJob(
  context: BotContext,
  params: {
    suggestionId: string;
    guildId: string;
    status: SuggestionStatus;
    reason: string;
  }
): Promise<{ id: string }> {
  const locale = await getGuildLocale(context.prisma, params.guildId);
  const updated = await updateSuggestionStatus(context, params, locale);
  return { id: updated.id };
}
