import { BalanceField } from '../types/prisma';
import { HolidayRepository } from '../repositories/HolidayRepository';
import { LeaveBalanceService } from '../domain/leave/LeaveBalanceService';
import { TeamShiftFacade } from './TeamShiftFacade';
import { MockNotificationFactory } from '../factories/NotificationFactory';
import { BackdatedLeaveFacade } from './BackdatedLeaveFacade';
import { LeavesAppSettingsService } from '../services/LeavesAppSettingsService';
import { EmployeeSettingsService } from '../services/EmployeeSettingsService';
import { AutoWeekdayAttendanceService } from '../services/AutoWeekdayAttendanceService';
import { LeaveFacade } from './LeaveFacade';
import { TeamManagementService } from '../services/TeamManagementService';
import { getPrisma } from '../infrastructure/database/PrismaSingleton';
import { parseDateOnly, todayInCompanyTz, toDateOnly } from '../domain/leave/dateUtils';
import { CreateLeaveDto } from '../types';
import { NotFoundError, ValidationError } from '../types/errors';

export class HrAdminFacade {
  constructor(
    private holidayRepo = new HolidayRepository(),
    private balanceService = new LeaveBalanceService(),
    private shiftFacade = new TeamShiftFacade(undefined, new MockNotificationFactory()),
    private appSettings = new LeavesAppSettingsService(),
    private employeeSettings = new EmployeeSettingsService(),
    private autoWeekdayAttendance = new AutoWeekdayAttendanceService(),
    private teamManagement = new TeamManagementService(),
  ) {}

  private prisma = getPrisma();

  listHolidays(year?: number) {
    if (year != null && Number.isFinite(year)) {
      return this.holidayRepo.listByYear(year);
    }
    return this.holidayRepo.list();
  }

  createHoliday(input: { date: string; name: string; recurringYearly: boolean }) {
    return this.holidayRepo.create({
      date: parseDateOnly(input.date),
      name: input.name.trim(),
      recurringYearly: input.recurringYearly,
    });
  }

  /**
   * Bulk-create holidays for a year (or any dates). Skips dates that already exist.
   */
  async bulkCreateHolidays(
    items: Array<{ date: string; name: string; recurringYearly?: boolean }>,
  ) {
    if (!Array.isArray(items) || !items.length) {
      throw new ValidationError('Provide at least one holiday');
    }
    if (items.length > 100) {
      throw new ValidationError('Maximum 100 holidays per bulk request');
    }

    const seen = new Set<string>();
    const prepared: Array<{ date: Date; name: string; recurringYearly: boolean }> = [];
    for (const item of items) {
      const dateStr = String(item.date || '').slice(0, 10);
      const name = String(item.name || '').trim();
      if (!/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) {
        throw new ValidationError(`Invalid date: ${item.date}`);
      }
      if (!name) throw new ValidationError(`Name required for ${dateStr}`);
      if (seen.has(dateStr)) continue;
      seen.add(dateStr);
      prepared.push({
        date: parseDateOnly(dateStr),
        name,
        recurringYearly: Boolean(item.recurringYearly),
      });
    }

    const existing = await this.holidayRepo.listInRange(
      prepared.reduce((min, p) => (p.date < min ? p.date : min), prepared[0].date),
      prepared.reduce((max, p) => (p.date > max ? p.date : max), prepared[0].date),
    );
    const existingDates = new Set(existing.map((h) => toDateOnly(h.date)));
    const toCreate = prepared.filter((p) => !existingDates.has(toDateOnly(p.date)));
    const skipped = prepared.length - toCreate.length;

    if (toCreate.length) {
      await this.holidayRepo.createMany(toCreate);
    }

    const yearHint = prepared[0].date.getUTCFullYear();
    const itemsAfter = await this.holidayRepo.listByYear(yearHint);
    return {
      created: toCreate.length,
      skipped,
      totalRequested: prepared.length,
      items: itemsAfter,
    };
  }

  updateHoliday(id: number, input: { date?: string; name?: string; recurringYearly?: boolean }) {
    return this.holidayRepo.update(id, {
      ...(input.date ? { date: parseDateOnly(input.date) } : {}),
      name: input.name,
      recurringYearly: input.recurringYearly,
    });
  }

  deleteHoliday(id: number) {
    return this.holidayRepo.delete(id);
  }

