/**
 * AnticipoService - Gestión de Anticipos de Obra
 *
 * Gestiona anticipos otorgados a subcontratistas: inicial, avance, materiales.
 *
 * @module Estimates
 */

import { DataSource, Repository, IsNull } from 'typeorm';
import { Anticipo, AdvanceType } from '../entities/anticipo.entity';

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

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

export interface CreateAnticipoDto {
  contratoId: string;
  advanceType: AdvanceType;
  advanceNumber: string;
  advanceDate: Date;
  grossAmount: number;
  taxAmount: number;
  netAmount: number;
  amortizationPercentage?: number;
  notes?: string;
}

export interface AnticipoFilters {
  contratoId?: string;
  advanceType?: AdvanceType;
  isFullyAmortized?: boolean;
  startDate?: Date;
  endDate?: Date;
  approvedById?: string;
}

export interface AnticipoStats {
  totalAnticipated: number;
  totalAmortized: number;
  pendingAmortization: number;
  totalApproved: number;
  totalPending: number;
  totalPaid: number;
  byType: {
    type: AdvanceType;
    count: number;
    totalAmount: number;
  }[];
}

export class AnticipoService {
  private repository: Repository<Anticipo>;

  constructor(dataSource: DataSource) {
    this.repository = dataSource.getRepository(Anticipo);
  }

  /**
   * Busca un anticipo por ID
   */
  async findById(ctx: ServiceContext, id: string): Promise<Anticipo | null> {
    return this.repository.findOne({
      where: {
        id,
        tenantId: ctx.tenantId,
        deletedAt: IsNull(),
      },
    });
  }

