/**
 * Leaves Service
 * @module HR
 */

import { Repository, FindOptionsWhere, Between, LessThanOrEqual, MoreThanOrEqual, In } from 'typeorm';
import { Leave, LeaveStatus } from '../entities/leave.entity';
import { LeaveAllocation } from '../entities/leave-allocation.entity';
import { LeaveType } from '../entities/leave-type.entity';
import {
  CreateLeaveDto,
  UpdateLeaveDto,
  SubmitLeaveDto,
  ApproveLeaveDto,
  RejectLeaveDto,
  CancelLeaveDto,
} from '../dto';

export interface LeaveSearchParams {
  tenantId: string;
  companyId?: string;
  employeeId?: string;
  leaveTypeId?: string;
  status?: LeaveStatus;
  approverId?: string;
  dateFrom?: Date;
  dateTo?: Date;
  limit?: number;
  offset?: number;
}

export class LeavesService {
  constructor(
    private readonly leaveRepository: Repository<Leave>,
    private readonly allocationRepository: Repository<LeaveAllocation>,
    private readonly leaveTypeRepository: Repository<LeaveType>
  ) {}

  /**
   * Find all leaves with filters
   */
  async findAll(params: LeaveSearchParams): Promise<{ data: Leave[]; total: number }> {
    const {
      tenantId,
      companyId,
      employeeId,
      leaveTypeId,
      status,
      approverId,
      dateFrom,
      dateTo,
      limit = 50,
      offset = 0,
    } = params;

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

    if (companyId) {
      queryBuilder.andWhere('leave.company_id = :companyId', { companyId });
    }

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

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

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

    if (approverId) {
      queryBuilder.andWhere('leave.approver_id = :approverId', { approverId });
    }

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

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

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

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

    return { data, total };
  }

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

  /**
   * Find leaves by employee
   */
  async findByEmployee(employeeId: string, tenantId: string): Promise<Leave[]> {
    return this.leaveRepository.find({
      where: { employeeId, tenantId },
      relations: ['leaveType'],
      order: { dateFrom: 'DESC' },
    });
  }

  /**
   * Find pending leaves for approval
   */
  async findPendingApproval(tenantId: string, approverId?: string): Promise<Leave[]> {
    const where: FindOptionsWhere<Leave> = {
      tenantId,
      status: 'submitted',
    };

    if (approverId) {
      where.approverId = approverId;
    }

    return this.leaveRepository.find({
      where,
      relations: ['leaveType'],
      order: { submittedAt: 'ASC' },
    });
  }

  /**
   * Create new leave request
   */
  async create(tenantId: string, dto: CreateLeaveDto): Promise<Leave> {
    const dateFrom = new Date(dto.dateFrom);
    const dateTo = new Date(dto.dateTo);

    // Validate date range
    if (dateFrom > dateTo) {
      throw new Error('La fecha de inicio no puede ser posterior a la fecha de fin');
    }

    // Check for overlapping leaves
    const overlapping = await this.leaveRepository.findOne({
      where: {
        employeeId: dto.employeeId,
        tenantId,
        status: In(['draft', 'submitted', 'approved']),
        dateFrom: LessThanOrEqual(dateTo),
        dateTo: MoreThanOrEqual(dateFrom),
      },
    });

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

    // Find allocation if not specified
    let allocationId = dto.allocationId;
    if (!allocationId) {
      const allocation = await this.allocationRepository.findOne({
        where: {
          employeeId: dto.employeeId,
          leaveTypeId: dto.leaveTypeId,
          tenantId,
          dateFrom: LessThanOrEqual(dateFrom),
          dateTo: MoreThanOrEqual(dateFrom),
        },
      });
      allocationId = allocation?.id;
    }

    const leave = this.leaveRepository.create({
      ...dto,
      tenantId,
      allocationId: allocationId || null,
      dateFrom,
      dateTo,
      status: 'draft',
    });

    return this.leaveRepository.save(leave);
  }

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

    if (leave.status !== 'draft') {
      throw new Error('Solo se pueden modificar solicitudes en borrador');
    }

    const dateFrom = dto.dateFrom ? new Date(dto.dateFrom) : leave.dateFrom;
    const dateTo = dto.dateTo ? new Date(dto.dateTo) : leave.dateTo;

    // Check for overlapping if dates changed
    if (dto.dateFrom || dto.dateTo) {
      const overlapping = await this.leaveRepository.findOne({
        where: {
          employeeId: leave.employeeId,
          tenantId,
          status: In(['draft', 'submitted', 'approved']),
          dateFrom: LessThanOrEqual(dateTo),
          dateTo: MoreThanOrEqual(dateFrom),
        },
      });

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

    Object.assign(leave, {
      ...dto,
      dateFrom,
      dateTo,
    });

    return this.leaveRepository.save(leave);
  }

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

