import {
  CryptoAsset,
  CryptoDepositStatus,
  CryptoWithdrawalStatus,
  LedgerDirection,
  LedgerEntryType,
  PaymentType,
  Prisma,
  TransactionStatus,
  WalletAccountType
} from "@prisma/client";
import { z } from "zod";
import { writeAuditLog } from "@/server/audit";
import { prisma } from "@/server/db";
import { AppError, ConflictError } from "@/server/errors";
import { applyAffiliateFirstDepositCommissionInTx } from "@/server/affiliation/service";
import { applyFirstDepositWelcomeBonusInTx } from "@/server/bonus/welcome";
import { ensureWallet, postLedgerEntry } from "@/server/wallet/ledger";
import { requireCryptoAssetConfig } from "@/server/crypto/assets";
import { getCryptoEurRate } from "@/server/crypto/rates";

const MIN_DEPOSIT_EUR = new Prisma.Decimal(25);
const MIN_WITHDRAWAL_EUR = new Prisma.Decimal(20);
const WITHDRAWAL_FEE_EUR = new Prisma.Decimal(2);
const WITHDRAWAL_LIMIT_EXCEEDED_MESSAGE = "Retrait supérieur à vos limites autorisées";

const withdrawalLimitsByVipTier: Record<string, { daily: Prisma.Decimal; monthly: Prisma.Decimal }> = {
  bronze: { daily: new Prisma.Decimal(300), monthly: new Prisma.Decimal(5000) },
  gold: { daily: new Prisma.Decimal(400), monthly: new Prisma.Decimal(7500) },
  platinum: { daily: new Prisma.Decimal(700), monthly: new Prisma.Decimal(10000) },
  jade: { daily: new Prisma.Decimal(900), monthly: new Prisma.Decimal(12500) },
  sapphire: { daily: new Prisma.Decimal(1500), monthly: new Prisma.Decimal(20000) },
  black: { daily: new Prisma.Decimal(1500), monthly: new Prisma.Decimal(40000) }
};

export const createCryptoWithdrawalSchema = z.object({
  asset: z.nativeEnum(CryptoAsset),
  amountEur: z.coerce.number().positive().max(100000),
  destinationAddress: z.string().min(12).max(160)
});

export const simulateCryptoDepositSchema = z.object({
  asset: z.nativeEnum(CryptoAsset),
  eurAmount: z.coerce.number().min(25).max(100000)
});

