import {
  AuditLogEvent,
  EmbedBuilder,
  type Guild,
  type GuildMember,
  type TextChannel
} from 'discord.js';
import type { BotContext } from '../../types.js';
import type { LogEventType } from '@nexumi/shared';
import { ensureGuild } from '../../guild.js';
import { logger } from '../../logger.js';

/** Audit-log entries can lag a moment behind the gateway event. */
const AUDIT_LOG_MAX_AGE_MS = 15_000;

export async function getLoggingConfig(prisma: BotContext['prisma'], guildId: string) {
  await ensureGuild(prisma, guildId);
  let config = await prisma.loggingConfig.findUnique({ where: { guildId } });
  if (!config) {
    config = await prisma.loggingConfig.create({ data: { guildId } });
  }
  return config;
}

/**
 * Discord does not send ban/kick reasons over the gateway.
 * Reasons must be read from the audit log (requires View Audit Log).
 */
async function fetchRecentAuditEntry(
  guild: Guild,
  type: AuditLogEvent,
  targetId: string
): Promise<{ reason: string | null; executorId: string | null } | null> {
  try {
    const auditLogs = await guild.fetchAuditLogs({ type, limit: 5 });
    const entry = auditLogs.entries.find((item) => {
      if (item.targetId !== targetId) {
        return false;
      }
      return Date.now() - item.createdTimestamp <= AUDIT_LOG_MAX_AGE_MS;
    });
    if (!entry) {
      return null;
    }
    return {
      reason: entry.reason ?? null,
      executorId: entry.executorId ?? entry.executor?.id ?? null
    };
  } catch (error) {
    logger.warn({ error, guildId: guild.id, type }, 'Failed to fetch audit log for logging');
    return null;
  }
}

export async function getLogChannelId(
  prisma: BotContext['prisma'],
  guildId: string,
  eventType: LogEventType
): Promise<string | null> {
  const entry = await prisma.logChannel.findUnique({
    where: { guildId_eventType: { guildId, eventType } }
  });
  return entry?.channelId ?? null;
}

function shouldIgnoreMember(
  config: Awaited<ReturnType<typeof getLoggingConfig>>,
  member: GuildMember | null,
  isBot: boolean
): boolean {
  if (isBot && config.ignoreBots) {
    return true;
  }
  if (!member) {
    return false;
  }
  return member.roles.cache.some((role) => config.ignoredRoleIds.includes(role.id));
}

function shouldIgnoreChannel(
  config: Awaited<ReturnType<typeof getLoggingConfig>>,
  channelId: string | null
): boolean {
  if (!channelId) {
    return false;
  }
  return config.ignoredChannelIds.includes(channelId);
}

async function resolveLogChannel(
  context: BotContext,
  guild: Guild,
  eventType: LogEventType
): Promise<TextChannel | null> {
  const config = await getLoggingConfig(context.prisma, guild.id);
  if (!config.enabled) {
    return null;
  }
  const channelId = await getLogChannelId(context.prisma, guild.id, eventType);
  if (!channelId) {
    return null;
  }
  if (shouldIgnoreChannel(config, channelId)) {
    return null;
  }
  const channel = await guild.channels.fetch(channelId).catch(() => null);
  if (!channel?.isTextBased() || channel.isDMBased()) {
    return null;
  }
  return channel as TextChannel;
}

export async function sendLogEmbed(
  context: BotContext,
  guild: Guild,
  eventType: LogEventType,
  title: string,
  description: string,
  color = 0x6366f1
): Promise<void> {
  const channel = await resolveLogChannel(context, guild, eventType);
  if (!channel) {
    return;
  }
  const embed = new EmbedBuilder()
    .setTitle(title)
    .setDescription(description.slice(0, 4096))
    .setColor(color)
    .setTimestamp();
  await channel.send({ embeds: [embed] }).catch((error) => {
    logger.warn({ error, guildId: guild.id, eventType }, 'Failed to send log embed');
  });
}

export async function logModAction(
  context: BotContext,
  input: {
    guildId: string;
    caseNumber: number;
    action: string;
    targetUserId: string;
    moderatorId: string;
    reason?: string | null;
  }
): Promise<void> {
  const guild = await context.client.guilds.fetch(input.guildId).catch(() => null);
  if (!guild) {
    return;
  }
  const description = [
    `**Case:** #${input.caseNumber}`,
    `**Action:** ${input.action}`,
    `**Target:** <@${input.targetUserId}> (\`${input.targetUserId}\`)`,
    `**Moderator:** <@${input.moderatorId}> (\`${input.moderatorId}\`)`,
    `**Reason:** ${input.reason ?? '—'}`
  ].join('\n');
  await sendLogEmbed(context, guild, 'MOD_ACTION', 'Moderation Action', description, 0xef4444);
}

