import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { PaymentGatewayEnum, PaymentMethodEnum, PaymentStatusEnum } from '../interface/payment';
import { Payment } from '../entities/payment.entity';
import { EntityManager } from '@mikro-orm/postgresql';
import { Order } from '../../orders/entities/order.entity';
import { Logger } from '@nestjs/common';
import { GatewayManager } from '../gateways/gateway.manager';
import { WalletTransaction } from 'src/modules/users/entities/wallet-transaction.entity';
import { OrderPaymentContext } from '../interface/payment';
import { OrderStatus } from 'src/modules/orders/interface/order.interface';
import { ChartPeriodEnum, PaymentChartDto } from '../dto/payment-chart.dto';
import { PaymentMessage, OrderMessage } from 'src/common/enums/message.enum';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { onlinePaymentSucceedEvent, paymentSucceedEvent } from '../events/payment.events';
import { WalletTransactionReason, WalletTransactionType } from 'src/modules/users/interface/wallet';

@Injectable()
export class PaymentsService {
  private readonly logger = new Logger(PaymentsService.name);

  constructor(
    private readonly em: EntityManager,
    private readonly gatewayManager: GatewayManager,
    private readonly eventEmitter: EventEmitter2,
  ) { }

  async payOrder(orderId: string): Promise<{ paymentUrl: string | null }> {
    const ctx = await this.loadAndValidateOrder(orderId);

    // Idempotency: avoid creating/charging again for already-paid orders
    if (ctx.order.status === OrderStatus.PAID) {
      return { paymentUrl: null };
    }

    switch (ctx.method) {
      case PaymentMethodEnum.Cash:
        await this.handleCashPayment(ctx);
        return { paymentUrl: null };

      case PaymentMethodEnum.Wallet:
        await this.handleWalletPayment(ctx);
        return { paymentUrl: null };

      case PaymentMethodEnum.Online:
        return this.handleOnlinePayment(ctx);

      default:
        throw new BadRequestException(PaymentMessage.UNSUPPORTED_PAYMENT_METHOD);
    }
  }

  private async loadAndValidateOrder(orderId: string): Promise<OrderPaymentContext> {
    const order = await this.em.findOne(
      Order,
      { id: orderId },
      { populate: ['user', 'restaurant', 'paymentMethod', 'paymentMethod.restaurant'] },
    );

    if (!order) {
      throw new NotFoundException(OrderMessage.NOT_FOUND);
    }

    if (order.total <= 0) {
      throw new BadRequestException(PaymentMessage.AMOUNT_MUST_BE_GREATER_THAN_ZERO);
    }

    const pm = order.paymentMethod;
    if (!pm) {
      throw new NotFoundException(PaymentMessage.PAYMENT_METHOD_NOT_FOUND);
    }

    if (pm.method === PaymentMethodEnum.Online) {
      if (!pm.gateway) throw new BadRequestException(PaymentMessage.GATEWAY_REQUIRED);
      if (!pm.merchantId) throw new BadRequestException(PaymentMessage.MERCHANT_ID_REQUIRED);
      if (!pm.restaurant?.domain) throw new BadRequestException(PaymentMessage.RESTAURANT_DOMAIN_REQUIRED);
    }

    return {
      order,
      amount: order.total,
      method: pm.method,
      gateway: pm.gateway ?? null,
      merchantId: pm.merchantId ?? null,
      restaurantDomain: pm.restaurant.domain ?? null,
    };
  }

  private async handleCashPayment(ctx: OrderPaymentContext): Promise<void> {
    await this.getOrCreateLatestPendingPayment(ctx.order.id, {
      amount: ctx.amount,
      method: PaymentMethodEnum.Cash,
      gateway: null,
    });
  }

