import {
  ActionRowBuilder,
  ButtonBuilder,
  ButtonStyle,
  EmbedBuilder,
  PermissionFlagsBits,
  StringSelectMenuBuilder,
  type APIMessageComponentEmoji,
  type GuildMember,
  type Message,
  type TextChannel
} from 'discord.js';
import {
  SelfRoleBehaviorSchema,
  SelfRoleEntrySchema,
  SelfRoleModeSchema,
  type SelfRoleBehavior,
  type SelfRoleEntry,
  type SelfRoleMode,
  t
} from '@nexumi/shared';
import type { SelfRolePanel } from '@prisma/client';
import { ensureGuild } from '../../guild.js';
import { getGuildLocale } from '../../i18n.js';
import { logger } from '../../logger.js';
import { safeRemoveBullJob } from '../../lib/safe-remove-job.js';
import { ticketQueue } from '../../queues.js';
import type { BotContext } from '../../types.js';

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

export const SELF_ROLE_BTN_PREFIX = 'sr:btn:';
export const SELF_ROLE_SEL_PREFIX = 'sr:sel:';

type RolesStorage = {
  entries: SelfRoleEntry[];
  temporaryDurationMs?: number;
};

export function parseRolesInput(input: string): SelfRoleEntry[] {
  const parts = input
    .split(',')
    .map((part) => part.trim())
    .filter(Boolean);

  if (parts.length === 0) {
    throw new SelfRoleError('invalid_roles');
  }

  const entries: SelfRoleEntry[] = [];
  for (const part of parts) {
    const segments = part.split(':');
    if (segments.length < 2) {
      throw new SelfRoleError('invalid_roles');
    }
    const [roleId, label, emoji] = segments;
    if (!roleId || !label) {
      throw new SelfRoleError('invalid_roles');
    }
    entries.push(
      SelfRoleEntrySchema.parse({
        roleId: roleId.trim(),
        label: label.trim(),
        emoji: emoji?.trim() || undefined
      })
    );
  }

  if (entries.length > 25) {
    throw new SelfRoleError('too_many_roles');
  }

  return entries;
}

export function parseRolesJson(raw: unknown): RolesStorage {
  if (Array.isArray(raw)) {
    return {
      entries: raw
        .map((item) => SelfRoleEntrySchema.safeParse(item))
        .filter((result) => result.success)
        .map((result) => result.data!)
    };
  }

  if (raw && typeof raw === 'object') {
    const obj = raw as { entries?: unknown; temporaryDurationMs?: unknown };
    const entries = Array.isArray(obj.entries)
      ? obj.entries
          .map((item) => SelfRoleEntrySchema.safeParse(item))
          .filter((result) => result.success)
          .map((result) => result.data!)
      : [];
    const temporaryDurationMs =
      typeof obj.temporaryDurationMs === 'number' && obj.temporaryDurationMs > 0
        ? obj.temporaryDurationMs
        : undefined;
    return { entries, temporaryDurationMs };
  }

  return { entries: [] };
}

export function serializeRolesJson(
  entries: SelfRoleEntry[],
  temporaryDurationMs?: number
): RolesStorage {
  if (temporaryDurationMs !== undefined) {
    return { entries, temporaryDurationMs };
  }
  return { entries };
}

function parseButtonEmoji(
  emoji?: string
): APIMessageComponentEmoji | undefined {
  if (!emoji) {
    return undefined;
  }

  const customMatch = emoji.match(/^<(a)?:(\w+):(\d+)>$/);
  if (customMatch) {
    return {
      id: customMatch[3]!,
      name: customMatch[2],
      animated: Boolean(customMatch[1])
    };
  }

  if (/^\d+$/.test(emoji)) {
    return { id: emoji };
  }

  return { name: emoji };
}

function parseReactionEmoji(emoji?: string): string {
  if (!emoji) {
    throw new SelfRoleError('reaction_emoji_required');
  }

  const customMatch = emoji.match(/^<(a)?:(\w+):(\d+)>$/);
  if (customMatch) {
    return customMatch[3]!;
  }

  if (/^\d+$/.test(emoji)) {
    return emoji;
  }

  return emoji;
}

