import {
  REST,
  Routes,
  type ChatInputCommandInteraction,
  type MessageContextMenuCommandInteraction
} from 'discord.js';
import { t, tf, type Locale } from '@nexumi/shared';
import { env } from './env.js';
import { logger } from './logger.js';
import { getGuildLocale } from './i18n.js';
import { evaluateCommandGate, applyCommandCooldown, type CommandGateReason } from './command-gates.js';
import {
  isGuildBlacklisted,
  isModuleEnabled,
  isUserBlacklisted,
  moduleForCommand
} from './module-gates.js';
import { isMaintenanceMode } from './presence.js';
import { recordCommandMetric } from './metrics.js';
import { captureException } from './sentry.js';
import { moderationCommands } from './modules/moderation/commands.js';
import { automodCommands } from './modules/automod/commands.js';
import { welcomeCommands } from './modules/welcome/commands.js';
import { verificationCommands } from './modules/verification/commands.js';
import { levelingCommands } from './modules/leveling/commands.js';
import { economyCommands } from './modules/economy/commands.js';
import { utilityCommands, utilityContextMenus } from './modules/utility/index.js';
import { funCommands } from './modules/fun/index.js';
import { giveawayCommands } from './modules/giveaways/index.js';
import { ticketsCommands } from './modules/tickets/index.js';
import { selfrolesCommands } from './modules/selfroles/index.js';
import { tagsCommands } from './modules/tags/index.js';
import { starboardCommands } from './modules/starboard/index.js';
import { suggestionsCommands } from './modules/suggestions/index.js';
import { birthdayCommands } from './modules/birthdays/index.js';
import { tempVoiceCommands } from './modules/tempvoice/index.js';
import { statsCommands } from './modules/stats/index.js';
import { feedsCommands } from './modules/feeds/index.js';
import { scheduleCommands } from './modules/scheduler/index.js';
import { guildBackupCommands } from './modules/guildbackup/index.js';
import { coreCommands } from './modules/core/index.js';
import { gdprCommands } from './modules/gdpr/index.js';
import type { BotContext, SlashCommand } from './types.js';

const commands: SlashCommand[] = [
  ...coreCommands,
  ...gdprCommands,
  ...moderationCommands,
  ...automodCommands,
  ...welcomeCommands,
  ...verificationCommands,
  ...levelingCommands,
  ...economyCommands,
  ...utilityCommands,
  ...funCommands,
  ...giveawayCommands,
  ...ticketsCommands,
  ...selfrolesCommands,
  ...tagsCommands,
  ...starboardCommands,
  ...suggestionsCommands,
  ...birthdayCommands,
  ...tempVoiceCommands,
  ...statsCommands,
  ...feedsCommands,
  ...scheduleCommands,
  ...guildBackupCommands
];
const map = new Map(commands.map((c) => [c.data.name, c]));
const contextMenuMap = new Map(utilityContextMenus.map((c) => [c.data.name, c]));

function buildCommandBody(): object[] {
  const seen = new Set<string>();
  const body: object[] = [];
  for (const command of [...commands, ...utilityContextMenus]) {
    const json = command.data.toJSON() as { name: string; type?: number };
    const key = `${json.type ?? 1}:${json.name}`;
    if (seen.has(key)) {
      logger.warn({ name: json.name, type: json.type }, 'Skipping duplicate command definition');
      continue;
    }
    seen.add(key);
    body.push(json);
  }
  return body;
}

/**
 * Guild-scoped leftovers + global commands both appear in Discord's picker.
 * Always clear the unused scope so each command shows once.
 */
async function clearGuildCommandLeftovers(rest: REST, clientId: string): Promise<number> {
  const guilds = (await rest.get(Routes.userGuilds())) as Array<{ id: string }>;
  let cleared = 0;
  for (const guild of guilds) {
    const existing = (await rest.get(
      Routes.applicationGuildCommands(clientId, guild.id)
    )) as unknown[];
    if (existing.length === 0) {
      continue;
    }
    await rest.put(Routes.applicationGuildCommands(clientId, guild.id), { body: [] });
    cleared += 1;
  }
  return cleared;
}

export async function registerCommands() {
  const rest = new REST({ version: '10' }).setToken(env.BOT_TOKEN);
  const body = buildCommandBody();

  // Guild commands sync instantly; global can take up to ~1h.
  if (env.BOT_GUILD_ID) {
    await rest.put(Routes.applicationGuildCommands(env.BOT_CLIENT_ID, env.BOT_GUILD_ID), {
      body
    });
    // Remove global copies so the guild does not show every command twice.
    await rest.put(Routes.applicationCommands(env.BOT_CLIENT_ID), { body: [] });
    logger.info(
      {
        scope: 'guild',
        guildId: env.BOT_GUILD_ID,
        count: body.length,
        clearedGlobal: true
      },
      'Registered guild application commands and cleared global commands'
    );
    return;
  }

  await rest.put(Routes.applicationCommands(env.BOT_CLIENT_ID), { body });

  // Wipe leftover guild-scoped registrations from earlier BOT_GUILD_ID / shard runs.
  let clearedGuilds = 0;
  try {
    clearedGuilds = await clearGuildCommandLeftovers(rest, env.BOT_CLIENT_ID);
  } catch (error) {
    logger.warn({ error }, 'Failed to clear leftover guild application commands');
  }

  logger.info(
    {
      scope: 'global',
      count: body.length,
      clearedGuilds
    },
    'Registered global application commands and cleared guild-scoped leftovers'
  );
}

