import { Injectable, Logger, NotFoundException, BadRequestException } from '@nestjs/common';
import { PrismaService } from '../../common/prisma.service';
import { SeatsService } from '../seats/seats.service';
import { TicketsService } from '../tickets/tickets.service';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { Prisma, PaymentIntentStatus, PaymentMethodType, PaymentRegion } from '@prisma/client';
import { InitiatePaymentDto, RefundDto, AddPaymentMethodDto, InitiateResponseDto, IntentStatusDto, PaymentRegionEnum } from './payments.dto';
import { ClientAction, PaymentProvider, ProviderStatus } from './payments.types';
import { TelebirrProvider } from './providers/telebirr.provider';
import { CbeBirrProvider } from './providers/cbe-birr.provider';
import { EBirrProvider } from './providers/ebirr.provider';
import { CardProvider } from './providers/card.provider';
import { WaafiProvider } from './providers/waafi.provider';
import { createMerchantOrderId } from './providers/telebirr.crypto';

const NON_TERMINAL_STATUSES: PaymentIntentStatus[] = [
  PaymentIntentStatus.REQUIRES_ACTION,
  PaymentIntentStatus.PROCESSING,
  PaymentIntentStatus.SUCCEEDED,
];

@Injectable()
export class PaymentsService {
  private readonly logger = new Logger(PaymentsService.name);
  private readonly providers: Map<PaymentMethodType, PaymentProvider>;

  constructor(
    private prisma: PrismaService,
    private seatsService: SeatsService,
    private ticketsService: TicketsService,
    private eventEmitter: EventEmitter2,
    private telebirrProvider: TelebirrProvider,
    private cbeBirrProvider: CbeBirrProvider,
    private eBirrProvider: EBirrProvider,
    private cardProvider: CardProvider,
    private waafiProvider: WaafiProvider,
  ) {
    this.providers = new Map<PaymentMethodType, PaymentProvider>([
      [PaymentMethodType.TELEBIRR, this.telebirrProvider],
      [PaymentMethodType.CBE_BIRR, this.cbeBirrProvider],
      [PaymentMethodType.EBIRR, this.eBirrProvider],
      [PaymentMethodType.CARD, this.cardProvider],
      [PaymentMethodType.WAAFI, this.waafiProvider],
    ]);
  }

  async initiatePayment(dto: InitiatePaymentDto): Promise<InitiateResponseDto> {
    const booking = await this.prisma.booking.findUnique({
      where: { id: dto.bookingId },
      include: { seats: true },
    });
    if (!booking) throw new NotFoundException('Booking not found');
    if (booking.status !== 'PENDING_PAYMENT') {
      throw new BadRequestException('Booking not payable');
    }

    const existing = await this.prisma.paymentIntent.findUnique({
      where: { bookingId: dto.bookingId },
    });
    if (existing && NON_TERMINAL_STATUSES.includes(existing.status)) {
      return this.formatIntentResponse(existing);
    }

    const method = dto.method as PaymentMethodType;

    if (method === PaymentMethodType.WALLET) {
      return this.initiateWalletPayment(booking);
    }

    const provider = this.providers.get(method);
    if (provider) {
      return this.initiateProviderPayment(booking, provider, dto.platform);
    }

    throw new BadRequestException(`Unsupported payment method: ${method}`);
  }

  private async initiateWalletPayment(
    booking: Prisma.BookingGetPayload<{ include: { seats: true } }>,
  ): Promise<InitiateResponseDto> {
    const debitResult = await this.prisma.$transaction(async (tx) => {
      const wallet = await tx.walletAccount.findUnique({
        where: { passengerId: booking.passengerId },
      });
      if (!wallet || wallet.balanceMinor < booking.totalMinor) {
        return { success: false };
      }
      const newBalance = wallet.balanceMinor - booking.totalMinor;
      await tx.walletAccount.update({
        where: { passengerId: booking.passengerId },
        data: { balanceMinor: newBalance },
      });
      await tx.walletLedgerEntry.create({
        data: {
          walletId: wallet.id,
          type: 'DEBIT',
          amountMinor: booking.totalMinor,
          balanceAfterMinor: newBalance,
          description: `Train Ticket - ${booking.bookingRef}`,
          relatedBookingId: booking.id,
        },
      });
      return { success: true };
    });

    if (!debitResult.success) {
      const failed = await this.prisma.paymentIntent.upsert({
        where: { bookingId: booking.id },
        update: {
          status: PaymentIntentStatus.FAILED,
          failureCode: 'INSUFFICIENT_BALANCE',
        },
        create: {
          bookingId: booking.id,
          amountMinor: booking.totalMinor,
          method: PaymentMethodType.WALLET,
          status: PaymentIntentStatus.FAILED,
          failureCode: 'INSUFFICIENT_BALANCE',
        },
      });
      return this.formatIntentResponse(failed);
    }

    const intent = await this.prisma.paymentIntent.upsert({
      where: { bookingId: booking.id },
      update: { status: PaymentIntentStatus.PROCESSING },
      create: {
        bookingId: booking.id,
        amountMinor: booking.totalMinor,
        method: PaymentMethodType.WALLET,
        status: PaymentIntentStatus.PROCESSING,
        providerRef: `WALLET-${Date.now()}`,
      },
    });
    await this.finalizePaymentSuccess({ intentId: intent.id });
    const refreshed = await this.prisma.paymentIntent.findUniqueOrThrow({
      where: { id: intent.id },
    });
    return this.formatIntentResponse(refreshed);
  }

