/**
 * DeductivaService - Gestion de Deductivas de Estimaciones
 *
 * Gestiona deductivas (penalizaciones) aplicadas a estimaciones:
 * - Trabajo no realizado
 * - Defectos de calidad
 * - Penalizaciones contractuales
 * - Ajustes de cantidad
 *
 * Las deductivas se almacenan como retenciones con retention_type = 'penalty'
 *
 * @module Estimates
 */

import { DataSource, Repository, IsNull } from 'typeorm';
import { Retencion, RetentionType } from '../entities/retencion.entity';
import { Estimacion } from '../entities/estimacion.entity';

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

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

/**
 * Tipos de deductiva
 */
export type DeductionType =
  | 'work_not_done'
  | 'quality_defect'
  | 'penalty'
  | 'quantity_adjustment'
  | 'other';

/**
 * DTO para crear una deductiva
 */
export interface CreateDeductivaDto {
  estimacionId: string;
  deductionType: DeductionType;
  description: string;
  amount: number;
  percentage?: number;
  relatedConceptCode?: string;
  justification: string;
  evidenceUrls?: string[];
  notes?: string;
}

/**
 * DTO para aprobar una deductiva
 */
export interface ApproveDeductivaDto {
  approvedAmount?: number;
  approvalNotes?: string;
}

/**
 * Filtros para buscar deductivas
 */
export interface DeductivaFilters {
  estimacionId?: string;
  contratoId?: string;
  deductionType?: DeductionType;
  isApproved?: boolean;
  dateFrom?: Date;
  dateTo?: Date;
  minAmount?: number;
  maxAmount?: number;
}

/**
 * Resumen de deductivas por contrato
 */
export interface DeductivaSummary {
  totalDeductivas: number;
  totalAmount: number;
  totalApproved: number;
  totalPending: number;
  byType: {
    type: DeductionType;
    count: number;
    amount: number;
  }[];
}

/**
 * Estadisticas de deductivas
 */
export interface DeductivaStats {
  totalCount: number;
  totalAmount: number;
  approvedCount: number;
  approvedAmount: number;
  pendingCount: number;
  pendingAmount: number;
  averageAmount: number;
  byType: Map<DeductionType, { count: number; amount: number }>;
  byEstimacion: Map<string, { count: number; amount: number }>;
}

/**
 * Mapeo de tipo de deduccion a descripcion
 */
const DEDUCTION_TYPE_DESCRIPTIONS: Record<DeductionType, string> = {
  work_not_done: 'Trabajo no realizado',
  quality_defect: 'Defecto de calidad',
  penalty: 'Penalizacion contractual',
  quantity_adjustment: 'Ajuste de cantidad',
  other: 'Otro',
};

export class DeductivaService {
  private retencionRepository: Repository<Retencion>;
  private estimacionRepository: Repository<Estimacion>;

  constructor(private readonly dataSource: DataSource) {
    this.retencionRepository = dataSource.getRepository(Retencion);
    this.estimacionRepository = dataSource.getRepository(Estimacion);
  }

  /**
   * Busca una deductiva por ID
   */
  async findById(ctx: ServiceContext, id: string): Promise<Retencion | null> {
    return this.retencionRepository.findOne({
      where: {
        id,
        tenantId: ctx.tenantId,
        retentionType: 'penalty' as RetentionType,
        deletedAt: IsNull(),
      },
      relations: ['estimacion'],
    });
  }

  /**
   * Obtener deductivas por estimacion
   */
  async findByEstimacion(
    ctx: ServiceContext,
    estimacionId: string
  ): Promise<Retencion[]> {
    return this.retencionRepository.find({
      where: {
        tenantId: ctx.tenantId,
        estimacionId,
        retentionType: 'penalty' as RetentionType,
        deletedAt: IsNull(),
      },
      order: { createdAt: 'DESC' },
    });
  }