function gateMessage(locale: Locale, reason: CommandGateReason, retryAfterSeconds?: number): string {
  switch (reason) {
    case 'disabled':
      return t(locale, 'generic.commandDisabled');
    case 'channel_denied':
    case 'channel_not_allowed':
      return t(locale, 'generic.commandChannelBlocked');
    case 'role_denied':
    case 'role_not_allowed':
      return t(locale, 'generic.commandRoleBlocked');
    case 'cooldown':
      return tf(locale, 'generic.commandCooldown', {
        seconds: retryAfterSeconds ?? 1
      });
    default:
      return t(locale, 'generic.noPermission');
  }
}

export async function routeCommand(interaction: ChatInputCommandInteraction, context: BotContext) {
  const started = Date.now();
  let ok = true;
  const locale = await getGuildLocale(context.prisma, interaction.guildId);

  try {
    if (await isUserBlacklisted(context.prisma, interaction.user.id)) {
      await interaction.reply({
        content: t(locale, 'generic.userBlacklisted'),
        ephemeral: true
      });
      return;
    }

    if (interaction.guildId && (await isGuildBlacklisted(context.prisma, interaction.guildId))) {
      await interaction.reply({
        content: t(locale, 'generic.guildBlacklisted'),
        ephemeral: true
      });
      return;
    }

    const maintenance = await isMaintenanceMode(context);
    const ownerIds = env.OWNER_USER_IDS ?? [];
    if (maintenance.enabled && !ownerIds.includes(interaction.user.id)) {
      await interaction.reply({
        content: maintenance.message?.trim() || t(locale, 'generic.maintenance'),
        ephemeral: true
      });
      return;
    }

    const command = map.get(interaction.commandName);
    if (!command) {
      await interaction.reply({ content: t(locale, 'generic.unknownCommand'), ephemeral: true });
      return;
    }

    const moduleId = moduleForCommand(interaction.commandName);
    if (moduleId && interaction.guildId) {
      const enabled = await isModuleEnabled(
        context.prisma,
        context.redis,
        interaction.guildId,
        moduleId
      );
      if (!enabled) {
        // Allow first-time setup while the module is still disabled (chicken-egg).
        const sub = interaction.options.getSubcommand(false);
        const isBootstrapSetup =
          interaction.commandName === 'starboard' && sub === 'setup';
        if (!isBootstrapSetup) {
          await interaction.reply({
            content: t(locale, 'generic.moduleDisabled'),
            ephemeral: true
          });
          return;
        }
      }
    }

    const gate = await evaluateCommandGate(interaction, context);
    if (!gate.ok) {
      await interaction.reply({
        content: gateMessage(locale, gate.reason, gate.retryAfterSeconds),
        ephemeral: true
      });
      return;
    }

    await command.execute(interaction, context);
    await applyCommandCooldown(interaction, context).catch(() => undefined);
  } catch (error) {
    ok = false;
    captureException(error, {
      command: interaction.commandName,
      guildId: interaction.guildId,
      userId: interaction.user.id
    });
    throw error;
  } finally {
    await recordCommandMetric(context.redis, {
      ok,
      durationMs: Date.now() - started
    }).catch(() => undefined);
  }
}

export async function routeContextMenu(
  interaction: MessageContextMenuCommandInteraction,
  context: BotContext
) {
  const locale = await getGuildLocale(context.prisma, interaction.guildId);

  if (await isUserBlacklisted(context.prisma, interaction.user.id)) {
    await interaction.reply({
      content: t(locale, 'generic.userBlacklisted'),
      ephemeral: true
    });
    return;
  }

  if (interaction.guildId && (await isGuildBlacklisted(context.prisma, interaction.guildId))) {
    await interaction.reply({
      content: t(locale, 'generic.guildBlacklisted'),
      ephemeral: true
    });
    return;
  }

  const maintenance = await isMaintenanceMode(context);
  const ownerIds = env.OWNER_USER_IDS ?? [];
  if (maintenance.enabled && !ownerIds.includes(interaction.user.id)) {
    await interaction.reply({
      content: maintenance.message?.trim() || t(locale, 'generic.maintenance'),
      ephemeral: true
    });
    return;
  }

  const command = contextMenuMap.get(interaction.commandName);
  if (!command) {
    await interaction.reply({ content: t(locale, 'generic.unknownCommand'), ephemeral: true });
    return;
  }
  await command.execute(interaction, context);
}
