import { CreateLeaveDto } from '../types';
import { LeaveRepository } from '../repositories/LeaveRepository';
import { HolidayRepository } from '../repositories/HolidayRepository';
import { LeaveValidationService } from '../domain/leave/LeaveValidationService';
import { SandwichLeaveCalculator } from '../domain/leave/SandwichLeaveCalculator';
import { WorkingDayCalculator } from '../domain/leave/WorkingDayCalculator';
import { LeaveBalanceService } from '../domain/leave/LeaveBalanceService';
import { LeaveMonthCounterService } from '../domain/leave/LeaveMonthCounterService';
import { TeamResolutionService } from '../domain/team/TeamResolutionService';
import { LeaveRequestBuilder } from '../builders/LeaveRequestBuilder';
import { NotificationFactory } from '../factories/NotificationFactory';
import { parseDateOnly, toDateOnly } from '../domain/leave/dateUtils';
import { mapLeaveRequest } from '../types/mappers';
import { ForbiddenError, NotFoundError, ValidationError } from '../types/errors';
import { getPrisma } from '../infrastructure/database/PrismaSingleton';

const MARK_WINDOW_DAYS = 7;
const MIN_APPROVAL_REASON = 20;

export class BackdatedLeaveFacade {
  private monthCounters = new LeaveMonthCounterService();

  constructor(
    private leaveRepo = new LeaveRepository(),
    private holidayRepo = new HolidayRepository(),
    private validation = new LeaveValidationService(),
    private teamService = new TeamResolutionService(),
    private balanceService = new LeaveBalanceService(),
    private notifications: NotificationFactory,
  ) {}

  async submit(employeeId: string, dto: CreateLeaveDto) {
    this.validation.validateBackdatedDates(dto.startDate, dto.endDate);
    this.validation.validateBackdatedType(dto.leaveType);

    const holidays = await this.holidayRepo.listInRange(parseDateOnly(dto.startDate), parseDateOnly(dto.endDate));
    const wdc = new WorkingDayCalculator(new Set(holidays.map((h) => h.date.toISOString().slice(0, 10))));
    const sandwich = new SandwichLeaveCalculator(wdc);
    const { leaveDays, sandwichApplied } = sandwich.compute(dto.startDate, dto.endDate, dto.duration);

    const entity = new LeaveRequestBuilder()
      .forEmployee(employeeId)
      .type(dto.leaveType)
      .dates(parseDateOnly(dto.startDate), parseDateOnly(dto.endDate))
      .duration(dto.duration, dto.halfDaySlot)
      .computedDays(leaveDays)
      .reason(dto.reason)
      .status('PENDING_BACKDATE_APPROVAL')
      .backdated(true)
      .sandwich(sandwichApplied)
      .build();

    const created = await this.leaveRepo.create(entity);
    const team = await this.teamService.resolveByBusinessId(employeeId);
    const prisma = getPrisma();
    for (const memberId of team.memberBusinessIds) {
      const m = await prisma.employees.findUnique({ where: { employee_id: memberId } });
      if (m) {
        await this.notifications.createEmail('BACKDATE_PEER_APPROVAL', { to: m.email }).send();
      }
    }
    return mapLeaveRequest(created);
  }

  async approve(peerId: string, id: string, approvalReason: string) {
    if (approvalReason.length < MIN_APPROVAL_REASON) {
      throw new ValidationError('Approval reason must be at least 20 characters');
    }
    const row = await this.leaveRepo.findById(id);
    if (!row || row.status !== 'PENDING_BACKDATE_APPROVAL') throw new NotFoundError();
    if (row.employee_id === peerId) throw new ForbiddenError('Cannot approve own backdated leave');

    const team = await this.teamService.resolveByBusinessId(row.employee_id);
    const isHrApprover = peerId.startsWith('HR:');
    if (!isHrApprover && !team.memberBusinessIds.includes(peerId) && team.leadBusinessId !== peerId) {
      throw new ForbiddenError('Not a team member');
    }

    const existing = row.leave_request_approvals.filter((a) => a.action === 'APPROVE');
    if (existing.some((a) => a.approver_employee_id === peerId)) {
      throw new ValidationError('Already approved');
    }

    await getPrisma().leave_request_approvals.create({
      data: {
        leave_request_id: id,
        approver_employee_id: peerId,
        action: 'APPROVE',
        approval_reason: approvalReason,
      },
    });

    const approvalCount = existing.length + 1;
    if (approvalCount >= 2) {
      const updated = await this.leaveRepo.update(id, {
        status: 'APPROVED_PENDING_MARK',
        final_approval_at: new Date(),
      });
      const emp = await getPrisma().employees.findUnique({ where: { employee_id: row.employee_id } });
      if (emp) {
        await this.notifications.createEmail('BACKDATE_MARK_REMINDER', { to: emp.email }).send();
      }
      return { ...mapLeaveRequest(updated), backdateApprovalCount: 2 };
    }

    const refreshed = await this.leaveRepo.findById(id);
    return { ...mapLeaveRequest(refreshed!), backdateApprovalCount: approvalCount };
  }

  async reject(peerId: string, id: string, reason: string) {
    const row = await this.leaveRepo.findById(id);
    if (!row || row.status !== 'PENDING_BACKDATE_APPROVAL') throw new NotFoundError();
    const updated = await this.leaveRepo.update(id, {
      status: 'REJECTED',
      leave_request_approvals: { create: { approver_employee_id: peerId, action: 'REJECT', comment: reason } },
    });
    return mapLeaveRequest(updated);
  }

  async mark(employeeId: string, id: string) {
    const row = await this.leaveRepo.findById(id);
    if (!row || row.employee_id !== employeeId) throw new NotFoundError();
    if (row.status !== 'APPROVED_PENDING_MARK') {
      throw new ValidationError('Leave is not ready to mark');
    }
    if (row.final_approval_at) {
      const deadline = new Date(row.final_approval_at);
      deadline.setDate(deadline.getDate() + MARK_WINDOW_DAYS);
      if (new Date() > deadline) {
        throw new ValidationError('Mark window expired');
      }
    }

    if (row.duration === 'HALF_DAY') {
      await this.monthCounters.assertHalfDayBalanceAvailable(
        row.employee_id,
        row.leave_type,
        toDateOnly(row.start_date),
      );
      await this.monthCounters.recordHalfDayLeave(
        row.employee_id,
        row.leave_type,
        toDateOnly(row.start_date),
        row.id,
      );
    } else {
      await this.balanceService.deduct(
        row.employee_id,
        row.leave_type,
        Number(row.leave_days),
        `Backdated leave marked ${row.id}`,
        'leave_request',
        row.id,
      );
    }

    const updated = await this.leaveRepo.update(id, {
      status: 'APPROVED',
      marked_at: new Date(),
      approved_at: new Date(),
    });

    const prisma = getPrisma();
    await prisma.attendance_daily_summary.upsert({
      where: {
        employee_id_date: { employee_id: row.employee_id, date: row.start_date },
      },
      create: {
        employee_id: row.employee_id,
        date: row.start_date,
        status: 'on_leave',
      },
      update: { status: 'on_leave' },
    });

    return mapLeaveRequest(updated);
  }
}
