import {
  ChannelType,
  OverwriteType,
  PermissionFlagsBits,
  type Guild,
  type GuildChannel,
  type NonThreadGuildBasedChannel,
  type OverwriteResolvable
} from 'discord.js';
import { GuildBackupPayloadSchema, type GuildBackupPayload } from '@nexumi/shared';
import type { GuildBackup } from '@prisma/client';
import { ensureGuild } from '../../guild.js';
import { logger } from '../../logger.js';
import { assertPremiumLimit, PremiumLimitError } from '../../premium.js';
import { guildBackupQueue } from '../../queues.js';
import type { BotContext } from '../../types.js';

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

const LARGE_RESTORE_THRESHOLD = 50;
const RESTORE_REASON = 'Nexumi guild backup restore';

export type RestoreSummary = {
  rolesCreated: number;
  rolesSkipped: number;
  channelsCreated: number;
  channelsUpdated: number;
  overwritesApplied: number;
  settingsApplied: string[];
};

export async function createGuildBackup(
  context: BotContext,
  guild: Guild,
  name: string,
  createdById: string
): Promise<GuildBackup> {
  await guild.roles.fetch();
  await guild.channels.fetch();

  const payload = buildPayloadFromGuild(guild);
  GuildBackupPayloadSchema.parse(payload);

  await ensureGuild(context.prisma, guild.id);

  const currentCount = await context.prisma.guildBackup.count({ where: { guildId: guild.id } });
  try {
    await assertPremiumLimit(context.prisma, guild.id, 'backups', currentCount);
  } catch (error) {
    if (error instanceof PremiumLimitError) {
      throw new GuildBackupError(
        error.code === 'feature_disabled' ? 'premium_feature' : 'premium_limit'
      );
    }
    throw error;
  }

  return context.prisma.guildBackup.create({
    data: {
      guildId: guild.id,
      name,
      createdById,
      payload
    }
  });
}

export function buildPayloadFromGuild(guild: Guild): GuildBackupPayload {
  const roles = [...guild.roles.cache.values()]
    .sort((a, b) => a.position - b.position)
    .map((role) => ({
      id: role.id,
      name: role.name,
      color: role.color,
      hoist: role.hoist,
      mentionable: role.mentionable,
      permissions: role.permissions.bitfield.toString(),
      position: role.position
    }));

  const channels = [...guild.channels.cache.values()]
    .filter(
      (channel): channel is NonThreadGuildBasedChannel =>
        !channel.isThread() && 'permissionOverwrites' in channel
    )
    .sort((a, b) => {
      if (a.parentId !== b.parentId) {
        return (a.parentId ?? '').localeCompare(b.parentId ?? '');
      }
      return a.rawPosition - b.rawPosition;
    })
    .map((channel) => {
      const base = {
        id: channel.id,
        name: channel.name,
        type: channel.type,
        parentId: channel.parentId,
        position: channel.rawPosition,
        permissionOverwrites: channel.permissionOverwrites.cache.map((overwrite) => ({
          id: overwrite.id,
          type: overwrite.type,
          allow: overwrite.allow.bitfield.toString(),
          deny: overwrite.deny.bitfield.toString()
        }))
      };

      if (channel.type === ChannelType.GuildText || channel.type === ChannelType.GuildAnnouncement) {
        return {
          ...base,
          topic: channel.topic ?? undefined,
          nsfw: channel.nsfw,
          rateLimitPerUser: channel.rateLimitPerUser ?? undefined
        };
      }

      if (channel.type === ChannelType.GuildVoice || channel.type === ChannelType.GuildStageVoice) {
        return {
          ...base,
          bitrate: channel.bitrate ?? undefined,
          userLimit: channel.userLimit ?? undefined
        };
      }

      if (channel.type === ChannelType.GuildForum) {
        return {
          ...base,
          topic: channel.topic ?? undefined,
          nsfw: channel.nsfw,
          rateLimitPerUser: channel.rateLimitPerUser ?? undefined
        };
      }

      return base;
    });

  return {
    version: 1,
    guildName: guild.name,
    roles,
    channels,
    settings: {
      verificationLevel: guild.verificationLevel,
      explicitContentFilter: guild.explicitContentFilter,
      defaultMessageNotifications: guild.defaultMessageNotifications,
      afkTimeout: guild.afkTimeout
    }
  };
}

export async function listGuildBackups(
  context: BotContext,
  guildId: string,
  limit = 15
): Promise<GuildBackup[]> {
  return context.prisma.guildBackup.findMany({
    where: { guildId },
    orderBy: { createdAt: 'desc' },
    take: limit
  });
}