  private async initiateProviderPayment(
    booking: Prisma.BookingGetPayload<{ include: { seats: true } }>,
    provider: PaymentProvider,
    platform: 'web' | 'mobile' | undefined,
  ): Promise<InitiateResponseDto> {
    const merchantOrderId = createMerchantOrderId();
    const result = await provider.initiate({
      merchantOrderId,
      bookingRef: booking.bookingRef,
      amountMinor: booking.totalMinor,
      currency: booking.currency,
      platform,
    });

    const intent = await this.prisma.paymentIntent.upsert({
      where: { bookingId: booking.id },
      update: {
        status: PaymentIntentStatus.REQUIRES_ACTION,
        method: provider.method,
        merchantOrderId,
        providerOrderId: result.providerOrderId,
        clientAction: result.clientAction as unknown as Prisma.InputJsonValue,
        rawInitiation: result.rawInitiation as Prisma.InputJsonValue,
        expiresAt: result.expiresAt,
        failureCode: null,
        failureMessage: null,
      },
      create: {
        bookingId: booking.id,
        amountMinor: booking.totalMinor,
        currency: booking.currency,
        method: provider.method,
        status: PaymentIntentStatus.REQUIRES_ACTION,
        merchantOrderId,
        providerOrderId: result.providerOrderId,
        clientAction: result.clientAction as unknown as Prisma.InputJsonValue,
        rawInitiation: result.rawInitiation as Prisma.InputJsonValue,
        expiresAt: result.expiresAt,
      },
    });
    return this.formatIntentResponse(intent);
  }



  private formatIntentResponse(
    intent: Prisma.PaymentIntentGetPayload<Record<string, never>>,
  ): InitiateResponseDto {
    const clientAction =
      intent.clientAction && typeof intent.clientAction === 'object'
        ? (intent.clientAction as unknown as ClientAction)
        : undefined;
    return {
      intentId: intent.id,
      status: intent.status,
      clientAction,
      merchantOrderId: intent.merchantOrderId ?? undefined,
    };
  }

  async getIntentByBookingId(bookingId: string): Promise<IntentStatusDto> {
    const intent = await this.prisma.paymentIntent.findUnique({
      where: { bookingId },
    });
    if (!intent) throw new NotFoundException('PaymentIntent not found');

    const refreshable =
      intent.status === PaymentIntentStatus.REQUIRES_ACTION ||
      intent.status === PaymentIntentStatus.PROCESSING;
    const stale = intent.updatedAt.getTime() < Date.now() - 5_000;
    const provider = this.providers.get(intent.method);

    if (refreshable && stale && intent.merchantOrderId && provider) {
      try {
        const status = await provider.queryStatus(intent.merchantOrderId);
        await this.applyProviderStatus(intent.id, status);
        const refreshed = await this.prisma.paymentIntent.findUniqueOrThrow({
          where: { id: intent.id },
        });
        return this.formatIntentStatus(refreshed);
      } catch (err) {
        const message = err instanceof Error ? err.message : String(err);
        this.logger.warn(
          `queryStatus failed for intent ${intent.id}: ${message}; returning cached`,
        );
      }
    }

    return this.formatIntentStatus(intent);
  }

  private async applyProviderStatus(
    intentId: string,
    status: ProviderStatus,
  ): Promise<void> {
    if (status.status === PaymentIntentStatus.SUCCEEDED) {
      await this.finalizePaymentSuccess({
        intentId,
        providerTxnId: status.providerTxnId,
      });
      return;
    }
    if (status.status === PaymentIntentStatus.FAILED) {
      await this.markPaymentFailed({
        intentId,
        failureCode: status.failureCode,
        failureMessage: status.failureMessage,
      });
      return;
    }
    await this.prisma.paymentIntent.update({
      where: { id: intentId },
      data: {
        status: status.status,
        providerTxnId: status.providerTxnId ?? undefined,
      },
    });
  }

  private formatIntentStatus(
    intent: Prisma.PaymentIntentGetPayload<Record<string, never>>,
  ): IntentStatusDto {
    const base = this.formatIntentResponse(intent);
    return {
      ...base,
      paidAt: intent.paidAt?.toISOString(),
      failureCode: intent.failureCode ?? undefined,
      failureMessage: intent.failureMessage ?? undefined,
    };
  }