    if (leave.status !== 'draft') {
      throw new Error('Solo se pueden eliminar solicitudes en borrador');
    }

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

  /**
   * Submit leave for approval
   */
  async submit(id: string, tenantId: string, dto?: SubmitLeaveDto): Promise<Leave | null> {
    const leave = await this.findOne(id, tenantId);
    if (!leave) return null;

    if (leave.status !== 'draft') {
      throw new Error('Solo se pueden enviar solicitudes en borrador');
    }

    // Validate leave type requirements
    const leaveType = await this.leaveTypeRepository.findOne({
      where: { id: leave.leaveTypeId, tenantId },
    });

    if (leaveType) {
      // Check minimum notice
      if (leaveType.minDaysNotice > 0) {
        const today = new Date();
        const daysUntilLeave = Math.floor(
          (leave.dateFrom.getTime() - today.getTime()) / (1000 * 60 * 60 * 24)
        );
        if (daysUntilLeave < leaveType.minDaysNotice) {
          throw new Error(`Se requieren al menos ${leaveType.minDaysNotice} dias de anticipacion`);
        }
      }

      // Check max days per request
      if (leaveType.maxDaysPerRequest && leave.daysRequested > leaveType.maxDaysPerRequest) {
        throw new Error(`El maximo de dias por solicitud es ${leaveType.maxDaysPerRequest}`);
      }

      // Check if document is required
      if (leaveType.requiresDocument && !leave.documentUrl) {
        throw new Error('Este tipo de ausencia requiere documentacion');
      }
    }

    // Check allocation has enough days
    if (leave.allocationId) {
      const allocation = await this.allocationRepository.findOne({
        where: { id: leave.allocationId, tenantId },
      });
      if (allocation) {
        const remaining = Number(allocation.daysAllocated) - Number(allocation.daysUsed);
        if (remaining < leave.daysRequested) {
          throw new Error(`Dias insuficientes. Disponibles: ${remaining}`);
        }
      }
    }

    leave.status = 'submitted';
    leave.submittedAt = new Date();
    if (dto?.requestReason) {
      leave.requestReason = dto.requestReason;
    }

    return this.leaveRepository.save(leave);
  }

  /**
   * Approve leave
   */
  async approve(
    id: string,
    tenantId: string,
    approverId: string,
    dto?: ApproveLeaveDto
  ): Promise<Leave | null> {
    const leave = await this.findOne(id, tenantId);
    if (!leave) return null;

    if (leave.status !== 'submitted') {
      throw new Error('Solo se pueden aprobar solicitudes enviadas');
    }

    // Use days from allocation
    if (leave.allocationId) {
      const allocation = await this.allocationRepository.findOne({
        where: { id: leave.allocationId, tenantId },
      });
      if (allocation) {
        const remaining = Number(allocation.daysAllocated) - Number(allocation.daysUsed);
        if (remaining < leave.daysRequested) {
          throw new Error(`Dias insuficientes en la asignacion. Disponibles: ${remaining}`);
        }
        allocation.daysUsed = Number(allocation.daysUsed) + Number(leave.daysRequested);
        await this.allocationRepository.save(allocation);
      }
    }

    leave.status = 'approved';
    leave.approverId = approverId;
    leave.approvedAt = new Date();
    if (dto?.notes) {
      leave.notes = dto.notes;
    }

    return this.leaveRepository.save(leave);
  }

  /**
   * Reject leave
   */
  async reject(
    id: string,
    tenantId: string,
    approverId: string,
    dto: RejectLeaveDto
  ): Promise<Leave | null> {
    const leave = await this.findOne(id, tenantId);
    if (!leave) return null;

    if (leave.status !== 'submitted') {
      throw new Error('Solo se pueden rechazar solicitudes enviadas');
    }

    leave.status = 'rejected';
    leave.approverId = approverId;
    leave.rejectionReason = dto.rejectionReason;

    return this.leaveRepository.save(leave);
  }

  /**
   * Cancel leave
   */
  async cancel(
    id: string,
    tenantId: string,
    cancelledBy: string,
    dto?: CancelLeaveDto
  ): Promise<Leave | null> {
    const leave = await this.findOne(id, tenantId);
    if (!leave) return null;

    if (!['draft', 'submitted', 'approved'].includes(leave.status)) {
      throw new Error('Solo se pueden cancelar solicitudes en borrador, enviadas o aprobadas');
    }

    // If was approved, return days to allocation
    if (leave.status === 'approved' && leave.allocationId) {
      const allocation = await this.allocationRepository.findOne({
        where: { id: leave.allocationId, tenantId },
      });
      if (allocation) {
        allocation.daysUsed = Math.max(0, Number(allocation.daysUsed) - Number(leave.daysRequested));
        await this.allocationRepository.save(allocation);
      }
    }

    leave.status = 'cancelled';
    leave.cancelledAt = new Date();
    leave.cancelledBy = cancelledBy;
    if (dto?.cancellationReason) {
      leave.notes = leave.notes
        ? `${leave.notes}\nCancelacion: ${dto.cancellationReason}`
        : `Cancelacion: ${dto.cancellationReason}`;
    }

    return this.leaveRepository.save(leave);
  }