  async adjustBalance(
    employeeId: string,
    field: BalanceField,
    delta: number,
    reason: string,
    hrUsername: string,
  ) {
    await this.balanceService.adjust(employeeId, field, delta, reason, hrUsername);
    await this.prisma.leaves_audit_log.create({
      data: {
        actor_id: hrUsername,
        action: 'BALANCE_ADJUST',
        entity: 'employee',
        entity_id: employeeId,
        after_json: JSON.stringify({ field, delta, reason }),
      },
    });
    return this.balanceService.getBalances(employeeId);
  }

  /** Define absolute CL/SL opening balances from previous/historical data. */
  async setOpeningBalances(
    employeeId: string,
    values: { sickLeave?: number; casualLeave?: number },
    reason: string,
    hrUsername: string,
  ) {
    const balances = await this.balanceService.setOpeningBalances(
      employeeId,
      values,
      reason || 'Opening balance (historical)',
      hrUsername,
    );
    await this.prisma.leaves_audit_log.create({
      data: {
        actor_id: hrUsername,
        action: 'BALANCE_SET_OPENING',
        entity: 'employee',
        entity_id: employeeId,
        after_json: JSON.stringify({ ...values, reason }),
      },
    });
    return balances;
  }

  /** Credit +1 CL and +1 SL for every active employee for the given YYYY-MM (idempotent). */
  async runMonthlyAccrual(yearMonth: string, hrUsername: string, leaveFacade: LeaveFacade) {
    if (!/^\d{4}-\d{2}$/.test(yearMonth)) {
      throw new ValidationError('yearMonth must be YYYY-MM');
    }
    const result = await leaveFacade.accrueAll(yearMonth);
    await this.prisma.leaves_audit_log.create({
      data: {
        actor_id: hrUsername,
        action: 'MONTHLY_ACCRUAL',
        entity: 'accrual_runs',
        entity_id: yearMonth,
        after_json: JSON.stringify(result),
      },
    });
    return result;
  }

  shiftCompliance(month?: string) {
    return this.shiftFacade.shiftPlanningStatus(month);
  }

  assignShiftPlanner(teamId: string, month: string, plannerEmployeeId: string, hrUsername: string) {
    return this.shiftFacade.hrAssignPlannerForTeam(teamId, month, plannerEmployeeId, hrUsername);
  }

  assignShiftVerifiers(teamId: string, month: string, verifierEmployeeIds: string[], hrUsername: string) {
    return this.shiftFacade.hrAssignVerifiersForTeam(teamId, month, verifierEmployeeIds, hrUsername);
  }

  getTeamShiftAssignments(teamId: string, month: string) {
    return this.shiftFacade.getHrTeamShiftAssignments(teamId, month);
  }

  getTimingManagerAssignments(teamId: string, month: string) {
    return this.shiftFacade.hrGetTimingManagerAssignments(teamId, month);
  }

  saveTimingManagerAssignments(
    teamId: string,
    month: string,
    assignments: Array<{
      employeeId: string;
      managerEmployeeId: string | null;
      shiftDefinitionId?: string | null;
      shiftStartTime?: string | null;
      shiftEndTime?: string | null;
    }>,
    hrUsername: string,
  ) {
    return this.shiftFacade.hrSaveTimingManagerAssignments(teamId, month, assignments, hrUsername);
  }

  listShiftDefinitions(teamId: string) {
    return this.shiftFacade.listShiftDefinitions(teamId);
  }

  upsertShiftDefinition(
    teamId: string,
    hrUsername: string,
    input: {
      id?: string;
      name: string;
      startTime: string;
      endTime: string;
      graceMinutes?: number;
      workingDays?: number[];
    },
  ) {
    return this.shiftFacade.upsertShiftDefinition(teamId, `HR:${hrUsername}`, input);
  }

  deleteShiftDefinition(teamId: string, definitionId: string) {
    return this.shiftFacade.deleteShiftDefinition(teamId, definitionId);
  }

  applyMappedShifts(teamId: string, month: string) {
    return this.shiftFacade.applyMappedShiftDefinitions(teamId, month, null);
  }