  /**
   * Crear nueva deductiva
   */
  async createDeductiva(
    ctx: ServiceContext,
    dto: CreateDeductivaDto
  ): Promise<Retencion> {
    const estimacion = await this.estimacionRepository.findOne({
      where: {
        id: dto.estimacionId,
        tenantId: ctx.tenantId,
        deletedAt: IsNull(),
      },
    });

    if (!estimacion) {
      throw new Error('Estimacion no encontrada');
    }

    if (estimacion.status !== 'draft' && estimacion.status !== 'submitted') {
      throw new Error('Solo se pueden agregar deductivas a estimaciones en borrador o enviadas');
    }

    if (dto.amount <= 0) {
      throw new Error('El monto de la deductiva debe ser mayor a cero');
    }

    const typeDescription = DEDUCTION_TYPE_DESCRIPTIONS[dto.deductionType] || 'Deductiva';
    const fullDescription = `[${typeDescription}] ${dto.description}`;

    const notesContent = this.buildDeductivaNotesJson(dto);

    const deductiva = this.retencionRepository.create({
      tenantId: ctx.tenantId,
      estimacionId: dto.estimacionId,
      retentionType: 'penalty' as RetentionType,
      description: fullDescription,
      percentage: dto.percentage ?? null,
      amount: dto.amount,
      notes: notesContent,
      createdById: ctx.userId ?? null,
    });

    const saved = await this.retencionRepository.save(deductiva);

    await this.recalculateEstimacionTotals(dto.estimacionId);

    return saved;
  }

  /**
   * Construir notas JSON con metadatos de deductiva
   */
  private buildDeductivaNotesJson(dto: CreateDeductivaDto): string {
    const metadata = {
      deductionType: dto.deductionType,
      relatedConceptCode: dto.relatedConceptCode || null,
      justification: dto.justification,
      evidenceUrls: dto.evidenceUrls || [],
      userNotes: dto.notes || null,
      approvalStatus: 'pending',
      approvedAt: null,
      approvedBy: null,
      approvedAmount: null,
      approvalNotes: null,
    };
    return JSON.stringify(metadata);
  }

  /**
   * Parsear metadatos de notas JSON
   */
  private parseDeductivaMetadata(notes: string | null): DeductivaMetadata | null {
    if (!notes) return null;
    try {
      return JSON.parse(notes) as DeductivaMetadata;
    } catch {
      return null;
    }
  }

  /**
   * Actualizar una deductiva
   */
  async update(
    ctx: ServiceContext,
    id: string,
    data: Partial<CreateDeductivaDto>
  ): Promise<Retencion | null> {
    const deductiva = await this.findById(ctx, id);
    if (!deductiva) {
      return null;
    }

    const metadata = this.parseDeductivaMetadata(deductiva.notes);
    if (metadata?.approvalStatus === 'approved') {
      throw new Error('No se puede modificar una deductiva aprobada');
    }

    if (deductiva.estimacion.status !== 'draft' && deductiva.estimacion.status !== 'submitted') {
      throw new Error('Solo se pueden modificar deductivas de estimaciones en borrador o enviadas');
    }

    if (data.description) {
      const typeDescription = DEDUCTION_TYPE_DESCRIPTIONS[data.deductionType || 'other'];
      deductiva.description = `[${typeDescription}] ${data.description}`;
    }

    if (data.amount !== undefined) {
      if (data.amount <= 0) {
        throw new Error('El monto de la deductiva debe ser mayor a cero');
      }
      deductiva.amount = data.amount;
    }

    if (data.percentage !== undefined) {
      deductiva.percentage = data.percentage;
    }

    if (metadata) {
      if (data.deductionType) metadata.deductionType = data.deductionType;
      if (data.justification) metadata.justification = data.justification;
      if (data.relatedConceptCode !== undefined) metadata.relatedConceptCode = data.relatedConceptCode || null;
      if (data.evidenceUrls) metadata.evidenceUrls = data.evidenceUrls;
      if (data.notes !== undefined) metadata.userNotes = data.notes || null;
      deductiva.notes = JSON.stringify(metadata);
    }

    deductiva.updatedById = ctx.userId ?? null;
    const saved = await this.retencionRepository.save(deductiva);

    await this.recalculateEstimacionTotals(deductiva.estimacionId);

    return saved;
  }

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

    const metadata = this.parseDeductivaMetadata(deductiva.notes);
    if (metadata?.approvalStatus === 'approved') {
      throw new Error('No se puede eliminar una deductiva aprobada');
    }