  /**
   * Busca anticipos por contrato
   */
  async findByContrato(
    ctx: ServiceContext,
    contratoId: string,
    page = 1,
    limit = 20
  ): Promise<PaginatedResult<Anticipo>> {
    const qb = this.repository
      .createQueryBuilder('a')
      .where('a.tenantId = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('a.contratoId = :contratoId', { contratoId })
      .andWhere('a.deletedAt IS NULL');

    const skip = (page - 1) * limit;
    qb.orderBy('a.advanceDate', 'DESC')
      .addOrderBy('a.advanceNumber', 'DESC')
      .skip(skip)
      .take(limit);

    const [data, total] = await qb.getManyAndCount();

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

  /**
   * Crea un nuevo anticipo
   */
  async createAnticipo(
    ctx: ServiceContext,
    dto: CreateAnticipoDto
  ): Promise<Anticipo> {
    // Validar que el monto neto sea correcto
    const expectedNet = dto.grossAmount - dto.taxAmount;
    if (Math.abs(expectedNet - dto.netAmount) > 0.01) {
      throw new Error('El monto neto no coincide con el cálculo (bruto - impuestos)');
    }

    const anticipo = this.repository.create({
      tenantId: ctx.tenantId,
      contratoId: dto.contratoId,
      advanceType: dto.advanceType,
      advanceNumber: dto.advanceNumber,
      advanceDate: dto.advanceDate,
      grossAmount: dto.grossAmount,
      taxAmount: dto.taxAmount,
      netAmount: dto.netAmount,
      amortizationPercentage: dto.amortizationPercentage ?? 0,
      amortizedAmount: 0,
      isFullyAmortized: false,
      notes: dto.notes ?? null,
      createdById: ctx.userId ?? null,
    });

    return this.repository.save(anticipo);
  }

  /**
   * Actualiza un anticipo
   */
  async update(
    ctx: ServiceContext,
    id: string,
    data: Partial<Anticipo>
  ): Promise<Anticipo | null> {
    const anticipo = await this.findById(ctx, id);
    if (!anticipo) {
      return null;
    }

    Object.assign(anticipo, data);
    anticipo.updatedById = ctx.userId ?? null;

    return this.repository.save(anticipo);
  }

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

    anticipo.deletedAt = new Date();
    anticipo.deletedById = ctx.userId ?? null;
    await this.repository.save(anticipo);
    return true;
  }

  /**
   * Aprueba un anticipo
   */
  async approveAnticipo(
    ctx: ServiceContext,
    id: string,
    approvedById: string,
    notes?: string
  ): Promise<Anticipo> {
    const anticipo = await this.findById(ctx, id);
    if (!anticipo) {
      throw new Error('Anticipo no encontrado');
    }

    if (anticipo.approvedAt) {
      throw new Error('El anticipo ya está aprobado');
    }

    anticipo.approvedAt = new Date();
    anticipo.approvedById = approvedById;

    if (notes) {
      anticipo.notes = anticipo.notes
        ? `${anticipo.notes}\n\nAprobación: ${notes}`
        : `Aprobación: ${notes}`;
    }

    anticipo.updatedById = ctx.userId ?? null;

    return this.repository.save(anticipo);
  }

  /**
   * Marca un anticipo como pagado
   */
  async markPaid(
    ctx: ServiceContext,
    id: string,
    paymentReference: string,
    paidAt?: Date
  ): Promise<Anticipo> {
    const anticipo = await this.findById(ctx, id);
    if (!anticipo) {
      throw new Error('Anticipo no encontrado');
    }

    if (!anticipo.approvedAt) {
      throw new Error('El anticipo debe estar aprobado antes de marcarse como pagado');
    }

    if (anticipo.paidAt) {
      throw new Error('El anticipo ya está marcado como pagado');
    }

    anticipo.paidAt = paidAt || new Date();
    anticipo.paymentReference = paymentReference;
    anticipo.updatedById = ctx.userId ?? null;

    return this.repository.save(anticipo);
  }

  /**
   * Obtiene estadísticas de anticipos
   */
  async getStats(
    ctx: ServiceContext,
    filters?: AnticipoFilters
  ): Promise<AnticipoStats> {
    const qb = this.repository
      .createQueryBuilder('a')
      .where('a.tenantId = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('a.deletedAt IS NULL');

    if (filters?.contratoId) {
      qb.andWhere('a.contratoId = :contratoId', { contratoId: filters.contratoId });
    }
    if (filters?.advanceType) {
      qb.andWhere('a.advanceType = :advanceType', { advanceType: filters.advanceType });
    }
    if (filters?.isFullyAmortized !== undefined) {
      qb.andWhere('a.isFullyAmortized = :isFullyAmortized', {
        isFullyAmortized: filters.isFullyAmortized,
      });
    }
    if (filters?.startDate) {
      qb.andWhere('a.advanceDate >= :startDate', { startDate: filters.startDate });
    }
    if (filters?.endDate) {
      qb.andWhere('a.advanceDate <= :endDate', { endDate: filters.endDate });
    }
    if (filters?.approvedById) {
      qb.andWhere('a.approvedById = :approvedById', { approvedById: filters.approvedById });
    }

    const anticipos = await qb.getMany();

    const stats: AnticipoStats = {
      totalAnticipated: 0,
      totalAmortized: 0,
      pendingAmortization: 0,
      totalApproved: 0,
      totalPending: 0,
      totalPaid: 0,
      byType: [],
    };

    const typeStats = new Map<AdvanceType, { count: number; totalAmount: number }>();

    anticipos.forEach((anticipo: Anticipo) => {
      const netAmount = Number(anticipo.netAmount) || 0;
      const amortizedAmount = Number(anticipo.amortizedAmount) || 0;

      stats.totalAnticipated += netAmount;
      stats.totalAmortized += amortizedAmount;
      stats.pendingAmortization += netAmount - amortizedAmount;

      if (anticipo.approvedAt) {
        stats.totalApproved += netAmount;
      } else {
        stats.totalPending += netAmount;
      }

      if (anticipo.paidAt) {
        stats.totalPaid += netAmount;
      }

      const existing = typeStats.get(anticipo.advanceType) || { count: 0, totalAmount: 0 };
      typeStats.set(anticipo.advanceType, {
        count: existing.count + 1,
        totalAmount: existing.totalAmount + netAmount,
      });
    });

    stats.byType = Array.from(typeStats.entries()).map(([type, data]) => ({
      type,
      count: data.count,
      totalAmount: data.totalAmount,
    }));

    return stats;
  }

  /**
   * Busca anticipos con filtros y paginación
   */
  async findWithFilters(
    ctx: ServiceContext,
    filters: AnticipoFilters,
    page = 1,
    limit = 20
  ): Promise<PaginatedResult<Anticipo>> {
    const qb = this.repository
      .createQueryBuilder('a')
      .where('a.tenantId = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('a.deletedAt IS NULL');

    if (filters.contratoId) {
      qb.andWhere('a.contratoId = :contratoId', { contratoId: filters.contratoId });
    }
    if (filters.advanceType) {
      qb.andWhere('a.advanceType = :advanceType', { advanceType: filters.advanceType });
    }
    if (filters.isFullyAmortized !== undefined) {
      qb.andWhere('a.isFullyAmortized = :isFullyAmortized', {
        isFullyAmortized: filters.isFullyAmortized,
      });
    }
    if (filters.startDate) {
      qb.andWhere('a.advanceDate >= :startDate', { startDate: filters.startDate });
    }
    if (filters.endDate) {
      qb.andWhere('a.advanceDate <= :endDate', { endDate: filters.endDate });
    }
    if (filters.approvedById) {
      qb.andWhere('a.approvedById = :approvedById', { approvedById: filters.approvedById });
    }

    const skip = (page - 1) * limit;
    qb.orderBy('a.advanceDate', 'DESC')
      .addOrderBy('a.advanceNumber', 'DESC')
      .skip(skip)
      .take(limit);

    const [data, total] = await qb.getManyAndCount();

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

  /**
   * Actualiza el monto amortizado de un anticipo
   */
  async updateAmortization(
    ctx: ServiceContext,
    id: string,
    amountToAmortize: number
  ): Promise<Anticipo> {
    const anticipo = await this.findById(ctx, id);
    if (!anticipo) {
      throw new Error('Anticipo no encontrado');
    }

    if (anticipo.isFullyAmortized) {
      throw new Error('El anticipo ya está completamente amortizado');
    }

    const currentAmortized = Number(anticipo.amortizedAmount) || 0;
    const netAmount = Number(anticipo.netAmount) || 0;
    const newAmortizedAmount = currentAmortized + amountToAmortize;

    if (newAmortizedAmount > netAmount) {
      throw new Error('El monto a amortizar excede el saldo del anticipo');
    }

    const isFullyAmortized = Math.abs(newAmortizedAmount - netAmount) < 0.01;

    anticipo.amortizedAmount = newAmortizedAmount;
    anticipo.isFullyAmortized = isFullyAmortized;
    anticipo.updatedById = ctx.userId ?? null;

    return this.repository.save(anticipo);
  }
}
