/**
 * HorasTrabajadasService - Servicio para registro de horas trabajadas
 *
 * RF-MAA017-008: Indicadores HSE - Horas Hombre Trabajadas
 *
 * @module HSE
 */

import { Repository, FindOptionsWhere, Between } from 'typeorm';
import { HorasTrabajadas, FuenteHoras } from '../entities/horas-trabajadas.entity';
import { ServiceContext, PaginatedResult } from '../../../shared/services/base.service';

export interface CreateHorasTrabajadasDto {
  fraccionamientoId: string;
  fecha: Date;
  horasTotales: number;
  trabajadoresPromedio: number;
  fuente?: FuenteHoras;
}

export interface UpdateHorasTrabajadasDto {
  horasTotales?: number;
  trabajadoresPromedio?: number;
  fuente?: FuenteHoras;
}

export interface HorasTrabajadasFilters {
  fraccionamientoId?: string;
  fuente?: FuenteHoras;
  fechaDesde?: Date;
  fechaHasta?: Date;
  year?: number;
  month?: number;
}

export interface HorasTrabajadasResumen {
  fraccionamientoId: string;
  fraccionamientoNombre?: string;
  periodo: string;
  totalHoras: number;
  promedioTrabajadores: number;
  diasRegistrados: number;
}

export interface HorasTrabajadasStats {
  totalHorasAcumuladas: number;
  horasUltimoMes: number;
  promedioTrabajadoresDiario: number;
  diasRegistrados: number;
  horasPromedioMensual: number;
}

export class HorasTrabajadasService {
  constructor(private readonly repository: Repository<HorasTrabajadas>) {}

