import { Repository } from 'typeorm';
import { AppDataSource } from '../../config/typeorm.js';
import { Payment, PaymentInvoice, Invoice, PaymentType, PaymentMethod, PaymentStatus, InvoiceStatus } from './entities/index.js';
import { NotFoundError, ValidationError } from '../../shared/errors/index.js';
import { logger } from '../../shared/utils/logger.js';

// ===== Interfaces =====

export interface PaymentInvoiceDto {
  invoiceId: string;
  invoiceNumber?: string;
  amount: number;
}

export interface PaymentWithRelations extends Payment {
  companyName?: string;
  partnerName?: string;
  currencyCode?: string;
  journalName?: string;
  invoices?: PaymentInvoiceDto[];
}

export interface CreatePaymentDto {
  companyId: string;
  partnerId: string;
  paymentType: 'inbound' | 'outbound';
  paymentMethod: 'cash' | 'bank_transfer' | 'check' | 'card' | 'other';
  amount: number;
  currencyId: string;
  paymentDate?: string;
  ref?: string;
  journalId: string;
  notes?: string;
}

export interface UpdatePaymentDto {
  partnerId?: string;
  paymentMethod?: 'cash' | 'bank_transfer' | 'check' | 'card' | 'other';
  amount?: number;
  currencyId?: string;
  paymentDate?: string;
  ref?: string | null;
  journalId?: string;
  notes?: string | null;
}

export interface ReconcileDto {
  invoices: { invoiceId: string; amount: number }[];
}

export interface PaymentFilters {
  companyId?: string;
  partnerId?: string;
  paymentType?: string;
  paymentMethod?: string;
  status?: string;
  dateFrom?: string;
  dateTo?: string;
  search?: string;
  page?: number;
  limit?: number;
}

// ===== PaymentsService Class =====

class PaymentsService {
  private paymentRepository: Repository<Payment>;
  private paymentInvoiceRepository: Repository<PaymentInvoice>;
  private invoiceRepository: Repository<Invoice>;

  constructor() {
    this.paymentRepository = AppDataSource.getRepository(Payment);
    this.paymentInvoiceRepository = AppDataSource.getRepository(PaymentInvoice);
    this.invoiceRepository = AppDataSource.getRepository(Invoice);
  }

  /**
   * Get all payments with filters and pagination
   */
  async findAll(
    tenantId: string,
    filters: PaymentFilters = {}
  ): Promise<{ data: PaymentWithRelations[]; total: number }> {
    try {
      const {
        companyId,
        partnerId,
        paymentType,
        paymentMethod,
        status,
        dateFrom,
        dateTo,
        search,
        page = 1,
        limit = 20
      } = filters;
      const skip = (page - 1) * limit;

      const queryBuilder = this.paymentRepository
        .createQueryBuilder('payment')
        .leftJoin('payment.company', 'company')
        .addSelect(['company.name'])
        .leftJoin('payment.journal', 'journal')
        .addSelect(['journal.name'])
        .where('payment.tenantId = :tenantId', { tenantId });

      // Apply filters
      if (companyId) {
        queryBuilder.andWhere('payment.companyId = :companyId', { companyId });
      }

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

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

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

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

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

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

      if (search) {
        queryBuilder.andWhere(
          '(payment.ref ILIKE :search OR partner.name ILIKE :search)',
          { search: `%${search}%` }
        );
      }

      // Get total count
      const total = await queryBuilder.getCount();

      // Get paginated results
      const payments = await queryBuilder
        .orderBy('payment.paymentDate', 'DESC')
        .addOrderBy('payment.createdAt', 'DESC')
        .skip(skip)
        .take(limit)
        .getMany();

      // Get partner and currency names using raw query (cross-schema joins)
      const paymentIds = payments.map(p => p.id);
      let additionalData: Map<string, { partnerName?: string; currencyCode?: string }> = new Map();

      if (paymentIds.length > 0) {
        const rawData = await this.paymentRepository.query(
          `SELECT p.id,
                  pr.name as partner_name,
                  cu.code as currency_code
           FROM financial.payments p
           LEFT JOIN core.partners pr ON p.partner_id = pr.id
           LEFT JOIN core.currencies cu ON p.currency_id = cu.id
           WHERE p.id = ANY($1)`,
          [paymentIds]
        );

        rawData.forEach((row: any) => {
          additionalData.set(row.id, {
            partnerName: row.partner_name,
            currencyCode: row.currency_code,
          });
        });
      }

      // Map to include relation names
      const data: PaymentWithRelations[] = payments.map(payment => {
        const additional = additionalData.get(payment.id) || {};
        return {
          ...payment,
          companyName: payment.company?.name,
          journalName: payment.journal?.name,
          partnerName: additional.partnerName,
          currencyCode: additional.currencyCode,
        };
      });

      logger.debug('Payments retrieved', { tenantId, count: data.length, total });

      return { data, total };
    } catch (error) {
      logger.error('Error retrieving payments', {
        error: (error as Error).message,
        tenantId,
      });
      throw error;
    }
  }