export async function logVerificationFail(
  context: BotContext,
  input: {
    guildId: string;
    userId: string;
    userTag?: string;
    reason: string;
    detail?: string;
    failAction?: string;
    matchedUserId?: string | null;
  }
): Promise<void> {
  const guild = await context.client.guilds.fetch(input.guildId).catch(() => null);
  if (!guild) {
    return;
  }
  const lines = [
    `**User:** ${input.userTag ? `${input.userTag} ` : ''}(<@${input.userId}>) (\`${input.userId}\`)`,
    `**Reason:** ${input.reason}`
  ];
  if (input.detail) {
    lines.push(`**Detail:** ${input.detail}`);
  }
  if (input.matchedUserId) {
    lines.push(`**Matched user:** <@${input.matchedUserId}> (\`${input.matchedUserId}\`)`);
  }
  if (input.failAction) {
    lines.push(`**Fail action:** ${input.failAction}`);
  }
  await sendLogEmbed(
    context,
    guild,
    'VERIFICATION_FAIL',
    'Verification Failed',
    lines.join('\n'),
    0xf97316
  );
}

export async function handleChannelDeleteForAntiNuke(
  context: BotContext,
  guild: Guild,
  channelId: string
): Promise<void> {
  const config = await getLoggingConfig(context.prisma, guild.id);
  if (shouldIgnoreChannel(config, channelId)) {
    return;
  }

  const auditLogs = await guild.fetchAuditLogs({
    type: AuditLogEvent.ChannelDelete,
    limit: 1
  });
  const entry = auditLogs.entries.first();
  if (!entry?.executor) {
    return;
  }

  const { handleAntiNukeAction } = await import('../automod/actions.js');
  await handleAntiNukeAction(context, guild.id, entry.executor.id, 'channel_delete');
}

export async function handleBanForAntiNuke(
  context: BotContext,
  guild: Guild
): Promise<void> {
  const auditLogs = await guild.fetchAuditLogs({
    type: AuditLogEvent.MemberBanAdd,
    limit: 1
  });
  const entry = auditLogs.entries.first();
  if (!entry?.executor) {
    return;
  }

  const { handleAntiNukeAction } = await import('../automod/actions.js');
  await handleAntiNukeAction(context, guild.id, entry.executor.id, 'ban');
}

export function memberIgnored(
  config: Awaited<ReturnType<typeof getLoggingConfig>>,
  member: GuildMember | null,
  isBot: boolean,
  channelId?: string | null
): boolean {
  if (shouldIgnoreChannel(config, channelId ?? null)) {
    return true;
  }
  return shouldIgnoreMember(config, member, isBot);
}

