import { PermissionFlagsBits, type Guild, type GuildBasedChannel } from 'discord.js';
import { Worker } from 'bullmq';
import { applyCountTemplate } from '@nexumi/shared';
import { z } from 'zod';
import { ensureGuild } from '../../guild.js';
import { logger } from '../../logger.js';
import { redis } from '../../redis.js';
import { statsQueue, statsQueueName } from '../../queues.js';
import type { BotContext } from '../../types.js';

export const StatsSetupSchema = z.object({
  membersChannelId: z.string(),
  onlineChannelId: z.string(),
  boostsChannelId: z.string(),
  membersTemplate: z.string().min(1).max(100).optional(),
  onlineTemplate: z.string().min(1).max(100).optional(),
  boostsTemplate: z.string().min(1).max(100).optional()
});

export type StatsSetupInput = z.infer<typeof StatsSetupSchema>;

export const STATS_REFRESH_CRON = '*/10 * * * *';
export const STATS_REFRESH_JOB_NAME = 'statsRefresh';
/** One-shot refresh for a single guild (dashboard save / slash refresh). */
export const STATS_GUILD_REFRESH_JOB_NAME = 'statsRefreshGuild';

export async function getStatsConfig(prisma: BotContext['prisma'], guildId: string) {
  await ensureGuild(prisma, guildId);
  return prisma.statsConfig.findUnique({ where: { guildId } });
}

export async function upsertStatsConfig(
  prisma: BotContext['prisma'],
  guildId: string,
  input: StatsSetupInput
) {
  await ensureGuild(prisma, guildId);

  const optionalTemplates = {
    ...(input.membersTemplate !== undefined ? { membersTemplate: input.membersTemplate } : {}),
    ...(input.onlineTemplate !== undefined ? { onlineTemplate: input.onlineTemplate } : {}),
    ...(input.boostsTemplate !== undefined ? { boostsTemplate: input.boostsTemplate } : {})
  };

  return prisma.statsConfig.upsert({
    where: { guildId },
    update: {
      enabled: true,
      membersChannelId: input.membersChannelId,
      onlineChannelId: input.onlineChannelId,
      boostsChannelId: input.boostsChannelId,
      ...optionalTemplates
    },
    create: {
      guildId,
      enabled: true,
      membersChannelId: input.membersChannelId,
      onlineChannelId: input.onlineChannelId,
      boostsChannelId: input.boostsChannelId,
      ...optionalTemplates
    }
  });
}

export function estimateOnlineCount(guild: Guild): number {
  if (guild.approximatePresenceCount != null) {
    return guild.approximatePresenceCount;
  }

  return guild.members.cache.filter((member) => {
    const status = member.presence?.status;
    return status !== undefined && status !== 'offline';
  }).size;
}

async function renameStatsChannel(
  guild: Guild,
  channelId: string | null | undefined,
  template: string,
  count: number
): Promise<void> {
  if (!channelId) {
    return;
  }

  const channel = (await guild.channels.fetch(channelId).catch(() => null)) as GuildBasedChannel | null;
  if (!channel || !('setName' in channel) || typeof channel.setName !== 'function') {
    logger.warn({ guildId: guild.id, channelId }, 'Stats channel invalid or not renameable');
    return;
  }

  const nextName = applyCountTemplate(template, count).slice(0, 100);
  if (channel.name === nextName) {
    return;
  }

  await channel.setName(nextName).catch((error) => {
    logger.warn({ error, guildId: guild.id, channelId }, 'Failed to rename stats channel');
  });
}

export async function refreshGuildStatsChannels(context: BotContext, guildId: string): Promise<void> {
  const config = await context.prisma.statsConfig.findUnique({ where: { guildId } });
  if (!config?.enabled) {
    return;
  }

  // withCounts populates approximatePresenceCount / approximateMemberCount.
  // GuildPresences is not privileged for us — presence cache would stay empty.
  const guild = await context.client.guilds
    .fetch({ guild: guildId, withCounts: true })
    .catch(() => null);
  if (!guild) {
    return;
  }

  const me = guild.members.me;
  if (!me?.permissions.has(PermissionFlagsBits.ManageChannels)) {
    logger.warn({ guildId }, 'Missing ManageChannels for stats channel refresh');
    return;
  }

  const memberCount = guild.approximateMemberCount ?? guild.memberCount;
  const onlineCount = estimateOnlineCount(guild);
  const boostCount = guild.premiumSubscriptionCount ?? 0;

  await Promise.all([
    renameStatsChannel(guild, config.membersChannelId, config.membersTemplate, memberCount),
    renameStatsChannel(guild, config.onlineChannelId, config.onlineTemplate, onlineCount),
    renameStatsChannel(guild, config.boostsChannelId, config.boostsTemplate, boostCount)
  ]);
}

export async function runStatsGuildRefreshJob(
  context: BotContext,
  data: { guildId: string }
): Promise<void> {
  await refreshGuildStatsChannels(context, data.guildId);
}

export async function updateStatsChannels(context: BotContext): Promise<void> {
  const configs = await context.prisma.statsConfig.findMany({
    where: { enabled: true }
  });

  for (const config of configs) {
    try {
      await refreshGuildStatsChannels(context, config.guildId);
    } catch (error) {
      logger.error({ error, guildId: config.guildId }, 'Stats channel refresh failed for guild');
    }
  }
}

export async function ensureStatsRecurringJobs(): Promise<void> {
  await statsQueue.add(
    STATS_REFRESH_JOB_NAME,
    {},
    {
      repeat: { pattern: STATS_REFRESH_CRON },
      removeOnComplete: 20,
      removeOnFail: 20
    }
  );
}

export function startStatsWorker(context: BotContext): Worker {
  const worker = new Worker(
    statsQueueName,
    async (job) => {
      if (job.name === STATS_REFRESH_JOB_NAME) {
        await updateStatsChannels(context);
        return;
      }
      if (job.name === STATS_GUILD_REFRESH_JOB_NAME) {
        const guildId = typeof job.data?.guildId === 'string' ? job.data.guildId : null;
        if (!guildId) {
          logger.warn({ jobId: job.id }, 'statsRefreshGuild missing guildId');
          return;
        }
        await runStatsGuildRefreshJob(context, { guildId });
      }
    },
    { connection: redis }
  );

  worker.on('failed', (job, error) => {
    logger.error({ jobId: job?.id, error }, 'Stats queue job failed');
  });

  return worker;
}
