import { PermissionFlagsBits, type Guild, type GuildMember, type TextChannel } from 'discord.js';
import { z } from 'zod';
import { applyTagPlaceholders } from '@nexumi/shared';
import { ensureGuild } from '../../guild.js';
import { logger } from '../../logger.js';
import { birthdayQueue } from '../../queues.js';
import type { BotContext } from '../../types.js';

export const BirthdaySetSchema = z.object({
  month: z.number().int().min(1).max(12),
  day: z.number().int().min(1).max(31),
  year: z.number().int().min(1900).max(new Date().getFullYear()).optional()
});

export type BirthdaySetInput = z.infer<typeof BirthdaySetSchema>;

const DEFAULT_MESSAGE = 'Happy birthday {user}! 🎂';
const ROLE_EXPIRE_MS = 24 * 60 * 60 * 1000;

function congratsRedisKey(guildId: string, userId: string, dateKey: string): string {
  return `birthday:sent:${guildId}:${userId}:${dateKey}`;
}

export function isValidTimezone(timezone: string): boolean {
  try {
    Intl.DateTimeFormat(undefined, { timeZone: timezone });
    return true;
  } catch {
    return false;
  }
}

export function getDatePartsInTimezone(
  timezone: string,
  date = new Date()
): { month: number; day: number; year: number } {
  const parts = new Intl.DateTimeFormat('en-US', {
    timeZone: timezone,
    year: 'numeric',
    month: 'numeric',
    day: 'numeric'
  }).formatToParts(date);

  return {
    year: Number(parts.find((p) => p.type === 'year')!.value),
    month: Number(parts.find((p) => p.type === 'month')!.value),
    day: Number(parts.find((p) => p.type === 'day')!.value)
  };
}

export function isValidCalendarDate(month: number, day: number, year?: number): boolean {
  const probeYear = year ?? 2000;
  const date = new Date(probeYear, month - 1, day);
  return date.getFullYear() === probeYear && date.getMonth() === month - 1 && date.getDate() === day;
}

export function computeAge(year: number, month: number, day: number, timezone: string): number {
  const today = getDatePartsInTimezone(timezone);
  let age = today.year - year;
  if (today.month < month || (today.month === month && today.day < day)) {
    age -= 1;
  }
  return age;
}

export function daysUntilBirthday(month: number, day: number, timezone: string): number {
  const today = getDatePartsInTimezone(timezone);
  let year = today.year;
  let candidate = new Date(Date.UTC(year, month - 1, day));
  const todayUtc = new Date(Date.UTC(today.year, today.month - 1, today.day));

  if (candidate.getTime() < todayUtc.getTime()) {
    year += 1;
    candidate = new Date(Date.UTC(year, month - 1, day));
  }

  return Math.round((candidate.getTime() - todayUtc.getTime()) / (24 * 60 * 60 * 1000));
}

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

export async function updateBirthdayConfig(
  prisma: BotContext['prisma'],
  guildId: string,
  data: {
    enabled?: boolean;
    channelId?: string | null;
    roleId?: string | null;
    message?: string | null;
    timezone?: string;
  }
) {
  await ensureGuild(prisma, guildId);
  return prisma.birthdayConfig.upsert({
    where: { guildId },
    update: data,
    create: { guildId, ...data }
  });
}

export async function setBirthday(
  prisma: BotContext['prisma'],
  guildId: string,
  userId: string,
  input: BirthdaySetInput
) {
  if (!isValidCalendarDate(input.month, input.day, input.year)) {
    throw new Error('invalid_date');
  }

  return prisma.birthday.upsert({
    where: { guildId_userId: { guildId, userId } },
    update: {
      month: input.month,
      day: input.day,
      year: input.year ?? null
    },
    create: {
      guildId,
      userId,
      month: input.month,
      day: input.day,
      year: input.year ?? null
    }
  });
}

export async function removeBirthday(prisma: BotContext['prisma'], guildId: string, userId: string) {
  const existing = await prisma.birthday.findUnique({
    where: { guildId_userId: { guildId, userId } }
  });
  if (!existing) {
    return null;
  }
  await prisma.birthday.delete({ where: { guildId_userId: { guildId, userId } } });
  return existing;
}

export async function listBirthdays(prisma: BotContext['prisma'], guildId: string, timezone: string) {
  const birthdays = await prisma.birthday.findMany({ where: { guildId } });
  return birthdays.sort((a, b) => {
    const diff = daysUntilBirthday(a.month, a.day, timezone) - daysUntilBirthday(b.month, b.day, timezone);
    if (diff !== 0) {
      return diff;
    }
    return a.userId.localeCompare(b.userId);
  });
}