  /**
   * Get payment by ID with invoices
   */
  async findById(id: string, tenantId: string): Promise<PaymentWithRelations> {
    try {
      const payment = await this.paymentRepository
        .createQueryBuilder('payment')
        .leftJoin('payment.company', 'company')
        .addSelect(['company.name'])
        .leftJoin('payment.journal', 'journal')
        .addSelect(['journal.name'])
        .where('payment.id = :id', { id })
        .andWhere('payment.tenantId = :tenantId', { tenantId })
        .getOne();

      if (!payment) {
        throw new NotFoundError('Pago no encontrado');
      }

      // Get partner and currency names using raw query
      const [rawData] = await this.paymentRepository.query(
        `SELECT pr.name as partner_name,
                cu.code as currency_code
         FROM financial.payments p
         LEFT JOIN core.partners pr ON p.partner_id = pr.id
         LEFT JOIN core.currencies cu ON p.currency_id = cu.id
         WHERE p.id = $1`,
        [id]
      );

      // Get reconciled invoices
      const invoicesRaw = await this.paymentRepository.query(
        `SELECT pi.invoice_id, pi.amount, i.number as invoice_number
         FROM financial.payment_invoice pi
         LEFT JOIN financial.invoices i ON pi.invoice_id = i.id
         WHERE pi.payment_id = $1
         ORDER BY pi.created_at`,
        [id]
      );

      const invoices: PaymentInvoiceDto[] = invoicesRaw.map((inv: any) => ({
        invoiceId: inv.invoice_id,
        invoiceNumber: inv.invoice_number,
        amount: parseFloat(inv.amount) || 0,
      }));

      return {
        ...payment,
        companyName: payment.company?.name,
        journalName: payment.journal?.name,
        partnerName: rawData?.partner_name,
        currencyCode: rawData?.currency_code,
        invoices,
      };
    } catch (error) {
      logger.error('Error finding payment', {
        error: (error as Error).message,
        id,
        tenantId,
      });
      throw error;
    }
  }

  /**
   * Create a new payment
   */
  async create(
    dto: CreatePaymentDto,
    tenantId: string,
    userId: string
  ): Promise<PaymentWithRelations> {
    try {
      if (dto.amount <= 0) {
        throw new ValidationError('El monto debe ser mayor a 0');
      }

      const paymentDate = dto.paymentDate || new Date().toISOString().split('T')[0];

      // Create payment
      const payment = this.paymentRepository.create({
        tenantId,
        companyId: dto.companyId,
        partnerId: dto.partnerId,
        paymentType: dto.paymentType as PaymentType,
        paymentMethod: dto.paymentMethod as PaymentMethod,
        amount: dto.amount,
        currencyId: dto.currencyId,
        paymentDate: new Date(paymentDate),
        ref: dto.ref || null,
        journalId: dto.journalId,
        notes: dto.notes || null,
        status: PaymentStatus.DRAFT,
        createdBy: userId,
      });

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

      logger.info('Payment created', {
        paymentId: savedPayment.id,
        tenantId,
        amount: savedPayment.amount,
        createdBy: userId,
      });

      return this.findById(savedPayment.id, tenantId);
    } catch (error) {
      logger.error('Error creating payment', {
        error: (error as Error).message,
        tenantId,
        dto,
      });
      throw error;
    }
  }