  async refund(dto: RefundDto) {
    const intent = await this.prisma.paymentIntent.findUnique({ where: { bookingId: dto.bookingId } });
    if (!intent || intent.status !== 'SUCCEEDED') throw new BadRequestException('No successful payment to refund');
    await this.prisma.paymentIntent.update({ where: { bookingId: dto.bookingId }, data: { status: 'CANCELLED' } });
    const booking = await this.prisma.booking.findUnique({ where: { id: dto.bookingId }, include: { seats: true } });
    if (booking) {
      await this.seatsService.releaseSeats(booking.seats.map((s) => s.seatId));
      await this.prisma.booking.update({ where: { id: dto.bookingId }, data: { status: 'CANCELLED' } });
    }
    return { refunded: true, bookingRef: booking?.bookingRef };
  }

  addPaymentMethod(dto: AddPaymentMethodDto) {
    const data = {
      type: dto.type as unknown as PaymentMethodType,
      displayName: dto.displayName,
      region: dto.region as unknown as PaymentRegion,
      currency: dto.currency ?? 'ETB',
      providerId: dto.providerId,
      enabled: dto.enabled ?? true,
      sortOrder: dto.sortOrder ?? 0,
    };
    return this.prisma.paymentMethod.upsert({
      where: { type: data.type },
      update: data,
      create: data,
    });
  }

  getSupportedPaymentMethods(region?: PaymentRegionEnum) {
    return this.prisma.paymentMethod.findMany({
      where: {
        enabled: true,
        ...(region
          ? { region: { in: [region, PaymentRegionEnum.GLOBAL] as unknown as PaymentRegion[] } }
          : {}),
      },
      orderBy: [{ sortOrder: 'asc' }, { displayName: 'asc' }],
    });
  }

  async finalizePaymentSuccess(input: {
    intentId: string;
    providerTxnId?: string;
    paidAt?: Date;
  }): Promise<{ alreadyFinalized: boolean }> {
    const intent = await this.prisma.paymentIntent.findUnique({
      where: { id: input.intentId },
    });
    if (!intent) throw new NotFoundException('PaymentIntent not found');
    if (intent.status === PaymentIntentStatus.SUCCEEDED) {
      return { alreadyFinalized: true };
    }
    if (intent.status === PaymentIntentStatus.CANCELLED) {
      throw new BadRequestException('PaymentIntent is cancelled; cannot finalize');
    }

    const booking = await this.prisma.booking.findUnique({
      where: { id: intent.bookingId },
      include: { seats: true },
    });
    if (!booking) throw new NotFoundException('Booking not found');

    const paidAt = input.paidAt ?? new Date();
    await this.prisma.$transaction(async (tx) => {
      await tx.paymentIntent.update({
        where: { id: intent.id },
        data: {
          status: PaymentIntentStatus.SUCCEEDED,
          providerTxnId: input.providerTxnId ?? intent.providerTxnId ?? undefined,
          paidAt,
        },
      });
      await tx.booking.update({
        where: { id: booking.id },
        data: { status: 'CONFIRMED' },
      });
    });

    await this.seatsService.confirmSeats(booking.seats.map((s) => s.seatId));
    await this.ticketsService.generate(booking.id);
    await this.awardLoyaltyPoints(booking.passengerId, booking.totalMinor, booking.id);
    this.eventEmitter.emit('payment.succeeded', { booking });
    return { alreadyFinalized: false };
  }

  async markPaymentFailed(input: {
    intentId: string;
    failureCode?: string;
    failureMessage?: string;
  }): Promise<void> {
    const intent = await this.prisma.paymentIntent.findUnique({
      where: { id: input.intentId },
    });
    if (!intent) throw new NotFoundException('PaymentIntent not found');
    if (
      intent.status === PaymentIntentStatus.SUCCEEDED ||
      intent.status === PaymentIntentStatus.CANCELLED
    ) {
      return;
    }
    await this.prisma.paymentIntent.update({
      where: { id: intent.id },
      data: {
        status: PaymentIntentStatus.FAILED,
        failureCode: input.failureCode,
        failureMessage: input.failureMessage,
      },
    });
  }

  private async awardLoyaltyPoints(passengerId: string, amountMinor: number, bookingId: string) {
    const points = Math.floor(amountMinor / 100);
    const account = await this.prisma.loyaltyAccount.findUnique({ where: { passengerId } });
    if (!account) return;
    const newBalance = account.pointsBalance + points;
    const tier = newBalance >= 10000 ? 'PLATINUM' : newBalance >= 5000 ? 'GOLD' : newBalance >= 2000 ? 'SILVER' : 'BRONZE';
    await this.prisma.loyaltyAccount.update({ where: { passengerId }, data: { pointsBalance: { increment: points }, tier: tier as any } });
    await this.prisma.loyaltyLedgerEntry.create({ data: { accountId: account.id, delta: points, reason: 'TRIP_COMPLETED', bookingId, balanceAfter: newBalance } });
  }
}