  async waivePenalty(penaltyId: string, hrUsername: string, reason: string) {
    const penalty = await this.prisma.lateness_penalties.findUnique({ where: { id: penaltyId } });
    if (!penalty || penalty.waived) throw new NotFoundError();

    await this.balanceService.adjust(
      penalty.employee_id,
      'casual_leave',
      Number(penalty.penalty_days),
      `Penalty waived: ${reason}`,
      hrUsername,
    );

    await this.prisma.lateness_penalties.update({
      where: { id: penaltyId },
      data: { waived: true, waived_by: hrUsername, waived_at: new Date() },
    });

    await this.prisma.leaves_audit_log.create({
      data: {
        actor_id: hrUsername,
        action: 'PENALTY_WAIVE',
        entity: 'lateness_penalty',
        entity_id: penaltyId,
        after_json: JSON.stringify({ reason }),
      },
    });
    return { waived: true };
  }

  async hrApproveBackdated(hrUsername: string, leaveId: string, approvalReason: string, backdatedFacade: BackdatedLeaveFacade) {
    return backdatedFacade.approve(`HR:${hrUsername}`, leaveId, approvalReason);
  }

  async getShiftCurrentMonthSetting() {
    const enabled = await this.appSettings.isShiftCurrentMonthEnabled();
    return { enabled };
  }

  async setShiftCurrentMonthSetting(enabled: boolean, hrUsername: string) {
    const result = await this.appSettings.setShiftCurrentMonthEnabled(enabled);
    await this.prisma.leaves_audit_log.create({
      data: {
        actor_id: hrUsername,
        action: 'SHIFT_CURRENT_MONTH_TOGGLE',
        entity: 'app_settings',
        entity_id: 'leaves.shift_current_month_enabled',
        after_json: JSON.stringify({ enabled }),
      },
    });
    return result;
  }

  async getOtpDebugSetting() {
    const enabled = await this.appSettings.isOtpDebugEnabled();
    return { enabled };
  }

  async setOtpDebugSetting(enabled: boolean, hrUsername: string) {
    const result = await this.appSettings.setOtpDebugEnabled(enabled);
    await this.prisma.leaves_audit_log.create({
      data: {
        actor_id: hrUsername,
        action: 'OTP_DEBUG_TOGGLE',
        entity: 'app_settings',
        entity_id: 'leaves.otp_debug_enabled',
        after_json: JSON.stringify({ enabled }),
      },
    });
    return result;
  }

  listEmployees(search?: string) {
    return this.employeeSettings.listEmployeesWithSettings(search);
  }

  async setAttendanceAutoWeekdays(employeeId: string, enabled: boolean, hrUsername: string) {
    const emp = await this.prisma.employees.findUnique({ where: { employee_id: employeeId } });
    if (!emp) throw new NotFoundError('Employee not found');
    const result = await this.employeeSettings.setAutoWeekdays(employeeId, enabled, hrUsername);
    await this.prisma.leaves_audit_log.create({
      data: {
        actor_id: hrUsername,
        action: 'ATTENDANCE_AUTO_WEEKDAYS',
        entity: 'employee',
        entity_id: employeeId,
        after_json: JSON.stringify({ enabled }),
      },
    });
    return result;
  }

  async applyDefaultAutoWeekdayLeaders(hrUsername: string) {
    const result = await this.employeeSettings.applyDefaultAutoWeekdayLeaders(hrUsername);
    await this.prisma.leaves_audit_log.create({
      data: {
        actor_id: hrUsername,
        action: 'APPLY_DEFAULT_AUTO_WEEKDAYS',
        entity: 'employee_settings',
        after_json: JSON.stringify(result),
      },
    });
    return result;
  }

  async recordLeave(hrUsername: string, employeeId: string, dto: CreateLeaveDto, leaveFacade: LeaveFacade) {
    return leaveFacade.hrRecordApprovedLeave(hrUsername, employeeId, dto);
  }

  syncAutoWeekdayAttendance(from?: string, to?: string) {
    const today = todayInCompanyTz();
    return this.autoWeekdayAttendance.syncRange(from ?? today, to ?? today);
  }

  listTeams() {
    return this.teamManagement.listTeams();
  }

  getTeam(teamId: string) {
    return this.teamManagement.getTeam(teamId);
  }

  addTeamMember(teamId: string, employeeId: string, hrUsername: string) {
    return this.teamManagement.addMember(teamId, employeeId, hrUsername);
  }

  removeTeamMember(teamId: string, employeeId: string, hrUsername: string) {
    return this.teamManagement.removeMember(teamId, employeeId, hrUsername);
  }

  setTeamLead(teamId: string, employeeId: string, hrUsername: string) {
    return this.teamManagement.setLead(teamId, employeeId, hrUsername);
  }
}
