import { ForbiddenException, Injectable } from '@nestjs/common';

import type { UsageListResponse, WalletResponse } from '@hexahost/contracts';

import { PrismaService } from '../prisma/prisma.service';

import { isExternalBilling } from './billing-provider';

@Injectable()
export class BillingService {
  constructor(private readonly prisma: PrismaService) {}

  async ensureWalletForUser(userId: string, grantCredits = 0): Promise<void> {
    const existing = await this.prisma.creditWallet.findUnique({
      where: { userId },
    });

    if (existing) {
      return;
    }

    const freePlan = await this.prisma.plan.findFirst({
      where: { slug: 'free', isActive: true },
    });

    const initialGrant = grantCredits || freePlan?.monthlyCreditGrant || 0;

    await this.prisma.$transaction(async (tx) => {
      const wallet = await tx.creditWallet.create({
        data: {
          userId,
          balance: initialGrant,
        },
      });

      if (initialGrant > 0) {
        await tx.creditTransaction.create({
          data: {
            walletId: wallet.id,
            amount: initialGrant,
            type: 'GRANT',
            referenceType: 'signup',
            referenceId: userId,
            idempotencyKey: `grant:signup:${userId}`,
          },
        });
      }
    });
  }

  async getWallet(userId: string): Promise<WalletResponse> {
    await this.ensureWalletForUser(userId);

    const wallet = await this.prisma.creditWallet.findUniqueOrThrow({
      where: { userId },
      include: {
        transactions: {
          orderBy: { createdAt: 'desc' },
          take: 20,
        },
      },
    });

    return {
      balance: wallet.balance,
      periodStart: wallet.periodStart.toISOString(),
      transactions: wallet.transactions.map((tx) => ({
        id: tx.id,
        amount: tx.amount,
        type: tx.type,
        referenceType: tx.referenceType,
        referenceId: tx.referenceId,
        createdAt: tx.createdAt.toISOString(),
      })),
    };
  }

  async listUsage(userId: string, limit = 20): Promise<UsageListResponse> {
    const records = await this.prisma.usageRecord.findMany({
      where: { userId },
      orderBy: { periodStart: 'desc' },
      take: limit,
    });

    return {
      records: records.map((record) => ({
        id: record.id,
        serverId: record.serverId,
        ramMb: record.ramMb,
        durationSeconds: record.durationSeconds,
        creditsCharged: record.creditsCharged,
        periodStart: record.periodStart.toISOString(),
        periodEnd: record.periodEnd.toISOString(),
        createdAt: record.createdAt.toISOString(),
      })),
    };
  }

  async assertCanStartServer(userId: string): Promise<void> {
    if (isExternalBilling()) {
      return;
    }

    await this.ensureWalletForUser(userId);

    const wallet = await this.prisma.creditWallet.findUnique({
      where: { userId },
    });

    if (!wallet || wallet.balance <= 0) {
      throw new ForbiddenException(
        'Insufficient credits to start a server. Wait for the next grant or upgrade your plan.',
      );
    }
  }

  async assertCanCreateServer(
    userId: string,
    planId?: string | null,
  ): Promise<void> {
    const plan = planId
      ? await this.prisma.plan.findUnique({ where: { id: planId } })
      : await this.prisma.plan.findFirst({ where: { slug: 'free', isActive: true } });

    if (!plan?.maxServersPerUser) {
      return;
    }

    const count = await this.prisma.gameServer.count({
      where: {
        userId,
        status: { not: 'DELETED' },
      },
    });

    if (count >= plan.maxServersPerUser) {
      throw new ForbiddenException(
        `Server quota reached (${plan.maxServersPerUser} servers on ${plan.name})`,
      );
    }
  }

  async grantMonthlyCredits(): Promise<number> {
    const plans = await this.prisma.plan.findMany({
      where: { isActive: true, monthlyCreditGrant: { gt: 0 } },
    });

    let granted = 0;

    for (const plan of plans) {
      const servers = await this.prisma.gameServer.findMany({
        where: { planId: plan.id },
        select: { userId: true },
        distinct: ['userId'],
      });

      for (const { userId } of servers) {
        await this.ensureWalletForUser(userId);
        const key = `grant:monthly:${plan.slug}:${userId}:${new Date().toISOString().slice(0, 7)}`;

        const wallet = await this.prisma.creditWallet.findUniqueOrThrow({
          where: { userId },
        });

        const existing = await this.prisma.creditTransaction.findUnique({
          where: { idempotencyKey: key },
        });

        if (existing) {
          continue;
        }

        await this.prisma.$transaction([
          this.prisma.creditWallet.update({
            where: { id: wallet.id },
            data: { balance: { increment: plan.monthlyCreditGrant } },
          }),
          this.prisma.creditTransaction.create({
            data: {
              walletId: wallet.id,
              amount: plan.monthlyCreditGrant,
              type: 'GRANT',
              referenceType: 'plan',
              referenceId: plan.id,
              idempotencyKey: key,
            },
          }),
        ]);

        granted += 1;
      }
    }

    return granted;
  }
}