export async function creditCryptoDeposit(input: {
  userId: string;
  asset: CryptoAsset;
  cryptoAmount: string | number | Prisma.Decimal;
  txHash: string;
  fromAddress?: string;
  confirmations?: number;
}) {
  const assetConfig = requireCryptoAssetConfig(input.asset);
  const address = await prisma.cryptoDepositAddress.findUniqueOrThrow({
    where: { userId_network: { userId: input.userId, network: assetConfig.network } }
  });
  const existing = await prisma.cryptoDeposit.findUnique({
    where: { network_txHash: { network: assetConfig.network, txHash: input.txHash } },
    include: { paymentTransaction: true }
  });

  if (existing) {
    return existing;
  }

  const cryptoAmount = new Prisma.Decimal(input.cryptoAmount);
  const rate = await getCryptoEurRate(input.asset);
  const eurAmount = cryptoAmount.mul(rate.rate);

  if (eurAmount.lt(MIN_DEPOSIT_EUR)) {
    throw new AppError("Le dépôt minimum est de 25€.", 422, "MIN_DEPOSIT_NOT_REACHED", {
      eurAmount: eurAmount.toFixed(2)
    });
  }

  return prisma.$transaction(async (tx) => {
    const wallet = await ensureWallet(input.userId, "EUR", tx);
    const transaction = await tx.paymentTransaction.create({
      data: {
        userId: input.userId,
        walletId: wallet.id,
        type: PaymentType.DEPOSIT,
        provider: "crypto-self-custody",
        providerRef: input.txHash,
        status: TransactionStatus.APPROVED,
        amount: eurAmount,
        currency: "EUR",
        idempotencyKey: `crypto-deposit-transaction:${assetConfig.network}:${input.txHash}`,
        method: `${input.asset}-${assetConfig.network}`,
        metadata: {
          asset: input.asset,
          network: assetConfig.network,
          cryptoAmount: cryptoAmount.toString(),
          eurRate: rate.rate.toString(),
          rateSource: rate.source
        }
      }
    });

    const deposit = await tx.cryptoDeposit.create({
      data: {
        userId: input.userId,
        addressId: address.id,
        walletId: wallet.id,
        paymentTransactionId: transaction.id,
        asset: input.asset,
        network: assetConfig.network,
        txHash: input.txHash,
        fromAddress: input.fromAddress,
        toAddress: address.address,
        cryptoAmount,
        confirmations: input.confirmations ?? assetConfig.requiredConfirmations,
        requiredConfirmations: assetConfig.requiredConfirmations,
        status: CryptoDepositStatus.CREDITED,
        eurRate: rate.rate,
        eurAmount,
        rateSource: rate.source,
        creditedAt: new Date()
      }
    });

    await postLedgerEntry(tx, {
      walletId: wallet.id,
      accountType: WalletAccountType.AVAILABLE,
      direction: LedgerDirection.CREDIT,
      type: LedgerEntryType.DEPOSIT,
      amount: eurAmount,
      currency: "EUR",
      referenceType: "crypto_deposit",
      referenceId: deposit.id,
      idempotencyKey: `crypto-deposit-ledger:${assetConfig.network}:${input.txHash}`,
      reason: `Dépôt ${input.asset} converti en EUR`,
      metadata: {
        txHash: input.txHash,
        asset: input.asset,
        network: assetConfig.network,
        cryptoAmount: cryptoAmount.toString(),
        eurRate: rate.rate.toString(),
        rateSource: rate.source
      }
    });

    const welcomeBonus = await applyFirstDepositWelcomeBonusInTx(tx, {
      userId: input.userId,
      walletId: wallet.id,
      depositId: deposit.id,
      depositAmount: eurAmount,
      currency: "EUR"
    });

    if (welcomeBonus) {
      await tx.cryptoDeposit.update({
        where: { id: deposit.id },
        data: {
          metadata: {
            welcomeBonusId: welcomeBonus.bonus.id,
            welcomeBonusAmount: welcomeBonus.amount.toString(),
            wagerRequired: welcomeBonus.wagerRequired.toString()
          }
        }
      });
    }

    await applyAffiliateFirstDepositCommissionInTx(tx, {
      userId: input.userId,
      eurAmount,
      referenceId: deposit.id
    });

    return deposit;
  });
}

