import type { EconomyConfig, MemberEconomy, Prisma, PrismaClient, ShopItem } from '@prisma/client';
import { ensureGuild } from '../../guild.js';
import type { BotContext } from '../../types.js';

type DbClient = PrismaClient | Prisma.TransactionClient;

export type EconomyErrorCode =
  | 'disabled'
  | 'insufficient'
  | 'cooldown'
  | 'invalid_amount'
  | 'self_transfer'
  | 'not_found'
  | 'out_of_stock'
  | 'role_assign_failed'
  | 'game_in_progress';

export class EconomyError extends Error {
  constructor(public readonly code: EconomyErrorCode) {
    super(code);
    this.name = 'EconomyError';
  }
}

const DAILY_COOLDOWN_MS = 24 * 60 * 60 * 1000;
const WEEKLY_COOLDOWN_MS = 7 * DAILY_COOLDOWN_MS;

export async function getEconomyConfig(
  prisma: DbClient,
  guildId: string
): Promise<EconomyConfig> {
  await ensureGuild(prisma as PrismaClient, guildId);
  return prisma.economyConfig.upsert({
    where: { guildId },
    update: {},
    create: { guildId }
  });
}

export async function assertEconomyEnabled(prisma: PrismaClient, guildId: string): Promise<EconomyConfig> {
  const config = await getEconomyConfig(prisma, guildId);
  if (!config.enabled) {
    throw new EconomyError('disabled');
  }
  return config;
}

export async function ensureMemberEconomy(
  prisma: DbClient,
  guildId: string,
  userId: string
): Promise<MemberEconomy> {
  const config = await getEconomyConfig(prisma, guildId);
  return prisma.memberEconomy.upsert({
    where: { guildId_userId: { guildId, userId } },
    update: {},
    create: {
      guildId,
      userId,
      balance: config.startingBalance
    }
  });
}

export async function getBalance(
  prisma: PrismaClient,
  guildId: string,
  userId: string
): Promise<number> {
  const member = await ensureMemberEconomy(prisma, guildId, userId);
  return member.balance;
}

function assertPositiveAmount(amount: number): void {
  if (!Number.isInteger(amount) || amount <= 0) {
    throw new EconomyError('invalid_amount');
  }
}

export async function addBalance(
  prisma: DbClient,
  guildId: string,
  userId: string,
  amount: number
): Promise<MemberEconomy> {
  await ensureMemberEconomy(prisma, guildId, userId);
  return prisma.memberEconomy.update({
    where: { guildId_userId: { guildId, userId } },
    data: { balance: { increment: amount } }
  });
}

export async function deductBalance(
  prisma: DbClient,
  guildId: string,
  userId: string,
  amount: number
): Promise<MemberEconomy> {
  assertPositiveAmount(amount);
  const member = await ensureMemberEconomy(prisma, guildId, userId);
  if (member.balance < amount) {
    throw new EconomyError('insufficient');
  }
  return prisma.memberEconomy.update({
    where: { guildId_userId: { guildId, userId } },
    data: { balance: { decrement: amount } }
  });
}

export async function transferBalance(
  prisma: PrismaClient,
  guildId: string,
  fromUserId: string,
  toUserId: string,
  amount: number
): Promise<{ sender: MemberEconomy; receiver: MemberEconomy }> {
  assertPositiveAmount(amount);
  if (fromUserId === toUserId) {
    throw new EconomyError('self_transfer');
  }

  return prisma.$transaction(async (tx) => {
    const sender = await ensureMemberEconomy(tx, guildId, fromUserId);
    if (sender.balance < amount) {
      throw new EconomyError('insufficient');
    }
    await ensureMemberEconomy(tx, guildId, toUserId);
    const updatedSender = await tx.memberEconomy.update({
      where: { guildId_userId: { guildId, userId: fromUserId } },
      data: { balance: { decrement: amount } }
    });
    const updatedReceiver = await tx.memberEconomy.update({
      where: { guildId_userId: { guildId, userId: toUserId } },
      data: { balance: { increment: amount } }
    });
    return { sender: updatedSender, receiver: updatedReceiver };
  });
}

function cooldownRemainingMs(lastAt: Date | null, cooldownMs: number): number {
  if (!lastAt) {
    return 0;
  }
  const elapsed = Date.now() - lastAt.getTime();
  return Math.max(0, cooldownMs - elapsed);
}

export async function claimDaily(
  prisma: PrismaClient,
  guildId: string,
  userId: string
): Promise<{ amount: number; balance: number }> {
  const config = await assertEconomyEnabled(prisma, guildId);
  const member = await ensureMemberEconomy(prisma, guildId, userId);
  const remaining = cooldownRemainingMs(member.lastDailyAt, DAILY_COOLDOWN_MS);
  if (remaining > 0) {
    throw new EconomyError('cooldown');
  }

  const updated = await prisma.memberEconomy.update({
    where: { guildId_userId: { guildId, userId } },
    data: {
      balance: { increment: config.dailyAmount },
      lastDailyAt: new Date()
    }
  });
  return { amount: config.dailyAmount, balance: updated.balance };
}

export async function claimWeekly(
  prisma: PrismaClient,
  guildId: string,
  userId: string
): Promise<{ amount: number; balance: number }> {
  const config = await assertEconomyEnabled(prisma, guildId);
  const member = await ensureMemberEconomy(prisma, guildId, userId);
  const remaining = cooldownRemainingMs(member.lastWeeklyAt, WEEKLY_COOLDOWN_MS);
  if (remaining > 0) {
    throw new EconomyError('cooldown');
  }

  const updated = await prisma.memberEconomy.update({
    where: { guildId_userId: { guildId, userId } },
    data: {
      balance: { increment: config.weeklyAmount },
      lastWeeklyAt: new Date()
    }
  });
  return { amount: config.weeklyAmount, balance: updated.balance };
}