export async function getGuildBackup(
  context: BotContext,
  guildId: string,
  backupId: string
): Promise<GuildBackup | null> {
  return context.prisma.guildBackup.findFirst({
    where: { id: backupId, guildId }
  });
}

export async function deleteGuildBackup(
  context: BotContext,
  guildId: string,
  backupId: string
): Promise<boolean> {
  const result = await context.prisma.guildBackup.deleteMany({
    where: { id: backupId, guildId }
  });
  return result.count > 0;
}

export function parseBackupPayload(raw: unknown): GuildBackupPayload {
  const parsed = GuildBackupPayloadSchema.safeParse(raw);
  if (!parsed.success) {
    throw new GuildBackupError('invalid_payload');
  }
  return parsed.data;
}

export function shouldQueueRestore(payload: GuildBackupPayload): boolean {
  return payload.roles.length + payload.channels.length >= LARGE_RESTORE_THRESHOLD;
}

export async function enqueueGuildBackupRestore(
  backupId: string,
  guildId: string,
  requestedById: string
): Promise<void> {
  await guildBackupQueue.add(
    'restoreGuildBackup',
    { backupId, guildId, requestedById },
    {
      jobId: `guild-backup-restore-${backupId}`,
      removeOnComplete: true,
      removeOnFail: 25
    }
  );
}

export async function runGuildBackupRestoreJob(
  context: BotContext,
  backupId: string,
  guildId: string
): Promise<RestoreSummary> {
  const backup = await getGuildBackup(context, guildId, backupId);
  if (!backup) {
    throw new GuildBackupError('not_found');
  }

  const guild = await context.client.guilds.fetch(guildId);
  const payload = parseBackupPayload(backup.payload);
  return restoreGuildBackup(guild, payload);
}

export async function restoreGuildBackup(
  guild: Guild,
  payload: GuildBackupPayload
): Promise<RestoreSummary> {
  await guild.roles.fetch();
  await guild.channels.fetch();

  const summary: RestoreSummary = {
    rolesCreated: 0,
    rolesSkipped: 0,
    channelsCreated: 0,
    channelsUpdated: 0,
    overwritesApplied: 0,
    settingsApplied: []
  };

  const roleIdMap = buildRoleIdMap(guild, payload);
  await createMissingRoles(guild, payload, roleIdMap, summary);
  await applyRolePositions(guild, payload, roleIdMap);

  const channelIdMap = new Map<string, string>();
  await restoreCategories(guild, payload, roleIdMap, channelIdMap, summary);
  await restoreOtherChannels(guild, payload, roleIdMap, channelIdMap, summary);
  await applyGuildSettings(guild, payload, summary);

  logger.info(
    {
      guildId: guild.id,
      backupGuildName: payload.guildName,
      ...summary
    },
    'Guild backup restore completed'
  );

  return summary;
}

function buildRoleIdMap(guild: Guild, payload: GuildBackupPayload): Map<string, string> {
  const map = new Map<string, string>();
  map.set(guild.id, guild.id);

  for (const roleData of payload.roles) {
    if (roleData.id === guild.id) {
      continue;
    }
    const existing = guild.roles.cache.find(
      (role) => role.name.toLowerCase() === roleData.name.toLowerCase()
    );
    if (existing) {
      map.set(roleData.id, existing.id);
    }
  }

  return map;
}

async function createMissingRoles(
  guild: Guild,
  payload: GuildBackupPayload,
  roleIdMap: Map<string, string>,
  summary: RestoreSummary
): Promise<void> {
  const sortedRoles = [...payload.roles].sort((a, b) => a.position - b.position);

  for (const roleData of sortedRoles) {
    if (roleData.id === guild.id) {
      continue;
    }

    if (roleIdMap.has(roleData.id)) {
      summary.rolesSkipped += 1;
      continue;
    }

    const existingByName = guild.roles.cache.find(
      (role) => role.name.toLowerCase() === roleData.name.toLowerCase()
    );
    if (existingByName) {
      roleIdMap.set(roleData.id, existingByName.id);
      summary.rolesSkipped += 1;
      continue;
    }

    try {
      const created = await guild.roles.create({
        name: roleData.name,
        color: roleData.color,
        hoist: roleData.hoist,
        mentionable: roleData.mentionable,
        permissions: BigInt(roleData.permissions),
        reason: RESTORE_REASON
      });
      roleIdMap.set(roleData.id, created.id);
      summary.rolesCreated += 1;
    } catch (error) {
      logger.warn({ error, roleName: roleData.name, guildId: guild.id }, 'Failed to create role');
      summary.rolesSkipped += 1;
    }
  }
}

