import {
  Injectable,
  BadRequestException,
  NotFoundException,
  ForbiddenException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, LessThanOrEqual, MoreThanOrEqual, ILike } from 'typeorm';

import { LeaveRequest } from './leave-request.entity';
import { LeaveStatus, LeaveType } from 'common/enums/leaves.enum';

import { LeaveBalance } from './leave-balance.entity';
import { User } from '../users/user.entity';
import { UserRole, EmploymentType, WorkMode } from 'common/enums/user.enum';

import { CreateLeaveRequestDto } from './dto/create-leave-request.dto';

import { NotificationType } from '../notifications/notification.entity';
import { NotificationsService } from 'src/notifications/notifications.service';

import { Cron } from '@nestjs/schedule';
import { PaginationQueryDto } from 'common/dto/pagination-query.dto';
import { LEAVE_BALANCES } from 'common/leave-config';

import { Attendance } from 'src/attendance/attendance.entity';
import { Between } from 'typeorm';

@Injectable()
export class LeaveService {
  private MAX_DEPT_CONCURRENT = 2;

  constructor(
    @InjectRepository(LeaveRequest)
    private leaveRequestRepository: Repository<LeaveRequest>,

    @InjectRepository(LeaveBalance)
    private leaveBalanceRepository: Repository<LeaveBalance>,

    @InjectRepository(User)
    private userRepository: Repository<User>,

    @InjectRepository(Attendance)
    private attendanceRepository: Repository<Attendance>,

    private notificationsService: NotificationsService,
  ) {}

  // ==============================
  // GET USER REQUESTS
  // ==============================
  async getUserRequests(userId: number, query: PaginationQueryDto) {
    const {
      page = 1,
      limit = 10,
      sortBy = 'createdAt',
      sortDirection = 'DESC',
    } = query;
    const skip = (page - 1) * limit;

    const [data, total] = await this.leaveRequestRepository.findAndCount({
      where: { userId },
      order: {
        [sortBy === 'user' ? 'createdAt' : sortBy]: sortDirection,
      } as any,
      skip,
      take: limit,
    });

    return { data, total };
  }

  // ==============================
  // GET USER BALANCES
  // ==============================
  async getUserBalances(userId: number) {
    return this.leaveBalanceRepository.find({
      where: {
        userId,
        year: new Date().getFullYear(),
      },
    });
  }

  // ==============================
  // GET ALL REQUESTS (HR)
  // ==============================
  async getAllRequests(query: PaginationQueryDto) {
    const {
      page = 1,
      limit = 10,
      search = '',
      sortBy = 'createdAt',
      sortDirection = 'DESC',
    } = query;
    const skip = (page - 1) * limit;

    let where: any = {};
    if (search) {
      where = [
        { user: { name: ILike(`%${search}%`) } },
        { user: { email: ILike(`%${search}%`) } },
        { reason: ILike(`%${search}%`) },
      ];
    }

    const [data, total] = await this.leaveRequestRepository.findAndCount({
      where,
      relations: ['user'],
      order: {
        [sortBy === 'user' ? 'createdAt' : sortBy]: sortDirection,
      } as any,
      skip,
      take: limit,
    });

    return { data, total };
  }

