/**
 * LeaveAllocations Service
 * @module HR
 */

import { Repository, FindOptionsWhere, Between, LessThanOrEqual, MoreThanOrEqual } from 'typeorm';
import { LeaveAllocation } from '../entities/leave-allocation.entity';
import { CreateLeaveAllocationDto, UpdateLeaveAllocationDto, AdjustLeaveAllocationDto } from '../dto';

export interface LeaveAllocationSearchParams {
  tenantId: string;
  employeeId?: string;
  leaveTypeId?: string;
  dateFrom?: Date;
  dateTo?: Date;
  hasRemainingDays?: boolean;
  limit?: number;
  offset?: number;
}

export class LeaveAllocationsService {
  constructor(private readonly allocationRepository: Repository<LeaveAllocation>) {}

  /**
   * Find all allocations with filters
   */
  async findAll(params: LeaveAllocationSearchParams): Promise<{ data: LeaveAllocation[]; total: number }> {
    const {
      tenantId,
      employeeId,
      leaveTypeId,
      dateFrom,
      dateTo,
      hasRemainingDays,
      limit = 50,
      offset = 0,
    } = params;

    const queryBuilder = this.allocationRepository
      .createQueryBuilder('allocation')
      .leftJoinAndSelect('allocation.leaveType', 'leaveType')
      .where('allocation.tenant_id = :tenantId', { tenantId });

    if (employeeId) {
      queryBuilder.andWhere('allocation.employee_id = :employeeId', { employeeId });
    }

    if (leaveTypeId) {
      queryBuilder.andWhere('allocation.leave_type_id = :leaveTypeId', { leaveTypeId });
    }

    if (dateFrom) {
      queryBuilder.andWhere('allocation.date_to >= :dateFrom', { dateFrom });
    }

    if (dateTo) {
      queryBuilder.andWhere('allocation.date_from <= :dateTo', { dateTo });
    }

    if (hasRemainingDays !== undefined) {
      if (hasRemainingDays) {
        queryBuilder.andWhere('allocation.days_remaining > 0');
      } else {
        queryBuilder.andWhere('allocation.days_remaining <= 0');
      }
    }

    queryBuilder.orderBy('allocation.date_from', 'DESC');

    const total = await queryBuilder.getCount();
    const data = await queryBuilder.skip(offset).take(limit).getMany();

    return { data, total };
  }

  /**
   * Find one allocation by ID
   */
  async findOne(id: string, tenantId: string): Promise<LeaveAllocation | null> {
    return this.allocationRepository.findOne({
      where: { id, tenantId },
      relations: ['leaveType'],
    });
  }

  /**
   * Find active allocation for employee and leave type
   */
  async findActiveAllocation(
    employeeId: string,
    leaveTypeId: string,
    tenantId: string,
    date?: Date
  ): Promise<LeaveAllocation | null> {
    const targetDate = date || new Date();

    return this.allocationRepository.findOne({
      where: {
        employeeId,
        leaveTypeId,
        tenantId,
        dateFrom: LessThanOrEqual(targetDate),
        dateTo: MoreThanOrEqual(targetDate),
      },
      relations: ['leaveType'],
    });
  }

  /**
   * Find all allocations for employee
   */
  async findByEmployee(employeeId: string, tenantId: string): Promise<LeaveAllocation[]> {
    return this.allocationRepository.find({
      where: { employeeId, tenantId },
      relations: ['leaveType'],
      order: { dateFrom: 'DESC' },
    });
  }

  /**
   * Find current allocations for employee
   */
  async findCurrentByEmployee(employeeId: string, tenantId: string): Promise<LeaveAllocation[]> {
    const today = new Date();

    return this.allocationRepository.find({
      where: {
        employeeId,
        tenantId,
        dateFrom: LessThanOrEqual(today),
        dateTo: MoreThanOrEqual(today),
      },
      relations: ['leaveType'],
      order: { dateFrom: 'DESC' },
    });
  }

