import type { Client, Guild, GuildMember, Invite } from 'discord.js';
import type { PrismaClient } from '@prisma/client';
import { ensureGuild } from '../../guild.js';
import type { Redis } from 'ioredis';

const FAKE_ACCOUNT_AGE_MS = 7 * 24 * 60 * 60 * 1000;

export type CachedInvite = {
  code: string;
  uses: number;
  inviterId: string | null;
};

function inviteCacheKey(guildId: string): string {
  return `stats:invites:${guildId}`;
}

function parseInviteCache(raw: string | null): Record<string, CachedInvite> {
  if (!raw) {
    return {};
  }
  try {
    return JSON.parse(raw) as Record<string, CachedInvite>;
  } catch {
    return {};
  }
}

export async function getInviteCache(redis: Redis, guildId: string): Promise<Record<string, CachedInvite>> {
  const raw = await redis.get(inviteCacheKey(guildId));
  return parseInviteCache(raw);
}

export async function setInviteCache(
  redis: Redis,
  guildId: string,
  invites: Record<string, CachedInvite>
): Promise<void> {
  await redis.set(inviteCacheKey(guildId), JSON.stringify(invites));
}

function inviteToCached(invite: Invite): CachedInvite {
  return {
    code: invite.code,
    uses: invite.uses ?? 0,
    inviterId: invite.inviter?.id ?? null
  };
}

export async function syncGuildInviteCache(guild: Guild, redis: Redis): Promise<void> {
  const fetched = await guild.invites.fetch().catch(() => null);
  if (!fetched) {
    return;
  }

  const cache: Record<string, CachedInvite> = {};
  for (const invite of fetched.values()) {
    cache[invite.code] = inviteToCached(invite);
  }
  await setInviteCache(redis, guild.id, cache);
}

export async function syncAllGuildInviteCaches(client: Client, redis: Redis): Promise<void> {
  await Promise.all(
    [...client.guilds.cache.values()].map((guild) => syncGuildInviteCache(guild, redis))
  );
}

export async function upsertInviteInCache(redis: Redis, guildId: string, invite: Invite): Promise<void> {
  const cache = await getInviteCache(redis, guildId);
  cache[invite.code] = inviteToCached(invite);
  await setInviteCache(redis, guildId, cache);
}

export async function removeInviteFromCache(redis: Redis, guildId: string, code: string): Promise<void> {
  const cache = await getInviteCache(redis, guildId);
  delete cache[code];
  await setInviteCache(redis, guildId, cache);
}

export function isFakeJoin(member: GuildMember): boolean {
  return Date.now() - member.user.createdTimestamp < FAKE_ACCOUNT_AGE_MS;
}

export async function resolveUsedInvite(
  redis: Redis,
  guild: Guild
): Promise<{ code: string; inviterId: string | null } | null> {
  const previous = await getInviteCache(redis, guild.id);
  const currentInvites = await guild.invites.fetch().catch(() => null);
  if (!currentInvites) {
    return null;
  }

  let matched: { code: string; inviterId: string | null } | null = null;

  for (const invite of currentInvites.values()) {
    const before = previous[invite.code];
    const currentUses = invite.uses ?? 0;
    const previousUses = before?.uses ?? 0;

    if (currentUses > previousUses) {
      matched = {
        code: invite.code,
        inviterId: invite.inviter?.id ?? before?.inviterId ?? null
      };
    }

    previous[invite.code] = inviteToCached(invite);
  }

  await setInviteCache(redis, guild.id, previous);
  return matched;
}

export async function recordMemberJoin(
  prisma: PrismaClient,
  guildId: string,
  userId: string,
  inviterId: string | null,
  code: string | null,
  isFake: boolean
): Promise<void> {
  await ensureGuild(prisma, guildId);

  await prisma.inviteJoin.upsert({
    where: { guildId_userId: { guildId, userId } },
    update: {
      inviterId,
      code,
      isFake,
      leftAt: null
    },
    create: {
      guildId,
      userId,
      inviterId,
      code,
      isFake
    }
  });

  if (!inviterId) {
    return;
  }

  await prisma.inviteStat.upsert({
    where: { guildId_inviterId: { guildId, inviterId } },
    update: {
      joins: { increment: 1 },
      ...(isFake ? { fakes: { increment: 1 } } : {}),
      ...(code ? { code } : {})
    },
    create: {
      guildId,
      inviterId,
      code,
      joins: 1,
      leaves: 0,
      fakes: isFake ? 1 : 0
    }
  });
}

export async function recordMemberLeave(prisma: PrismaClient, guildId: string, userId: string): Promise<void> {
  const join = await prisma.inviteJoin.findUnique({
    where: { guildId_userId: { guildId, userId } }
  });

  if (!join || join.leftAt) {
    return;
  }

  await prisma.inviteJoin.update({
    where: { guildId_userId: { guildId, userId } },
    data: { leftAt: new Date() }
  });

  if (!join.inviterId) {
    return;
  }

  await prisma.inviteStat.updateMany({
    where: { guildId, inviterId: join.inviterId },
    data: { leaves: { increment: 1 } }
  });
}

export async function getInviteStatsForUser(
  prisma: PrismaClient,
  guildId: string,
  inviterId: string
) {
  return prisma.inviteStat.findUnique({
    where: { guildId_inviterId: { guildId, inviterId } }
  });
}

export async function getInviteLeaderboard(prisma: PrismaClient, guildId: string, limit = 10) {
  return prisma.inviteStat.findMany({
    where: { guildId },
    orderBy: [{ joins: 'desc' }, { inviterId: 'asc' }],
    take: limit
  });
}