export async function createCryptoWithdrawal(input: {
  userId: string;
  asset: CryptoAsset;
  amountEur: string | number | Prisma.Decimal;
  destinationAddress: string;
}) {
  const assetConfig = requireCryptoAssetConfig(input.asset);
  const amountEur = new Prisma.Decimal(input.amountEur);

  if (amountEur.lt(MIN_WITHDRAWAL_EUR)) {
    throw new AppError("Le retrait minimum est de 20€.", 422, "MIN_WITHDRAWAL_NOT_REACHED");
  }

  const rate = await getCryptoEurRate(input.asset);
  const totalDebitEur = amountEur.plus(WITHDRAWAL_FEE_EUR);
  const cryptoAmount = amountEur.div(rate.rate);

  return prisma.$transaction(async (tx) => {
    await assertWithdrawalLimits(tx, input.userId, amountEur);
    const wallet = await ensureWallet(input.userId, "EUR", tx);
    const request = await tx.withdrawalRequest.create({
      data: {
        userId: input.userId,
        walletId: wallet.id,
        amount: amountEur,
        currency: "EUR",
        status: TransactionStatus.PENDING,
        provider: "crypto-self-custody",
        destination: {
          asset: input.asset,
          network: assetConfig.network,
          address: input.destinationAddress,
          feeEur: WITHDRAWAL_FEE_EUR.toString(),
          totalDebitEur: totalDebitEur.toString(),
          cryptoAmount: cryptoAmount.toString(),
          eurRate: rate.rate.toString(),
          rateSource: rate.source
        }
      }
    });

    const withdrawal = await tx.cryptoWithdrawal.create({
      data: {
        userId: input.userId,
        walletId: wallet.id,
        withdrawalRequestId: request.id,
        asset: input.asset,
        network: assetConfig.network,
        destinationAddress: input.destinationAddress,
        eurAmount: amountEur,
        feeEur: WITHDRAWAL_FEE_EUR,
        totalDebitEur,
        cryptoAmount,
        eurRate: rate.rate,
        rateSource: rate.source,
        status: CryptoWithdrawalStatus.REQUESTED
      }
    });

    await postLedgerEntry(tx, {
      walletId: wallet.id,
      accountType: WalletAccountType.AVAILABLE,
      direction: LedgerDirection.DEBIT,
      type: LedgerEntryType.WITHDRAWAL,
      amount: totalDebitEur,
      currency: "EUR",
      referenceType: "crypto_withdrawal_lock",
      referenceId: withdrawal.id,
      idempotencyKey: `crypto-withdrawal-lock-debit:${withdrawal.id}`,
      reason: "Retrait crypto en attente de validation admin",
      metadata: withdrawalMetadata(withdrawal)
    });

    await postLedgerEntry(tx, {
      walletId: wallet.id,
      accountType: WalletAccountType.LOCKED,
      direction: LedgerDirection.CREDIT,
      type: LedgerEntryType.WITHDRAWAL,
      amount: totalDebitEur,
      currency: "EUR",
      referenceType: "crypto_withdrawal_lock",
      referenceId: withdrawal.id,
      idempotencyKey: `crypto-withdrawal-lock-credit:${withdrawal.id}`,
      reason: "Fonds réservés pour retrait crypto",
      metadata: withdrawalMetadata(withdrawal)
    });

    return withdrawal;
  });
}

export async function cancelCryptoWithdrawal(input: { userId: string; withdrawalId: string }) {
  return prisma.$transaction(async (tx) => {
    const withdrawal = await tx.cryptoWithdrawal.findFirst({
      where: { id: input.withdrawalId, userId: input.userId },
      include: { withdrawalRequest: true }
    });

    if (!withdrawal) {
      throw new AppError("Retrait introuvable", 404, "WITHDRAWAL_NOT_FOUND");
    }

    if (withdrawal.status !== CryptoWithdrawalStatus.REQUESTED) {
      throw new ConflictError("Ce retrait ne peut plus être annulé.", "WITHDRAWAL_NOT_CANCELLABLE");
    }

    await postLedgerEntry(tx, {
      walletId: withdrawal.walletId,
      accountType: WalletAccountType.LOCKED,
      direction: LedgerDirection.DEBIT,
      type: LedgerEntryType.WITHDRAWAL,
      amount: withdrawal.totalDebitEur,
      currency: "EUR",
      referenceType: "crypto_withdrawal_cancel",
      referenceId: withdrawal.id,
      idempotencyKey: `crypto-withdrawal-cancel-locked:${withdrawal.id}`,
      reason: "Retrait crypto annulé par le joueur",
      metadata: withdrawalMetadata(withdrawal)
    });

    await postLedgerEntry(tx, {
      walletId: withdrawal.walletId,
      accountType: WalletAccountType.AVAILABLE,
      direction: LedgerDirection.CREDIT,
      type: LedgerEntryType.WITHDRAWAL,
      amount: withdrawal.totalDebitEur,
      currency: "EUR",
      referenceType: "crypto_withdrawal_cancel",
      referenceId: withdrawal.id,
      idempotencyKey: `crypto-withdrawal-cancel-available:${withdrawal.id}`,
      reason: "Fonds de retrait crypto libérés après annulation joueur",
      metadata: withdrawalMetadata(withdrawal)
    });

    await tx.withdrawalRequest.updateMany({
      where: { id: withdrawal.withdrawalRequestId ?? "" },
      data: {
        status: TransactionStatus.CANCELLED,
        reviewedAt: new Date(),
        reviewReason: "Annulé par le joueur"
      }
    });

    return tx.cryptoWithdrawal.update({
      where: { id: withdrawal.id },
      data: {
        status: CryptoWithdrawalStatus.REJECTED,
        reviewedAt: new Date(),
        reviewReason: "Annulé par le joueur"
      }
    });
  });
}