  /**
   * Create new allocation
   */
  async create(tenantId: string, dto: CreateLeaveAllocationDto): Promise<LeaveAllocation> {
    // Check for overlapping allocation
    const dateFrom = new Date(dto.dateFrom);
    const dateTo = new Date(dto.dateTo);

    const overlapping = await this.allocationRepository.findOne({
      where: {
        employeeId: dto.employeeId,
        leaveTypeId: dto.leaveTypeId,
        tenantId,
        dateFrom: LessThanOrEqual(dateTo),
        dateTo: MoreThanOrEqual(dateFrom),
      },
    });

    if (overlapping) {
      throw new Error('Ya existe una asignacion para este tipo de ausencia en el periodo especificado');
    }

    const allocation = this.allocationRepository.create({
      ...dto,
      tenantId,
      dateFrom,
      dateTo,
      daysUsed: 0,
    });

    return this.allocationRepository.save(allocation);
  }

  /**
   * Update allocation
   */
  async update(id: string, tenantId: string, dto: UpdateLeaveAllocationDto): Promise<LeaveAllocation | null> {
    const allocation = await this.findOne(id, tenantId);
    if (!allocation) return null;

    // If changing dates, check for overlapping
    if (dto.dateFrom || dto.dateTo) {
      const dateFrom = dto.dateFrom ? new Date(dto.dateFrom) : allocation.dateFrom;
      const dateTo = dto.dateTo ? new Date(dto.dateTo) : allocation.dateTo;

      const overlapping = await this.allocationRepository.findOne({
        where: {
          employeeId: allocation.employeeId,
          leaveTypeId: allocation.leaveTypeId,
          tenantId,
          dateFrom: LessThanOrEqual(dateTo),
          dateTo: MoreThanOrEqual(dateFrom),
        },
      });

      if (overlapping && overlapping.id !== id) {
        throw new Error('Ya existe una asignacion para este tipo de ausencia en el periodo especificado');
      }
    }

    Object.assign(allocation, {
      ...dto,
      dateFrom: dto.dateFrom ? new Date(dto.dateFrom) : allocation.dateFrom,
      dateTo: dto.dateTo ? new Date(dto.dateTo) : allocation.dateTo,
    });

    return this.allocationRepository.save(allocation);
  }

  /**
   * Delete allocation
   */
  async delete(id: string, tenantId: string): Promise<boolean> {
    const allocation = await this.findOne(id, tenantId);
    if (!allocation) return false;

    // Cannot delete if days have been used
    if (allocation.daysUsed > 0) {
      throw new Error('No se puede eliminar una asignacion con dias utilizados');
    }

    const result = await this.allocationRepository.delete(id);
    return (result.affected ?? 0) > 0;
  }

  /**
   * Adjust allocated days
   */
  async adjustDays(
    id: string,
    tenantId: string,
    dto: AdjustLeaveAllocationDto
  ): Promise<LeaveAllocation | null> {
    const allocation = await this.findOne(id, tenantId);
    if (!allocation) return null;

    const newAllocated = Number(allocation.daysAllocated) + dto.adjustment;

    if (newAllocated < Number(allocation.daysUsed)) {
      throw new Error('Los dias asignados no pueden ser menores que los dias utilizados');
    }

    allocation.daysAllocated = newAllocated;
    if (dto.reason) {
      allocation.notes = allocation.notes
        ? `${allocation.notes}\n[${new Date().toISOString()}] Ajuste: ${dto.adjustment} dias. Motivo: ${dto.reason}`
        : `[${new Date().toISOString()}] Ajuste: ${dto.adjustment} dias. Motivo: ${dto.reason}`;
    }

    return this.allocationRepository.save(allocation);
  }

  /**
   * Use days from allocation (called when leave is approved)
   */
  async useDays(id: string, tenantId: string, days: number): Promise<LeaveAllocation | null> {
    const allocation = await this.findOne(id, tenantId);
    if (!allocation) return null;

    const remaining = Number(allocation.daysAllocated) - Number(allocation.daysUsed);
    if (days > remaining) {
      throw new Error(`Dias insuficientes. Disponibles: ${remaining}, Solicitados: ${days}`);
    }

    allocation.daysUsed = Number(allocation.daysUsed) + days;
    return this.allocationRepository.save(allocation);
  }

