import { LedgerDirection, LedgerEntryType, Prisma, WalletAccountType } from "@prisma/client";
import { prisma } from "@/server/db";
import { ConflictError } from "@/server/errors";

type Db = typeof prisma | Prisma.TransactionClient;

export type Money = string | number | Prisma.Decimal;

export function decimal(value: Money) {
  return new Prisma.Decimal(value);
}

export async function ensureWallet(userId: string, currency: string, db: Db = prisma) {
  const existing = await db.wallet.findUnique({
    where: { userId_currency: { userId, currency } },
    include: { accounts: true }
  });

  if (existing) {
    return existing;
  }

  return db.wallet.create({
    data: {
      userId,
      currency,
      accounts: {
        create: [
          { type: WalletAccountType.AVAILABLE },
          { type: WalletAccountType.BONUS },
          { type: WalletAccountType.LOCKED }
        ]
      }
    },
    include: { accounts: true }
  });
}

export async function walletSnapshot(userId: string, currency: string) {
  const wallet = await ensureWallet(userId, currency);
  return wallet.accounts.reduce<Record<string, string>>((acc, account) => {
    acc[account.type] = account.balance.toString();
    return acc;
  }, {});
}

export async function postLedgerEntry(
  db: Prisma.TransactionClient,
  input: {
    walletId: string;
    accountType: WalletAccountType;
    direction: LedgerDirection;
    type: LedgerEntryType;
    amount: Money;
    currency: string;
    referenceType: string;
    referenceId?: string;
    idempotencyKey?: string;
    reason?: string;
    metadata?: Prisma.InputJsonValue;
  }
) {
  if (input.idempotencyKey) {
    const existing = await db.ledgerEntry.findUnique({ where: { idempotencyKey: input.idempotencyKey } });
    if (existing) return existing;
  }

  const amount = decimal(input.amount);
  if (amount.lte(0)) {
    throw new ConflictError("Ledger amount must be positive", "INVALID_LEDGER_AMOUNT");
  }

  const account = await db.walletAccount.findUnique({
    where: { walletId_type: { walletId: input.walletId, type: input.accountType } }
  });

  if (!account) {
    throw new ConflictError("Wallet account not found", "WALLET_ACCOUNT_NOT_FOUND");
  }

  if (input.direction === LedgerDirection.DEBIT) {
    const updated = await db.walletAccount.updateMany({
      where: {
        id: account.id,
        balance: { gte: amount }
      },
      data: {
        balance: { decrement: amount },
        version: { increment: 1 }
      }
    });

    if (updated.count !== 1) {
      throw new ConflictError("Insufficient wallet balance", "INSUFFICIENT_FUNDS");
    }
  } else {
    await db.walletAccount.update({
      where: { id: account.id },
      data: {
        balance: { increment: amount },
        version: { increment: 1 }
      }
    });
  }

  const updatedAccount = await db.walletAccount.findUniqueOrThrow({ where: { id: account.id } });

  return db.ledgerEntry.create({
    data: {
      walletId: input.walletId,
      accountId: account.id,
      direction: input.direction,
      type: input.type,
      amount,
      balanceAfter: updatedAccount.balance,
      currency: input.currency,
      referenceType: input.referenceType,
      referenceId: input.referenceId,
      idempotencyKey: input.idempotencyKey,
      reason: input.reason,
      metadata: input.metadata
    }
  });
}

export async function creditAvailable(input: {
  userId: string;
  currency: string;
  amount: Money;
  type: LedgerEntryType;
  referenceType: string;
  referenceId?: string;
  idempotencyKey?: string;
  reason?: string;
  metadata?: Prisma.InputJsonValue;
}) {
  return prisma.$transaction(async (tx) => {
    const wallet = await ensureWallet(input.userId, input.currency, tx);
    return postLedgerEntry(tx, {
      walletId: wallet.id,
      accountType: WalletAccountType.AVAILABLE,
      direction: LedgerDirection.CREDIT,
      type: input.type,
      amount: input.amount,
      currency: input.currency,
      referenceType: input.referenceType,
      referenceId: input.referenceId,
      idempotencyKey: input.idempotencyKey,
      reason: input.reason,
      metadata: input.metadata
    });
  });
}