export function buildPanelEmbed(panel: SelfRolePanel, locale: 'de' | 'en'): EmbedBuilder {
  const { entries } = parseRolesJson(panel.roles);
  const roleLines = entries.map((entry) => {
    const prefix = entry.emoji ? `${entry.emoji} ` : '';
    return `${prefix}**${entry.label}** — <@&${entry.roleId}>`;
  });

  const embed = new EmbedBuilder()
    .setTitle(panel.title)
    .setColor(0x6366f1)
    .setDescription(panel.description ?? t(locale, 'selfroles.panel.defaultDescription'));

  if (roleLines.length > 0) {
    embed.addFields({
      name: t(locale, 'selfroles.panel.rolesField'),
      value: roleLines.join('\n')
    });
  }

  const behavior = SelfRoleBehaviorSchema.parse(panel.behavior);
  if (behavior === 'TEMPORARY') {
    const { temporaryDurationMs } = parseRolesJson(panel.roles);
    if (temporaryDurationMs) {
      embed.setFooter({
        text: t(locale, 'selfroles.panel.temporaryFooter')
      });
    }
  }

  return embed;
}

export function buildPanelComponents(panel: SelfRolePanel): ActionRowBuilder<
  ButtonBuilder | StringSelectMenuBuilder
>[] {
  const mode = SelfRoleModeSchema.parse(panel.mode);
  const { entries } = parseRolesJson(panel.roles);

  if (mode === 'REACTIONS' || entries.length === 0) {
    return [];
  }

  if (mode === 'DROPDOWN') {
    const select = new StringSelectMenuBuilder()
      .setCustomId(`${SELF_ROLE_SEL_PREFIX}${panel.id}`)
      .setPlaceholder('Select a role')
      .setMinValues(1)
      .setMaxValues(1)
      .addOptions(
        entries.map((entry) => ({
          label: entry.label.slice(0, 100),
          value: entry.roleId,
          description: entry.description?.slice(0, 100),
          emoji: parseButtonEmoji(entry.emoji)
        }))
      );

    return [new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(select)];
  }

  const rows: ActionRowBuilder<ButtonBuilder>[] = [];
  let currentRow = new ActionRowBuilder<ButtonBuilder>();
  let buttonsInRow = 0;

  for (const entry of entries) {
    if (buttonsInRow >= 5) {
      rows.push(currentRow);
      currentRow = new ActionRowBuilder<ButtonBuilder>();
      buttonsInRow = 0;
    }

    const button = new ButtonBuilder()
      .setCustomId(`${SELF_ROLE_BTN_PREFIX}${panel.id}:${entry.roleId}`)
      .setLabel(entry.label.slice(0, 80))
      .setStyle(ButtonStyle.Secondary);

    const emoji = parseButtonEmoji(entry.emoji);
    if (emoji) {
      button.setEmoji(emoji);
    }

    currentRow.addComponents(button);
    buttonsInRow += 1;

    if (rows.length >= 5) {
      break;
    }
  }

  if (buttonsInRow > 0 && rows.length < 5) {
    rows.push(currentRow);
  }

  return rows;
}

async function applyReactionEmojis(message: Message, entries: SelfRoleEntry[]): Promise<void> {
  for (const entry of entries) {
    try {
      const emoji = parseReactionEmoji(entry.emoji);
      await message.react(emoji);
    } catch (error) {
      logger.warn({ error, panelId: message.id, roleId: entry.roleId }, 'Failed to add self-role reaction');
    }
  }
}

export async function scheduleSelfRoleExpiry(
  guildId: string,
  userId: string,
  roleId: string,
  expiresAt: Date
): Promise<void> {
  const jobId = `selfrole-expire-${guildId}-${userId}-${roleId}`;
  const existing = await ticketQueue.getJob(jobId);
  await safeRemoveBullJob(existing, { label: 'selfRoleExpire', jobId });

  const delay = Math.max(0, expiresAt.getTime() - Date.now());
  await ticketQueue.add(
    'selfRoleExpire',
    { guildId, userId, roleId },
    {
      delay,
      jobId,
      removeOnComplete: true,
      removeOnFail: 50
    }
  );
}