  // TODO clean this code and refactor it
  private async handleWalletPayment(ctx: OrderPaymentContext): Promise<void> {
    await this.em.transactional(async em => {
      const order = await em.findOne(Order, { id: ctx.order.id }, { populate: ['user', 'restaurant'] });
      if (!order) throw new NotFoundException(OrderMessage.NOT_FOUND);

      if (order.status === OrderStatus.PAID) {
        return;
      }

      const walletTransaction = await em.findOne(WalletTransaction, {
        user: { id: order.user.id },
        restaurant: { id: order.restaurant.id },
      }, {
        orderBy: { createdAt: 'DESC' }
      });

      if (!walletTransaction) {
        throw new NotFoundException('User wallet not found');
      }

      if (walletTransaction.balance < ctx.amount) {
        throw new BadRequestException('Insufficient wallet balance');
      }

      const newBalance = walletTransaction.balance - ctx.amount;


      const payment = await this.getOrCreateLatestPendingPayment(order.id, {
        em,
        amount: ctx.amount,
        method: PaymentMethodEnum.Wallet,
        gateway: null,
      });

      const newWalletTransaction = em.create(WalletTransaction, {
        user: order.user,
        restaurant: order.restaurant,
        amount: ctx.amount,
        type: WalletTransactionType.DEBIT,
        reason: WalletTransactionReason.ORDER_PAYMENT,
        balance: newBalance,
      });

      payment.status = PaymentStatusEnum.Paid;
      payment.paidAt = new Date();
      order.status = OrderStatus.PAID;

      em.persist([ payment, order, newWalletTransaction]);
      await em.flush();
    });
    this.eventEmitter.emit(
      paymentSucceedEvent.name,
      new paymentSucceedEvent(ctx.order.id, ctx.order.restaurant.id, String(ctx.order?.orderNumber) || '', ctx.method, ctx.amount),
    );
  }

  private async handleOnlinePayment(ctx: OrderPaymentContext): Promise<{ paymentUrl: string }> {
    const gateway = this.gatewayManager.get(ctx.gateway!);

    const payment = await this.getOrCreateLatestPendingPayment(ctx.order.id, {
      amount: ctx.amount,
      method: PaymentMethodEnum.Online,
      gateway: ctx.gateway!,
    });

    // If we already requested a gateway transaction, just return the same URL (idempotent)
    if (payment.transactionId) {
      return { paymentUrl: gateway.getPaymentUrl(payment.transactionId) };
    }

    const { transactionId } = await gateway.requestPayment({
      amount: ctx.amount,
      orderId: ctx.order.id,
      merchantId: ctx.merchantId!,
      domain: ctx.restaurantDomain!,
    });

    payment.gateway = ctx.gateway;
    payment.transactionId = transactionId;
    payment.status = PaymentStatusEnum.Pending;

    await this.em.persistAndFlush(payment);

    return {
      paymentUrl: gateway.getPaymentUrl(transactionId),
    };
  }

  async verifyOnlinePayment(transactionId: string, orderId: string): Promise<Payment> {
    const payment = await this.em.transactional(async em => {
      const payment = await em.findOne(
        Payment,
        { transactionId, order: { id: orderId } },
        { populate: ['order', 'order.paymentMethod'] },
      );

      if (!payment) {
        throw new NotFoundException(PaymentMessage.PAYMENT_NOT_FOUND);
      }

      if (payment.status === PaymentStatusEnum.Paid) {
        return payment;
      }

      const pm = payment.order.paymentMethod;
      if (!pm?.merchantId || !payment.gateway) {
        throw new BadRequestException(PaymentMessage.INVALID_PAYMENT_CONFIGURATION);
      }

      const gateway = this.gatewayManager.get(payment.gateway);

      const result = await gateway.verifyPayment({
        merchantId: pm.merchantId,
        amount: payment.amount,
        transactionId: payment.transactionId!,
      });

      payment.verifyResponse = result.raw;

      if (!result.success) {
        this.failPayment(payment);
        return payment;
      }

      this.markPaid(payment, result.referenceId);
      if (payment.order.status === OrderStatus.PENDING_PAYMENT) {
        payment.order.status = OrderStatus.PAID;
      }

      await em.flush();
      return payment;
    });
    this.eventEmitter.emit(
      onlinePaymentSucceedEvent.name,
      new onlinePaymentSucceedEvent(payment.id, payment.order.id, payment.order.restaurant.id, String(payment.order?.orderNumber) || '', payment.amount),
    );
    return payment;
  }

