import { BadRequestException, HttpException, HttpStatus, Injectable } from '@nestjs/common';
import type { Prisma } from '@prisma/client';
import { createHash, randomUUID } from 'node:crypto';
import { AuthenticatedUser } from '../auth/auth.types';
import { PrismaService } from '../prisma/prisma.service';

const DEFAULT_OWNER_KEY = 'default-business-wallet';
const DEFAULT_CURRENCY = 'IDR';
const DEFAULT_MARKETING_RATE_MINOR = 500;
const MINIMUM_TOPUP_MINOR = 50000;
const MIDTRANS_PAYMENT_TYPES = ['qris', 'gopay', 'shopeepay', 'bank_transfer', 'credit_card'];

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

  estimateBroadcastCost(recipientCount: number) {
    const normalizedRecipients = Math.max(0, Math.floor(Number(recipientCount) || 0));
    const unitCostMinor = DEFAULT_MARKETING_RATE_MINOR;
    const estimatedAmountMinor = normalizedRecipients * unitCostMinor;

    return {
      currency: DEFAULT_CURRENCY,
      recipientCount: normalizedRecipients,
      unitCostMinor,
      estimatedAmountMinor,
    };
  }

  async getSummary(limit = 20) {
    const wallet = await this.getOrCreateWallet();
    const transactions = await this.prisma.walletTransaction.findMany({
      where: { walletId: wallet.id },
      orderBy: { createdAt: 'desc' },
      take: Math.min(Math.max(limit, 1), 100),
    });

    return {
      wallet: this.serializeWallet(wallet),
      transactions: transactions.map((transaction) => ({
        id: transaction.id,
        type: transaction.type,
        direction: transaction.direction,
        amountMinor: transaction.amountMinor,
        balanceBeforeMinor: transaction.balanceBeforeMinor,
        balanceAfterMinor: transaction.balanceAfterMinor,
        status: transaction.status,
        referenceType: transaction.referenceType,
        referenceId: transaction.referenceId,
        description: transaction.description,
        createdAt: transaction.createdAt.toISOString(),
      })),
      pricing: {
        broadcastMarketingUnitCostMinor: DEFAULT_MARKETING_RATE_MINOR,
        currency: DEFAULT_CURRENCY,
      },
    };
  }

  async createManualTopup(amountMinor: number, description: string | undefined, user: AuthenticatedUser, ipAddress?: string) {
    const normalizedAmount = Math.floor(Number(amountMinor) || 0);
    if (normalizedAmount < MINIMUM_TOPUP_MINOR) {
      throw new BadRequestException('Top up amount must be at least Rp 50.000.');
    }

    const actor = await this.findActor(user.sub, user.email);
    const wallet = await this.getOrCreateWallet();

    const result = await this.prisma.$transaction(async (tx) => {
      const current = await tx.wallet.findUnique({ where: { id: wallet.id } });
      if (!current) throw new BadRequestException('Wallet not found');

      const nextBalance = current.balanceMinor + normalizedAmount;
      const updated = await tx.wallet.update({
        where: { id: current.id },
        data: { balanceMinor: nextBalance },
      });

      const transaction = await tx.walletTransaction.create({
        data: {
          id: randomUUID(),
          walletId: current.id,
          type: 'manual_topup',
          direction: 'credit',
          amountMinor: normalizedAmount,
          balanceBeforeMinor: current.balanceMinor,
          balanceAfterMinor: nextBalance,
          status: 'posted',
          referenceType: 'manual_adjustment',
          referenceId: randomUUID(),
          description: description || 'Manual wallet top up',
          metadataJson: {
            actorUserId: actor.id,
            actorEmail: actor.email,
            ipAddress: ipAddress || null,
          } as Prisma.InputJsonValue,
        },
      });

      await tx.auditLog.create({
        data: {
          actorUserId: actor.id,
          actorName: actor.name,
          actorEmail: actor.email,
          actionType: 'Wallet Manual Top Up',
          module: 'Wallet',
          ipAddress: ipAddress || null,
          severity: 'default',
          details: `Added Rp ${normalizedAmount.toLocaleString('id-ID')} to wallet.`,
          metadataJson: {
            walletId: current.id,
            transactionId: transaction.id,
            amountMinor: normalizedAmount,
          } as Prisma.InputJsonValue,
        },
      });

      return { wallet: updated, transaction };
    });

    return {
      wallet: this.serializeWallet(result.wallet),
      transaction: {
        id: result.transaction.id,
        amountMinor: result.transaction.amountMinor,
        direction: result.transaction.direction,
        type: result.transaction.type,
        createdAt: result.transaction.createdAt.toISOString(),
      },
    };
  }

  async createMidtransTopup(amountMinor: number, description: string | undefined, user: AuthenticatedUser) {
    const normalizedAmount = Math.floor(Number(amountMinor) || 0);
    if (normalizedAmount < MINIMUM_TOPUP_MINOR) {
      throw new BadRequestException('Top up amount must be at least Rp 50.000.');
    }

    const serverKey = process.env.MIDTRANS_SERVER_KEY;
    if (!serverKey) {
      throw new BadRequestException('Midtrans server key is not configured.');
    }

    const actor = await this.findActor(user.sub, user.email);
    const wallet = await this.getOrCreateWallet();
    const providerOrderId = `BIZONE-TOPUP-${Date.now()}-${randomUUID().slice(0, 8)}`;
    const frontendOrigin = process.env.FRONTEND_ORIGIN || 'http://localhost:3000';
    const apiBaseUrl = this.getMidtransSnapBaseUrl();
    const enabledPayments = this.getMidtransEnabledPayments();

    const order = await this.prisma.paymentOrder.create({
      data: {
        id: randomUUID(),
        walletId: wallet.id,
        provider: 'midtrans',
        providerOrderId,
        amountMinor: normalizedAmount,
        currency: DEFAULT_CURRENCY,
        status: 'pending',
        expiredAt: new Date(Date.now() + 24 * 60 * 60 * 1000),
        metadataJson: {
          description: description || 'Wallet top up',
          actorUserId: actor.id,
          actorEmail: actor.email,
          enabledPayments,
          gatewayFeeChargedToCustomer: true,
        } as Prisma.InputJsonValue,
      },
    });

    const response = await fetch(`${apiBaseUrl}/snap/v1/transactions`, {
      method: 'POST',
      headers: {
        Authorization: `Basic ${Buffer.from(`${serverKey}:`).toString('base64')}`,
        'Content-Type': 'application/json',
        Accept: 'application/json',
      },
      body: JSON.stringify({
        transaction_details: {
          order_id: providerOrderId,
          gross_amount: normalizedAmount,
        },
        enabled_payments: enabledPayments,
        credit_card: {
          secure: true,
        },
        customer_details: {
          first_name: actor.name || 'BizOne User',
          email: actor.email || undefined,
        },
        item_details: [
          {
            id: 'wallet-topup',
            price: normalizedAmount,
            quantity: 1,
            name: description || 'BizOne Wallet Top Up',
          },
        ],
        callbacks: {
          finish: `${frontendOrigin}/dashboard/wallet`,
        },
        expiry: {
          unit: 'hour',
          duration: 24,
        },
      }),
    });

    const payload = await response.json().catch(() => ({})) as { token?: string; redirect_url?: string; error_messages?: string[] };
    if (!response.ok || !payload.token) {
      await this.prisma.paymentOrder.update({
        where: { id: order.id },
        data: {
          status: 'failed',
          metadataJson: {
            ...(order.metadataJson && typeof order.metadataJson === 'object' && !Array.isArray(order.metadataJson) ? order.metadataJson : {}),
            midtransError: payload.error_messages || [`Midtrans request failed with status ${response.status}`],
          } as Prisma.InputJsonValue,
        },
      });
      throw new BadRequestException(payload.error_messages?.join(', ') || 'Failed to create Midtrans payment.');
    }

    const updated = await this.prisma.paymentOrder.update({
      where: { id: order.id },
      data: {
        snapToken: payload.token,
        redirectUrl: payload.redirect_url || null,
      },
    });

    return {
      id: updated.id,
      providerOrderId: updated.providerOrderId,
      amountMinor: updated.amountMinor,
      currency: updated.currency,
      status: updated.status,
      snapToken: updated.snapToken,
      redirectUrl: updated.redirectUrl,
      enabledPayments,
    };
  }

  async handleMidtransNotification(payload: Record<string, unknown>) {
    const serverKey = process.env.MIDTRANS_SERVER_KEY;
    if (!serverKey) {
      throw new BadRequestException('Midtrans server key is not configured.');
    }

    const orderId = String(payload.order_id || '');
    const statusCode = String(payload.status_code || '');
    const grossAmount = String(payload.gross_amount || '');
    const signatureKey = String(payload.signature_key || '');
    const transactionStatus = String(payload.transaction_status || '');
    const fraudStatus = String(payload.fraud_status || '');
    const expectedSignature = createHash('sha512')
      .update(`${orderId}${statusCode}${grossAmount}${serverKey}`)
      .digest('hex');

    if (!orderId || signatureKey !== expectedSignature) {
      throw new BadRequestException('Invalid Midtrans notification signature.');
    }

    const merchantId = String(payload.merchant_id || '');
    if (orderId.startsWith('payment_notif_test_') && merchantId === process.env.MIDTRANS_MERCHANT_ID) {
      return {
        received: true,
        status: 'test_notification',
        providerOrderId: orderId,
      };
    }

    const paymentOrder = await this.prisma.paymentOrder.findUnique({
      where: { providerOrderId: orderId },
    });
    if (!paymentOrder) {
      throw new BadRequestException('Payment order not found.');
    }

    const isSuccess =
      transactionStatus === 'settlement' ||
      (transactionStatus === 'capture' && (!fraudStatus || fraudStatus === 'accept'));
    const isFailed = ['cancel', 'deny', 'expire', 'failure'].includes(transactionStatus);
    const nextStatus = isSuccess ? 'paid' : isFailed ? 'failed' : transactionStatus || 'pending';

    if (!isSuccess) {
      await this.prisma.paymentOrder.update({
        where: { id: paymentOrder.id },
        data: {
          status: nextStatus,
          metadataJson: {
            ...(paymentOrder.metadataJson && typeof paymentOrder.metadataJson === 'object' && !Array.isArray(paymentOrder.metadataJson) ? paymentOrder.metadataJson : {}),
            lastNotification: payload,
          } as Prisma.InputJsonValue,
        },
      });
      return { received: true, status: nextStatus };
    }

    const result = await this.prisma.$transaction(async (tx) => {
      const currentOrder = await tx.paymentOrder.findUnique({
        where: { id: paymentOrder.id },
      });
      if (!currentOrder) throw new BadRequestException('Payment order not found.');
      if (currentOrder.status === 'paid') {
        return { credited: false, alreadyPaid: true };
      }

      const wallet = await tx.wallet.findUnique({ where: { id: currentOrder.walletId } });
      if (!wallet) throw new BadRequestException('Wallet not found.');

      const nextBalance = wallet.balanceMinor + currentOrder.amountMinor;
      const updatedWallet = await tx.wallet.update({
        where: { id: wallet.id },
        data: { balanceMinor: nextBalance },
      });

      const transaction = await tx.walletTransaction.create({
        data: {
          id: randomUUID(),
          walletId: wallet.id,
          type: 'midtrans_topup',
          direction: 'credit',
          amountMinor: currentOrder.amountMinor,
          balanceBeforeMinor: wallet.balanceMinor,
          balanceAfterMinor: nextBalance,
          status: 'posted',
          referenceType: 'payment_order',
          referenceId: currentOrder.id,
          description: `Midtrans top up ${currentOrder.providerOrderId}`,
          metadataJson: {
            providerOrderId: currentOrder.providerOrderId,
            notification: payload,
          } as Prisma.InputJsonValue,
        },
      });

      await tx.paymentOrder.update({
        where: { id: currentOrder.id },
        data: {
          status: 'paid',
          paidAt: new Date(),
          metadataJson: {
            ...(currentOrder.metadataJson && typeof currentOrder.metadataJson === 'object' && !Array.isArray(currentOrder.metadataJson) ? currentOrder.metadataJson : {}),
            walletTransactionId: transaction.id,
            lastNotification: payload,
          } as Prisma.InputJsonValue,
        },
      });

      return { credited: true, wallet: this.serializeWallet(updatedWallet), transactionId: transaction.id };
    });

    return { received: true, status: 'paid', ...result };
  }

  async assertSufficientBalanceForBroadcast(campaign: { id: string; name: string; totalRecipients: number }) {
    const estimate = this.estimateBroadcastCost(campaign.totalRecipients);
    if (estimate.estimatedAmountMinor <= 0) {
      return { sufficient: true, availableAmountMinor: 0, ...estimate };
    }

    const wallet = await this.getOrCreateWallet();
    const available = wallet.balanceMinor - wallet.heldMinor;

    if (available < estimate.estimatedAmountMinor) {
      throw new HttpException(
        {
          message: 'Insufficient wallet balance for this broadcast.',
          requiredAmountMinor: estimate.estimatedAmountMinor,
          availableAmountMinor: available,
          currency: wallet.currency,
        },
        HttpStatus.PAYMENT_REQUIRED,
      );
    }

    return {
      sufficient: true,
      availableAmountMinor: available,
      ...estimate,
    };
  }

  async chargeSuccessfulBroadcastInTransaction(
    tx: Prisma.TransactionClient,
    campaign: { id: string; name: string },
    successfulCount: number,
  ) {
    const estimate = this.estimateBroadcastCost(successfulCount);
    if (estimate.estimatedAmountMinor <= 0) {
      return { charged: false, reason: 'no_successful_messages', ...estimate };
    }

    const existingCharge = await tx.walletTransaction.findFirst({
      where: {
        type: 'broadcast_charge',
        referenceType: 'campaign',
        referenceId: campaign.id,
        status: 'posted',
      },
    });

    if (existingCharge) {
      return { charged: false, alreadyCharged: true, transactionId: existingCharge.id, ...estimate };
    }

    const current = await tx.wallet.upsert({
      where: { ownerKey: DEFAULT_OWNER_KEY },
      update: {},
      create: {
        id: randomUUID(),
        ownerKey: DEFAULT_OWNER_KEY,
        currency: DEFAULT_CURRENCY,
        balanceMinor: 0,
        heldMinor: 0,
        status: 'active',
      },
    });

    const available = current.balanceMinor - current.heldMinor;
    if (available < estimate.estimatedAmountMinor) {
      throw new HttpException(
        {
          message: 'Insufficient wallet balance for successful broadcast delivery.',
          requiredAmountMinor: estimate.estimatedAmountMinor,
          availableAmountMinor: available,
          currency: current.currency,
        },
        HttpStatus.PAYMENT_REQUIRED,
      );
    }

    const nextBalance = current.balanceMinor - estimate.estimatedAmountMinor;
    const updated = await tx.wallet.update({
      where: { id: current.id },
      data: { balanceMinor: nextBalance },
    });

    const transaction = await tx.walletTransaction.create({
      data: {
        id: randomUUID(),
        walletId: current.id,
        type: 'broadcast_charge',
        direction: 'debit',
        amountMinor: estimate.estimatedAmountMinor,
        balanceBeforeMinor: current.balanceMinor,
        balanceAfterMinor: nextBalance,
        status: 'posted',
        referenceType: 'campaign',
        referenceId: campaign.id,
        description: `Broadcast success charge for ${campaign.name}`,
        metadataJson: {
          campaignId: campaign.id,
          campaignName: campaign.name,
          successfulMessageCount: estimate.recipientCount,
          unitCostMinor: estimate.unitCostMinor,
        } as Prisma.InputJsonValue,
      },
    });

    return {
      charged: true,
      transactionId: transaction.id,
      wallet: this.serializeWallet(updated),
      ...estimate,
    };
  }

  private async getOrCreateWallet() {
    return this.prisma.wallet.upsert({
      where: { ownerKey: DEFAULT_OWNER_KEY },
      update: {},
      create: {
        id: randomUUID(),
        ownerKey: DEFAULT_OWNER_KEY,
        currency: DEFAULT_CURRENCY,
        balanceMinor: 0,
        heldMinor: 0,
        status: 'active',
      },
    });
  }

  private getMidtransSnapBaseUrl() {
    return process.env.MIDTRANS_ENV === 'production'
      ? 'https://app.midtrans.com'
      : 'https://app.sandbox.midtrans.com';
  }

  private getMidtransEnabledPayments() {
    const configured = (process.env.MIDTRANS_ALLOWED_PAYMENT_TYPES || '')
      .split(',')
      .map((item) => item.trim())
      .filter(Boolean);
    const selected = configured.length > 0 ? configured : MIDTRANS_PAYMENT_TYPES;
    return selected.filter((item) => MIDTRANS_PAYMENT_TYPES.includes(item));
  }

  private serializeWallet(wallet: { id: string; ownerKey: string; currency: string; balanceMinor: number; heldMinor: number; status: string; updatedAt: Date }) {
    return {
      id: wallet.id,
      ownerKey: wallet.ownerKey,
      currency: wallet.currency,
      balanceMinor: wallet.balanceMinor,
      heldMinor: wallet.heldMinor,
      availableBalanceMinor: wallet.balanceMinor - wallet.heldMinor,
      status: wallet.status,
      updatedAt: wallet.updatedAt.toISOString(),
    };
  }

  private async findActor(userId: string, email?: string) {
    const actor = await this.prisma.user.findUnique({
      where: { id: userId },
      select: { id: true, name: true, email: true },
    });

    return actor || { id: userId, name: email || 'System User', email: email || null };
  }
}
