import { CreateLeaveDto } from '../types';
import { LeaveRepository } from '../repositories/LeaveRepository';
import { HolidayRepository } from '../repositories/HolidayRepository';
import { LeaveValidationService } from '../domain/leave/LeaveValidationService';
import { WorkingDayCalculator } from '../domain/leave/WorkingDayCalculator';
import { SandwichLeaveCalculator } from '../domain/leave/SandwichLeaveCalculator';
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, quarterKey, todayInCompanyTz, toDateOnly } from '../domain/leave/dateUtils';
import { mapLeaveRequest } from '../types/mappers';
import { ForbiddenError, NotFoundError, ValidationError } from '../types/errors';
import { getPrisma } from '../infrastructure/database/PrismaSingleton';

export class LeaveFacade {
  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,
  ) {}

  private async buildCalculators(from: string, to: string) {
    const holidays = await this.holidayRepo.listInRange(parseDateOnly(from), parseDateOnly(to));
    const holidaySet = new Set(holidays.map((h) => h.date.toISOString().slice(0, 10)));
    const wdc = new WorkingDayCalculator(holidaySet);
    return { wdc, sandwich: new SandwichLeaveCalculator(wdc) };
  }

  async applyLeave(employeeId: string, dto: CreateLeaveDto) {
    this.validation.validateStandardDates(dto.startDate, dto.endDate);
    this.validation.validateCasualNotice(dto.leaveType, dto.startDate);

    const q = quarterKey(dto.startDate);
    const qStart = parseDateOnly(`${q.slice(0, 4)}-${String((parseInt(q.slice(-1), 10) - 1) * 3 + 1).padStart(2, '0')}-01`);
    const qEnd = parseDateOnly(dto.endDate);
    const emergencyUsed = await this.leaveRepo.countEmergencyInQuarter(employeeId, qStart, qEnd);
    this.validation.validateEmergencyQuarterly(
      dto.leaveType,
      dto.startDate,
      emergencyUsed > 0 ? new Set([q]) : new Set(),
    );

    const { sandwich } = await this.buildCalculators(dto.startDate, dto.endDate);
    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')
      .backdated(false)
      .sandwich(sandwichApplied)
      .attachment(dto.attachmentUrl)
      .build();

    const created = await this.leaveRepo.create(entity);
    const team = await this.teamService.resolveByBusinessId(employeeId);
    const prisma = getPrisma();
    if (team.leadBusinessId) {
      const lead = await prisma.employees.findUnique({ where: { employee_id: team.leadBusinessId } });
      if (lead) {
        await this.notifications.createEmail('LEAVE_SUBMITTED', {
          to: lead.email,
          employeeName: employeeId,
          startDate: dto.startDate,
          endDate: dto.endDate,
        }).send();
      }
    }
    return mapLeaveRequest(created);
  }

  async listLeaves(employeeId: string, status?: string) {
    const rows = await this.leaveRepo.listByEmployee(employeeId, status);
    return { items: rows.map(mapLeaveRequest) };
  }

  async listPendingApprovals(approverId: string) {
    const team = await this.teamService.resolveByBusinessId(approverId);
    const prisma = getPrisma();
    const rows = await prisma.leave_requests.findMany({
      where: {
        employee_id: { in: team.memberBusinessIds.filter((id) => id !== approverId) },
        status: { in: ['PENDING', 'PENDING_BACKDATE_APPROVAL'] },
      },
      include: { leave_request_approvals: true },
      orderBy: { submitted_at: 'desc' },
    });
    return { items: rows.map(mapLeaveRequest) };
  }

  async getLeave(id: string, requesterId: string) {
    const row = await this.leaveRepo.findById(id);
    if (!row) throw new NotFoundError('Leave request not found');
    if (row.employee_id !== requesterId) {
      const team = await this.teamService.resolveByBusinessId(requesterId);
      if (!team.memberBusinessIds.includes(row.employee_id) && team.leadBusinessId !== requesterId) {
        throw new ForbiddenError();
      }
    }
    return mapLeaveRequest(row);
  }

  async cancelLeave(id: string, employeeId: string) {
    const row = await this.leaveRepo.findById(id);
    if (!row || row.employee_id !== employeeId) throw new NotFoundError();
    if (!['PENDING', 'PENDING_BACKDATE_APPROVAL'].includes(row.status)) {
      throw new ValidationError('Only pending leave can be cancelled');
    }
    const updated = await this.leaveRepo.update(id, { status: 'CANCELLED' });
    return mapLeaveRequest(updated);
  }

  async approveLeave(approverId: string, id: string, comment?: string) {
    const row = await this.leaveRepo.findById(id);
    if (!row || row.status !== 'PENDING') throw new NotFoundError();
    if (row.employee_id === approverId) throw new ForbiddenError('Cannot approve own leave');

    const team = await this.teamService.resolveByBusinessId(row.employee_id);
    if (team.leadBusinessId !== approverId) {
      throw new ForbiddenError('Only team manager can approve future leave');
    }

    try {
      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),
          `Leave approved ${row.id}`,
          'leave_request',
          row.id,
        );
      }
    } catch {
      const rejected = await this.leaveRepo.update(id, { status: 'REJECTED' });
      return mapLeaveRequest(rejected);
    }

    const updated = await this.leaveRepo.update(id, {
      status: 'APPROVED',
      approved_at: new Date(),
      leave_request_approvals: {
        create: {
          approver_employee_id: approverId,
          action: 'APPROVE',
          comment,
        },
      },
    });

    const prisma = getPrisma();
    const emp = await prisma.employees.findUnique({ where: { employee_id: row.employee_id } });
    if (emp) {
      await this.notifications.createEmail('LEAVE_APPROVED', { to: emp.email }).send();
    }
    return mapLeaveRequest(updated);
  }

  async rejectLeave(approverId: string, id: string, comment?: string) {
    const row = await this.leaveRepo.findById(id);
    if (!row || row.status !== 'PENDING') throw new NotFoundError();
    if (row.employee_id === approverId) throw new ForbiddenError();
    const updated = await this.leaveRepo.update(id, {
      status: 'REJECTED',
      leave_request_approvals: { create: { approver_employee_id: approverId, action: 'REJECT', comment } },
    });
    return mapLeaveRequest(updated);
  }

  async getBalances(employeeId: string) {
    const balances = await this.balanceService.getBalances(employeeId);
    const yearMonth = todayInCompanyTz().slice(0, 7);
    const counters = await this.monthCounters.getCounters(employeeId, yearMonth);
    return {
      ...balances,
      monthCounters: counters,
      policy: {
        types: ['CASUAL', 'SICK'] as const,
        monthlyCredit: { casualLeave: 1, sickLeave: 1 },
        halfDayMaxHours: 2,
        halfDaysPerLeave: 3,
        shortLeavesPerLeave: 3,
      },
    };
  }

  /** HR records an already-approved leave (rollout / alignment with actual time off). */
  async hrRecordApprovedLeave(hrUsername: string, employeeId: string, dto: CreateLeaveDto) {
    const prisma = getPrisma();
    const emp = await prisma.employees.findUnique({ where: { employee_id: employeeId } });
    if (!emp) throw new NotFoundError('Employee not found');

    const today = todayInCompanyTz();
    const isBackdated = dto.startDate < today;

    const { sandwich } = await this.buildCalculators(dto.startDate, dto.endDate);
    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('APPROVED')
      .backdated(isBackdated)
      .sandwich(sandwichApplied)
      .attachment(dto.attachmentUrl)
      .build();

    try {
      if (dto.duration === 'HALF_DAY') {
        await this.monthCounters.assertHalfDayBalanceAvailable(employeeId, dto.leaveType, dto.startDate);
        await this.monthCounters.recordHalfDayLeave(employeeId, dto.leaveType, dto.startDate, `HR:${hrUsername}`);
      } else {
        await this.balanceService.deduct(
          employeeId,
          dto.leaveType,
          leaveDays,
          `HR recorded leave`,
          'hr_admin',
          hrUsername,
        );
      }
    } catch {
      throw new ValidationError('Insufficient balance to record this leave');
    }

    const created = await this.leaveRepo.create({
      ...entity,
      approved_at: new Date(),
      leave_request_approvals: {
        create: {
          approver_employee_id: `HR:${hrUsername}`,
          action: 'APPROVE',
          comment: 'Recorded by HR admin',
        },
      },
    });

    await prisma.leaves_audit_log.create({
      data: {
        actor_id: hrUsername,
        action: 'HR_RECORD_LEAVE',
        entity: 'leave_request',
        entity_id: created.id,
        after_json: JSON.stringify({
          employeeId,
          leaveType: dto.leaveType,
          startDate: dto.startDate,
          endDate: dto.endDate,
          leaveDays,
        }),
      },
    });

    return mapLeaveRequest(created);
  }

  async accrueAll(yearMonth: string) {
    const prisma = getPrisma();
    const existing = await prisma.accrual_runs.findUnique({ where: { year_month: yearMonth } });
    if (existing) return { skipped: true, yearMonth };

    const employees = await prisma.employees.findMany({ where: { status: 'active' } });
    for (const emp of employees) {
      const sick = (emp.earned_leave ?? 0) + 1;
      const casual = (emp.casual_leave ?? 0) + 1;
      await prisma.employees.update({
        where: { employee_id: emp.employee_id },
        data: { earned_leave: sick, casual_leave: casual },
      });
      await prisma.leave_balance_ledger.createMany({
        data: [
          {
            employee_id: emp.employee_id,
            field: 'earned_leave',
            delta: 1,
            balance_after: sick,
            reason: `Monthly accrual ${yearMonth}`,
            ref_type: 'accrual',
            ref_id: yearMonth,
          },
          {
            employee_id: emp.employee_id,
            field: 'casual_leave',
            delta: 1,
            balance_after: casual,
            reason: `Monthly accrual ${yearMonth}`,
            ref_type: 'accrual',
            ref_id: yearMonth,
          },
        ],
      });
    }
    await prisma.accrual_runs.create({ data: { year_month: yearMonth } });
    return { credited: employees.length, yearMonth };
  }
}