  /**
   * Update a payment (only draft payments)
   */
  async update(
    id: string,
    dto: UpdatePaymentDto,
    tenantId: string,
    userId: string
  ): Promise<PaymentWithRelations> {
    try {
      const existing = await this.findById(id, tenantId);

      if (existing.status !== PaymentStatus.DRAFT) {
        throw new ValidationError('Solo se pueden editar pagos en estado borrador');
      }

      // Build update data
      const updateData: Partial<Payment> = {
        updatedBy: userId,
        updatedAt: new Date(),
      };

      if (dto.partnerId !== undefined) updateData.partnerId = dto.partnerId;
      if (dto.paymentMethod !== undefined) updateData.paymentMethod = dto.paymentMethod as PaymentMethod;
      if (dto.amount !== undefined) {
        if (dto.amount <= 0) {
          throw new ValidationError('El monto debe ser mayor a 0');
        }
        updateData.amount = dto.amount;
      }
      if (dto.currencyId !== undefined) updateData.currencyId = dto.currencyId;
      if (dto.paymentDate !== undefined) updateData.paymentDate = new Date(dto.paymentDate);
      if (dto.ref !== undefined) updateData.ref = dto.ref;
      if (dto.journalId !== undefined) updateData.journalId = dto.journalId;
      if (dto.notes !== undefined) updateData.notes = dto.notes;

      await this.paymentRepository.update({ id, tenantId }, updateData);

      logger.info('Payment updated', {
        paymentId: id,
        tenantId,
        updatedBy: userId,
      });

      return this.findById(id, tenantId);
    } catch (error) {
      logger.error('Error updating payment', {
        error: (error as Error).message,
        id,
        tenantId,
      });
      throw error;
    }
  }

  /**
   * Delete a payment (only draft payments)
   */
  async delete(id: string, tenantId: string): Promise<void> {
    try {
      const existing = await this.findById(id, tenantId);

      if (existing.status !== PaymentStatus.DRAFT) {
        throw new ValidationError('Solo se pueden eliminar pagos en estado borrador');
      }

      await this.paymentRepository.delete({ id, tenantId });

      logger.info('Payment deleted', {
        paymentId: id,
        tenantId,
      });
    } catch (error) {
      logger.error('Error deleting payment', {
        error: (error as Error).message,
        id,
        tenantId,
      });
      throw error;
    }
  }

  /**
   * Post a payment (draft -> posted)
   */
  async post(id: string, tenantId: string, userId: string): Promise<PaymentWithRelations> {
    try {
      const payment = await this.findById(id, tenantId);

      if (payment.status !== PaymentStatus.DRAFT) {
        throw new ValidationError('Solo se pueden publicar pagos en estado borrador');
      }

      await this.paymentRepository.update(
        { id, tenantId },
        {
          status: PaymentStatus.POSTED,
          postedAt: new Date(),
          postedBy: userId,
          updatedBy: userId,
          updatedAt: new Date(),
        }
      );

      logger.info('Payment posted', {
        paymentId: id,
        tenantId,
        postedBy: userId,
      });

      return this.findById(id, tenantId);
    } catch (error) {
      logger.error('Error posting payment', {
        error: (error as Error).message,
        id,
        tenantId,
      });
      throw error;
    }
  }

