import type { LevelingConfig, MemberLevel } from '@prisma/client';
import {
  levelFromXp,
  parseMultiplierMap,
  progressInLevel,
  randomInt,
  t,
  tf,
  type LevelUpMode
} from '@nexumi/shared';
import {
  PermissionFlagsBits,
  type Guild,
  type GuildMember,
  type SendableChannels
} from 'discord.js';
import { ensureGuild } from '../../guild.js';
import { logger } from '../../logger.js';
import type { BotContext } from '../../types.js';
import { getGuildLocale } from '../../i18n.js';

const COOLDOWN_PREFIX = 'leveling:cd:';
const VOICE_PREFIX = 'leveling:voice:';

export function cooldownKey(guildId: string, userId: string): string {
  return `${COOLDOWN_PREFIX}${guildId}:${userId}`;
}

export function voiceKey(guildId: string, userId: string): string {
  return `${VOICE_PREFIX}${guildId}:${userId}`;
}

export async function getConfig(
  prisma: BotContext['prisma'],
  guildId: string
): Promise<LevelingConfig> {
  await ensureGuild(prisma, guildId);
  return prisma.levelingConfig.upsert({
    where: { guildId },
    update: {},
    create: { guildId }
  });
}

export async function getOrCreateMemberLevel(
  prisma: BotContext['prisma'],
  guildId: string,
  userId: string
): Promise<MemberLevel> {
  await ensureGuild(prisma, guildId);
  return prisma.memberLevel.upsert({
    where: { guildId_userId: { guildId, userId } },
    update: {},
    create: { guildId, userId, xp: 0, level: 0 }
  });
}

function resolveRoleMultiplier(config: LevelingConfig, member: GuildMember): number {
  const multipliers = parseMultiplierMap(config.roleMultipliers);
  let highest = 1;
  for (const role of member.roles.cache.values()) {
    const value = multipliers[role.id];
    if (value !== undefined && value > highest) {
      highest = value;
    }
  }
  return highest;
}

function resolveChannelMultiplier(config: LevelingConfig, channelId: string): number {
  const multipliers = parseMultiplierMap(config.channelMultipliers);
  return multipliers[channelId] ?? 1;
}

export function calculateTextXp(
  config: LevelingConfig,
  member: GuildMember,
  channelId: string
): number {
  const base = randomInt(config.textXpMin, config.textXpMax);
  const roleMultiplier = resolveRoleMultiplier(config, member);
  const channelMultiplier = resolveChannelMultiplier(config, channelId);
  return Math.max(1, Math.round(base * roleMultiplier * channelMultiplier));
}

export function calculateVoiceXp(config: LevelingConfig, minutes: number): number {
  if (minutes <= 0) {
    return 0;
  }
  return config.voiceXpPerMinute * minutes;
}

export function isNoXpMember(config: LevelingConfig, member: GuildMember): boolean {
  return config.noXpRoleIds.some((roleId) => member.roles.cache.has(roleId));
}

export function isNoXpChannel(config: LevelingConfig, channelId: string): boolean {
  return config.noXpChannelIds.includes(channelId);
}

export type XpChangeResult = {
  memberLevel: MemberLevel;
  previousLevel: number;
  newLevel: number;
  xpDelta: number;
  leveledUp: boolean;
};

async function persistMemberXp(
  prisma: BotContext['prisma'],
  guildId: string,
  userId: string,
  xp: number
): Promise<MemberLevel> {
  const level = levelFromXp(xp);
  return prisma.memberLevel.upsert({
    where: { guildId_userId: { guildId, userId } },
    update: { xp, level },
    create: { guildId, userId, xp, level }
  });
}