export async function registerLoggingEvents(context: BotContext): Promise<void> {
  const { client } = context;

  client.on('messageUpdate', async (oldMessage, newMessage) => {
    if (!newMessage.guild || newMessage.author?.bot) {
      return;
    }
    const config = await getLoggingConfig(context.prisma, newMessage.guild.id);
    if (memberIgnored(config, newMessage.member, newMessage.author?.bot ?? false, newMessage.channel.id)) {
      return;
    }
    const before = oldMessage.content ?? '—';
    const after = newMessage.content ?? '—';
    if (before === after) {
      return;
    }
    await sendLogEmbed(
      context,
      newMessage.guild,
      'MESSAGE_EDIT',
      'Message Edited',
      `**Author:** <@${newMessage.author?.id}>\n**Channel:** <#${newMessage.channel.id}>\n**Before:** ${before}\n**After:** ${after}`
    );
  });

  client.on('messageDelete', async (message) => {
    if (!message.guild || message.author?.bot) {
      return;
    }
    const config = await getLoggingConfig(context.prisma, message.guild.id);
    if (memberIgnored(config, message.member, message.author?.bot ?? false, message.channel.id)) {
      return;
    }
    await sendLogEmbed(
      context,
      message.guild,
      'MESSAGE_DELETE',
      'Message Deleted',
      `**Author:** <@${message.author?.id}>\n**Channel:** <#${message.channel.id}>\n**Content:** ${message.content ?? '—'}`
    );
  });

  client.on('messageDeleteBulk', async (messages, channel) => {
    if (!channel.guild) {
      return;
    }
    await sendLogEmbed(
      context,
      channel.guild,
      'MESSAGE_BULK_DELETE',
      'Bulk Delete',
      `**Channel:** <#${channel.id}>\n**Count:** ${messages.size}`
    );
  });

  client.on('guildMemberAdd', async (member) => {
    const config = await getLoggingConfig(context.prisma, member.guild.id);
    if (memberIgnored(config, member, member.user.bot)) {
      return;
    }
    await sendLogEmbed(
      context,
      member.guild,
      'MEMBER_JOIN',
      'Member Joined',
      `**User:** ${member.user.tag} (<@${member.id}>)`
    );
  });

  client.on('guildMemberRemove', async (member) => {
    if (!member.guild) {
      return;
    }
    const config = await getLoggingConfig(context.prisma, member.guild.id);
    if (memberIgnored(config, member as GuildMember, member.user.bot)) {
      return;
    }

    const kickEntry = await fetchRecentAuditEntry(
      member.guild,
      AuditLogEvent.MemberKick,
      member.id
    );

    if (kickEntry) {
      await sendLogEmbed(
        context,
        member.guild,
        'MEMBER_LEAVE',
        'Member Kicked',
        [
          `**User:** ${member.user.tag} (<@${member.id}>)`,
          `**Moderator:** ${kickEntry.executorId ? `<@${kickEntry.executorId}>` : '—'}`,
          `**Reason:** ${kickEntry.reason ?? '—'}`
        ].join('\n'),
        0xf59e0b
      );
      return;
    }

    await sendLogEmbed(
      context,
      member.guild,
      'MEMBER_LEAVE',
      'Member Left',
      `**User:** ${member.user.tag} (<@${member.id}>)`
    );
  });

  client.on('guildBanAdd', async (ban) => {
    const guild = ban.guild;
    await handleBanForAntiNuke(context, guild);

    const audit = await fetchRecentAuditEntry(guild, AuditLogEvent.MemberBanAdd, ban.user.id);
    const reason = audit?.reason ?? ban.reason ?? '—';
    const executorLine = audit?.executorId
      ? `\n**Moderator:** <@${audit.executorId}>`
      : '';

    await sendLogEmbed(
      context,
      guild,
      'MEMBER_BAN',
      'Member Banned',
      `**User:** ${ban.user.tag} (<@${ban.user.id}>)${executorLine}\n**Reason:** ${reason}`
    );
  });

  client.on('guildBanRemove', async (ban) => {
    await sendLogEmbed(
      context,
      ban.guild,
      'MEMBER_UNBAN',
      'Member Unbanned',
      `**User:** ${ban.user.tag} (<@${ban.user.id}>)`
    );
  });

  client.on('guildMemberUpdate', async (oldMember, newMember) => {
    const config = await getLoggingConfig(context.prisma, newMember.guild.id);
    if (memberIgnored(config, newMember, newMember.user.bot)) {
      return;
    }
    const changes: string[] = [];
    if (oldMember.nickname !== newMember.nickname) {
      changes.push(`**Nickname:** ${oldMember.nickname ?? '—'} → ${newMember.nickname ?? '—'}`);
    }
    const addedRoles = newMember.roles.cache.filter((role) => !oldMember.roles.cache.has(role.id));
    const removedRoles = oldMember.roles.cache.filter((role) => !newMember.roles.cache.has(role.id));
    if (addedRoles.size > 0) {
      changes.push(`**Roles added:** ${addedRoles.map((r) => r.name).join(', ')}`);
    }
    if (removedRoles.size > 0) {
      changes.push(`**Roles removed:** ${removedRoles.map((r) => r.name).join(', ')}`);
    }
    if (changes.length === 0) {
      return;
    }
    await sendLogEmbed(
      context,
      newMember.guild,
      'MEMBER_UPDATE',
      'Member Updated',
      `**User:** <@${newMember.id}>\n${changes.join('\n')}`
    );
  });

  client.on('channelCreate', async (channel) => {
    if (!channel.guild) {
      return;
    }
    await sendLogEmbed(
      context,
      channel.guild,
      'CHANNEL_CREATE',
      'Channel Created',
      `**Channel:** ${channel.name} (<#${channel.id}>)`
    );
  });

  client.on('channelDelete', async (channel) => {
    if (!('guild' in channel) || !channel.guild) {
      return;
    }
    await handleChannelDeleteForAntiNuke(context, channel.guild, channel.id);
    await sendLogEmbed(
      context,
      channel.guild,
      'CHANNEL_DELETE',
      'Channel Deleted',
      `**Channel:** ${'name' in channel ? channel.name : 'Unknown'} (\`${channel.id}\`)`
    );
  });

  client.on('channelUpdate', async (oldChannel, newChannel) => {
    if (!('guild' in newChannel) || !newChannel.guild) {
      return;
    }
    await sendLogEmbed(
      context,
      newChannel.guild,
      'CHANNEL_UPDATE',
      'Channel Updated',
      `**Channel:** <#${newChannel.id}>\n**Before:** ${'name' in oldChannel ? oldChannel.name : '—'}\n**After:** ${'name' in newChannel ? newChannel.name : '—'}`
    );
  });

  client.on('voiceStateUpdate', async (oldState, newState) => {
    const guild = newState.guild;
    const member = newState.member;
    if (!member || member.user.bot) {
      return;
    }
    const config = await getLoggingConfig(context.prisma, guild.id);
    if (memberIgnored(config, member, member.user.bot)) {
      return;
    }

    if (!oldState.channelId && newState.channelId) {
      await sendLogEmbed(
        context,
        guild,
        'VOICE_JOIN',
        'Voice Join',
        `**User:** <@${member.id}>\n**Channel:** <#${newState.channelId}>`
      );
      return;
    }

    if (oldState.channelId && !newState.channelId) {
      await sendLogEmbed(
        context,
        guild,
        'VOICE_LEAVE',
        'Voice Leave',
        `**User:** <@${member.id}>\n**Channel:** <#${oldState.channelId}>`
      );
      return;
    }

    if (oldState.channelId && newState.channelId && oldState.channelId !== newState.channelId) {
      await sendLogEmbed(
        context,
        guild,
        'VOICE_MOVE',
        'Voice Move',
        `**User:** <@${member.id}>\n**From:** <#${oldState.channelId}>\n**To:** <#${newState.channelId}>`
      );
    }
  });

  client.on('inviteCreate', async (invite) => {
    if (!invite.guild || !('members' in invite.guild)) {
      return;
    }
    await sendLogEmbed(
      context,
      invite.guild,
      'INVITE_CREATE',
      'Invite Created',
      `**Code:** ${invite.code}\n**Channel:** <#${invite.channelId}>\n**Creator:** ${invite.inviter ? `<@${invite.inviter.id}>` : '—'}`
    );
  });

  client.on('emojiCreate', async (emoji) => {
    await sendLogEmbed(
      context,
      emoji.guild!,
      'EMOJI_CREATE',
      'Emoji Created',
      `**Name:** ${emoji.name}`
    );
  });

  client.on('emojiUpdate', async (oldEmoji, newEmoji) => {
    await sendLogEmbed(
      context,
      newEmoji.guild!,
      'EMOJI_UPDATE',
      'Emoji Updated',
      `**Before:** ${oldEmoji.name}\n**After:** ${newEmoji.name}`
    );
  });

  client.on('emojiDelete', async (emoji) => {
    await sendLogEmbed(
      context,
      emoji.guild!,
      'EMOJI_DELETE',
      'Emoji Deleted',
      `**Name:** ${emoji.name}`
    );
  });

  client.on('stickerCreate', async (sticker) => {
    if (!sticker.guild) {
      return;
    }
    await sendLogEmbed(
      context,
      sticker.guild,
      'STICKER_CREATE',
      'Sticker Created',
      `**Name:** ${sticker.name}`
    );
  });

  client.on('stickerUpdate', async (oldSticker, newSticker) => {
    if (!newSticker.guild) {
      return;
    }
    await sendLogEmbed(
      context,
      newSticker.guild,
      'STICKER_UPDATE',
      'Sticker Updated',
      `**Before:** ${oldSticker.name}\n**After:** ${newSticker.name}`
    );
  });

  client.on('stickerDelete', async (sticker) => {
    if (!sticker.guild) {
      return;
    }
    await sendLogEmbed(
      context,
      sticker.guild,
      'STICKER_DELETE',
      'Sticker Deleted',
      `**Name:** ${sticker.name}`
    );
  });

  client.on('threadCreate', async (thread) => {
    if (!thread.guild) {
      return;
    }
    await sendLogEmbed(
      context,
      thread.guild,
      'THREAD_CREATE',
      'Thread Created',
      `**Thread:** ${thread.name} (<#${thread.id}>)`
    );
  });

  client.on('threadUpdate', async (oldThread, newThread) => {
    if (!newThread.guild) {
      return;
    }
    await sendLogEmbed(
      context,
      newThread.guild,
      'THREAD_UPDATE',
      'Thread Updated',
      `**Before:** ${oldThread.name}\n**After:** ${newThread.name}`
    );
  });

  client.on('threadDelete', async (thread) => {
    if (!thread.guild) {
      return;
    }
    await sendLogEmbed(
      context,
      thread.guild,
      'THREAD_DELETE',
      'Thread Deleted',
      `**Thread:** ${thread.name} (\`${thread.id}\`)`
    );
  });

  client.on('guildUpdate', async (oldGuild, newGuild) => {
    const changes: string[] = [];
    if (oldGuild.name !== newGuild.name) {
      changes.push(`**Name:** ${oldGuild.name} → ${newGuild.name}`);
    }
    if (changes.length === 0) {
      return;
    }
    await sendLogEmbed(
      context,
      newGuild,
      'GUILD_UPDATE',
      'Guild Updated',
      changes.join('\n')
    );
  });
}