async function applyRolePositions(
  guild: Guild,
  payload: GuildBackupPayload,
  roleIdMap: Map<string, string>
): Promise<void> {
  const positions = payload.roles
    .map((roleData) => {
      const mappedId = roleIdMap.get(roleData.id);
      if (!mappedId || mappedId === guild.id) {
        return null;
      }
      return { role: mappedId, position: roleData.position };
    })
    .filter((entry): entry is { role: string; position: number } => entry !== null);

  if (positions.length === 0) {
    return;
  }

  try {
    await guild.roles.setPositions(positions);
  } catch (error) {
    logger.warn({ error, guildId: guild.id }, 'Failed to apply role positions during restore');
  }
}

function resolveParentId(
  payload: GuildBackupPayload,
  channelData: GuildBackupPayload['channels'][number],
  channelIdMap: Map<string, string>,
  guild: Guild
): string | undefined {
  if (!channelData.parentId) {
    return undefined;
  }

  const mappedParentId = channelIdMap.get(channelData.parentId);
  if (mappedParentId) {
    return mappedParentId;
  }

  const parentData = payload.channels.find((channel) => channel.id === channelData.parentId);
  if (!parentData) {
    return undefined;
  }

  const existingParent = findChannelByName(guild, parentData.name, undefined);
  return existingParent?.id;
}

function findChannelByName(
  guild: Guild,
  name: string,
  parentId: string | undefined
): GuildChannel | undefined {
  return guild.channels.cache.find((channel) => {
    if (!('name' in channel) || channel.name.toLowerCase() !== name.toLowerCase()) {
      return false;
    }
    if (parentId === undefined) {
      return channel.parentId === null;
    }
    return channel.parentId === parentId;
  }) as GuildChannel | undefined;
}

function mapOverwrites(
  payloadOverwrites: GuildBackupPayload['channels'][number]['permissionOverwrites'],
  roleIdMap: Map<string, string>
): OverwriteResolvable[] {
  const mapped: OverwriteResolvable[] = [];

  for (const overwrite of payloadOverwrites) {
    let targetId = overwrite.id;
    if (overwrite.type === OverwriteType.Role) {
      const mapped = roleIdMap.get(overwrite.id);
      if (!mapped) {
        continue;
      }
      targetId = mapped;
    }

    mapped.push({
      id: targetId,
      type: overwrite.type,
      allow: BigInt(overwrite.allow),
      deny: BigInt(overwrite.deny)
    });
  }

  return mapped;
}

async function restoreCategories(
  guild: Guild,
  payload: GuildBackupPayload,
  roleIdMap: Map<string, string>,
  channelIdMap: Map<string, string>,
  summary: RestoreSummary
): Promise<void> {
  const categories = payload.channels
    .filter((channel) => channel.type === ChannelType.GuildCategory)
    .sort((a, b) => a.position - b.position);

  for (const categoryData of categories) {
    const existing = findChannelByName(guild, categoryData.name, undefined);
    if (existing) {
      channelIdMap.set(categoryData.id, existing.id);
      await applyChannelOverwrites(existing, categoryData, roleIdMap, summary);
      summary.channelsUpdated += 1;
      continue;
    }

    try {
      const created = await guild.channels.create({
        name: categoryData.name,
        type: ChannelType.GuildCategory,
        permissionOverwrites: mapOverwrites(categoryData.permissionOverwrites, roleIdMap),
        reason: RESTORE_REASON
      });
      channelIdMap.set(categoryData.id, created.id);
      summary.channelsCreated += 1;
      summary.overwritesApplied += categoryData.permissionOverwrites.length;
    } catch (error) {
      logger.warn(
        { error, channelName: categoryData.name, guildId: guild.id },
        'Failed to create category'
      );
    }
  }
}

async function restoreOtherChannels(
  guild: Guild,
  payload: GuildBackupPayload,
  roleIdMap: Map<string, string>,
  channelIdMap: Map<string, string>,
  summary: RestoreSummary
): Promise<void> {
  const channels = payload.channels
    .filter((channel) => channel.type !== ChannelType.GuildCategory)
    .sort((a, b) => a.position - b.position);

  for (const channelData of channels) {
    const parentId = resolveParentId(payload, channelData, channelIdMap, guild);
    const existing = findChannelByName(guild, channelData.name, parentId);

    if (existing) {
      channelIdMap.set(channelData.id, existing.id);
      await applyChannelOverwrites(existing, channelData, roleIdMap, summary);
      summary.channelsUpdated += 1;
      continue;
    }

    try {
      const created = await createChannelFromBackup(guild, channelData, parentId, roleIdMap);
      if (created) {
        channelIdMap.set(channelData.id, created.id);
        summary.channelsCreated += 1;
        summary.overwritesApplied += channelData.permissionOverwrites.length;
      }
    } catch (error) {
      logger.warn(
        { error, channelName: channelData.name, guildId: guild.id },
        'Failed to create channel'
      );
    }
  }
}