  /**
   * Return days to allocation (called when leave is cancelled)
   */
  async returnDays(id: string, tenantId: string, days: number): Promise<LeaveAllocation | null> {
    const allocation = await this.findOne(id, tenantId);
    if (!allocation) return null;

    const newUsed = Number(allocation.daysUsed) - days;
    allocation.daysUsed = Math.max(0, newUsed);

    return this.allocationRepository.save(allocation);
  }

  /**
   * Get remaining days for employee by leave type
   */
  async getRemainingDays(
    employeeId: string,
    leaveTypeId: string,
    tenantId: string
  ): Promise<number> {
    const allocation = await this.findActiveAllocation(employeeId, leaveTypeId, tenantId);
    if (!allocation) return 0;

    return Number(allocation.daysAllocated) - Number(allocation.daysUsed);
  }

  /**
   * Get all remaining days for employee
   */
  async getAllRemainingDaysForEmployee(
    employeeId: string,
    tenantId: string
  ): Promise<Array<{ leaveTypeId: string; leaveTypeName: string; daysRemaining: number }>> {
    const allocations = await this.findCurrentByEmployee(employeeId, tenantId);

    return allocations.map(allocation => ({
      leaveTypeId: allocation.leaveTypeId,
      leaveTypeName: allocation.leaveType?.name || '',
      daysRemaining: Number(allocation.daysAllocated) - Number(allocation.daysUsed),
    }));
  }

  /**
   * Get expiring allocations
   */
  async getExpiring(tenantId: string, daysAhead: number = 30): Promise<LeaveAllocation[]> {
    const today = new Date();
    const futureDate = new Date(today.getTime() + daysAhead * 24 * 60 * 60 * 1000);

    return this.allocationRepository.find({
      where: {
        tenantId,
        dateTo: Between(today, futureDate),
      },
      relations: ['leaveType'],
      order: { dateTo: 'ASC' },
    });
  }

  /**
   * Get allocations with unused days (expiring soon)
   */
  async getUnusedExpiring(tenantId: string, daysAhead: number = 30): Promise<LeaveAllocation[]> {
    const today = new Date();
    const futureDate = new Date(today.getTime() + daysAhead * 24 * 60 * 60 * 1000);

    return this.allocationRepository
      .createQueryBuilder('allocation')
      .leftJoinAndSelect('allocation.leaveType', 'leaveType')
      .where('allocation.tenant_id = :tenantId', { tenantId })
      .andWhere('allocation.date_to BETWEEN :today AND :futureDate', { today, futureDate })
      .andWhere('allocation.days_remaining > 0')
      .orderBy('allocation.date_to', 'ASC')
      .getMany();
  }

  /**
   * Bulk create allocations for multiple employees
   */
  async bulkCreate(
    tenantId: string,
    employeeIds: string[],
    leaveTypeId: string,
    daysAllocated: number,
    dateFrom: Date,
    dateTo: Date,
    notes?: string
  ): Promise<LeaveAllocation[]> {
    const allocations: LeaveAllocation[] = [];

    for (const employeeId of employeeIds) {
      // Check for overlapping
      const overlapping = await this.allocationRepository.findOne({
        where: {
          employeeId,
          leaveTypeId,
          tenantId,
          dateFrom: LessThanOrEqual(dateTo),
          dateTo: MoreThanOrEqual(dateFrom),
        },
      });

      if (!overlapping) {
        const allocation = this.allocationRepository.create({
          employeeId,
          leaveTypeId,
          tenantId,
          daysAllocated,
          daysUsed: 0,
          dateFrom,
          dateTo,
          notes,
        });
        allocations.push(allocation);
      }
    }

    if (allocations.length > 0) {
      return this.allocationRepository.save(allocations);
    }

    return [];
  }
}
