/**
 * ARService - Servicio de Cuentas por Cobrar
 *
 * Gestión de cuentas por cobrar y cobranza.
 *
 * @module Finance
 */

import { DataSource, Repository, IsNull } from 'typeorm';
import {
  AccountReceivable,
  ARStatus,
  ARDocumentType,
  ARPayment,
  CollectionMethod,
} from '../entities';

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

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

interface CreateARDto {
  documentType: ARDocumentType;
  documentNumber: string;
  documentDate: Date;
  dueDate: Date;
  partnerId: string;
  partnerName: string;
  partnerRfc?: string;
  projectId?: string;
  projectCode?: string;
  contractId?: string;
  estimationId?: 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 CreateCollectionDto {
  collectionMethod: CollectionMethod;
  collectionDate: Date;
  bankAccountId: string;
  collectionAmount: number;
  currencyCode?: string;
  exchangeRate?: number;
  reference?: string;
  depositReference?: string;
  transferReference?: string;
  collectionConcept?: string;
  notes?: string;
  accountReceivableIds: string[];
}

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

export class ARService {
  private arRepository: Repository<AccountReceivable>;
  private collectionRepository: Repository<ARPayment>;
  dataSource: DataSource;

  constructor(dataSource: DataSource) {
    this.dataSource = dataSource;
    this.arRepository = dataSource.getRepository(AccountReceivable);
    this.collectionRepository = dataSource.getRepository(ARPayment);
  }

  // ==================== CUENTAS POR COBRAR ====================

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

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

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

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

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

    if (startDate) {
      queryBuilder.andWhere('ar.invoiceDate >= :startDate', { startDate });
    }

    if (endDate) {
      queryBuilder.andWhere('ar.invoiceDate <= :endDate', { endDate });
    }

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

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

    queryBuilder.orderBy('ar.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<AccountReceivable | null> {
    return this.arRepository.findOne({
      where: {
        id,
        tenantId: ctx.tenantId,
        deletedAt: IsNull(),
      },
      relations: ['collections'],
    });
  }

  async create(ctx: ServiceContext, data: CreateARDto): Promise<AccountReceivable> {
    // Calcular retención total y monto neto
    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 ar = this.arRepository.create({
      tenantId: ctx.tenantId,
      documentType: data.documentType,
      documentNumber: data.documentNumber,
      invoiceDate: data.documentDate,
      dueDate: data.dueDate,
      customerId: data.partnerId,
      customerName: data.partnerName,
      customerRfc: data.partnerRfc,
      projectId: data.projectId,
      projectCode: data.projectCode,
      subtotal,
      taxAmount: data.taxAmount ?? 0,
      retentionAmount,
      totalAmount,
      collectedAmount: 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',
      collectionAttempts: 0,
      createdBy: ctx.userId,
    });

    return this.arRepository.save(ar);
  }

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

    if (ar.status === 'collected' || ar.status === 'cancelled' || ar.status === 'written_off') {
      throw new Error('No se puede modificar esta cuenta');
    }

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

    // Recalcular montos si cambió el monto original
    if (data.originalAmount !== undefined) {
      ar.totalAmount = data.originalAmount;
      ar.balance = ar.totalAmount - ar.retentionAmount - ar.collectedAmount;
    }

    return this.arRepository.save(ar);
  }

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

    if (ar.collectedAmount > 0) {
      throw new Error('No se puede cancelar una cuenta con cobros aplicados');
    }

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

