/**
 * APService - Servicio de Cuentas por Pagar
 *
 * Gestión de cuentas por pagar y pagos a proveedores.
 *
 * @module Finance
 */

import { DataSource, Repository, IsNull } from 'typeorm';
import {
  AccountPayable,
  APPayment,
  PaymentMethod,
} from '../entities';

type APStatus = 'pending' | 'partial' | 'paid' | 'overdue' | 'cancelled' | 'disputed';
type APDocumentType = 'invoice' | 'credit_note' | 'debit_note' | 'advance' | 'retention';

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

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

interface CreateAPDto {
  documentType: APDocumentType;
  documentNumber: string;
  documentDate: Date;
  dueDate: Date;
  partnerId: string;
  partnerName: string;
  partnerRfc?: string;
  projectId?: string;
  projectCode?: string;
  contractId?: string;
  purchaseOrderId?: string;
  originalAmount: number;
  currencyCode?: string;
  exchangeRate?: number;
  taxAmount?: number;
  retentionIsr?: number;
  retentionIva?: number;
  guaranteeFund?: number;
  cfdiUuid?: string;
  cfdiXml?: string;
  description?: string;
  paymentTermDays?: number;
  ledgerAccountId?: string;
  notes?: string;
  metadata?: Record<string, any>;
}

interface CreatePaymentDto {
  paymentMethod: PaymentMethod;
  paymentDate: Date;
  bankAccountId: string;
  paymentAmount: number;
  currencyCode?: string;
  exchangeRate?: number;
  reference?: string;
  checkNumber?: string;
  transferReference?: string;
  beneficiaryName?: string;
  beneficiaryAccount?: string;
  beneficiaryBank?: string;
  paymentConcept?: string;
  notes?: string;
  accountPayableIds: string[];
}

interface AgingBucket {
  current: number;
  days1to30: number;
  days31to60: number;
  days61to90: number;
  over90: number;
  total: number;
}

export class APService {
  private apRepository: Repository<AccountPayable>;
  private paymentRepository: Repository<APPayment>;

  constructor(dataSource: DataSource) {
    this.apRepository = dataSource.getRepository(AccountPayable);
    this.paymentRepository = dataSource.getRepository(APPayment);
  }

  // ==================== CUENTAS POR PAGAR ====================

