/**
 * PaymentService - Payment Management Service
 *
 * Handles payment processing and allocation to invoices.
 *
 * @module Invoices
 */

import { DataSource, Repository, IsNull } from 'typeorm';
import { Payment, PaymentAllocation, Invoice } from '../entities';

interface ServiceContext {
  tenantId: string;
  userId?: string;
}

interface PaginatedResult<T> {
  data: T[];
  total: number;
  page: number;
  limit: number;
  totalPages: number;
}

export type PaymentType = 'received' | 'made';
export type PaymentStatus = 'draft' | 'confirmed' | 'reconciled' | 'cancelled';

export interface CreatePaymentDto {
  paymentType?: PaymentType;
  partnerId: string;
  partnerName?: string;
  currency?: string;
  amount: number;
  exchangeRate?: number;
  paymentDate?: Date;
  paymentMethod: string;
  reference?: string;
  bankAccountId?: string;
  notes?: string;
  invoiceAllocations?: {
    invoiceId: string;
    amount: number;
  }[];
}

export interface PaymentFilters {
  paymentType?: PaymentType;
  partnerId?: string;
  status?: PaymentStatus;
  paymentMethod?: string;
  startDate?: Date;
  endDate?: Date;
  search?: string;
}

interface PaymentSummary {
  totalPayments: number;
  totalAmount: number;
  byMethod: Record<string, { count: number; amount: number }>;
  byStatus: Partial<Record<PaymentStatus, { count: number; amount: number }>>;
}

export class PaymentService {
  private repository: Repository<Payment>;
  private allocationRepository: Repository<PaymentAllocation>;
  private invoiceRepository: Repository<Invoice>;

  constructor(dataSource: DataSource) {
    this.repository = dataSource.getRepository(Payment);
    this.allocationRepository = dataSource.getRepository(PaymentAllocation);
    this.invoiceRepository = dataSource.getRepository(Invoice);
  }

  // ==================== CRUD OPERATIONS ====================