  async findAll(
    ctx: ServiceContext,
    filters: HorasTrabajadasFilters = {},
    page: number = 1,
    limit: number = 50
  ): Promise<PaginatedResult<HorasTrabajadas>> {
    const skip = (page - 1) * limit;

    const queryBuilder = this.repository
      .createQueryBuilder('h')
      .leftJoinAndSelect('h.fraccionamiento', 'fraccionamiento')
      .where('h.tenant_id = :tenantId', { tenantId: ctx.tenantId });

    if (filters.fraccionamientoId) {
      queryBuilder.andWhere('h.fraccionamiento_id = :fraccionamientoId', {
        fraccionamientoId: filters.fraccionamientoId,
      });
    }

    if (filters.fuente) {
      queryBuilder.andWhere('h.fuente = :fuente', { fuente: filters.fuente });
    }

    if (filters.fechaDesde) {
      queryBuilder.andWhere('h.fecha >= :fechaDesde', { fechaDesde: filters.fechaDesde });
    }

    if (filters.fechaHasta) {
      queryBuilder.andWhere('h.fecha <= :fechaHasta', { fechaHasta: filters.fechaHasta });
    }

    if (filters.year) {
      queryBuilder.andWhere('EXTRACT(YEAR FROM h.fecha) = :year', { year: filters.year });
    }

    if (filters.month) {
      queryBuilder.andWhere('EXTRACT(MONTH FROM h.fecha) = :month', { month: filters.month });
    }

    queryBuilder
      .orderBy('h.fecha', 'DESC')
      .skip(skip)
      .take(limit);

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

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

  async findById(ctx: ServiceContext, id: string): Promise<HorasTrabajadas | null> {
    return this.repository.findOne({
      where: { id, tenantId: ctx.tenantId } as FindOptionsWhere<HorasTrabajadas>,
      relations: ['fraccionamiento'],
    });
  }

  async findByFecha(
    ctx: ServiceContext,
    fraccionamientoId: string,
    fecha: Date
  ): Promise<HorasTrabajadas | null> {
    return this.repository.findOne({
      where: {
        tenantId: ctx.tenantId,
        fraccionamientoId,
        fecha,
      } as FindOptionsWhere<HorasTrabajadas>,
    });
  }

  async create(ctx: ServiceContext, dto: CreateHorasTrabajadasDto): Promise<HorasTrabajadas> {
    const existing = await this.findByFecha(ctx, dto.fraccionamientoId, dto.fecha);
    if (existing) {
      throw new Error(`Ya existe un registro de horas para esta fecha y fraccionamiento`);
    }

    const horas = this.repository.create({
      tenantId: ctx.tenantId,
      fraccionamientoId: dto.fraccionamientoId,
      fecha: dto.fecha,
      horasTotales: dto.horasTotales,
      trabajadoresPromedio: dto.trabajadoresPromedio,
      fuente: dto.fuente || 'manual',
    });

    return this.repository.save(horas);
  }

  async upsert(ctx: ServiceContext, dto: CreateHorasTrabajadasDto): Promise<HorasTrabajadas> {
    const existing = await this.findByFecha(ctx, dto.fraccionamientoId, dto.fecha);

    if (existing) {
      existing.horasTotales = dto.horasTotales;
      existing.trabajadoresPromedio = dto.trabajadoresPromedio;
      existing.fuente = dto.fuente || existing.fuente;
      return this.repository.save(existing);
    }

    return this.create(ctx, dto);
  }

  async update(ctx: ServiceContext, id: string, dto: UpdateHorasTrabajadasDto): Promise<HorasTrabajadas | null> {
    const horas = await this.findById(ctx, id);
    if (!horas) return null;

    if (dto.horasTotales !== undefined) horas.horasTotales = dto.horasTotales;
    if (dto.trabajadoresPromedio !== undefined) horas.trabajadoresPromedio = dto.trabajadoresPromedio;
    if (dto.fuente) horas.fuente = dto.fuente;

    return this.repository.save(horas);
  }

  async delete(ctx: ServiceContext, id: string): Promise<boolean> {
    const horas = await this.findById(ctx, id);
    if (!horas) return false;

    await this.repository.remove(horas);
    return true;
  }

  // ========== Consultas por Período ==========
  async getHorasByPeriodo(
    ctx: ServiceContext,
    fraccionamientoId: string,
    fechaInicio: Date,
    fechaFin: Date
  ): Promise<HorasTrabajadas[]> {
    return this.repository.find({
      where: {
        tenantId: ctx.tenantId,
        fraccionamientoId,
        fecha: Between(fechaInicio, fechaFin),
      } as FindOptionsWhere<HorasTrabajadas>,
      order: { fecha: 'ASC' },
    });
  }

  async getTotalHorasByPeriodo(
    ctx: ServiceContext,
    fraccionamientoId: string,
    fechaInicio: Date,
    fechaFin: Date
  ): Promise<number> {
    const result = await this.repository
      .createQueryBuilder('h')
      .select('SUM(h.horas_totales)', 'total')
      .where('h.tenant_id = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('h.fraccionamiento_id = :fraccionamientoId', { fraccionamientoId })
      .andWhere('h.fecha >= :fechaInicio', { fechaInicio })
      .andWhere('h.fecha <= :fechaFin', { fechaFin })
      .getRawOne();

    return parseFloat(result?.total || '0');
  }

  async getHorasByYear(ctx: ServiceContext, fraccionamientoId: string, year: number): Promise<HorasTrabajadas[]> {
    return this.repository
      .createQueryBuilder('h')
      .where('h.tenant_id = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('h.fraccionamiento_id = :fraccionamientoId', { fraccionamientoId })
      .andWhere('EXTRACT(YEAR FROM h.fecha) = :year', { year })
      .orderBy('h.fecha', 'ASC')
      .getMany();
  }

  // ========== Resumen Mensual ==========
  async getResumenMensual(
    ctx: ServiceContext,
    fraccionamientoId: string,
    year: number
  ): Promise<HorasTrabajadasResumen[]> {
    const result = await this.repository
      .createQueryBuilder('h')
      .select([
        'h.fraccionamiento_id AS "fraccionamientoId"',
        "TO_CHAR(h.fecha, 'YYYY-MM') AS periodo",
        'SUM(h.horas_totales) AS "totalHoras"',
        'AVG(h.trabajadores_promedio) AS "promedioTrabajadores"',
        'COUNT(*) AS "diasRegistrados"',
      ])
      .where('h.tenant_id = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('h.fraccionamiento_id = :fraccionamientoId', { fraccionamientoId })
      .andWhere('EXTRACT(YEAR FROM h.fecha) = :year', { year })
      .groupBy('h.fraccionamiento_id')
      .addGroupBy("TO_CHAR(h.fecha, 'YYYY-MM')")
      .orderBy('periodo', 'ASC')
      .getRawMany();

    return result.map((r) => ({
      fraccionamientoId: r.fraccionamientoId,
      periodo: r.periodo,
      totalHoras: parseFloat(r.totalHoras || '0'),
      promedioTrabajadores: Math.round(parseFloat(r.promedioTrabajadores || '0')),
      diasRegistrados: parseInt(r.diasRegistrados, 10),
    }));
  }

  // ========== Acumulados ==========
  async getHorasAcumuladasYear(ctx: ServiceContext, fraccionamientoId: string, year: number): Promise<number> {
    const result = await this.repository
      .createQueryBuilder('h')
      .select('SUM(h.horas_totales)', 'total')
      .where('h.tenant_id = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('h.fraccionamiento_id = :fraccionamientoId', { fraccionamientoId })
      .andWhere('EXTRACT(YEAR FROM h.fecha) = :year', { year })
      .getRawOne();

    return parseFloat(result?.total || '0');
  }

  async getHorasAcumuladasHistorico(ctx: ServiceContext, fraccionamientoId: string): Promise<number> {
    const result = await this.repository
      .createQueryBuilder('h')
      .select('SUM(h.horas_totales)', 'total')
      .where('h.tenant_id = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('h.fraccionamiento_id = :fraccionamientoId', { fraccionamientoId })
      .getRawOne();

    return parseFloat(result?.total || '0');
  }

  // ========== Estadísticas ==========
  async getStats(ctx: ServiceContext, fraccionamientoId: string): Promise<HorasTrabajadasStats> {
    const today = new Date();
    const ultimoDiaMesAnterior = new Date(today.getFullYear(), today.getMonth(), 0);
    const primerDiaMesAnterior = new Date(today.getFullYear(), today.getMonth() - 1, 1);

    const totalHorasAcumuladas = await this.getHorasAcumuladasHistorico(ctx, fraccionamientoId);

    const horasUltimoMes = await this.getTotalHorasByPeriodo(
      ctx,
      fraccionamientoId,
      primerDiaMesAnterior,
      ultimoDiaMesAnterior
    );

    const registros = await this.repository
      .createQueryBuilder('h')
      .select([
        'AVG(h.trabajadores_promedio) AS "promedioTrabajadores"',
        'COUNT(*) AS "diasRegistrados"',
      ])
      .where('h.tenant_id = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('h.fraccionamiento_id = :fraccionamientoId', { fraccionamientoId })
      .getRawOne();

    const promedioTrabajadoresDiario = Math.round(parseFloat(registros?.promedioTrabajadores || '0'));
    const diasRegistrados = parseInt(registros?.diasRegistrados || '0', 10);

    const mesesConDatos = await this.repository
      .createQueryBuilder('h')
      .select("COUNT(DISTINCT TO_CHAR(h.fecha, 'YYYY-MM'))", 'meses')
      .where('h.tenant_id = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('h.fraccionamiento_id = :fraccionamientoId', { fraccionamientoId })
      .getRawOne();

    const numMeses = parseInt(mesesConDatos?.meses || '1', 10);
    const horasPromedioMensual = numMeses > 0 ? Math.round(totalHorasAcumuladas / numMeses) : 0;

    return {
      totalHorasAcumuladas,
      horasUltimoMes,
      promedioTrabajadoresDiario,
      diasRegistrados,
      horasPromedioMensual,
    };
  }

  // ========== Importación desde Asistencia ==========
  async importarDesdeAsistencia(
    ctx: ServiceContext,
    fraccionamientoId: string,
    fecha: Date,
    horasTotales: number,
    trabajadoresPromedio: number
  ): Promise<HorasTrabajadas> {
    return this.upsert(ctx, {
      fraccionamientoId,
      fecha,
      horasTotales,
      trabajadoresPromedio,
      fuente: 'asistencia',
    });
  }

  // ========== Cálculo de Índices ==========
  async calcularIndiceIncidencia(
    ctx: ServiceContext,
    fraccionamientoId: string,
    numeroIncidentes: number,
    fechaInicio: Date,
    fechaFin: Date
  ): Promise<number> {
    const totalHoras = await this.getTotalHorasByPeriodo(ctx, fraccionamientoId, fechaInicio, fechaFin);
    if (totalHoras === 0) return 0;

    return (numeroIncidentes * 1000000) / totalHoras;
  }

  async calcularIndiceFrecuencia(
    ctx: ServiceContext,
    fraccionamientoId: string,
    accidentesConBaja: number,
    fechaInicio: Date,
    fechaFin: Date
  ): Promise<number> {
    const totalHoras = await this.getTotalHorasByPeriodo(ctx, fraccionamientoId, fechaInicio, fechaFin);
    if (totalHoras === 0) return 0;

    return (accidentesConBaja * 1000000) / totalHoras;
  }

  async calcularIndiceGravedad(
    ctx: ServiceContext,
    fraccionamientoId: string,
    diasPerdidos: number,
    fechaInicio: Date,
    fechaFin: Date
  ): Promise<number> {
    const totalHoras = await this.getTotalHorasByPeriodo(ctx, fraccionamientoId, fechaInicio, fechaFin);
    if (totalHoras === 0) return 0;

    return (diasPerdidos * 1000000) / totalHoras;
  }
}