async function createChannelFromBackup(
  guild: Guild,
  channelData: GuildBackupPayload['channels'][number],
  parentId: string | undefined,
  roleIdMap: Map<string, string>
): Promise<NonThreadGuildBasedChannel | null> {
  const overwrites = mapOverwrites(channelData.permissionOverwrites, roleIdMap);
  const base = {
    name: channelData.name,
    parent: parentId,
    permissionOverwrites: overwrites,
    reason: RESTORE_REASON
  };

  switch (channelData.type) {
    case ChannelType.GuildText:
      return guild.channels.create({
        ...base,
        type: ChannelType.GuildText,
        topic: channelData.topic ?? undefined,
        nsfw: channelData.nsfw ?? false,
        rateLimitPerUser: channelData.rateLimitPerUser ?? 0
      });
    case ChannelType.GuildAnnouncement:
      return guild.channels.create({
        ...base,
        type: ChannelType.GuildAnnouncement,
        topic: channelData.topic ?? undefined,
        nsfw: channelData.nsfw ?? false,
        rateLimitPerUser: channelData.rateLimitPerUser ?? 0
      });
    case ChannelType.GuildVoice:
      return guild.channels.create({
        ...base,
        type: ChannelType.GuildVoice,
        bitrate: channelData.bitrate ?? 64_000,
        userLimit: channelData.userLimit ?? 0
      });
    case ChannelType.GuildStageVoice:
      return guild.channels.create({
        ...base,
        type: ChannelType.GuildStageVoice,
        bitrate: channelData.bitrate ?? 64_000,
        userLimit: channelData.userLimit ?? 0
      });
    case ChannelType.GuildForum:
      return guild.channels.create({
        ...base,
        type: ChannelType.GuildForum,
        topic: channelData.topic ?? undefined,
        nsfw: channelData.nsfw ?? false,
        rateLimitPerUser: channelData.rateLimitPerUser ?? 0
      });
    default:
      logger.warn(
        { channelType: channelData.type, channelName: channelData.name },
        'Unsupported channel type in backup restore'
      );
      return null;
  }
}

async function applyChannelOverwrites(
  channel: GuildChannel,
  channelData: GuildBackupPayload['channels'][number],
  roleIdMap: Map<string, string>,
  summary: RestoreSummary
): Promise<void> {
  if (!('permissionOverwrites' in channel)) {
    return;
  }

  const overwrites = mapOverwrites(channelData.permissionOverwrites, roleIdMap);
  if (overwrites.length === 0) {
    return;
  }

  try {
    await channel.permissionOverwrites.set(overwrites, RESTORE_REASON);
    summary.overwritesApplied += overwrites.length;
  } catch (error) {
    logger.warn(
      { error, channelId: channel.id, guildId: channel.guild.id },
      'Failed to apply channel overwrites'
    );
  }
}

async function applyGuildSettings(
  guild: Guild,
  payload: GuildBackupPayload,
  summary: RestoreSummary
): Promise<void> {
  const settings = payload.settings;
  if (!settings) {
    return;
  }

  if (settings.verificationLevel !== undefined) {
    try {
      await guild.setVerificationLevel(settings.verificationLevel, RESTORE_REASON);
      summary.settingsApplied.push('verificationLevel');
    } catch (error) {
      logger.warn({ error, guildId: guild.id }, 'Failed to restore verification level');
    }
  }

  if (settings.explicitContentFilter !== undefined) {
    try {
      await guild.setExplicitContentFilter(settings.explicitContentFilter, RESTORE_REASON);
      summary.settingsApplied.push('explicitContentFilter');
    } catch (error) {
      logger.warn({ error, guildId: guild.id }, 'Failed to restore explicit content filter');
    }
  }

  if (settings.defaultMessageNotifications !== undefined) {
    try {
      await guild.setDefaultMessageNotifications(
        settings.defaultMessageNotifications,
        RESTORE_REASON
      );
      summary.settingsApplied.push('defaultMessageNotifications');
    } catch (error) {
      logger.warn({ error, guildId: guild.id }, 'Failed to restore default notifications');
    }
  }

  if (settings.afkTimeout !== undefined) {
    try {
      await guild.setAFKTimeout(settings.afkTimeout, RESTORE_REASON);
      summary.settingsApplied.push('afkTimeout');
    } catch (error) {
      logger.warn({ error, guildId: guild.id }, 'Failed to restore AFK timeout');
    }
  }
}

export function ensureBotCanRestore(guild: Guild): boolean {
  const me = guild.members.me;
  if (!me) {
    return false;
  }
  return me.permissions.has(PermissionFlagsBits.ManageRoles | PermissionFlagsBits.ManageChannels);
}