  /**
   * Find all payments with pagination and filters
   */
  async findAll(
    ctx: ServiceContext,
    filters: PaymentFilters = {},
    page = 1,
    limit = 20
  ): Promise<PaginatedResult<Payment>> {
    const queryBuilder = this.repository
      .createQueryBuilder('payment')
      .where('payment.tenantId = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('payment.deletedAt IS NULL');

    if (filters.paymentType) {
      queryBuilder.andWhere('payment.paymentType = :paymentType', {
        paymentType: filters.paymentType,
      });
    }

    if (filters.partnerId) {
      queryBuilder.andWhere('payment.partnerId = :partnerId', {
        partnerId: filters.partnerId,
      });
    }

    if (filters.status) {
      queryBuilder.andWhere('payment.status = :status', { status: filters.status });
    }

    if (filters.paymentMethod) {
      queryBuilder.andWhere('payment.paymentMethod = :paymentMethod', {
        paymentMethod: filters.paymentMethod,
      });
    }

    if (filters.startDate) {
      queryBuilder.andWhere('payment.paymentDate >= :startDate', {
        startDate: filters.startDate,
      });
    }

    if (filters.endDate) {
      queryBuilder.andWhere('payment.paymentDate <= :endDate', {
        endDate: filters.endDate,
      });
    }

    if (filters.search) {
      queryBuilder.andWhere(
        '(payment.paymentNumber ILIKE :search OR payment.partnerName ILIKE :search OR payment.reference ILIKE :search)',
        { search: `%${filters.search}%` }
      );
    }

    queryBuilder.orderBy('payment.paymentDate', 'DESC');

    const total = await queryBuilder.getCount();
    const data = await queryBuilder
      .skip((page - 1) * limit)
      .take(limit)
      .getMany();

    return {
      data,
      total,
      page,
      limit,
      totalPages: Math.ceil(total / limit),
    };
  }

  /**
   * Find payment by ID
   */
  async findById(ctx: ServiceContext, id: string): Promise<Payment | null> {
    return this.repository.findOne({
      where: {
        id,
        tenantId: ctx.tenantId,
        deletedAt: IsNull(),
      },
    });
  }

  /**
   * Find payment with allocations
   */
  async findWithAllocations(ctx: ServiceContext, id: string): Promise<Payment | null> {
    const payment = await this.findById(ctx, id);
    if (!payment) {
      return null;
    }

    const allocations = await this.allocationRepository.find({
      where: { paymentId: id },
      relations: ['invoice'],
    });

    (payment as any).allocations = allocations;
    return payment;
  }

  /**
   * Create new payment
   */
  async create(ctx: ServiceContext, data: CreatePaymentDto): Promise<Payment> {
    const paymentNumber = await this.generatePaymentNumber(ctx, data.paymentType || 'received');

    const payment = this.repository.create({
      tenantId: ctx.tenantId,
      paymentNumber,
      paymentType: data.paymentType || 'received',
      partnerId: data.partnerId,
      partnerName: data.partnerName || '',
      currency: data.currency || 'MXN',
      amount: data.amount,
      exchangeRate: data.exchangeRate || 1,
      paymentDate: data.paymentDate || new Date(),
      paymentMethod: data.paymentMethod,
      reference: data.reference,
      bankAccountId: data.bankAccountId,
      notes: data.notes,
      status: 'draft',
      createdBy: ctx.userId,
    });

    const savedPayment = await this.repository.save(payment);

    // Process allocations if provided
    if (data.invoiceAllocations && data.invoiceAllocations.length > 0) {
      await this.allocateToInvoices(ctx, savedPayment.id, data.invoiceAllocations);
    }

    return this.findById(ctx, savedPayment.id) as Promise<Payment>;
  }

  /**
   * Update payment
   */
  async update(
    ctx: ServiceContext,
    id: string,
    data: Partial<CreatePaymentDto>
  ): Promise<Payment | null> {
    const payment = await this.findById(ctx, id);
    if (!payment) {
      return null;
    }

    if (payment.status !== 'draft') {
      throw new Error('Solo se pueden modificar pagos en borrador');
    }

    if (data.partnerId !== undefined) payment.partnerId = data.partnerId;
    if (data.partnerName !== undefined) payment.partnerName = data.partnerName || '';
    if (data.amount !== undefined) payment.amount = data.amount;
    if (data.paymentDate !== undefined) payment.paymentDate = data.paymentDate;
    if (data.paymentMethod !== undefined) payment.paymentMethod = data.paymentMethod;
    if (data.reference !== undefined) payment.reference = data.reference || '';
    if (data.bankAccountId !== undefined) payment.bankAccountId = data.bankAccountId || '';
    if (data.notes !== undefined) payment.notes = data.notes || '';

    return this.repository.save(payment);
  }

  /**
   * Soft delete payment
   */
  async softDelete(ctx: ServiceContext, id: string): Promise<boolean> {
    const payment = await this.findById(ctx, id);
    if (!payment) {
      return false;
    }

    if (payment.status !== 'draft') {
      throw new Error('Solo se pueden eliminar pagos en borrador');
    }

    payment.deletedAt = new Date();
    await this.repository.save(payment);
    return true;
  }

  // ==================== ALLOCATION ====================

  /**
   * Allocate payment to invoices
   */
  async allocateToInvoices(
    ctx: ServiceContext,
    paymentId: string,
    allocations: { invoiceId: string; amount: number }[]
  ): Promise<PaymentAllocation[]> {
    const payment = await this.findById(ctx, paymentId);
    if (!payment) {
      throw new Error('Pago no encontrado');
    }

    if (payment.status === 'cancelled') {
      throw new Error('No se puede asignar a un pago cancelado');
    }

    // Validate total allocation
    const totalAllocation = allocations.reduce((sum, a) => sum + a.amount, 0);
    const existingAllocations = await this.allocationRepository.find({
      where: { paymentId },
    });
    const existingTotal = existingAllocations.reduce(
      (sum, a) => sum + Number(a.amount),
      0
    );

    if (existingTotal + totalAllocation > Number(payment.amount)) {
      throw new Error('El total de asignaciones excede el monto del pago');
    }

    const savedAllocations: PaymentAllocation[] = [];

    for (const allocation of allocations) {
      const invoice = await this.invoiceRepository.findOne({
        where: {
          id: allocation.invoiceId,
          tenantId: ctx.tenantId,
          deletedAt: IsNull(),
        },
      });

      if (!invoice) {
        throw new Error(`Factura ${allocation.invoiceId} no encontrada`);
      }

      // Validate invoice status
      if (!['sent', 'partial', 'overdue'].includes(invoice.status)) {
        throw new Error(
          `Factura ${invoice.invoiceNumber} no puede recibir pagos en estado ${invoice.status}`
        );
      }

      // Validate amount
      const amountDue = Number(invoice.total) - Number(invoice.amountPaid);
      if (allocation.amount > amountDue) {
        throw new Error(
          `El monto de asignacion excede el saldo pendiente de la factura ${invoice.invoiceNumber}`
        );
      }

      // Create allocation
      const paymentAllocation = this.allocationRepository.create({
        paymentId,
        invoiceId: allocation.invoiceId,
        amount: allocation.amount,
        allocationDate: new Date(),
        createdBy: ctx.userId,
      });

      savedAllocations.push(await this.allocationRepository.save(paymentAllocation));

      // Update invoice
      const newAmountPaid = Number(invoice.amountPaid) + allocation.amount;
      invoice.amountPaid = newAmountPaid;
      invoice.paidAmount = newAmountPaid;

      if (newAmountPaid >= Number(invoice.total)) {
        invoice.status = 'paid';
        invoice.paidAt = new Date();
        invoice.paymentDate = new Date();
      } else {
        invoice.status = 'partial';
      }

      invoice.updatedBy = ctx.userId ?? null;
      await this.invoiceRepository.save(invoice);
    }

    return savedAllocations;
  }

  /**
   * Remove allocation
   */
  async removeAllocation(ctx: ServiceContext, allocationId: string): Promise<boolean> {
    const allocation = await this.allocationRepository.findOne({
      where: { id: allocationId },
      relations: ['payment', 'invoice'],
    });

    if (!allocation || allocation.payment.tenantId !== ctx.tenantId) {
      return false;
    }

    if (allocation.payment.status === 'reconciled') {
      throw new Error('No se pueden eliminar asignaciones de pagos conciliados');
    }

    // Revert invoice amounts
    const invoice = allocation.invoice;
    const newAmountPaid = Number(invoice.amountPaid) - Number(allocation.amount);
    invoice.amountPaid = Math.max(0, newAmountPaid);
    invoice.paidAmount = invoice.amountPaid;

    if (invoice.status === 'paid' || invoice.status === 'partial') {
      if (invoice.amountPaid <= 0) {
        invoice.status = 'sent';
        invoice.paidAt = null;
        invoice.paymentDate = null;
      } else {
        invoice.status = 'partial';
      }
    }

    invoice.updatedBy = ctx.userId ?? null;
    await this.invoiceRepository.save(invoice);

    await this.allocationRepository.remove(allocation);
    return true;
  }

  /**
   * Get allocations for a payment
   */
  async getAllocations(
    ctx: ServiceContext,
    paymentId: string
  ): Promise<PaymentAllocation[]> {
    const payment = await this.findById(ctx, paymentId);
    if (!payment) {
      return [];
    }

    return this.allocationRepository.find({
      where: { paymentId },
      relations: ['invoice'],
    });
  }

  // ==================== STATUS WORKFLOW ====================

  /**
   * Confirm payment (draft -> confirmed)
   */
  async confirm(ctx: ServiceContext, id: string): Promise<Payment | null> {
    const payment = await this.findById(ctx, id);
    if (!payment) {
      return null;
    }

    if (payment.status !== 'draft') {
      throw new Error('Solo se pueden confirmar pagos en borrador');
    }

    payment.status = 'confirmed';
    return this.repository.save(payment);
  }

  /**
   * Reconcile payment (confirmed -> reconciled)
   */
  async reconcile(ctx: ServiceContext, id: string): Promise<Payment | null> {
    const payment = await this.findById(ctx, id);
    if (!payment) {
      return null;
    }

    if (payment.status !== 'confirmed') {
      throw new Error('Solo se pueden conciliar pagos confirmados');
    }

    payment.status = 'reconciled';
    return this.repository.save(payment);
  }

  /**
   * Cancel payment
   */
  async cancel(ctx: ServiceContext, id: string, reason?: string): Promise<Payment | null> {
    const payment = await this.findById(ctx, id);
    if (!payment) {
      return null;
    }

    if (payment.status === 'reconciled') {
      throw new Error('No se pueden cancelar pagos conciliados');
    }

    // Remove all allocations and revert invoice amounts
    const allocations = await this.getAllocations(ctx, id);
    for (const allocation of allocations) {
      await this.removeAllocation(ctx, allocation.id);
    }

    payment.status = 'cancelled';
    payment.notes = `${payment.notes || ''}\n[CANCELADO]: ${reason || 'Sin motivo especificado'}`;

    return this.repository.save(payment);
  }

  // ==================== REPORTS ====================

  /**
   * Get payment summary
   */
  async getSummary(
    ctx: ServiceContext,
    filters: PaymentFilters = {}
  ): Promise<PaymentSummary> {
    const queryBuilder = this.repository
      .createQueryBuilder('payment')
      .where('payment.tenantId = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('payment.deletedAt IS NULL')
      .andWhere('payment.status != :cancelled', { cancelled: 'cancelled' });

    if (filters.paymentType) {
      queryBuilder.andWhere('payment.paymentType = :paymentType', {
        paymentType: filters.paymentType,
      });
    }

    if (filters.startDate) {
      queryBuilder.andWhere('payment.paymentDate >= :startDate', {
        startDate: filters.startDate,
      });
    }

    if (filters.endDate) {
      queryBuilder.andWhere('payment.paymentDate <= :endDate', {
        endDate: filters.endDate,
      });
    }

    const payments = await queryBuilder.getMany();

    const summary: PaymentSummary = {
      totalPayments: payments.length,
      totalAmount: 0,
      byMethod: {},
      byStatus: {},
    };

    for (const payment of payments) {
      const amount = Number(payment.amount) || 0;

      summary.totalAmount += amount;

      // By method
      if (!summary.byMethod[payment.paymentMethod]) {
        summary.byMethod[payment.paymentMethod] = { count: 0, amount: 0 };
      }
      summary.byMethod[payment.paymentMethod].count++;
      summary.byMethod[payment.paymentMethod].amount += amount;

      // By status
      if (!summary.byStatus[payment.status]) {
        summary.byStatus[payment.status] = { count: 0, amount: 0 };
      }
      summary.byStatus[payment.status]!.count++;
      summary.byStatus[payment.status]!.amount += amount;
    }

    return summary;
  }

  /**
   * Get payments by partner
   */
  async findByPartner(
    ctx: ServiceContext,
    partnerId: string,
    page = 1,
    limit = 20
  ): Promise<PaginatedResult<Payment>> {
    return this.findAll(ctx, { partnerId }, page, limit);
  }

  /**
   * Get unallocated amount for a payment
   */
  async getUnallocatedAmount(ctx: ServiceContext, paymentId: string): Promise<number> {
    const payment = await this.findById(ctx, paymentId);
    if (!payment) {
      return 0;
    }

    const allocations = await this.allocationRepository.find({
      where: { paymentId },
    });

    const allocatedAmount = allocations.reduce(
      (sum, a) => sum + Number(a.amount),
      0
    );

    return Number(payment.amount) - allocatedAmount;
  }

  /**
   * Get invoices available for allocation
   */
  async getInvoicesForAllocation(
    ctx: ServiceContext,
    partnerId: string
  ): Promise<Invoice[]> {
    return this.invoiceRepository.find({
      where: {
        tenantId: ctx.tenantId,
        partnerId,
        deletedAt: IsNull(),
      },
    }).then((invoices) =>
      invoices.filter((inv) => ['sent', 'partial', 'overdue'].includes(inv.status))
    );
  }

  // ==================== PRIVATE HELPERS ====================

  private async generatePaymentNumber(
    ctx: ServiceContext,
    paymentType: PaymentType
  ): Promise<string> {
    const year = new Date().getFullYear();
    const prefix = paymentType === 'received' ? 'REC' : 'PAG';

    const lastPayment = await this.repository
      .createQueryBuilder('payment')
      .where('payment.tenantId = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('payment.paymentNumber LIKE :pattern', {
        pattern: `${prefix}-${year}%`,
      })
      .orderBy('payment.paymentNumber', 'DESC')
      .getOne();

    let sequence = 1;
    if (lastPayment) {
      const match = lastPayment.paymentNumber.match(/(\d+)$/);
      if (match) {
        sequence = parseInt(match[1], 10) + 1;
      }
    }

    return `${prefix}-${year}-${String(sequence).padStart(6, '0')}`;
  }
}