export async function debitAvailableInTx(
  tx: Prisma.TransactionClient,
  input: {
    userId: string;
    currency: string;
    amount: Money;
    type: LedgerEntryType;
    referenceType: string;
    referenceId?: string;
    idempotencyKey?: string;
    reason?: string;
    metadata?: Prisma.InputJsonValue;
  }
) {
  const wallet = await ensureWallet(input.userId, input.currency, tx);
  const entry = await postLedgerEntry(tx, {
    walletId: wallet.id,
    accountType: WalletAccountType.AVAILABLE,
    direction: LedgerDirection.DEBIT,
    type: input.type,
    amount: input.amount,
    currency: input.currency,
    referenceType: input.referenceType,
    referenceId: input.referenceId,
    idempotencyKey: input.idempotencyKey,
    reason: input.reason,
    metadata: input.metadata
  });

  return { wallet, entry };
}

export async function debitPlayableBalanceInTx(
  tx: Prisma.TransactionClient,
  input: {
    userId: string;
    currency: string;
    amount: Money;
    type: LedgerEntryType;
    referenceType: string;
    referenceId?: string;
    idempotencyKey?: string;
    reason?: string;
    metadata?: Prisma.InputJsonValue;
  }
) {
  const wallet = await ensureWallet(input.userId, input.currency, tx);
  const amount = decimal(input.amount);

  if (amount.lte(0)) {
    throw new ConflictError("Ledger amount must be positive", "INVALID_LEDGER_AMOUNT");
  }

  await tx.$queryRaw`SELECT id FROM "WalletAccount" WHERE "walletId" = CAST(${wallet.id} AS uuid) FOR UPDATE`;

  const accounts = await tx.walletAccount.findMany({
    where: {
      walletId: wallet.id,
      type: { in: [WalletAccountType.AVAILABLE, WalletAccountType.BONUS] }
    }
  });

  const available = accounts.find((account) => account.type === WalletAccountType.AVAILABLE);
  const bonus = accounts.find((account) => account.type === WalletAccountType.BONUS);
  const availableBalance = available?.balance ?? decimal(0);
  const bonusBalance = bonus?.balance ?? decimal(0);

  if (availableBalance.plus(bonusBalance).lt(amount)) {
    throw new ConflictError("Insufficient wallet balance", "INSUFFICIENT_FUNDS");
  }

  const availableDebit = availableBalance.gte(amount) ? amount : availableBalance;
  const bonusDebit = amount.sub(availableDebit);
  const entries = [];

  if (availableDebit.gt(0)) {
    entries.push(
      await postLedgerEntry(tx, {
        walletId: wallet.id,
        accountType: WalletAccountType.AVAILABLE,
        direction: LedgerDirection.DEBIT,
        type: input.type,
        amount: availableDebit,
        currency: input.currency,
        referenceType: input.referenceType,
        referenceId: input.referenceId,
        idempotencyKey: input.idempotencyKey ? `${input.idempotencyKey}:available` : undefined,
        reason: input.reason,
        metadata: input.metadata
      })
    );
  }

  if (bonusDebit.gt(0)) {
    entries.push(
      await postLedgerEntry(tx, {
        walletId: wallet.id,
        accountType: WalletAccountType.BONUS,
        direction: LedgerDirection.DEBIT,
        type: input.type,
        amount: bonusDebit,
        currency: input.currency,
        referenceType: input.referenceType,
        referenceId: input.referenceId,
        idempotencyKey: input.idempotencyKey ? `${input.idempotencyKey}:bonus` : undefined,
        reason: input.reason,
        metadata: input.metadata
      })
    );
  }

  return { wallet, entries, availableDebit, bonusDebit };
}