export async function scheduleBirthdayRoleExpire(
  guildId: string,
  userId: string,
  roleId: string
): Promise<void> {
  await birthdayQueue.add(
    'birthdayRoleExpire',
    { guildId, userId, roleId },
    {
      delay: ROLE_EXPIRE_MS,
      jobId: `birthday-role-${guildId}-${userId}-${Date.now()}`,
      removeOnComplete: 1000,
      removeOnFail: 500
    }
  );
}

function renderBirthdayMessage(
  template: string | null | undefined,
  member: GuildMember,
  age?: number
): string {
  const vars: Record<string, string> = {
    user: `<@${member.id}>`,
    'user.name': member.displayName,
    'user.tag': member.user.tag
  };
  if (age !== undefined) {
    vars.age = String(age);
  }
  return applyTagPlaceholders(template?.trim() || DEFAULT_MESSAGE, vars);
}

async function sendBirthdayCongrats(
  context: BotContext,
  guild: Guild,
  config: Awaited<ReturnType<typeof getBirthdayConfig>>,
  birthday: { userId: string; month: number; day: number; year: number | null },
  dateKey: string
): Promise<void> {
  if (!config.channelId) {
    return;
  }

  const dedupeKey = congratsRedisKey(guild.id, birthday.userId, dateKey);
  const alreadySent = await context.redis.get(dedupeKey);
  if (alreadySent) {
    return;
  }

  const channel = await guild.channels.fetch(config.channelId).catch(() => null);
  if (!channel?.isTextBased() || channel.isDMBased()) {
    logger.warn({ guildId: guild.id, channelId: config.channelId }, 'Birthday channel invalid');
    return;
  }

  const member = await guild.members.fetch(birthday.userId).catch(() => null);
  if (!member) {
    return;
  }

  const age =
    birthday.year !== null
      ? computeAge(birthday.year, birthday.month, birthday.day, config.timezone)
      : undefined;
  const content = renderBirthdayMessage(config.message, member, age);

  await (channel as TextChannel).send({ content });

  if (config.roleId) {
    const me = guild.members.me;
    const role = guild.roles.cache.get(config.roleId);
    if (
      me?.permissions.has(PermissionFlagsBits.ManageRoles) &&
      role &&
      role.position < me.roles.highest.position &&
      !member.roles.cache.has(role.id)
    ) {
      await member.roles.add(role).catch((error) => {
        logger.warn({ error, guildId: guild.id, userId: birthday.userId }, 'Failed to assign birthday role');
      });
      await scheduleBirthdayRoleExpire(guild.id, birthday.userId, role.id);
    }
  }

  await context.redis.set(dedupeKey, '1', 'EX', 60 * 60 * 26);
  logger.info({ guildId: guild.id, userId: birthday.userId }, 'Birthday congratulations sent');
}

export async function expireBirthdayRole(
  context: BotContext,
  guildId: string,
  userId: string,
  roleId: string
): Promise<void> {
  const guild = await context.client.guilds.fetch(guildId).catch(() => null);
  if (!guild) {
    return;
  }

  const member = await guild.members.fetch(userId).catch(() => null);
  if (!member) {
    return;
  }

  const me = guild.members.me;
  const role = guild.roles.cache.get(roleId);
  if (
    !me?.permissions.has(PermissionFlagsBits.ManageRoles) ||
    !role ||
    role.position >= me.roles.highest.position
  ) {
    return;
  }

  if (member.roles.cache.has(roleId)) {
    await member.roles.remove(roleId).catch((error) => {
      logger.warn({ error, guildId, userId, roleId }, 'Failed to remove birthday role');
    });
  }
}

export async function runBirthdayCheck(context: BotContext): Promise<void> {
  const configs = await context.prisma.birthdayConfig.findMany({
    where: { enabled: true, channelId: { not: null } }
  });

  for (const config of configs) {
    try {
      const guild = await context.client.guilds.fetch(config.guildId).catch(() => null);
      if (!guild) {
        continue;
      }

      const { month, day, year } = getDatePartsInTimezone(config.timezone);
      const dateKey = `${year}-${month}-${day}`;
      const birthdays = await context.prisma.birthday.findMany({
        where: { guildId: config.guildId, month, day }
      });

      await Promise.all(
        birthdays.map((birthday) => sendBirthdayCongrats(context, guild, config, birthday, dateKey))
      );
    } catch (error) {
      logger.error({ error, guildId: config.guildId }, 'Birthday check failed for guild');
    }
  }
}