  // ==============================
  // CREATE REQUEST
  // ==============================
  async createRequest(userId: number, dto: CreateLeaveRequestDto) {
    const user = await this.userRepository.findOne({
      where: { id: userId },
    });

    if (!user) throw new NotFoundException('User not found');

    // ==============================
    // RULE: Freelancer
    // ==============================
    if (
      user.employmentType === EmploymentType.FREELANCE &&
      dto.leaveType !== LeaveType.UNPAID
    ) {
      throw new BadRequestException(
        'Freelancers can only request unpaid leave',
      );
    }

    const startDate = new Date(dto.startDate);
    const endDate = new Date(dto.endDate);

    if (startDate > endDate) {
      throw new BadRequestException('Invalid date range');
    }

    const daysRequested = this.calculateDays(startDate, endDate);

    // ==============================
    // BALANCE CHECK
    // ==============================
    const balance = await this.leaveBalanceRepository.findOne({
      where: {
        userId,
        leaveType: dto.leaveType,
        year: new Date().getFullYear(),
      },
    });

    if (dto.leaveType !== LeaveType.CRITICAL) {
      // --- 6 months---
      if (dto.leaveType === LeaveType.ANNUAL) {
        if (!user.hireDate) {
          throw new BadRequestException('Hire date not set for user');
        }
        const today = new Date();
        const hireDate = new Date(user.hireDate);
        const monthsDiff =
          (today.getFullYear() - hireDate.getFullYear()) * 12 +
          (today.getMonth() - hireDate.getMonth());
        if (monthsDiff < 6) {
          throw new BadRequestException(
            'Cannot take annual leave before completing 6 months of employment',
          );
        }
      }
    }

    if (!balance && dto.leaveType !== LeaveType.UNPAID) {
      throw new BadRequestException('Leave balance not found');
    }

    if (
      dto.leaveType !== LeaveType.UNPAID &&
      (!balance || balance.balance < daysRequested)
    ) {
      throw new BadRequestException('Insufficient leave balance');
    }

    // ==============================
    // CONFLICT CHECK
    // ==============================
    const conflict = await this.leaveRequestRepository.findOne({
      where: {
        userId,
        status: LeaveStatus.APPROVED,
        startDate: LessThanOrEqual(endDate),
        endDate: MoreThanOrEqual(startDate),
      },
    });

    if (conflict) {
      throw new BadRequestException(
        'Leave overlaps with another approved leave',
      );
    }

    if (dto.leaveType == LeaveType.CRITICAL) {
      const overlappingSameDept = await this.leaveRequestRepository
        .createQueryBuilder('leave')
        .leftJoin('leave.user', 'user')
        .where('user.department = :dept', { dept: user.department })
        .andWhere('leave.status = :status', { status: LeaveStatus.APPROVED })
        .andWhere('leave.startDate <= :end', { end: endDate })
        .andWhere('leave.endDate >= :start', { start: startDate })
        .getCount();

      if (overlappingSameDept >= this.MAX_DEPT_CONCURRENT) {
        throw new BadRequestException(
          `Too many employees from ${user.department} are on leave`,
        );
      }
    }

    // ==============================
    // SAVE REQUEST
    // ==============================
    const leave = this.leaveRequestRepository.create({
      userId,
      leaveType: dto.leaveType,
      startDate,
      endDate,
      reason: dto.reason,
    });

    const saved = await this.leaveRequestRepository.save(leave);

    // ==============================
    // NOTIFY HR
    // ==============================
    const hrAdmins = await this.userRepository.find({
      where: { role: UserRole.HR_ADMIN },
    });

    for (const hr of hrAdmins) {
      await this.notificationsService.createNotification({
        userId: hr.id,
        title: 'New Leave Request',
        message: `${user.name} requested ${dto.leaveType}`,
        type: NotificationType.LEAVE_REQUEST,
      });
    }

    return saved;
  }

  // ==============================
  // APPROVE
  // ==============================
  async approveRequest(requestId: number, hrAdminId: number) {
    const request = await this.leaveRequestRepository.findOne({
      where: { id: requestId },
    });

    if (!request) throw new NotFoundException('Request not found');

    if (request.status !== LeaveStatus.PENDING) {
      throw new BadRequestException('Already processed');
    }

    const days = this.calculateDays(request.startDate, request.endDate);

    if (request.leaveType !== LeaveType.UNPAID) {
      const balance = await this.leaveBalanceRepository.findOne({
        where: {
          userId: request.userId,
          leaveType: request.leaveType,
          year: new Date().getFullYear(),
        },
      });

      if (!balance) throw new BadRequestException('Balance not found');

      if (balance.balance < days) {
        throw new BadRequestException('Insufficient balance');
      }

      balance.used += days;
      balance.balance = balance.totalAllocated - balance.used;

      await this.leaveBalanceRepository.save(balance);
    }

    request.status = LeaveStatus.APPROVED;

    const saved = await this.leaveRequestRepository.save(request);

    const today = new Date();
    today.setHours(0, 0, 0, 0);

    const startDate = new Date(request.startDate);
    startDate.setHours(0, 0, 0, 0);

    if (startDate.getTime() === today.getTime()) {
      const attendance = await this.attendanceRepository.findOne({
        where: {
          userId: request.userId,
          checkIn: Between(
            today,
            new Date(today.getTime() + 24 * 60 * 60 * 1000),
          ),
        },
      });

      if (attendance) {
        attendance.checkOut = new Date();
        await this.attendanceRepository.save(attendance);
      }
    }

    await this.notificationsService.createNotification({
      userId: request.userId,
      title: 'Leave Approved',
      message: `Your leave has been approved`,
      type: NotificationType.LEAVE_APPROVED,
    });

    return saved;
  }