export async function reviewCryptoWithdrawal(input: {
  withdrawalId: string;
  actorUserId: string;
  action: "approve" | "reject" | "mark_broadcasted";
  reason?: string;
  txHash?: string;
}) {
  return prisma.$transaction(async (tx) => {
    const withdrawal = await tx.cryptoWithdrawal.findUniqueOrThrow({
      where: { id: input.withdrawalId },
      include: { withdrawalRequest: true }
    });

    if (input.action === "mark_broadcasted") {
      if (withdrawal.status !== CryptoWithdrawalStatus.APPROVED) {
        throw new ConflictError("Withdrawal must be approved before tx hash update", "INVALID_WITHDRAWAL_STATUS");
      }

      const updated = await tx.cryptoWithdrawal.update({
        where: { id: withdrawal.id },
        data: {
          status: CryptoWithdrawalStatus.BROADCASTED,
          txHash: input.txHash,
          reviewedById: input.actorUserId,
          reviewedAt: new Date(),
          reviewReason: input.reason
        }
      });

      await writeAuditLog(
        {
          actorUserId: input.actorUserId,
          action: "admin.crypto.withdrawal.broadcasted",
          entityType: "CryptoWithdrawal",
          entityId: withdrawal.id,
          after: { txHash: input.txHash, reason: input.reason }
        },
        tx
      );

      return updated;
    }

    if (withdrawal.status !== CryptoWithdrawalStatus.REQUESTED) {
      throw new ConflictError("Withdrawal already reviewed", "WITHDRAWAL_ALREADY_REVIEWED");
    }

    if (input.action === "reject") {
      await postLedgerEntry(tx, {
        walletId: withdrawal.walletId,
        accountType: WalletAccountType.LOCKED,
        direction: LedgerDirection.DEBIT,
        type: LedgerEntryType.WITHDRAWAL,
        amount: withdrawal.totalDebitEur,
        currency: "EUR",
        referenceType: "crypto_withdrawal_reject",
        referenceId: withdrawal.id,
        idempotencyKey: `crypto-withdrawal-reject-locked:${withdrawal.id}`,
        reason: input.reason ?? "Retrait crypto refusé",
        metadata: withdrawalMetadata(withdrawal)
      });

      await postLedgerEntry(tx, {
        walletId: withdrawal.walletId,
        accountType: WalletAccountType.AVAILABLE,
        direction: LedgerDirection.CREDIT,
        type: LedgerEntryType.WITHDRAWAL,
        amount: withdrawal.totalDebitEur,
        currency: "EUR",
        referenceType: "crypto_withdrawal_reject",
        referenceId: withdrawal.id,
        idempotencyKey: `crypto-withdrawal-reject-available:${withdrawal.id}`,
        reason: input.reason ?? "Fonds de retrait crypto libérés",
        metadata: withdrawalMetadata(withdrawal)
      });

      await tx.withdrawalRequest.updateMany({
        where: { id: withdrawal.withdrawalRequestId ?? "" },
        data: {
          status: TransactionStatus.REJECTED,
          reviewedById: input.actorUserId,
          reviewedAt: new Date(),
          reviewReason: input.reason
        }
      });

      const updated = await tx.cryptoWithdrawal.update({
        where: { id: withdrawal.id },
        data: {
          status: CryptoWithdrawalStatus.REJECTED,
          reviewedById: input.actorUserId,
          reviewedAt: new Date(),
          reviewReason: input.reason
        }
      });

      await writeAuditLog(
        {
          actorUserId: input.actorUserId,
          action: "admin.crypto.withdrawal.reject",
          entityType: "CryptoWithdrawal",
          entityId: withdrawal.id,
          after: { reason: input.reason }
        },
        tx
      );

      return updated;
    }

    await postLedgerEntry(tx, {
      walletId: withdrawal.walletId,
      accountType: WalletAccountType.LOCKED,
      direction: LedgerDirection.DEBIT,
      type: LedgerEntryType.WITHDRAWAL,
      amount: withdrawal.totalDebitEur,
      currency: "EUR",
      referenceType: "crypto_withdrawal",
      referenceId: withdrawal.id,
      idempotencyKey: `crypto-withdrawal-settle:${withdrawal.id}`,
      reason: input.reason ?? "Retrait crypto validé par admin",
      metadata: withdrawalMetadata(withdrawal)
    });

    await tx.withdrawalRequest.updateMany({
      where: { id: withdrawal.withdrawalRequestId ?? "" },
      data: {
        status: TransactionStatus.APPROVED,
        reviewedById: input.actorUserId,
        reviewedAt: new Date(),
        reviewReason: input.reason
      }
    });

    const updated = await tx.cryptoWithdrawal.update({
      where: { id: withdrawal.id },
      data: {
        status: input.txHash ? CryptoWithdrawalStatus.BROADCASTED : CryptoWithdrawalStatus.APPROVED,
        txHash: input.txHash,
        reviewedById: input.actorUserId,
        reviewedAt: new Date(),
        reviewReason: input.reason
      }
    });

    await writeAuditLog(
      {
        actorUserId: input.actorUserId,
        action: "admin.crypto.withdrawal.approve",
        entityType: "CryptoWithdrawal",
        entityId: withdrawal.id,
        after: { txHash: input.txHash, reason: input.reason }
      },
      tx
    );

    return updated;
  });
}