    return this.arRepository.save(ar);
  }

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

    ar.status = 'written_off';
    ar.notes = `${ar.notes || ''}\n[CASTIGO]: ${reason}`;
    ar.updatedBy = ctx.userId;

    return this.arRepository.save(ar);
  }

  async recordCollectionAttempt(
    ctx: ServiceContext,
    id: string,
    notes: string
  ): Promise<AccountReceivable> {
    const ar = await this.findById(ctx, id);
    if (!ar) {
      throw new Error('Cuenta por cobrar no encontrada');
    }

    ar.collectionAttempts += 1;
    ar.lastCollectionAttempt = new Date();
    ar.notes = `${ar.notes || ''}\n[GESTIÓN ${ar.collectionAttempts}]: ${notes}`;
    ar.updatedBy = ctx.userId;

    return this.arRepository.save(ar);
  }

  // ==================== COBROS ====================

  async createCollection(ctx: ServiceContext, data: CreateCollectionDto): Promise<ARPayment> {
    // Validar que las cuentas por cobrar existen y calcular total
    let totalToApply = 0;
    const arRecords: AccountReceivable[] = [];

    for (const arId of data.accountReceivableIds) {
      const ar = await this.findById(ctx, arId);
      if (!ar) {
        throw new Error(`Cuenta por cobrar ${arId} no encontrada`);
      }
      if (ar.status === 'collected' || ar.status === 'cancelled' || ar.status === 'written_off') {
        throw new Error(`Cuenta por cobrar ${ar.documentNumber} ya está cobrada, cancelada o castigada`);
      }
      arRecords.push(ar);
      totalToApply += Number(ar.balance);
    }

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

    // Generar número de cobro
    const collectionNumber = await this.generateCollectionNumber(ctx);

    // Crear cobro
    const collection = this.collectionRepository.create({
      tenantId: ctx.tenantId,
      collectionNumber,
      collectionMethod: data.collectionMethod,
      collectionDate: data.collectionDate,
      bankAccountId: data.bankAccountId,
      accountReceivableId: arRecords[0]?.id,
      amount: data.collectionAmount,
      currency: data.currencyCode ?? 'MXN',
      exchangeRate: data.exchangeRate ?? 1,
      transferReference: data.transferReference,
      notes: data.notes,
      status: 'pending',
      createdBy: ctx.userId,
    });

    const savedCollection = await this.collectionRepository.save(collection);

    // Aplicar cobro a las cuentas por cobrar (FIFO por fecha de vencimiento)
    let remainingAmount = data.collectionAmount;
    const sortedAR = arRecords.sort(
      (a, b) => new Date(a.dueDate).getTime() - new Date(b.dueDate).getTime()
    );

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

    for (const ar of sortedAR) {
      if (remainingAmount <= 0) break;

      const arBalance = Number(ar.balance);
      const amountToApply = Math.min(remainingAmount, arBalance);
      applications.push({ arId: ar.id, amount: amountToApply });

      ar.collectedAmount = Number(ar.collectedAmount) + amountToApply;
      ar.balance = arBalance - amountToApply;
      ar.collectionDate = data.collectionDate;

      if (ar.balance <= 0.01) {
        ar.status = 'collected';
      } else {
        ar.status = 'partial';
      }

      ar.updatedBy = ctx.userId;
      await this.arRepository.save(ar);

      remainingAmount -= amountToApply;
    }

    // Guardar aplicaciones en metadata del cobro
    savedCollection.metadata = { applications };
    const result = await this.collectionRepository.save(savedCollection);

    return result;
  }

  private async generateCollectionNumber(ctx: ServiceContext): Promise<string> {
    const year = new Date().getFullYear();
    const lastCollection = await this.collectionRepository
      .createQueryBuilder('collection')
      .where('collection.tenantId = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('collection.collectionNumber LIKE :pattern', { pattern: `CB-${year}%` })
      .orderBy('collection.collectionNumber', 'DESC')
      .getOne();

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

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

  async confirmCollection(ctx: ServiceContext, collectionId: string): Promise<ARPayment> {
    const collection = await this.collectionRepository.findOne({
      where: {
        id: collectionId,
        tenantId: ctx.tenantId,
      },
    });

    if (!collection) {
      throw new Error('Cobro no encontrado');
    }

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

    collection.status = 'deposited';
    collection.depositDate = new Date();
    collection.updatedBy = ctx.userId;

    return this.collectionRepository.save(collection);
  }

  async cancelCollection(
    ctx: ServiceContext,
    collectionId: string,
    reason: string
  ): Promise<ARPayment> {
    const collection = await this.collectionRepository.findOne({
      where: {
        id: collectionId,
        tenantId: ctx.tenantId,
      },
    });

    if (!collection) {
      throw new Error('Cobro no encontrado');
    }

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

    // Reversar aplicaciones
    const applications = (collection.metadata as any)?.applications || [];
    for (const app of applications) {
      const ar = await this.arRepository.findOne({ where: { id: app.arId } });
      if (ar) {
        ar.collectedAmount = Number(ar.collectedAmount) - app.amount;
        ar.balance = Number(ar.balance) + app.amount;
        const expectedBalance = Number(ar.totalAmount) - Number(ar.retentionAmount);
        ar.status = Number(ar.balance) >= expectedBalance ? 'pending' : 'partial';
        ar.updatedBy = ctx.userId;
        await this.arRepository.save(ar);
      }
    }

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

    return this.collectionRepository.save(collection);
  }

  // ==================== 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.arRepository
      .createQueryBuilder('ar')
      .where('ar.tenantId = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('ar.status IN (:...statuses)', { statuses: ['pending', 'partial'] })
      .andWhere('ar.deletedAt IS NULL');

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

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

    const arRecords = 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 ar of arRecords) {
      const daysOverdue = Math.floor(
        (asOfDate.getTime() - new Date(ar.dueDate).getTime()) / (1000 * 60 * 60 * 24)
      );
      const balance = Number(ar.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 cliente
      if (!partnerMap.has(ar.customerId)) {
        partnerMap.set(ar.customerId, {
          partnerId: ar.customerId,
          partnerName: ar.customerName,
          aging: {
            current: 0,
            days1to30: 0,
            days31to60: 0,
            days61to90: 0,
            over90: 0,
            total: 0,
          },
        });
      }

      const partnerData = partnerMap.get(ar.customerId)!;
      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 getCollectionForecast(
    ctx: ServiceContext,
    startDate: Date,
    endDate: Date,
    options: { partnerId?: string; projectId?: string } = {}
  ): Promise<{ date: Date; documents: AccountReceivable[]; totalAmount: number }[]> {
    const { partnerId, projectId } = options;

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

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

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

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

    const arRecords = await queryBuilder.getMany();

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

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

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

      const entry = forecastMap.get(dateKey)!;
      entry.documents.push(ar);
      entry.totalAmount += Number(ar.balance);
    }

    return Array.from(forecastMap.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;
    collectionEfficiency: 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 startOfMonth = new Date(today.getFullYear(), today.getMonth(), 1);

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

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

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

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

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

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

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

    // Eficiencia de cobranza (cobrado vs facturado en el mes)
    const monthlyInvoiced = await this.arRepository
      .createQueryBuilder('ar')
      .where('ar.tenantId = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('ar.invoiceDate BETWEEN :startOfMonth AND :endOfMonth', {
        startOfMonth,
        endOfMonth,
      })
      .select('SUM(ar.totalAmount)', 'total')
      .getRawOne();

    const monthlyCollected = await this.collectionRepository
      .createQueryBuilder('col')
      .where('col.tenantId = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('col.collectionDate BETWEEN :startOfMonth AND :endOfMonth', {
        startOfMonth,
        endOfMonth,
      })
      .andWhere('col.status != :cancelled', { cancelled: 'cancelled' })
      .select('SUM(col.amount)', 'total')
      .getRawOne();

    const invoicedAmount = parseFloat(monthlyInvoiced?.total) || 0;
    const collectedAmount = parseFloat(monthlyCollected?.total) || 0;
    const collectionEfficiency = invoicedAmount > 0 ? (collectedAmount / invoicedAmount) * 100 : 0;

    return {
      totalPending: parseFloat(totalPending?.total) || 0,
      totalOverdue: parseFloat(totalOverdue?.total) || 0,
      dueThisWeek: parseFloat(dueThisWeek?.total) || 0,
      dueThisMonth: parseFloat(dueThisMonth?.total) || 0,
      countPending,
      countOverdue,
      collectionEfficiency: Math.round(collectionEfficiency * 100) / 100,
    };
  }
}