export async function cancelSelfRoleExpiry(
  guildId: string,
  userId: string,
  roleId: string
): Promise<void> {
  const jobId = `selfrole-expire-${guildId}-${userId}-${roleId}`;
  const job = await ticketQueue.getJob(jobId);
  await safeRemoveBullJob(job, { label: 'selfRoleExpire', jobId });
}

export async function expireSelfRole(
  context: BotContext,
  guildId: string,
  userId: string,
  roleId: string
): Promise<void> {
  const guild = await context.client.guilds.fetch(guildId).catch(() => null);
  if (!guild) {
    return;
  }

  const member = await guild.members.fetch(userId).catch(() => null);
  if (!member?.roles.cache.has(roleId)) {
    return;
  }

  const me = guild.members.me;
  if (!me?.permissions.has(PermissionFlagsBits.ManageRoles)) {
    logger.warn({ guildId, roleId }, 'Missing ManageRoles for self-role expiry');
    return;
  }

  const role = guild.roles.cache.get(roleId);
  if (!role || role.position >= me.roles.highest.position) {
    logger.warn({ guildId, roleId }, 'Cannot remove self-role above bot hierarchy');
    return;
  }

  await member.roles.remove(roleId, 'Temporary self-role expired').catch((error) => {
    logger.warn({ error, guildId, userId, roleId }, 'Failed to remove expired self-role');
  });
}

async function assertAssignableRole(member: GuildMember, roleId: string): Promise<void> {
  const guild = member.guild;
  const me = guild.members.me;
  if (!me?.permissions.has(PermissionFlagsBits.ManageRoles)) {
    throw new SelfRoleError('bot_missing_permissions');
  }

  const role = guild.roles.cache.get(roleId);
  if (!role) {
    throw new SelfRoleError('role_not_found');
  }

  if (role.position >= me.roles.highest.position) {
    throw new SelfRoleError('role_hierarchy');
  }

  if (role.managed) {
    throw new SelfRoleError('role_managed');
  }
}

export async function toggleSelfRole(
  context: BotContext,
  panel: SelfRolePanel,
  member: GuildMember,
  roleId: string
): Promise<'added' | 'removed' | 'unchanged'> {
  const { entries, temporaryDurationMs } = parseRolesJson(panel.roles);
  const entry = entries.find((item) => item.roleId === roleId);
  if (!entry) {
    throw new SelfRoleError('role_not_in_panel');
  }

  await assertAssignableRole(member, roleId);

  const behavior = SelfRoleBehaviorSchema.parse(panel.behavior);
  const panelRoleIds = entries.map((item) => item.roleId);
  const hasRole = member.roles.cache.has(roleId);

  if (behavior === 'VERIFY') {
    if (hasRole) {
      return 'unchanged';
    }
    await member.roles.add(roleId);
    return 'added';
  }

  if (behavior === 'UNIQUE') {
    if (hasRole) {
      await member.roles.remove(roleId);
      return 'removed';
    }

    const toRemove = panelRoleIds.filter((id) => id !== roleId && member.roles.cache.has(id));
    if (toRemove.length > 0) {
      await member.roles.remove(toRemove);
    }

    await member.roles.add(roleId);
    return 'added';
  }

  if (behavior === 'TEMPORARY') {
    if (hasRole) {
      await member.roles.remove(roleId);
      await cancelSelfRoleExpiry(member.guild.id, member.id, roleId);
      return 'removed';
    }

    await member.roles.add(roleId);
    if (temporaryDurationMs) {
      const expiresAt = new Date(Date.now() + temporaryDurationMs);
      await scheduleSelfRoleExpiry(member.guild.id, member.id, roleId, expiresAt);
    }
    return 'added';
  }

  if (hasRole) {
    await member.roles.remove(roleId);
    return 'removed';
  }

  await member.roles.add(roleId);
  return 'added';
}

export async function findPanelByMessageId(
  context: BotContext,
  messageId: string
): Promise<SelfRolePanel | null> {
  return context.prisma.selfRolePanel.findFirst({
    where: { messageId }
  });
}