  findAllPaymentsByRestaurantId(restId: string, userId: string) {
    return this.em.find(
      Payment,
      { order: { restaurant: { id: restId }, user: { id: userId } } },
      { populate: ['order', 'order.paymentMethod'] },
    );
  }

  async verifyCashPayment(id: string): Promise<Payment> {
    const payment = await this.em.transactional(async em => {
      const payment = await em.findOne(Payment, id, { populate: ['order'] });
      if (!payment) {
        throw new NotFoundException(PaymentMessage.PAYMENT_NOT_FOUND);
      }
      if (payment.method !== PaymentMethodEnum.Cash) {
        throw new BadRequestException(PaymentMessage.PAYMENT_METHOD_NOT_CASH);
      }
      if (payment.status === PaymentStatusEnum.Paid) {
        throw new BadRequestException(PaymentMessage.PAYMENT_ALREADY_PAID);
      }
      if (payment.order.status === OrderStatus.PENDING_PAYMENT) {
        payment.order.status = OrderStatus.PAID;
      }
      payment.status = PaymentStatusEnum.Paid;
      payment.paidAt = new Date();
      em.persist(payment);
      em.persist(payment.order);
      await em.flush();
      return payment;
    });
    return payment;
  }

  private markPaid(payment: Payment, referenceId: string) {
    payment.status = PaymentStatusEnum.Paid;
    payment.paidAt = new Date();
    payment.referenceId = referenceId;
  }

  private failPayment(payment: Payment) {
    payment.status = PaymentStatusEnum.Failed;
    payment.failedAt = new Date();
    payment.order.status = OrderStatus.CANCELED;
  }


  private async getOrCreateLatestPendingPayment(
    orderId: string,
    params: {
      amount: number;
      method: PaymentMethodEnum;
      gateway: PaymentGatewayEnum | null;
      em?: EntityManager;
    },
  ): Promise<Payment> {
    const em = params.em ?? this.em;

    const existing = await em.findOne(
      Payment,
      { order: { id: orderId }, status: PaymentStatusEnum.Pending },
      { orderBy: { createdAt: 'DESC' } },
    );

    if (existing) {
      if (existing.method !== params.method) {
        throw new BadRequestException(PaymentMessage.EXISTING_PENDING_PAYMENT_MISMATCH);
      }

      return existing;
    }

    const orderRef = em.getReference(Order, orderId);
    const payment = em.create(Payment, {
      amount: params.amount,
      order: orderRef,
      method: params.method,
      gateway: params.gateway,
      transactionId: null,
      status: PaymentStatusEnum.Pending,
    });

    em.persist(payment);
    await em.flush();
    return payment;
  }