  // ==============================
  // REJECT
  // ==============================
  async rejectRequest(requestId: number, hrAdminId: number, reason?: string) {
    const request = await this.leaveRequestRepository.findOne({
      where: { id: requestId },
    });

    if (!request) throw new NotFoundException();

    if (request.status !== LeaveStatus.PENDING) {
      throw new BadRequestException('Already processed');
    }

    request.status = LeaveStatus.REJECTED;

    const saved = await this.leaveRequestRepository.save(request);

    await this.notificationsService.createNotification({
      userId: request.userId,
      title: 'Leave Rejected',
      message: reason || 'Rejected',
      type: NotificationType.LEAVE_REJECTED,
    });

    return saved;
  }

  async cancelRequest(id: number, userId: number) {
    const request = await this.leaveRequestRepository.findOne({
      where: { id },
    });

    if (!request) throw new NotFoundException();

    if (request.userId !== userId) {
      throw new ForbiddenException();
    }

    if (request.status !== LeaveStatus.PENDING) {
      throw new BadRequestException('Cannot cancel processed request');
    }

    request.status = LeaveStatus.CANCELLED;

    return this.leaveRequestRepository.save(request);
  }

  // ==============================
  // BALANCE UPDATE
  // ==============================
  async updateBalance(userId: number, leaveType: LeaveType, total: number) {
    const year = new Date().getFullYear();

    let balance = await this.leaveBalanceRepository.findOne({
      where: { userId, leaveType, year },
    });

    if (!balance) {
      balance = this.leaveBalanceRepository.create({
        userId,
        leaveType,
        totalAllocated: total,
        used: 0,
        balance: total,
        year,
      });
    } else {
      balance.totalAllocated = Number(total);

      const used = isNaN(Number(balance.used)) ? 0 : Number(balance.used);
      balance.balance = total - used;
    }

    return this.leaveBalanceRepository.save(balance);
  }

  // ==============================
  // INITIAL ASSIGN
  // ==============================
  async assignInitialLeave(user: User) {
    const year = new Date().getFullYear();

    const effectiveType =
      user.workMode === WorkMode.REMOTE
        ? EmploymentType.FREELANCE
        : user.employmentType;

    const balances = LEAVE_BALANCES[effectiveType];

    for (const [type, value] of Object.entries(balances)) {
      const leaveType = type as LeaveType;

      const existing = await this.leaveBalanceRepository.findOne({
        where: { userId: user.id, leaveType, year },
      });

      if (existing) {
        existing.totalAllocated = value;
        existing.balance = value;
        existing.used = 0;
        await this.leaveBalanceRepository.save(existing);
      } else {
        await this.leaveBalanceRepository.save(
          this.leaveBalanceRepository.create({
            userId: user.id,
            leaveType,
            totalAllocated: value,
            balance: value,
            used: 0,
            year,
          }),
        );
      }
    }
  }

  async assignInitialLeaveToUser(userId: number) {
    const user = await this.userRepository.findOne({ where: { id: userId } });

    if (!user) throw new NotFoundException('User not found');

    return this.assignInitialLeave(user);
  }

  // ==============================
  // MONTHLY ACCRUAL
  // ==============================
  @Cron('0 0 1 * *')
  async monthlyAccrual() {
    const users = await this.userRepository.find();

    for (const user of users) {
      if (user.employmentType === EmploymentType.FREELANCE) continue;

      const monthly =
        user.employmentType === EmploymentType.FULL_TIME ? 1.75 : 1;

      const balance = await this.leaveBalanceRepository.findOne({
        where: {
          userId: user.id,
          leaveType: LeaveType.ANNUAL,
          year: new Date().getFullYear(),
        },
      });

      if (!balance) continue;

      balance.totalAllocated += monthly;
      balance.balance += monthly;

      await this.leaveBalanceRepository.save(balance);
    }
  }

  // ==============================
  // CALCULATE DAYS
  // ==============================
  private calculateDays(start: Date, end: Date): number {
    const diff = (end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24);

    return Math.ceil(diff) + 1;
  }
}