export async function claimWork(
  prisma: PrismaClient,
  guildId: string,
  userId: string,
  earned: number
): Promise<{ amount: number; balance: number }> {
  await assertEconomyEnabled(prisma, guildId);
  const config = await getEconomyConfig(prisma, guildId);
  const member = await ensureMemberEconomy(prisma, guildId, userId);
  const cooldownMs = config.workCooldownSeconds * 1000;
  const remaining = cooldownRemainingMs(member.lastWorkAt, cooldownMs);
  if (remaining > 0) {
    throw new EconomyError('cooldown');
  }

  const updated = await prisma.memberEconomy.update({
    where: { guildId_userId: { guildId, userId } },
    data: {
      balance: { increment: earned },
      lastWorkAt: new Date()
    }
  });
  return { amount: earned, balance: updated.balance };
}

export async function getWorkCooldownRemaining(
  prisma: PrismaClient,
  guildId: string,
  userId: string
): Promise<number> {
  const config = await getEconomyConfig(prisma, guildId);
  const member = await ensureMemberEconomy(prisma, guildId, userId);
  return cooldownRemainingMs(member.lastWorkAt, config.workCooldownSeconds * 1000);
}

export async function getDailyCooldownRemaining(
  prisma: PrismaClient,
  guildId: string,
  userId: string
): Promise<number> {
  const member = await ensureMemberEconomy(prisma, guildId, userId);
  return cooldownRemainingMs(member.lastDailyAt, DAILY_COOLDOWN_MS);
}

export async function getWeeklyCooldownRemaining(
  prisma: PrismaClient,
  guildId: string,
  userId: string
): Promise<number> {
  const member = await ensureMemberEconomy(prisma, guildId, userId);
  return cooldownRemainingMs(member.lastWeeklyAt, WEEKLY_COOLDOWN_MS);
}

export async function listShopItems(prisma: PrismaClient, guildId: string): Promise<ShopItem[]> {
  await assertEconomyEnabled(prisma, guildId);
  return prisma.shopItem.findMany({
    where: { guildId, enabled: true },
    orderBy: { price: 'asc' }
  });
}

export async function buyShopItem(
  context: BotContext,
  guildId: string,
  userId: string,
  itemId: string
): Promise<{ item: ShopItem; balance: number }> {
  await assertEconomyEnabled(context.prisma, guildId);

  return context.prisma.$transaction(async (tx) => {
    const item = await tx.shopItem.findFirst({
      where: { id: itemId, guildId, enabled: true }
    });
    if (!item) {
      throw new EconomyError('not_found');
    }

    if (item.stock !== null && item.stock <= 0) {
      throw new EconomyError('out_of_stock');
    }

    const member = await ensureMemberEconomy(tx, guildId, userId);
    if (member.balance < item.price) {
      throw new EconomyError('insufficient');
    }

    const updatedMember = await tx.memberEconomy.update({
      where: { guildId_userId: { guildId, userId } },
      data: { balance: { decrement: item.price } }
    });

    await tx.inventoryItem.upsert({
      where: { guildId_userId_itemId: { guildId, userId, itemId: item.id } },
      update: { quantity: { increment: 1 } },
      create: { guildId, userId, itemId: item.id, quantity: 1 }
    });

    if (item.stock !== null) {
      await tx.shopItem.update({
        where: { id: item.id },
        data: { stock: { decrement: 1 } }
      });
    }

    if (item.roleId) {
      const guild = context.client.guilds.cache.get(guildId) ?? (await context.client.guilds.fetch(guildId));
      const memberObj = await guild.members.fetch(userId);
      try {
        await memberObj.roles.add(item.roleId, 'Economy shop purchase');
      } catch {
        throw new EconomyError('role_assign_failed');
      }
    }

    return { item, balance: updatedMember.balance };
  });
}

export async function getInventory(
  prisma: PrismaClient,
  guildId: string,
  userId: string
) {
  await assertEconomyEnabled(prisma, guildId);
  return prisma.inventoryItem.findMany({
    where: { guildId, userId },
    include: { item: true },
    orderBy: { createdAt: 'desc' }
  });
}

export async function getLeaderboard(prisma: PrismaClient, guildId: string, limit = 10) {
  await assertEconomyEnabled(prisma, guildId);
  return prisma.memberEconomy.findMany({
    where: { guildId },
    orderBy: { balance: 'desc' },
    take: limit
  });
}

export async function adminGiveBalance(
  prisma: PrismaClient,
  guildId: string,
  userId: string,
  amount: number
): Promise<MemberEconomy> {
  assertPositiveAmount(amount);
  await assertEconomyEnabled(prisma, guildId);
  return addBalance(prisma, guildId, userId, amount);
}

export async function adminRemoveBalance(
  prisma: PrismaClient,
  guildId: string,
  userId: string,
  amount: number
): Promise<MemberEconomy> {
  return deductBalance(prisma, guildId, userId, amount);
}

export async function adminResetEconomy(
  prisma: PrismaClient,
  guildId: string,
  userId: string
): Promise<void> {
  await assertEconomyEnabled(prisma, guildId);
  const config = await getEconomyConfig(prisma, guildId);
  await prisma.$transaction([
    prisma.memberEconomy.upsert({
      where: { guildId_userId: { guildId, userId } },
      update: {
        balance: config.startingBalance,
        lastDailyAt: null,
        lastWeeklyAt: null,
        lastWorkAt: null
      },
      create: {
        guildId,
        userId,
        balance: config.startingBalance
      }
    }),
    prisma.inventoryItem.deleteMany({ where: { guildId, userId } })
  ]);
}

export function formatCooldownSeconds(ms: number): number {
  return Math.ceil(ms / 1000);
}