  async getChartData(dto: PaymentChartDto, restaurantId: string): Promise<Array<{ date: string; cash: number; online: number }>> {
    const { startDate, endDate, type = ChartPeriodEnum.Daily } = dto;

    const start = startDate ? new Date(startDate) : new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
    const end = endDate ? new Date(endDate) : new Date();

    const startOfDay = new Date(start);
    startOfDay.setHours(0, 0, 0, 0);

    const endOfDay = new Date(end);
    endOfDay.setHours(23, 59, 59, 999);

    // 1. Map your Enum to valid Postgres date_trunc units
    // 'Daily' is not valid in PG, it must be 'day'
    const pgPeriod = {
      [ChartPeriodEnum.Daily]: 'day',
      [ChartPeriodEnum.Weekly]: 'week',
      [ChartPeriodEnum.Monthly]: 'month',
    }[type] || 'day';

    const params: any[] = [startOfDay, endOfDay];
    let restaurantFilter = '';

    if (restaurantId) {
      params.push(restaurantId);
      restaurantFilter = `AND o.restaurant_id = ?`;
    }

    // MikroORM uses ? placeholders for parameter binding
    // Ensure paid_at is not NULL and handle timestamp casting properly
    const query = `
      SELECT
        TO_CHAR(DATE_TRUNC('${pgPeriod}', CAST(p.paid_at AS timestamp)), 'YYYY-MM-DD') as "date",
        COALESCE(SUM(CASE WHEN p.method = '${PaymentMethodEnum.Cash}' THEN p.amount ELSE 0 END), 0)::numeric as cash,
        COALESCE(SUM(CASE WHEN p.method = '${PaymentMethodEnum.Online}' THEN p.amount ELSE 0 END), 0)::numeric as online
      FROM payments p
      INNER JOIN orders o ON p.order_id = o.id
      WHERE p.status = '${PaymentStatusEnum.Paid}'
        AND p.paid_at IS NOT NULL
        AND p.paid_at >= ?
        AND p.paid_at <= ?
        ${restaurantFilter}
      GROUP BY DATE_TRUNC('${pgPeriod}', CAST(p.paid_at AS timestamp))
      ORDER BY DATE_TRUNC('${pgPeriod}', CAST(p.paid_at AS timestamp)) ASC
    `;

    this.logger.debug(`Chart query params: startOfDay=${startOfDay.toISOString()}, endOfDay=${endOfDay.toISOString()}, restaurantId=${restaurantId}`);
    const result = await this.em.execute(query, params);
    this.logger.debug(`Chart query returned ${result.length} rows`);

    const dataMap = new Map<string, { cash: number; online: number }>();
    result.forEach((row: any) => {
      dataMap.set(row.date, {
        cash: Number(row.cash),
        online: Number(row.online),
      });
    });

    const allDates = this.generateDateRange(startOfDay, endOfDay, type);

    return allDates.map(date => ({
      date,
      cash: dataMap.get(date)?.cash ?? 0,
      online: dataMap.get(date)?.online ?? 0,
    }));
  }

  private generateDateRange(start: Date, end: Date, type: ChartPeriodEnum): string[] {
    const dates: string[] = [];
    const current = new Date(start);
    const endDate = new Date(end);

    // Normalize start date based on period type
    switch (type) {
      case ChartPeriodEnum.Weekly:
        // Start of week (Monday) - PostgreSQL DATE_TRUNC('week') uses ISO week (Monday as first day)
        const dayOfWeek = current.getDay();
        const diff = current.getDate() - dayOfWeek + (dayOfWeek === 0 ? -6 : 1);
        current.setDate(diff);
        current.setHours(0, 0, 0, 0);
        // Also normalize end date to include the week containing it
        const endDayOfWeek = endDate.getDay();
        const endDiff = endDate.getDate() - endDayOfWeek + (endDayOfWeek === 0 ? -6 : 1);
        endDate.setDate(endDiff);
        endDate.setHours(23, 59, 59, 999);
        break;
      case ChartPeriodEnum.Monthly:
        // Start of month
        current.setDate(1);
        current.setHours(0, 0, 0, 0);
        // Also normalize end date to include the month containing it
        endDate.setDate(1);
        endDate.setHours(23, 59, 59, 999);
        break;
      case ChartPeriodEnum.Daily:
      default:
        current.setHours(0, 0, 0, 0);
        endDate.setHours(23, 59, 59, 999);
    }

    while (current <= endDate) {
      const dateStr = current.toISOString().split('T')[0];
      dates.push(dateStr);

      // Increment based on period type
      switch (type) {
        case ChartPeriodEnum.Weekly:
          current.setDate(current.getDate() + 7);
          break;
        case ChartPeriodEnum.Monthly:
          current.setMonth(current.getMonth() + 1);
          break;
        case ChartPeriodEnum.Daily:
        default:
          current.setDate(current.getDate() + 1);
      }
    }

    return dates;
  }
}