  async findAll(
    ctx: ServiceContext,
    options: {
      page?: number;
      limit?: number;
      status?: APStatus;
      partnerId?: string;
      projectId?: string;
      startDate?: Date;
      endDate?: Date;
      overdue?: boolean;
      search?: string;
    } = {}
  ): Promise<PaginatedResult<AccountPayable>> {
    const {
      page = 1,
      limit = 20,
      status,
      partnerId,
      projectId,
      startDate,
      endDate,
      overdue,
      search,
    } = options;

    const queryBuilder = this.apRepository
      .createQueryBuilder('ap')
      .where('ap.tenantId = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('ap.deletedAt IS NULL');

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

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

    if (projectId) {
      queryBuilder.andWhere('ap.projectId = :projectId', { projectId });
    }

    if (startDate) {
      queryBuilder.andWhere('ap.documentDate >= :startDate', { startDate });
    }

    if (endDate) {
      queryBuilder.andWhere('ap.documentDate <= :endDate', { endDate });
    }

    if (overdue) {
      queryBuilder.andWhere('ap.dueDate < :today', { today: new Date() });
      queryBuilder.andWhere('ap.status IN (:...statuses)', {
        statuses: ['pending', 'partial'],
      });
    }

    if (search) {
      queryBuilder.andWhere(
        '(ap.documentNumber ILIKE :search OR ap.supplierName ILIKE :search)',
        { search: `%${search}%` }
      );
    }

    queryBuilder.orderBy('ap.dueDate', 'ASC');

    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),
    };
  }

  async findById(ctx: ServiceContext, id: string): Promise<AccountPayable | null> {
    return this.apRepository.findOne({
      where: {
        id,
        tenantId: ctx.tenantId,
        deletedAt: IsNull(),
      },
      relations: ['payments'],
    });
  }

  async create(ctx: ServiceContext, data: CreateAPDto): Promise<AccountPayable> {
    // Calcular retención total
    const retentionAmount =
      (data.retentionIsr ?? 0) +
      (data.retentionIva ?? 0) +
      (data.guaranteeFund ?? 0);
    const subtotal = data.originalAmount - (data.taxAmount ?? 0);
    const totalAmount = data.originalAmount;
    const balance = totalAmount - retentionAmount;

    const ap = this.apRepository.create({
      tenantId: ctx.tenantId,
      documentType: data.documentType,
      documentNumber: data.documentNumber,
      invoiceDate: data.documentDate,
      dueDate: data.dueDate,
      supplierId: data.partnerId,
      supplierName: data.partnerName,
      supplierRfc: data.partnerRfc,
      projectId: data.projectId,
      projectCode: data.projectCode,
      subtotal,
      taxAmount: data.taxAmount ?? 0,
      retentionAmount,
      totalAmount,
      paidAmount: 0,
      balance,
      currency: data.currencyCode ?? 'MXN',
      exchangeRate: data.exchangeRate ?? 1,
      cfdiUuid: data.cfdiUuid,
      paymentDays: data.paymentTermDays ?? 30,
      notes: data.notes,
      metadata: data.metadata,
      status: 'pending',
      createdBy: ctx.userId,
    });

    const result = await this.apRepository.save(ap);
    return result;
  }

  async update(
    ctx: ServiceContext,
    id: string,
    data: Partial<CreateAPDto>
  ): Promise<AccountPayable> {
    const ap = await this.findById(ctx, id);
    if (!ap) {
      throw new Error('Cuenta por pagar no encontrada');
    }

    if (ap.status === 'paid' || ap.status === 'cancelled') {
      throw new Error('No se puede modificar una cuenta pagada o cancelada');
    }

    Object.assign(ap, {
      ...data,
      updatedBy: ctx.userId,
    });

    // Recalcular saldo si cambió el monto original
    if (data.originalAmount !== undefined) {
      ap.totalAmount = data.originalAmount;
      ap.balance = Number(ap.totalAmount) - Number(ap.retentionAmount) - Number(ap.paidAmount);
    }

    return this.apRepository.save(ap);
  }

  async cancel(ctx: ServiceContext, id: string, reason: string): Promise<AccountPayable> {
    const ap = await this.findById(ctx, id);
    if (!ap) {
      throw new Error('Cuenta por pagar no encontrada');
    }

    if (ap.paidAmount > 0) {
      throw new Error('No se puede cancelar una cuenta con pagos aplicados');
    }

    ap.status = 'cancelled';
    ap.notes = `${ap.notes || ''}\n[CANCELADA]: ${reason}`;
    ap.updatedBy = ctx.userId;

    return this.apRepository.save(ap);
  }

  // ==================== PAGOS ====================

  async createPayment(ctx: ServiceContext, data: CreatePaymentDto): Promise<APPayment> {
    // Validar que las cuentas por pagar existen y calcular total
    let totalToApply = 0;
    const apRecords: AccountPayable[] = [];

    for (const apId of data.accountPayableIds) {
      const ap = await this.findById(ctx, apId);
      if (!ap) {
        throw new Error(`Cuenta por pagar ${apId} no encontrada`);
      }
      if (ap.status === 'paid' || ap.status === 'cancelled') {
        throw new Error(`Cuenta por pagar ${ap.documentNumber} ya está pagada o cancelada`);
      }
      apRecords.push(ap);
      totalToApply += ap.balance;
    }

    // Validar monto
    if (data.paymentAmount > totalToApply) {
      throw new Error(
        `El monto del pago (${data.paymentAmount}) excede el saldo pendiente (${totalToApply})`
      );
    }

    // Generar número de pago
    const paymentNumber = await this.generatePaymentNumber(ctx);

    // Crear pago
    const payment = this.paymentRepository.create({
      tenantId: ctx.tenantId,
      paymentNumber,
      paymentMethod: data.paymentMethod,
      paymentDate: data.paymentDate,
      bankAccountId: data.bankAccountId,
      amount: data.paymentAmount,
      currency: data.currencyCode ?? 'MXN',
      exchangeRate: data.exchangeRate ?? 1,
      checkNumber: data.checkNumber,
      transferReference: data.transferReference,
      beneficiaryName: data.beneficiaryName,
      beneficiaryAccount: data.beneficiaryAccount,
      beneficiaryBank: data.beneficiaryBank,
      notes: data.notes,
      status: 'pending',
      createdBy: ctx.userId,
    });

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

    // Aplicar pago a las cuentas por pagar (FIFO por fecha de vencimiento)
    let remainingAmount = data.paymentAmount;
    const sortedAP = apRecords.sort(
      (a, b) => new Date(a.dueDate).getTime() - new Date(b.dueDate).getTime()
    );

    const applications: { apId: string; amount: number }[] = [];

    for (const ap of sortedAP) {
      if (remainingAmount <= 0) break;

      const amountToApply = Math.min(remainingAmount, ap.balance);
      applications.push({ apId: ap.id, amount: amountToApply });

      ap.paidAmount += amountToApply;
      ap.balance -= amountToApply;
      ap.paymentDate = data.paymentDate;

      if (ap.balance <= 0.01) {
        ap.status = 'paid';
      } else {
        ap.status = 'partial';
      }

      ap.updatedBy = ctx.userId;
      await this.apRepository.save(ap);

      remainingAmount -= amountToApply;
    }

    // Guardar aplicaciones en metadata del pago
    savedPayment.metadata = { applications };
    await this.paymentRepository.save(savedPayment);

    return savedPayment;
  }

  private async generatePaymentNumber(ctx: ServiceContext): Promise<string> {
    const year = new Date().getFullYear();
    const lastPayment = await this.paymentRepository
      .createQueryBuilder('payment')
      .where('payment.tenantId = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('payment.paymentNumber LIKE :pattern', { pattern: `PG-${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 `PG-${year}-${String(sequence).padStart(6, '0')}`;
  }

  async confirmPayment(ctx: ServiceContext, paymentId: string): Promise<APPayment> {
    const payment = await this.paymentRepository.findOne({
      where: {
        id: paymentId,
        tenantId: ctx.tenantId,
      },
    });

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

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

    payment.status = 'processed';
    payment.updatedBy = ctx.userId;

    return this.paymentRepository.save(payment);
  }

  async cancelPayment(
    ctx: ServiceContext,
    paymentId: string,
    reason: string
  ): Promise<APPayment> {
    const payment = await this.paymentRepository.findOne({
      where: {
        id: paymentId,
        tenantId: ctx.tenantId,
      },
    });

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

    if (payment.status === 'cancelled' || payment.status === 'reconciled') {
      throw new Error('No se puede cancelar este pago');
    }

    // Reversar aplicaciones
    const applications = (payment.metadata as any)?.applications || [];
    for (const app of applications) {
      const ap = await this.apRepository.findOne({ where: { id: app.apId } });
      if (ap) {
        ap.paidAmount -= app.amount;
        ap.balance += app.amount;
        ap.status = ap.balance >= ap.totalAmount ? 'pending' : 'partial';
        ap.updatedBy = ctx.userId;
        await this.apRepository.save(ap);
      }
    }

    payment.status = 'cancelled';
    payment.notes = `${payment.notes || ''}\n[CANCELADO]: ${reason}`;
    payment.updatedBy = ctx.userId;

    return this.paymentRepository.save(payment);
  }

  // ==================== REPORTES ====================

  async getAgingReport(
    ctx: ServiceContext,
    options: {
      partnerId?: string;
      projectId?: string;
      asOfDate?: Date;
    } = {}
  ): Promise<{
    summary: AgingBucket;
    byPartner: { partnerId: string; partnerName: string; aging: AgingBucket }[];
  }> {
    const { partnerId, projectId, asOfDate = new Date() } = options;

    const queryBuilder = this.apRepository
      .createQueryBuilder('ap')
      .where('ap.tenantId = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('ap.status IN (:...statuses)', { statuses: ['pending', 'partial'] })
      .andWhere('ap.deletedAt IS NULL');

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

    if (projectId) {
      queryBuilder.andWhere('ap.projectId = :projectId', { projectId });
    }

    const apRecords = await queryBuilder.getMany();

    const summary: AgingBucket = {
      current: 0,
      days1to30: 0,
      days31to60: 0,
      days61to90: 0,
      over90: 0,
      total: 0,
    };

    const partnerMap = new Map<
      string,
      { partnerId: string; partnerName: string; aging: AgingBucket }
    >();

    for (const ap of apRecords) {
      const daysOverdue = Math.floor(
        (asOfDate.getTime() - new Date(ap.dueDate).getTime()) / (1000 * 60 * 60 * 24)
      );
      const balance = ap.balance;

      // Clasificar en bucket
      let bucket: keyof AgingBucket;
      if (daysOverdue <= 0) {
        bucket = 'current';
      } else if (daysOverdue <= 30) {
        bucket = 'days1to30';
      } else if (daysOverdue <= 60) {
        bucket = 'days31to60';
      } else if (daysOverdue <= 90) {
        bucket = 'days61to90';
      } else {
        bucket = 'over90';
      }

      summary[bucket] += balance;
      summary.total += balance;

      // Por proveedor
      if (!partnerMap.has(ap.supplierId)) {
        partnerMap.set(ap.supplierId, {
          partnerId: ap.supplierId,
          partnerName: ap.supplierName,
          aging: {
            current: 0,
            days1to30: 0,
            days31to60: 0,
            days61to90: 0,
            over90: 0,
            total: 0,
          },
        });
      }

      const partnerData = partnerMap.get(ap.supplierId)!;
      partnerData.aging[bucket] += balance;
      partnerData.aging.total += balance;
    }

    return {
      summary,
      byPartner: Array.from(partnerMap.values()).sort((a, b) => b.aging.total - a.aging.total),
    };
  }

  async getPaymentSchedule(
    ctx: ServiceContext,
    startDate: Date,
    endDate: Date,
    options: { partnerId?: string; projectId?: string } = {}
  ): Promise<{ date: Date; documents: AccountPayable[]; totalAmount: number }[]> {
    const { partnerId, projectId } = options;

    const queryBuilder = this.apRepository
      .createQueryBuilder('ap')
      .where('ap.tenantId = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('ap.status IN (:...statuses)', { statuses: ['pending', 'partial'] })
      .andWhere('ap.dueDate BETWEEN :startDate AND :endDate', { startDate, endDate })
      .andWhere('ap.deletedAt IS NULL');

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

    if (projectId) {
      queryBuilder.andWhere('ap.projectId = :projectId', { projectId });
    }

    queryBuilder.orderBy('ap.dueDate', 'ASC');

    const apRecords = await queryBuilder.getMany();

    // Agrupar por fecha de vencimiento
    const scheduleMap = new Map<string, { date: Date; documents: AccountPayable[]; totalAmount: number }>();

    for (const ap of apRecords) {
      const dateKey = new Date(ap.dueDate).toISOString().split('T')[0];

      if (!scheduleMap.has(dateKey)) {
        scheduleMap.set(dateKey, {
          date: new Date(ap.dueDate),
          documents: [],
          totalAmount: 0,
        });
      }

      const entry = scheduleMap.get(dateKey)!;
      entry.documents.push(ap);
      entry.totalAmount += ap.balance;
    }

    return Array.from(scheduleMap.values()).sort(
      (a, b) => a.date.getTime() - b.date.getTime()
    );
  }

  async getDashboardStats(ctx: ServiceContext): Promise<{
    totalPending: number;
    totalOverdue: number;
    dueThisWeek: number;
    dueThisMonth: number;
    countPending: number;
    countOverdue: number;
  }> {
    const today = new Date();
    const endOfWeek = new Date(today);
    endOfWeek.setDate(today.getDate() + (7 - today.getDay()));
    const endOfMonth = new Date(today.getFullYear(), today.getMonth() + 1, 0);

    const baseQuery = this.apRepository
      .createQueryBuilder('ap')
      .where('ap.tenantId = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('ap.status IN (:...statuses)', { statuses: ['pending', 'partial'] })
      .andWhere('ap.deletedAt IS NULL');

    // Total pendiente
    const totalPending = await baseQuery
      .clone()
      .select('SUM(ap.balance)', 'total')
      .getRawOne();

    // Vencido
    const totalOverdue = await baseQuery
      .clone()
      .andWhere('ap.dueDate < :today', { today })
      .select('SUM(ap.balance)', 'total')
      .getRawOne();

    // Por vencer esta semana
    const dueThisWeek = await baseQuery
      .clone()
      .andWhere('ap.dueDate BETWEEN :today AND :endOfWeek', { today, endOfWeek })
      .select('SUM(ap.balance)', 'total')
      .getRawOne();

    // Por vencer este mes
    const dueThisMonth = await baseQuery
      .clone()
      .andWhere('ap.dueDate BETWEEN :today AND :endOfMonth', { today, endOfMonth })
      .select('SUM(ap.balance)', 'total')
      .getRawOne();

    // Conteos
    const countPending = await baseQuery.clone().getCount();

    const countOverdue = await baseQuery
      .clone()
      .andWhere('ap.dueDate < :today', { today })
      .getCount();

    return {
      totalPending: parseFloat(totalPending?.total) || 0,
      totalOverdue: parseFloat(totalOverdue?.total) || 0,
      dueThisWeek: parseFloat(dueThisWeek?.total) || 0,
      dueThisMonth: parseFloat(dueThisMonth?.total) || 0,
      countPending,
      countOverdue,
    };
  }
}
