import { forwardRef, Inject, Injectable, BadRequestException, ForbiddenException, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { LeaveRequest, LeaveStatus } from './entities/leave-request.entity';
import { LeaveBalance } from './entities/leave-balance.entity';
import { LeaveType } from './entities/leave-type.entity';
import { User, Role } from '../users/entities/user.entity';
import { CreateLeaveRequestDto } from './dto/create-leave-request.dto';
import { ValidateLeaveDto } from './dto/validate-leave.dto';
import { calculateWorkingDays } from './utils/days-calculator';
import { PublicHoliday } from '../holidays/entities/public-holiday.entity';
import { AuditLog } from '../audit/entities/audit-log.entity';
import { PlanningService } from '../planning/planning.service';
import { NotificationsService } from '../notifications/notifications.service';
import { NotificationType } from '../notifications/entities/notification.entity';

@Injectable()
export class LeavesService {
  constructor(
    @InjectRepository(LeaveRequest)
    private readonly leaveRequestRepo: Repository<LeaveRequest>,

    @InjectRepository(LeaveBalance)
    private readonly leaveBalanceRepo: Repository<LeaveBalance>,

    @InjectRepository(LeaveType)
    private readonly leaveTypeRepo: Repository<LeaveType>,

    @InjectRepository(PublicHoliday)
    private readonly holidayRepo: Repository<PublicHoliday>,

    @InjectRepository(User)                   
    private readonly userRepo: Repository<User>,

    @InjectRepository(AuditLog)                    
    private readonly auditLogRepo: Repository<AuditLog>,

    @Inject(forwardRef(() => PlanningService))
    private readonly planningService: PlanningService,

    private readonly notificationsService: NotificationsService,
  ) {}

  // ─── Détail d'une demande (propriétaire uniquement) ──────────────────────────
  async getOne(userId: number, requestId: number): Promise<LeaveRequest> {
    const request = await this.leaveRequestRepo.findOne({
      where: { id: requestId, user: { id: userId } },
      relations: ['leave_type', 'n1_validator', 'n2_validator', 'n3_validator'],
    });
    if (!request) throw new NotFoundException('Demande introuvable');
    return request;
  }

  // ─── Créer une demande ───────────────────────────────────────────────────────
  async createRequest(user: User, dto: CreateLeaveRequestDto): Promise<LeaveRequest> {
    const leaveType = await this.leaveTypeRepo.findOne({ where: { id: dto.leave_type_id } });
    if (!leaveType) throw new NotFoundException('Type de congé introuvable');

    const holidays = await this.holidayRepo.find();
    const holidayDates = holidays.map(h => h.date);

    const start = new Date(dto.start_date);
    const end = new Date(dto.end_date);

    if (start > end) throw new BadRequestException('La date de début doit être avant la date de fin');

    const daysCount = calculateWorkingDays(start, end, user.work_schedule, holidayDates);
    if (daysCount <= 0) throw new BadRequestException('Aucun jour ouvré dans la période sélectionnée');

    if (leaveType.deducts_balance) {
      const balance = await this.getBalance(user.id, dto.leave_type_id);
      if (balance.remaining < daysCount) {
        throw new BadRequestException(
          `Solde insuffisant : ${balance.remaining} jour(s) disponible(s), ${daysCount} demandé(s)`
        );
      }
    }

    const request = this.leaveRequestRepo.create({
      user,
      leave_type: leaveType,
      start_date: start,
      end_date: end,
      days_count: daysCount,
      reason: dto.reason,
      status: LeaveStatus.PENDING_N1,
    });

    return this.leaveRequestRepo.save(request);
  }

  // ─── Valider / Rejeter (workflow N1 → N2 → N3) ───────────────────────────────
  async validate(validator: User, requestId: number, dto: ValidateLeaveDto): Promise<LeaveRequest> {
    const request = await this.leaveRequestRepo.findOne({
      where: { id: requestId },
      relations: ['user', 'leave_type', 'user.team'],
    });

    if (!request) throw new NotFoundException('Demande introuvable');

    const now = new Date();

    switch (request.status) {
      case LeaveStatus.PENDING_N1:
        if (validator.role !== Role.TEAM_LEAD &&
            validator.role !== Role.AGENCY_MANAGER &&
            validator.role !== Role.DIRECTOR) {
          throw new ForbiddenException('Validation N1 réservée au chef d\'équipe');
        }
        request.n1_validator = validator;
        request.n1_validated_at = now;
        request.n1_comment = dto.comment || '';
        request.status = dto.approved ? LeaveStatus.PENDING_N2 : LeaveStatus.REJECTED;
        break;

      case LeaveStatus.PENDING_N2:
        if (validator.role !== Role.AGENCY_MANAGER && validator.role !== Role.DIRECTOR) {
          throw new ForbiddenException('Validation N2 réservée au responsable d\'agence');
        }
        if (!dto.approved && !dto.comment) {
          throw new BadRequestException('Un commentaire est obligatoire en cas de refus');
        }
        request.n2_validator = validator;
        request.n2_validated_at = now;
        request.n2_comment = dto.comment || '';
        request.status = dto.approved ? LeaveStatus.PENDING_N3 : LeaveStatus.REJECTED;
        break;

      case LeaveStatus.PENDING_N3:
        if (validator.role !== Role.DIRECTOR) {
          throw new ForbiddenException('Validation N3 réservée au directeur');
        }
        if (!dto.approved && !dto.comment) {
          throw new BadRequestException('Un commentaire est obligatoire en cas de refus');
        }
        request.n3_validator = validator;
        request.n3_validated_at = now;
        request.n3_comment = dto.comment || '';
        request.status = dto.approved ? LeaveStatus.APPROVED : LeaveStatus.REJECTED;

        if (dto.approved && request.leave_type.deducts_balance) {
          await this.deductBalance(request.user.id, request.leave_type.id, request.days_count);
        }
        break;

      default:
        throw new BadRequestException('Cette demande ne peut plus être modifiée');
    }

    const saved = await this.leaveRequestRepo.save(request);

    // Notifier le demandeur
    const statusMessages: Record<string, string> = {
      [LeaveStatus.PENDING_N2]: `Votre demande de congé du ${saved.start_date} a été validée par votre chef d'équipe`,
      [LeaveStatus.PENDING_N3]: `Votre demande de congé du ${saved.start_date} a été validée par le responsable d'agence`,
      [LeaveStatus.APPROVED]:   `Votre demande de congé du ${saved.start_date} a été approuvée définitivement`,
      [LeaveStatus.REJECTED]:   `Votre demande de congé du ${saved.start_date} a été rejetée`,
    };

    if (statusMessages[saved.status]) {
      await this.notificationsService.create(
        saved.user,
        saved.status === LeaveStatus.REJECTED
          ? NotificationType.LEAVE_REJECTED
          : NotificationType.LEAVE_APPROVED,
        statusMessages[saved.status],
        `/leaves`,
      );
    }

    return saved;
  }

  // ─── Annuler une demande (par le salarié) ────────────────────────────────────
  async cancel(user: User, requestId: number): Promise<LeaveRequest> {
    const request = await this.leaveRequestRepo.findOne({
      where: { id: requestId, user: { id: user.id } },
    });

    if (!request) throw new NotFoundException('Demande introuvable');

    if (request.status === LeaveStatus.APPROVED || request.status === LeaveStatus.REJECTED) {
      throw new BadRequestException('Impossible d\'annuler une demande déjà traitée');
    }

    request.status = LeaveStatus.CANCELLED;
    return this.leaveRequestRepo.save(request);
  }

  // ─── Solde d'un collaborateur ────────────────────────────────────────────────
  async getBalance(userId: number, leaveTypeId: number): Promise<LeaveBalance & { remaining: number }> {
    const year = new Date().getFullYear();
    let balance = await this.leaveBalanceRepo.findOne({
      where: { user: { id: userId }, leave_type: { id: leaveTypeId }, year },
    });

    if (!balance) {
      balance = this.leaveBalanceRepo.create({
        user: { id: userId } as User,
        leave_type: { id: leaveTypeId } as LeaveType,
        year,
        acquired: 0,
        taken: 0,
        adjusted: 0,
      });
      await this.leaveBalanceRepo.save(balance);
    }

    return { ...balance, remaining: +balance.acquired - +balance.taken + +balance.adjusted };
  }

  // ─── Déduire du solde ────────────────────────────────────────────────────────
  private async deductBalance(userId: number, leaveTypeId: number, days: number): Promise<void> {
    const balance = await this.getBalance(userId, leaveTypeId);
    await this.leaveBalanceRepo.update(balance.id, {
      taken: +balance.taken + days,
    });
  }

  // ─── Historique des demandes ─────────────────────────────────────────────────
  async getMyRequests(userId: number): Promise<LeaveRequest[]> {
    return this.leaveRequestRepo.find({
      where: { user: { id: userId } },
      relations: ['leave_type'],
      order: { created_at: 'DESC' },
    });
  }

  // ─── Demandes en attente pour un valideur ───────────────────────────────────
  async getPendingForValidator(validator: User): Promise<LeaveRequest[]> {
    let status: LeaveStatus;

    switch (validator.role) {
      case Role.TEAM_LEAD:      status = LeaveStatus.PENDING_N1; break;
      case Role.AGENCY_MANAGER: status = LeaveStatus.PENDING_N2; break;
      case Role.DIRECTOR:       status = LeaveStatus.PENDING_N3; break;
      default: return [];
    }

    return this.leaveRequestRepo.find({
      where: { status },
      relations: ['user', 'leave_type', 'user.team'],
      order: { created_at: 'ASC' },
    });
  }

  // ─── Calculer les jours ouvrés (endpoint temps réel) ────────────────────────
  async calculateDays(
    userId: number,
    startDate: string,
    endDate: string,
    leaveTypeId: number,
  ): Promise<{ days_count: number; balance?: number }> {
    const user = await this.userRepo.findOne({ where: { id: userId } });
    if (!user) throw new NotFoundException('Utilisateur introuvable');

    const start = new Date(startDate);
    const end = new Date(endDate);

    if (start > end) throw new BadRequestException('La date de début doit être avant la date de fin');

    const holidays = await this.holidayRepo.find();
    const holidayDates = holidays.map(h => h.date);

    const days_count = calculateWorkingDays(start, end, user.work_schedule, holidayDates);

    const leaveType = await this.leaveTypeRepo.findOne({ where: { id: leaveTypeId } });
    if (leaveType?.deducts_balance) {
      const balance = await this.getBalance(userId, leaveTypeId);
      return { days_count, balance: balance.remaining };
    }

    return { days_count };
  }

  // ─── Attacher une pièce jointe ───────────────────────────────────────────────
async attachFile(requestId: number, userId: number, filename: string): Promise<LeaveRequest> {
  const request = await this.leaveRequestRepo.findOne({
    where: { id: requestId, user: { id: userId } },
  });

  if (!request) throw new NotFoundException('Demande introuvable');

  if (request.status !== LeaveStatus.PENDING_N1) {
    throw new BadRequestException(
      'La pièce jointe ne peut être ajoutée que sur une demande en attente N1'
    );
  }

  // Supprimer l'ancien fichier si existant
  if (request.attachment_url) {
    const fs = await import('fs/promises');
    try {
      await fs.unlink(request.attachment_url);
    } catch {
      // Fichier déjà supprimé, on continue
    }
  }

  request.attachment_url = `uploads/leaves/${filename}`;
  return this.leaveRequestRepo.save(request);
}

  // ─── Soumettre avec pièce jointe en une seule requête ───────────────────────
  async createRequestWithFile(
    user: User,
    dto: CreateLeaveRequestDto,
    filename?: string,
  ): Promise<LeaveRequest & { threshold_warning?: string }> {
    const leaveType = await this.leaveTypeRepo.findOne({ where: { id: dto.leave_type_id } });
    if (!leaveType) throw new NotFoundException('Type de congé introuvable');

    if (leaveType.requires_attachment && !filename) {
      throw new BadRequestException(
        `Une pièce jointe est obligatoire pour le type : ${leaveType.label}`
      );
    }

    const holidays = await this.holidayRepo.find();
    const holidayDates = holidays.map(h => h.date);

    const start = new Date(dto.start_date);
    const end   = new Date(dto.end_date);

    if (start > end) {
      throw new BadRequestException('La date de début doit être avant la date de fin');
    }

    const daysCount = calculateWorkingDays(start, end, user.work_schedule, holidayDates);
    if (daysCount <= 0) {
      throw new BadRequestException('Aucun jour ouvré dans la période sélectionnée');
    }

    if (leaveType.deducts_balance) {
      const balance = await this.getBalance(user.id, dto.leave_type_id);
      if (balance.remaining < daysCount) {
        throw new BadRequestException(
          `Solde insuffisant : ${balance.remaining} jour(s) disponible(s), ${daysCount} demandé(s)`
        );
      }
    }

    // ── Vérification seuil minimum de présence ──────────────────────────────────
    const thresholdCheck = await this.planningService.checkNewRequestConflict(
      user.id,
      dto.start_date,
      dto.end_date,
    );

    // Bloquer si conflit strict (0 personne présente)
    const hardConflicts = thresholdCheck.conflicts.filter(c => c.presence <= 0);
    if (hardConflicts.length > 0) {
      throw new BadRequestException(
        `Demande impossible : aucune présence assurée les jours suivants : ${
          hardConflicts.map(c => c.date).join(', ')
        }`
      );
    }

    const request = this.leaveRequestRepo.create({
      user,
      leave_type: leaveType,
      start_date: start,
      end_date:   end,
      days_count: daysCount,
      reason:     dto.reason,
      attachment_url: filename ? `uploads/leaves/${filename}` : undefined,
      status: LeaveStatus.PENDING_N1,
    });

    const saved = await this.leaveRequestRepo.save(request);

    // Notifier le demandeur
    await this.notificationsService.create(
      user,
      NotificationType.LEAVE_SUBMITTED,
      `Votre demande de congé du ${dto.start_date} au ${dto.end_date} a bien été soumise`,
      `/leaves`,
    );

    return { ...saved, threshold_warning: thresholdCheck.warning_message };
  }

  // ─── Ajustement manuel du solde (RH/Directeur) ──────────────────────────────
  async adjustBalance(
    adminUser: User,
    userId: number,
    leaveTypeId: number,
    adjustment: number,
    reason: string,
  ): Promise<LeaveBalance & { remaining: number }> {
    if (!reason || reason.trim().length < 5) {
      throw new BadRequestException('Un motif détaillé est obligatoire pour tout ajustement');
    }

    const balance = await this.getBalance(userId, leaveTypeId);

    const oldValue = {
      acquired: balance.acquired,
      taken: balance.taken,
      adjusted: balance.adjusted,
      remaining: balance.remaining,
    };

    await this.leaveBalanceRepo.update(balance.id, {
      adjusted: +balance.adjusted + adjustment,
      adjustment_reason: reason,
    });

    const updated = await this.getBalance(userId, leaveTypeId);

    // Tracer dans le journal d'audit
    await this.auditLogRepo.save(
      this.auditLogRepo.create({
        user: adminUser,
        action: 'BALANCE_ADJUSTED',
        entity_type: 'leave_balance',
        entity_id: balance.id,
        old_value: oldValue,
        new_value: {
          acquired: updated.acquired,
          taken: updated.taken,
          adjusted: updated.adjusted,
          remaining: updated.remaining,
        },
      }),
    );

    return updated;
  }

  // ─── Tous les soldes d'un collaborateur (tous types) ────────────────────────
  async getAllBalances(userId: number): Promise<(LeaveBalance & { remaining: number })[]> {
    const leaveTypes = await this.leaveTypeRepo.find({ where: { is_active: true } });

    const balances = await Promise.all(
      leaveTypes.map(lt => this.getBalance(userId, lt.id)),
    );

    return balances;
  }

  // ─── Soldes de toute l'équipe (pour managers) ───────────────────────────────
  async getTeamBalances(
    teamId: number,
    leaveTypeId: number,
  ): Promise<{ user: Partial<User>; balance: LeaveBalance & { remaining: number } }[]> {
    const users = await this.userRepo.find({
      where: { team: { id: teamId }, is_active: true },
    });

    const result = await Promise.all(
      users.map(async u => ({
        user: {
          id: u.id,
          matricule: u.matricule,
          first_name: u.first_name,
          last_name: u.last_name,
          work_schedule: u.work_schedule,
        },
        balance: await this.getBalance(u.id, leaveTypeId),
      })),
    );

    return result;
  }

  async findUserById(userId: number): Promise<User> {
    const user = await this.userRepo.findOne({ where: { id: userId } });
    if (!user) throw new NotFoundException('Utilisateur introuvable');
    return user;
  }
}