    if (deductiva.estimacion.status !== 'draft' && deductiva.estimacion.status !== 'submitted') {
      throw new Error('Solo se pueden eliminar deductivas de estimaciones en borrador o enviadas');
    }

    const estimacionId = deductiva.estimacionId;

    deductiva.deletedAt = new Date();
    deductiva.deletedById = ctx.userId ?? null;
    await this.retencionRepository.save(deductiva);

    await this.recalculateEstimacionTotals(estimacionId);

    return true;
  }

  /**
   * Aprobar una deductiva
   */
  async approve(
    ctx: ServiceContext,
    id: string,
    dto?: ApproveDeductivaDto
  ): Promise<Retencion | null> {
    const deductiva = await this.findById(ctx, id);
    if (!deductiva) {
      return null;
    }

    const metadata = this.parseDeductivaMetadata(deductiva.notes);
    if (!metadata) {
      throw new Error('Metadatos de deductiva no encontrados');
    }

    if (metadata.approvalStatus === 'approved') {
      throw new Error('La deductiva ya esta aprobada');
    }

    metadata.approvalStatus = 'approved';
    metadata.approvedAt = new Date().toISOString();
    metadata.approvedBy = ctx.userId || null;

    if (dto?.approvedAmount !== undefined) {
      if (dto.approvedAmount < 0 || dto.approvedAmount > deductiva.amount) {
        throw new Error('El monto aprobado debe estar entre 0 y el monto original');
      }
      metadata.approvedAmount = dto.approvedAmount;
      deductiva.amount = dto.approvedAmount;
    } else {
      metadata.approvedAmount = deductiva.amount;
    }

    if (dto?.approvalNotes) {
      metadata.approvalNotes = dto.approvalNotes;
    }

    deductiva.notes = JSON.stringify(metadata);
    deductiva.updatedById = ctx.userId ?? null;
    const saved = await this.retencionRepository.save(deductiva);

    await this.recalculateEstimacionTotals(deductiva.estimacionId);

    return saved;
  }

  /**
   * Rechazar una deductiva
   */
  async reject(
    ctx: ServiceContext,
    id: string,
    reason: string
  ): Promise<Retencion | null> {
    const deductiva = await this.findById(ctx, id);
    if (!deductiva) {
      return null;
    }

    const metadata = this.parseDeductivaMetadata(deductiva.notes);
    if (!metadata) {
      throw new Error('Metadatos de deductiva no encontrados');
    }

    if (metadata.approvalStatus === 'approved') {
      throw new Error('No se puede rechazar una deductiva ya aprobada');
    }

    metadata.approvalStatus = 'rejected';
    metadata.approvedAt = new Date().toISOString();
    metadata.approvedBy = ctx.userId || null;
    metadata.approvedAmount = 0;
    metadata.approvalNotes = `Rechazada: ${reason}`;

    deductiva.amount = 0;
    deductiva.notes = JSON.stringify(metadata);
    deductiva.updatedById = ctx.userId ?? null;
    const saved = await this.retencionRepository.save(deductiva);

    await this.recalculateEstimacionTotals(deductiva.estimacionId);

    return saved;
  }

  /**
   * Obtener deductivas con filtros
   */
  async findWithFilters(
    ctx: ServiceContext,
    filters: DeductivaFilters,
    page = 1,
    limit = 20
  ): Promise<PaginatedResult<Retencion>> {
    const qb = this.retencionRepository
      .createQueryBuilder('r')
      .leftJoin('r.estimacion', 'e')
      .where('r.tenant_id = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('r.retention_type = :retentionType', { retentionType: 'penalty' })
      .andWhere('r.deleted_at IS NULL');

    if (filters.estimacionId) {
      qb.andWhere('r.estimacion_id = :estimacionId', { estimacionId: filters.estimacionId });
    }
    if (filters.contratoId) {
      qb.andWhere('e.contrato_id = :contratoId', { contratoId: filters.contratoId });
    }
    if (filters.dateFrom) {
      qb.andWhere('r.created_at >= :dateFrom', { dateFrom: filters.dateFrom });
    }
    if (filters.dateTo) {
      qb.andWhere('r.created_at <= :dateTo', { dateTo: filters.dateTo });
    }
    if (filters.minAmount !== undefined) {
      qb.andWhere('r.amount >= :minAmount', { minAmount: filters.minAmount });
    }
    if (filters.maxAmount !== undefined) {
      qb.andWhere('r.amount <= :maxAmount', { maxAmount: filters.maxAmount });
    }

    const skip = (page - 1) * limit;
    qb.orderBy('r.created_at', 'DESC').skip(skip).take(limit);

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

    let filteredData = data;
    if (filters.deductionType) {
      filteredData = data.filter((d) => {
        const metadata = this.parseDeductivaMetadata(d.notes);
        return metadata?.deductionType === filters.deductionType;
      });
    }
    if (filters.isApproved !== undefined) {
      filteredData = filteredData.filter((d) => {
        const metadata = this.parseDeductivaMetadata(d.notes);
        return filters.isApproved
          ? metadata?.approvalStatus === 'approved'
          : metadata?.approvalStatus !== 'approved';
      });
    }

    return {
      data: filteredData,
      total: filteredData.length !== data.length ? filteredData.length : total,
      page,
      limit,
      totalPages: Math.ceil((filteredData.length !== data.length ? filteredData.length : total) / limit),
    };
  }

  /**
   * Obtener resumen de deductivas por contrato
   */
  async getContractSummary(
    ctx: ServiceContext,
    contratoId: string
  ): Promise<DeductivaSummary> {
    const deductivas = await this.retencionRepository
      .createQueryBuilder('r')
      .leftJoin('r.estimacion', 'e')
      .where('r.tenant_id = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('e.contrato_id = :contratoId', { contratoId })
      .andWhere('r.retention_type = :retentionType', { retentionType: 'penalty' })
      .andWhere('r.deleted_at IS NULL')
      .getMany();

    const summary: DeductivaSummary = {
      totalDeductivas: deductivas.length,
      totalAmount: 0,
      totalApproved: 0,
      totalPending: 0,
      byType: [],
    };

    const byTypeMap = new Map<DeductionType, { count: number; amount: number }>();

    deductivas.forEach((d) => {
      const amount = Number(d.amount) || 0;
      summary.totalAmount += amount;

      const metadata = this.parseDeductivaMetadata(d.notes);
      if (metadata?.approvalStatus === 'approved') {
        summary.totalApproved += amount;
      } else {
        summary.totalPending += amount;
      }

      const deductionType = (metadata?.deductionType || 'other') as DeductionType;
      const existing = byTypeMap.get(deductionType) || { count: 0, amount: 0 };
      byTypeMap.set(deductionType, {
        count: existing.count + 1,
        amount: existing.amount + amount,
      });
    });

    summary.byType = Array.from(byTypeMap.entries()).map(([type, data]) => ({
      type,
      count: data.count,
      amount: data.amount,
    }));

    return summary;
  }

  /**
   * Obtener estadisticas detalladas de deductivas
   */
  async getStats(
    ctx: ServiceContext,
    filters?: DeductivaFilters
  ): Promise<DeductivaStats> {
    const qb = this.retencionRepository
      .createQueryBuilder('r')
      .leftJoin('r.estimacion', 'e')
      .where('r.tenant_id = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('r.retention_type = :retentionType', { retentionType: 'penalty' })
      .andWhere('r.deleted_at IS NULL');

    if (filters?.contratoId) {
      qb.andWhere('e.contrato_id = :contratoId', { contratoId: filters.contratoId });
    }
    if (filters?.dateFrom) {
      qb.andWhere('r.created_at >= :dateFrom', { dateFrom: filters.dateFrom });
    }
    if (filters?.dateTo) {
      qb.andWhere('r.created_at <= :dateTo', { dateTo: filters.dateTo });
    }

    const deductivas = await qb.getMany();

    const stats: DeductivaStats = {
      totalCount: deductivas.length,
      totalAmount: 0,
      approvedCount: 0,
      approvedAmount: 0,
      pendingCount: 0,
      pendingAmount: 0,
      averageAmount: 0,
      byType: new Map(),
      byEstimacion: new Map(),
    };

    deductivas.forEach((d) => {
      const amount = Number(d.amount) || 0;
      stats.totalAmount += amount;

      const metadata = this.parseDeductivaMetadata(d.notes);

      if (metadata?.approvalStatus === 'approved') {
        stats.approvedCount++;
        stats.approvedAmount += amount;
      } else {
        stats.pendingCount++;
        stats.pendingAmount += amount;
      }

      const deductionType = (metadata?.deductionType || 'other') as DeductionType;
      const typeStats = stats.byType.get(deductionType) || { count: 0, amount: 0 };
      stats.byType.set(deductionType, {
        count: typeStats.count + 1,
        amount: typeStats.amount + amount,
      });

      const estStats = stats.byEstimacion.get(d.estimacionId) || { count: 0, amount: 0 };
      stats.byEstimacion.set(d.estimacionId, {
        count: estStats.count + 1,
        amount: estStats.amount + amount,
      });
    });

    stats.averageAmount = stats.totalCount > 0 ? stats.totalAmount / stats.totalCount : 0;

    return stats;
  }

  /**
   * Obtener deductivas pendientes de aprobacion
   */
  async getPendingApproval(
    ctx: ServiceContext,
    contratoId?: string
  ): Promise<Retencion[]> {
    const qb = this.retencionRepository
      .createQueryBuilder('r')
      .leftJoinAndSelect('r.estimacion', 'e')
      .where('r.tenant_id = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('r.retention_type = :retentionType', { retentionType: 'penalty' })
      .andWhere('r.deleted_at IS NULL');

    if (contratoId) {
      qb.andWhere('e.contrato_id = :contratoId', { contratoId });
    }

    const all = await qb.orderBy('r.created_at', 'ASC').getMany();

    return all.filter((d) => {
      const metadata = this.parseDeductivaMetadata(d.notes);
      return metadata?.approvalStatus === 'pending';
    });
  }

  /**
   * Calcular deductiva por porcentaje del subtotal
   */
  async calculateByPercentage(
    ctx: ServiceContext,
    estimacionId: string,
    percentage: number
  ): Promise<{ calculatedAmount: number; estimacionSubtotal: number }> {
    const estimacion = await this.estimacionRepository.findOne({
      where: {
        id: estimacionId,
        tenantId: ctx.tenantId,
        deletedAt: IsNull(),
      },
    });

    if (!estimacion) {
      throw new Error('Estimacion no encontrada');
    }

    if (percentage < 0 || percentage > 100) {
      throw new Error('El porcentaje debe estar entre 0 y 100');
    }

    const subtotal = Number(estimacion.subtotal) || 0;
    const calculatedAmount = subtotal * (percentage / 100);

    return {
      calculatedAmount: Math.round(calculatedAmount * 100) / 100,
      estimacionSubtotal: subtotal,
    };
  }

  /**
   * Obtener deductiva con metadatos parseados
   */
  async findByIdWithMetadata(
    ctx: ServiceContext,
    id: string
  ): Promise<DeductivaWithMetadata | null> {
    const deductiva = await this.findById(ctx, id);
    if (!deductiva) {
      return null;
    }

    const metadata = this.parseDeductivaMetadata(deductiva.notes);

    return {
      ...deductiva,
      metadata: metadata || undefined,
    };
  }

  /**
   * Recalcular totales de estimacion despues de cambios en deductivas
   */
  private async recalculateEstimacionTotals(estimacionId: string): Promise<void> {
    await this.dataSource.query('SELECT estimates.calculate_estimate_totals($1)', [estimacionId]);
  }
}

/**
 * Interface para metadatos de deductiva almacenados en notas
 */
interface DeductivaMetadata {
  deductionType: DeductionType;
  relatedConceptCode: string | null;
  justification: string;
  evidenceUrls: string[];
  userNotes: string | null;
  approvalStatus: 'pending' | 'approved' | 'rejected';
  approvedAt: string | null;
  approvedBy: string | null;
  approvedAmount: number | null;
  approvalNotes: string | null;
}

/**
 * Interface para deductiva con metadatos parseados
 */
export interface DeductivaWithMetadata extends Retencion {
  metadata?: DeductivaMetadata;
}