async function postOrEditPanelMessage(
  context: BotContext,
  panel: SelfRolePanel,
  locale: 'de' | 'en',
  previous?: { channelId: string; messageId: string | null }
): Promise<SelfRolePanel> {
  const mode = SelfRoleModeSchema.parse(panel.mode);
  const { entries } = parseRolesJson(panel.roles);
  const channelChanged =
    previous !== undefined && previous.channelId !== panel.channelId;

  if (panel.messageId && !channelChanged) {
    try {
      const channel = (await context.client.channels.fetch(panel.channelId)) as TextChannel;
      const message = await channel.messages.fetch(panel.messageId);
      await message.edit({
        embeds: [buildPanelEmbed(panel, locale)],
        components: buildPanelComponents(panel)
      });

      if (mode === 'REACTIONS') {
        for (const reaction of message.reactions.cache.values()) {
          await reaction.remove().catch(() => undefined);
        }
        await applyReactionEmojis(message, entries);
      }

      return panel;
    } catch (error) {
      logger.warn({ error, panelId: panel.id }, 'Failed to edit self-role panel message; recreating');
    }
  }

  if (previous?.messageId && channelChanged) {
    try {
      const oldChannel = (await context.client.channels.fetch(previous.channelId)) as TextChannel;
      const oldMessage = await oldChannel.messages.fetch(previous.messageId);
      await oldMessage.delete();
    } catch {
      // Previous message may already be gone
    }
  }

  const channel = await context.client.channels.fetch(panel.channelId);
  if (!channel || !channel.isTextBased() || channel.isDMBased()) {
    throw new SelfRoleError('invalid_channel');
  }

  const message = await channel.send({
    embeds: [buildPanelEmbed(panel, locale)],
    components: buildPanelComponents(panel)
  });

  if (mode === 'REACTIONS') {
    await applyReactionEmojis(message, entries);
  }

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

export async function createSelfRolePanel(
  context: BotContext,
  params: {
    guildId: string;
    channelId: string;
    title: string;
    description?: string | null;
    mode: SelfRoleMode;
    behavior: SelfRoleBehavior;
    roles: SelfRoleEntry[];
    temporaryDurationMs?: number;
    expiresAt?: Date | null;
  },
  locale: 'de' | 'en'
): Promise<SelfRolePanel> {
  await ensureGuild(context.prisma, params.guildId);

  if (params.behavior === 'TEMPORARY' && !params.temporaryDurationMs) {
    throw new SelfRoleError('duration_required');
  }

  if (params.mode === 'REACTIONS') {
    const missingEmoji = params.roles.some((entry) => !entry.emoji);
    if (missingEmoji) {
      throw new SelfRoleError('reaction_emoji_required');
    }
  }

  const panel = await context.prisma.selfRolePanel.create({
    data: {
      guildId: params.guildId,
      channelId: params.channelId,
      title: params.title,
      description: params.description ?? null,
      mode: params.mode,
      behavior: params.behavior,
      roles: serializeRolesJson(params.roles, params.temporaryDurationMs),
      expiresAt: params.expiresAt ?? null
    }
  });

  return postOrEditPanelMessage(context, panel, locale);
}

export async function updateSelfRolePanel(
  context: BotContext,
  panelId: string,
  guildId: string,
  updates: {
    channelId?: string;
    title?: string;
    description?: string | null;
    mode?: SelfRoleMode;
    behavior?: SelfRoleBehavior;
    roles?: SelfRoleEntry[];
    temporaryDurationMs?: number;
    expiresAt?: Date | null;
  },
  locale: 'de' | 'en'
): Promise<SelfRolePanel> {
  const existing = await context.prisma.selfRolePanel.findFirst({
    where: { id: panelId, guildId }
  });

  if (!existing) {
    throw new SelfRoleError('not_found');
  }

  const mode = updates.mode ?? SelfRoleModeSchema.parse(existing.mode);
  const behavior = updates.behavior ?? SelfRoleBehaviorSchema.parse(existing.behavior);
  const roles = updates.roles ?? parseRolesJson(existing.roles).entries;
  const { temporaryDurationMs: existingDuration } = parseRolesJson(existing.roles);
  const temporaryDurationMs = updates.temporaryDurationMs ?? existingDuration;

  if (behavior === 'TEMPORARY' && !temporaryDurationMs) {
    throw new SelfRoleError('duration_required');
  }

  if (mode === 'REACTIONS' && roles.some((entry) => !entry.emoji)) {
    throw new SelfRoleError('reaction_emoji_required');
  }

  const previous = { channelId: existing.channelId, messageId: existing.messageId };
  const panel = await context.prisma.selfRolePanel.update({
    where: { id: panelId },
    data: {
      channelId: updates.channelId ?? existing.channelId,
      title: updates.title ?? existing.title,
      description: updates.description !== undefined ? updates.description : existing.description,
      mode,
      behavior,
      roles: serializeRolesJson(roles, temporaryDurationMs),
      ...(updates.expiresAt !== undefined ? { expiresAt: updates.expiresAt } : {})
    }
  });

  return postOrEditPanelMessage(context, panel, locale, previous);
}

/**
 * BullMQ entry points for dashboard create/update/delete. The WebUI has no
 * discord.js Client, so panel posting is delegated here (same pattern as
 * giveaways / dashboard messages).
 */
export async function runSelfRolePanelCreateJob(
  context: BotContext,
  params: {
    guildId: string;
    channelId: string;
    title: string;
    description?: string | null;
    mode: SelfRoleMode;
    behavior: SelfRoleBehavior;
    roles: SelfRoleEntry[];
    temporaryDurationMs?: number;
    expiresAt?: string | null;
  }
): Promise<{ id: string }> {
  const locale = await getGuildLocale(context.prisma, params.guildId);
  const panel = await createSelfRolePanel(
    context,
    {
      ...params,
      expiresAt: params.expiresAt ? new Date(params.expiresAt) : null
    },
    locale
  );
  return { id: panel.id };
}

export async function runSelfRolePanelUpdateJob(
  context: BotContext,
  params: {
    panelId: string;
    guildId: string;
    channelId?: string;
    title?: string;
    description?: string | null;
    mode?: SelfRoleMode;
    behavior?: SelfRoleBehavior;
    roles?: SelfRoleEntry[];
    temporaryDurationMs?: number;
    expiresAt?: string | null;
  }
): Promise<{ id: string }> {
  const locale = await getGuildLocale(context.prisma, params.guildId);
  const panel = await updateSelfRolePanel(
    context,
    params.panelId,
    params.guildId,
    {
      channelId: params.channelId,
      title: params.title,
      description: params.description,
      mode: params.mode,
      behavior: params.behavior,
      roles: params.roles,
      temporaryDurationMs: params.temporaryDurationMs,
      expiresAt:
        params.expiresAt === undefined
          ? undefined
          : params.expiresAt
            ? new Date(params.expiresAt)
            : null
    },
    locale
  );
  return { id: panel.id };
}

export async function runSelfRolePanelDeleteJob(
  context: BotContext,
  panelId: string,
  guildId: string
): Promise<void> {
  await deleteSelfRolePanel(context, panelId, guildId);
}

export async function deleteSelfRolePanel(
  context: BotContext,
  panelId: string,
  guildId: string
): Promise<void> {
  const panel = await context.prisma.selfRolePanel.findFirst({
    where: { id: panelId, guildId }
  });

  if (!panel) {
    throw new SelfRoleError('not_found');
  }

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

  await context.prisma.selfRolePanel.delete({ where: { id: panelId } });
}

export function matchReactionEntry(
  entries: SelfRoleEntry[],
  emojiId: string | null,
  emojiName: string
): SelfRoleEntry | null {
  for (const entry of entries) {
    if (!entry.emoji) {
      continue;
    }

    const customMatch = entry.emoji.match(/^<(a)?:(\w+):(\d+)>$/);
    if (customMatch) {
      if (emojiId === customMatch[3]) {
        return entry;
      }
      continue;
    }

    if (/^\d+$/.test(entry.emoji)) {
      if (emojiId === entry.emoji) {
        return entry;
      }
      continue;
    }

    if (!emojiId && entry.emoji === emojiName) {
      return entry;
    }
  }

  return null;
}