  /**
   * Reconcile payment with invoices
   * Uses QueryRunner for transaction management
   */
  async reconcile(
    id: string,
    dto: ReconcileDto,
    tenantId: string,
    userId: string
  ): Promise<PaymentWithRelations> {
    const payment = await this.findById(id, tenantId);

    if (payment.status === PaymentStatus.DRAFT) {
      throw new ValidationError('Debe publicar el pago antes de conciliar');
    }

    if (payment.status === PaymentStatus.CANCELLED) {
      throw new ValidationError('No se puede conciliar un pago cancelado');
    }

    // Validate total amount matches
    const totalReconciled = dto.invoices.reduce((sum, inv) => sum + inv.amount, 0);
    if (totalReconciled > payment.amount) {
      throw new ValidationError('El monto total conciliado excede el monto del pago');
    }

    // Use QueryRunner for transaction
    const queryRunner = AppDataSource.createQueryRunner();
    await queryRunner.connect();
    await queryRunner.startTransaction();

    try {
      // Remove existing reconciliations
      await queryRunner.manager.delete(PaymentInvoice, { paymentId: id });

      // Add new reconciliations
      for (const inv of dto.invoices) {
        // Validate invoice exists and belongs to same partner
        const invoice = await queryRunner.manager.findOne(Invoice, {
          where: {
            id: inv.invoiceId,
            tenantId,
          },
        });

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

        if (invoice.partnerId !== payment.partnerId) {
          throw new ValidationError('La factura debe pertenecer al mismo cliente/proveedor');
        }

        if (invoice.status !== InvoiceStatus.OPEN) {
          throw new ValidationError('Solo se pueden conciliar facturas abiertas');
        }

        if (inv.amount > invoice.amountResidual) {
          throw new ValidationError(`El monto excede el saldo pendiente de la factura`);
        }

        // Create payment-invoice link
        const paymentInvoice = queryRunner.manager.create(PaymentInvoice, {
          paymentId: id,
          invoiceId: inv.invoiceId,
          amount: inv.amount,
        });
        await queryRunner.manager.save(PaymentInvoice, paymentInvoice);

        // Update invoice amounts
        const newAmountPaid = Number(invoice.amountPaid) + inv.amount;
        const newAmountResidual = Number(invoice.amountResidual) - inv.amount;
        const newStatus = newAmountResidual <= 0 ? InvoiceStatus.PAID : invoice.status;

        await queryRunner.manager.update(
          Invoice,
          { id: inv.invoiceId },
          {
            amountPaid: newAmountPaid,
            amountResidual: newAmountResidual,
            status: newStatus,
          }
        );
      }

      // Update payment status
      await queryRunner.manager.update(
        Payment,
        { id },
        {
          status: PaymentStatus.RECONCILED,
          updatedBy: userId,
          updatedAt: new Date(),
        }
      );

      await queryRunner.commitTransaction();

      logger.info('Payment reconciled', {
        paymentId: id,
        tenantId,
        invoiceCount: dto.invoices.length,
        updatedBy: userId,
      });

      return this.findById(id, tenantId);
    } catch (error) {
      await queryRunner.rollbackTransaction();
      logger.error('Error reconciling payment', {
        error: (error as Error).message,
        id,
        tenantId,
      });
      throw error;
    } finally {
      await queryRunner.release();
    }
  }

  /**
   * Cancel a payment
   * Uses QueryRunner for transaction management
   */
  async cancel(
    id: string,
    tenantId: string,
    userId: string
  ): Promise<PaymentWithRelations> {
    const payment = await this.findById(id, tenantId);

    if (payment.status === PaymentStatus.CANCELLED) {
      throw new ValidationError('El pago ya está cancelado');
    }

    // Use QueryRunner for transaction
    const queryRunner = AppDataSource.createQueryRunner();
    await queryRunner.connect();
    await queryRunner.startTransaction();

    try {
      // Reverse reconciliations if any
      if (payment.invoices && payment.invoices.length > 0) {
        for (const inv of payment.invoices) {
          const invoice = await queryRunner.manager.findOne(Invoice, {
            where: { id: inv.invoiceId },
          });

          if (invoice) {
            const newAmountPaid = Number(invoice.amountPaid) - inv.amount;
            const newAmountResidual = Number(invoice.amountResidual) + inv.amount;

            await queryRunner.manager.update(
              Invoice,
              { id: inv.invoiceId },
              {
                amountPaid: newAmountPaid,
                amountResidual: newAmountResidual,
                status: InvoiceStatus.OPEN,
              }
            );
          }
        }

        // Remove payment-invoice links
        await queryRunner.manager.delete(PaymentInvoice, { paymentId: id });
      }

      // Cancel payment
      await queryRunner.manager.update(
        Payment,
        { id },
        {
          status: PaymentStatus.CANCELLED,
          updatedBy: userId,
          updatedAt: new Date(),
        }
      );

      await queryRunner.commitTransaction();

      logger.info('Payment cancelled', {
        paymentId: id,
        tenantId,
        cancelledBy: userId,
      });

      return this.findById(id, tenantId);
    } catch (error) {
      await queryRunner.rollbackTransaction();
      logger.error('Error cancelling payment', {
        error: (error as Error).message,
        id,
        tenantId,
      });
      throw error;
    } finally {
      await queryRunner.release();
    }
  }
}

// ===== Export Singleton Instance =====

export const paymentsService = new PaymentsService();

// Re-export enums for backwards compatibility
export { PaymentType, PaymentMethod, PaymentStatus };