export async function addXp(
  context: BotContext,
  guild: Guild,
  member: GuildMember,
  amount: number,
  options?: { channelId?: string; skipNotifications?: boolean; force?: boolean }
): Promise<XpChangeResult | null> {
  if (amount <= 0) {
    return null;
  }

  const config = await getConfig(context.prisma, guild.id);
  if (!config.enabled && !options?.force) {
    return null;
  }

  const existing = await getOrCreateMemberLevel(context.prisma, guild.id, member.id);
  const previousLevel = existing.level;
  const newXp = existing.xp + amount;
  const memberLevel = await persistMemberXp(context.prisma, guild.id, member.id, newXp);
  const leveledUp = memberLevel.level > previousLevel;

  if (leveledUp && !options?.skipNotifications) {
    await applyLevelRewards(context, guild, member, previousLevel, memberLevel.level, config);
    await sendLevelUpNotification(context, guild, member, memberLevel.level, config, options?.channelId);
  }

  return {
    memberLevel,
    previousLevel,
    newLevel: memberLevel.level,
    xpDelta: amount,
    leveledUp
  };
}

export async function setXp(
  context: BotContext,
  guild: Guild,
  member: GuildMember,
  xp: number,
  channelId?: string
): Promise<XpChangeResult> {
  const config = await getConfig(context.prisma, guild.id);
  const existing = await getOrCreateMemberLevel(context.prisma, guild.id, member.id);
  const previousLevel = existing.level;
  const clampedXp = Math.max(0, xp);
  const memberLevel = await persistMemberXp(context.prisma, guild.id, member.id, clampedXp);
  const leveledUp = memberLevel.level > previousLevel;

  if (leveledUp) {
    await applyLevelRewards(context, guild, member, previousLevel, memberLevel.level, config);
    await sendLevelUpNotification(context, guild, member, memberLevel.level, config, channelId);
  }

  return {
    memberLevel,
    previousLevel,
    newLevel: memberLevel.level,
    xpDelta: clampedXp - existing.xp,
    leveledUp
  };
}

export async function removeXp(
  context: BotContext,
  guild: Guild,
  member: GuildMember,
  amount: number,
  channelId?: string
): Promise<XpChangeResult | null> {
  if (amount <= 0) {
    return null;
  }
  const existing = await getOrCreateMemberLevel(context.prisma, guild.id, member.id);
  return setXp(context, guild, member, Math.max(0, existing.xp - amount), channelId);
}

export async function resetXp(
  context: BotContext,
  guild: Guild,
  member: GuildMember,
  channelId?: string
): Promise<XpChangeResult> {
  return setXp(context, guild, member, 0, channelId);
}

export async function getLeaderboard(
  prisma: BotContext['prisma'],
  guildId: string,
  limit = 10
): Promise<MemberLevel[]> {
  return prisma.memberLevel.findMany({
    where: { guildId },
    orderBy: [{ xp: 'desc' }, { updatedAt: 'asc' }],
    take: limit
  });
}

export async function getRankPosition(
  prisma: BotContext['prisma'],
  guildId: string,
  userId: string
): Promise<number> {
  const member = await getOrCreateMemberLevel(prisma, guildId, userId);
  const higher = await prisma.memberLevel.count({
    where: { guildId, xp: { gt: member.xp } }
  });
  return higher + 1;
}

export async function applyLevelRewards(
  context: BotContext,
  guild: Guild,
  member: GuildMember,
  previousLevel: number,
  newLevel: number,
  config?: LevelingConfig
): Promise<void> {
  if (newLevel <= previousLevel) {
    return;
  }

  const levelingConfig = config ?? (await getConfig(context.prisma, guild.id));
  const me = guild.members.me;
  if (!me?.permissions.has(PermissionFlagsBits.ManageRoles)) {
    return;
  }

  const rewards = await context.prisma.levelReward.findMany({
    where: { guildId: guild.id },
    orderBy: { level: 'asc' }
  });
  if (rewards.length === 0) {
    return;
  }

  const rewardRoleIds = new Set(rewards.map((reward) => reward.roleId));
  const assignable = (roleId: string) => {
    const role = guild.roles.cache.get(roleId);
    return role && role.id !== guild.id && role.position < me.roles.highest.position;
  };

  const rolesToAdd: string[] = [];

  if (levelingConfig.stackRewards) {
    for (let level = previousLevel + 1; level <= newLevel; level += 1) {
      const reward = rewards.find((entry) => entry.level === level);
      if (reward && assignable(reward.roleId)) {
        rolesToAdd.push(reward.roleId);
      }
    }
  } else {
    const reward = [...rewards].reverse().find((entry) => entry.level <= newLevel);
    const removable = rewards
      .map((entry) => entry.roleId)
      .filter((roleId) => member.roles.cache.has(roleId) && rewardRoleIds.has(roleId));

    if (removable.length > 0) {
      await member.roles.remove(removable).catch((error) => {
        logger.warn({ error, memberId: member.id }, 'Failed to remove level reward roles');
      });
    }

    if (reward && assignable(reward.roleId)) {
      rolesToAdd.push(reward.roleId);
    }
  }

  if (rolesToAdd.length > 0) {
    await member.roles.add([...new Set(rolesToAdd)]).catch((error) => {
      logger.warn({ error, memberId: member.id }, 'Failed to assign level reward roles');
    });
  }
}

