import { Prisma } from '@prisma/client';
import { BalanceField, LeaveType } from '../../types/prisma';
import { getPrisma } from '../../infrastructure/database/PrismaSingleton';
import { LeaveBalancesDto } from '../../types';
import { ValidationError } from '../../types/errors';

export class LeaveBalanceService {
  private prisma = getPrisma();

  async getBalances(employeeId: string): Promise<LeaveBalancesDto> {
    const employee = await this.prisma.employees.findUnique({ where: { employee_id: employeeId } });
    if (!employee) throw new ValidationError('Employee not found');

    const compOff = await this.prisma.comp_off_credits.count({
      where: { employee_id: employeeId, status: 'AVAILABLE' },
    });

    const pending = await this.prisma.leave_requests.aggregate({
      where: {
        employee_id: employeeId,
        status: { in: ['PENDING', 'PENDING_BACKDATE_APPROVAL', 'APPROVED_PENDING_MARK'] },
      },
      _sum: { leave_days: true },
    });

    return {
      sickLeave: employee.earned_leave ?? 0,
      casualLeave: employee.casual_leave ?? 0,
      compOff,
      pendingDeductions: Number(pending._sum.leave_days ?? 0),
    };
  }

  fieldForLeaveType(type: LeaveType): BalanceField {
    switch (type) {
      case 'SICK':
        return 'earned_leave';
      case 'CASUAL':
        return 'casual_leave';
      case 'COMP_OFF':
        return 'comp_off';
      case 'EMERGENCY_MEDICAL':
        return 'earned_leave';
      default:
        return 'earned_leave';
    }
  }

  async deduct(
    employeeId: string,
    type: LeaveType,
    days: number,
    reason: string,
    refType: string,
    refId: string,
    tx?: Prisma.TransactionClient,
  ): Promise<void> {
    const db = tx ?? this.prisma;
    const employee = await db.employees.findUnique({ where: { employee_id: employeeId } });
    if (!employee) throw new ValidationError('Employee not found');

    if (type === 'COMP_OFF') {
      const credits = await db.comp_off_credits.findMany({
        where: { employee_id: employeeId, status: 'AVAILABLE' },
        orderBy: { expires_at: 'asc' },
        take: Math.ceil(days),
      });
      if (credits.length < days) {
        throw new ValidationError('Insufficient comp-off balance');
      }
      for (const c of credits) {
        await db.comp_off_credits.update({ where: { id: c.id }, data: { status: 'USED' } });
      }
      await db.leave_balance_ledger.create({
        data: {
          employee_id: employeeId,
          field: 'comp_off',
          delta: -days,
          balance_after: credits.length - days,
          reason,
          ref_type: refType,
          ref_id: refId,
        },
      });
      return;
    }

    let field = this.fieldForLeaveType(type);
    let sick = employee.earned_leave ?? 0;
    let casual = employee.casual_leave ?? 0;

    if (type === 'SICK' && sick < days && casual >= days - sick) {
      const fromSick = sick;
      const fromCasual = days - fromSick;
      if (fromSick > 0) {
        await this.applyDelta(db, employeeId, 'earned_leave', -fromSick, sick - fromSick, reason, refType, refId);
        sick = 0;
      }
      await this.applyDelta(db, employeeId, 'casual_leave', -fromCasual, casual - fromCasual, reason, refType, refId);
      return;
    }

    const current = field === 'earned_leave' ? sick : casual;
    if (current < days) {
      throw new ValidationError('Insufficient leave balance');
    }
    await this.applyDelta(db, employeeId, field, -days, current - days, reason, refType, refId);
  }

  async adjust(
    employeeId: string,
    field: BalanceField,
    delta: number,
    reason: string,
    createdBy: string,
  ): Promise<void> {
    const employee = await this.prisma.employees.findUnique({ where: { employee_id: employeeId } });
    if (!employee) throw new ValidationError('Employee not found');

    let balanceAfter = 0;
    if (field === 'earned_leave') {
      const next = (employee.earned_leave ?? 0) + delta;
      await this.prisma.employees.update({ where: { employee_id: employeeId }, data: { earned_leave: next } });
      balanceAfter = next;
    } else if (field === 'casual_leave') {
      const next = (employee.casual_leave ?? 0) + delta;
      await this.prisma.employees.update({ where: { employee_id: employeeId }, data: { casual_leave: next } });
      balanceAfter = next;
    } else {
      for (let i = 0; i < delta; i++) {
        await this.prisma.comp_off_credits.create({
          data: {
            employee_id: employeeId,
            earned_date: new Date(),
            expires_at: new Date(Date.now() + 90 * 86400000),
            status: 'AVAILABLE',
          },
        });
      }
      balanceAfter = await this.prisma.comp_off_credits.count({ where: { employee_id: employeeId, status: 'AVAILABLE' } });
    }

    await this.prisma.leave_balance_ledger.create({
      data: { employee_id: employeeId, field, delta, balance_after: balanceAfter, reason, created_by: createdBy },
    });
  }

  /** Set absolute opening balances (CL / SL) for historical data import. */
  async setOpeningBalances(
    employeeId: string,
    values: { sickLeave?: number; casualLeave?: number },
    reason: string,
    createdBy: string,
  ): Promise<LeaveBalancesDto> {
    const employee = await this.prisma.employees.findUnique({ where: { employee_id: employeeId } });
    if (!employee) throw new ValidationError('Employee not found');

    if (values.sickLeave != null) {
      if (!Number.isInteger(values.sickLeave) || values.sickLeave < 0) {
        throw new ValidationError('Sick leave must be a non-negative integer');
      }
      const delta = values.sickLeave - (employee.earned_leave ?? 0);
      if (delta !== 0) {
        await this.adjust(employeeId, 'earned_leave', delta, reason || 'Opening sick leave balance', createdBy);
      }
    }
    if (values.casualLeave != null) {
      if (!Number.isInteger(values.casualLeave) || values.casualLeave < 0) {
        throw new ValidationError('Casual leave must be a non-negative integer');
      }
      const refreshed = await this.prisma.employees.findUnique({ where: { employee_id: employeeId } });
      const delta = values.casualLeave - (refreshed?.casual_leave ?? 0);
      if (delta !== 0) {
        await this.adjust(employeeId, 'casual_leave', delta, reason || 'Opening casual leave balance', createdBy);
      }
    }
    return this.getBalances(employeeId);
  }

  private async applyDelta(
    db: Prisma.TransactionClient,
    employeeId: string,
    field: BalanceField,
    delta: number,
    balanceAfter: number,
    reason: string,
    refType: string,
    refId: string,
  ): Promise<void> {
    if (field === 'earned_leave') {
      await db.employees.update({ where: { employee_id: employeeId }, data: { earned_leave: balanceAfter } });
    } else {
      await db.employees.update({ where: { employee_id: employeeId }, data: { casual_leave: balanceAfter } });
    }
    await db.leave_balance_ledger.create({
      data: {
        employee_id: employeeId,
        field,
        delta,
        balance_after: balanceAfter,
        reason,
        ref_type: refType,
        ref_id: refId,
      },
    });
  }
}