  /**
   * Get leaves by date range
   */
  async getByDateRange(
    tenantId: string,
    dateFrom: Date,
    dateTo: Date,
    employeeId?: string
  ): Promise<Leave[]> {
    const where: FindOptionsWhere<Leave> = {
      tenantId,
      status: 'approved',
      dateFrom: LessThanOrEqual(dateTo),
      dateTo: MoreThanOrEqual(dateFrom),
    };

    if (employeeId) {
      where.employeeId = employeeId;
    }

    return this.leaveRepository.find({
      where,
      relations: ['leaveType'],
      order: { dateFrom: 'ASC' },
    });
  }

  /**
   * Get count by status
   */
  async getCountByStatus(tenantId: string): Promise<Record<string, number>> {
    const result = await this.leaveRepository
      .createQueryBuilder('leave')
      .select('leave.status', 'status')
      .addSelect('COUNT(*)', 'count')
      .where('leave.tenant_id = :tenantId', { tenantId })
      .groupBy('leave.status')
      .getRawMany();

    return result.reduce((acc, row) => {
      acc[row.status] = parseInt(row.count, 10);
      return acc;
    }, {} as Record<string, number>);
  }

  /**
   * Get upcoming approved leaves
   */
  async getUpcoming(tenantId: string, daysAhead: number = 30): Promise<Leave[]> {
    const today = new Date();
    const futureDate = new Date(today.getTime() + daysAhead * 24 * 60 * 60 * 1000);

    return this.leaveRepository.find({
      where: {
        tenantId,
        status: 'approved',
        dateFrom: Between(today, futureDate),
      },
      relations: ['leaveType'],
      order: { dateFrom: 'ASC' },
    });
  }

  /**
   * Get employees on leave today
   */
  async getOnLeaveToday(tenantId: string): Promise<Leave[]> {
    const today = new Date();

    return this.leaveRepository.find({
      where: {
        tenantId,
        status: 'approved',
        dateFrom: LessThanOrEqual(today),
        dateTo: MoreThanOrEqual(today),
      },
      relations: ['leaveType'],
    });
  }

  /**
   * Check if employee is on leave on a specific date
   */
  async isOnLeave(employeeId: string, tenantId: string, date: Date): Promise<boolean> {
    const leave = await this.leaveRepository.findOne({
      where: {
        employeeId,
        tenantId,
        status: 'approved',
        dateFrom: LessThanOrEqual(date),
        dateTo: MoreThanOrEqual(date),
      },
    });

    return leave !== null;
  }

  /**
   * Get leave history for employee
   */
  async getEmployeeHistory(
    employeeId: string,
    tenantId: string,
    year?: number
  ): Promise<Leave[]> {
    const queryBuilder = this.leaveRepository
      .createQueryBuilder('leave')
      .leftJoinAndSelect('leave.leaveType', 'leaveType')
      .where('leave.employee_id = :employeeId', { employeeId })
      .andWhere('leave.tenant_id = :tenantId', { tenantId });

    if (year) {
      queryBuilder.andWhere('EXTRACT(YEAR FROM leave.date_from) = :year', { year });
    }

    return queryBuilder.orderBy('leave.date_from', 'DESC').getMany();
  }

  /**
   * Get leave summary for employee
   */
  async getEmployeeSummary(
    employeeId: string,
    tenantId: string,
    year?: number
  ): Promise<Array<{ leaveTypeId: string; leaveTypeName: string; daysUsed: number }>> {
    const queryBuilder = this.leaveRepository
      .createQueryBuilder('leave')
      .leftJoin('leave.leaveType', 'leaveType')
      .select('leave.leave_type_id', 'leaveTypeId')
      .addSelect('leaveType.name', 'leaveTypeName')
      .addSelect('SUM(leave.days_requested)', 'daysUsed')
      .where('leave.employee_id = :employeeId', { employeeId })
      .andWhere('leave.tenant_id = :tenantId', { tenantId })
      .andWhere('leave.status = :status', { status: 'approved' });

    if (year) {
      queryBuilder.andWhere('EXTRACT(YEAR FROM leave.date_from) = :year', { year });
    }

    const result = await queryBuilder
      .groupBy('leave.leave_type_id')
      .addGroupBy('leaveType.name')
      .getRawMany();

    return result.map(row => ({
      leaveTypeId: row.leaveTypeId,
      leaveTypeName: row.leaveTypeName || '',
      daysUsed: parseFloat(row.daysUsed) || 0,
    }));
  }
}