function renderLevelUpMessage(
  template: string,
  member: GuildMember,
  level: number
): string {
  return template
    .replaceAll('{user}', `<@${member.id}>`)
    .replaceAll('{username}', member.displayName)
    .replaceAll('{level}', String(level));
}

async function sendLevelUpNotification(
  context: BotContext,
  guild: Guild,
  member: GuildMember,
  level: number,
  config: LevelingConfig,
  fallbackChannelId?: string
): Promise<void> {
  const mode = config.levelUpMode as LevelUpMode;
  if (mode === 'OFF') {
    return;
  }

  const locale = await getGuildLocale(context.prisma, guild.id);
  const template = config.levelUpMessage ?? t(locale, 'leveling.levelUp.default');
  const content = renderLevelUpMessage(template, member, level);

  if (mode === 'DM') {
    await member.send({ content }).catch(() => undefined);
    return;
  }

  const channelId = config.levelUpChannelId ?? fallbackChannelId;
  if (!channelId) {
    return;
  }

  const channel = await guild.channels.fetch(channelId).catch(() => null);
  if (!channel?.isTextBased() || channel.isDMBased()) {
    return;
  }

  await (channel as SendableChannels).send({ content }).catch((error) => {
    logger.warn({ error, channelId }, 'Failed to send level-up message');
  });
}

export async function awardVoiceXpForSession(
  context: BotContext,
  guild: Guild,
  member: GuildMember,
  joinedAtMs: number,
  channelId: string
): Promise<void> {
  const config = await getConfig(context.prisma, guild.id);
  if (!config.enabled || isNoXpMember(config, member) || isNoXpChannel(config, channelId)) {
    return;
  }

  const minutes = Math.floor((Date.now() - joinedAtMs) / 60_000);
  const baseXp = calculateVoiceXp(config, minutes);
  if (baseXp <= 0) {
    return;
  }

  const roleMultiplier = resolveRoleMultiplier(config, member);
  const channelMultiplier = resolveChannelMultiplier(config, channelId);
  const amount = Math.max(1, Math.round(baseXp * roleMultiplier * channelMultiplier));
  await addXp(context, guild, member, amount, { channelId });
}

export async function handleTextXpMessage(
  context: BotContext,
  member: GuildMember,
  channelId: string
): Promise<void> {
  if (member.user.bot) {
    return;
  }

  const config = await getConfig(context.prisma, member.guild.id);
  if (!config.enabled || isNoXpMember(config, member) || isNoXpChannel(config, channelId)) {
    return;
  }

  const key = cooldownKey(member.guild.id, member.id);
  const onCooldown = await context.redis.get(key);
  if (onCooldown) {
    return;
  }

  const amount = calculateTextXp(config, member, channelId);
  const result = await addXp(context, member.guild, member, amount, { channelId });
  if (result) {
    await context.redis.set(key, '1', 'EX', config.textCooldownSeconds);
  }
}

export function memberProgress(member: MemberLevel) {
  return progressInLevel(member.xp);
}

export function formatLeaderboardLine(
  locale: 'de' | 'en',
  rank: number,
  userLabel: string,
  level: number,
  xp: number
): string {
  return tf(locale, 'leveling.leaderboard.entry', {
    rank,
    user: userLabel,
    level,
    xp
  });
}