async function assertWithdrawalLimits(tx: Prisma.TransactionClient, userId: string, amountEur: Prisma.Decimal) {
  const vipProgress = await tx.userVipProgress.findUnique({
    where: { userId },
    select: { tierKey: true }
  });
  const limits = withdrawalLimitForTier(vipProgress?.tierKey);
  const now = new Date();
  const dayStart = new Date(now);
  dayStart.setHours(0, 0, 0, 0);
  const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
  const activeStatuses = {
    notIn: [CryptoWithdrawalStatus.REJECTED, CryptoWithdrawalStatus.FAILED]
  };
  const [dailyWithdrawals, monthlyWithdrawals] = await Promise.all([
    tx.cryptoWithdrawal.aggregate({
      where: { userId, status: activeStatuses, createdAt: { gte: dayStart } },
      _sum: { eurAmount: true }
    }),
    tx.cryptoWithdrawal.aggregate({
      where: { userId, status: activeStatuses, createdAt: { gte: monthStart } },
      _sum: { eurAmount: true }
    })
  ]);
  const nextDailyTotal = new Prisma.Decimal(dailyWithdrawals._sum.eurAmount ?? 0).plus(amountEur);
  const nextMonthlyTotal = new Prisma.Decimal(monthlyWithdrawals._sum.eurAmount ?? 0).plus(amountEur);

  if (nextDailyTotal.gt(limits.daily) || nextMonthlyTotal.gt(limits.monthly)) {
    throw new AppError(WITHDRAWAL_LIMIT_EXCEEDED_MESSAGE, 422, "WITHDRAWAL_LIMIT_EXCEEDED", {
      dailyLimit: limits.daily.toFixed(2),
      monthlyLimit: limits.monthly.toFixed(2)
    });
  }
}

function withdrawalLimitForTier(tierKey?: string | null) {
  if (tierKey && withdrawalLimitsByVipTier[tierKey]) {
    return withdrawalLimitsByVipTier[tierKey];
  }
  return withdrawalLimitsByVipTier.bronze;
}

function withdrawalMetadata(withdrawal: {
  asset: CryptoAsset;
  network: string;
  destinationAddress: string;
  eurAmount: Prisma.Decimal;
  feeEur: Prisma.Decimal;
  totalDebitEur: Prisma.Decimal;
  cryptoAmount: Prisma.Decimal;
  eurRate: Prisma.Decimal;
  rateSource: string;
}) {
  return {
    asset: withdrawal.asset,
    network: withdrawal.network,
    destinationAddress: withdrawal.destinationAddress,
    eurAmount: withdrawal.eurAmount.toString(),
    feeEur: withdrawal.feeEur.toString(),
    totalDebitEur: withdrawal.totalDebitEur.toString(),
    cryptoAmount: withdrawal.cryptoAmount.toString(),
    eurRate: withdrawal.eurRate.toString(),
    rateSource: withdrawal.rateSource
  } satisfies Prisma.InputJsonValue;
}
