import { getPrisma } from '../infrastructure/database/PrismaSingleton';
import { TeamResolutionService } from '../domain/team/TeamResolutionService';
import { LeaveRepository } from '../repositories/LeaveRepository';
import { LeaveMonthCounterService } from '../domain/leave/LeaveMonthCounterService';
import { NotificationFactory } from '../factories/NotificationFactory';
import { CompensationFacade } from './CompensationFacade';
import { parseDateOnly, toDateOnly, getYearMonth, isWeekend, todayInCompanyTz } from '../domain/leave/dateUtils';
import { hoursBetween, isEarlyLeave, isLoginLate, logoutDayStatus, MIN_FULL_DAY_HOURS } from '../domain/attendance/AttendanceDayRules';
import { ConflictError, ForbiddenError, NotFoundError } from '../types/errors';
import { HolidayRepository } from '../repositories/HolidayRepository';
import { EmployeeSettingsService } from '../services/EmployeeSettingsService';
import { AutoWeekdayAttendanceService } from '../services/AutoWeekdayAttendanceService';

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

  constructor(
    private teamService = new TeamResolutionService(),
    private leaveRepo = new LeaveRepository(),
    private holidayRepo = new HolidayRepository(),
    private notifications: NotificationFactory,
    private compensationFacade = new CompensationFacade(),
    private employeeSettings = new EmployeeSettingsService(),
    private autoWeekdayAttendance = new AutoWeekdayAttendanceService(),
  ) {}

  private prisma = getPrisma();

  private async getShiftTimes(
    employeeId: string,
    dateStr: string,
  ): Promise<{ start: string; end: string; grace: number }> {
    const team = await this.teamService.resolveByBusinessId(employeeId);
    const month = getYearMonth(dateStr);
    const schedule = await this.prisma.team_shift_schedules.findUnique({
      where: { team_id_effective_month: { team_id: team.teamId, effective_month: month } },
    });
    return {
      start: schedule?.shift_start_time ?? '09:30',
      end: schedule?.shift_end_time ?? '18:30',
      grace: schedule?.grace_minutes ?? 10,
    };
  }

  async recordLogin(employeeId: string, timestamp: string, source = 'desktop') {
    const ts = new Date(timestamp);
    const dateStr = toDateOnly(ts);
    const idempotencyKey = `login:${employeeId}:${dateStr}`;

    const existing = await this.prisma.attendance_events.findUnique({ where: { idempotency_key: idempotencyKey } });
    if (existing) return { duplicate: true };

    const approvedLeave = await this.leaveRepo.findApprovedOverlapping(employeeId, parseDateOnly(dateStr));
    if (approvedLeave && approvedLeave.duration === 'FULL_DAY') {
      throw new ConflictError('Login conflicts with approved full-day leave');
    }

    await this.prisma.attendance_events.create({
      data: {
        employee_id: employeeId,
        event_type: 'LOGIN',
        timestamp: ts,
        source,
        idempotency_key: idempotencyKey,
      },
    });

    const { start, grace } = await this.getShiftTimes(employeeId, dateStr);
    const isLate = isLoginLate(ts, start, grace);

    const month = getYearMonth(dateStr);
    const summaries = await this.prisma.attendance_daily_summary.findMany({
      where: { employee_id: employeeId, date: { gte: parseDateOnly(`${month}-01`) } },
    });
    const lateCountMonth = summaries.filter((s) => s.is_late).length + (isLate ? 1 : 0);

    await this.prisma.attendance_daily_summary.upsert({
      where: { employee_id_date: { employee_id: employeeId, date: parseDateOnly(dateStr) } },
      create: {
        employee_id: employeeId,
        date: parseDateOnly(dateStr),
        status: isLate ? 'late' : 'present',
        login_at: ts,
        is_late: isLate,
        late_count_month: lateCountMonth,
      },
      update: {
        status: isLate ? 'late' : 'present',
        login_at: ts,
        is_late: isLate,
        late_count_month: lateCountMonth,
      },
    });

    // Policy: late join = short leave; every 3 short leaves in a month → 1 leave.
    if (isLate && !approvedLeave) {
      const result = await this.monthCounters.recordShortLeave(employeeId, dateStr, 'LATE_JOIN', idempotencyKey);
      if (result.deducted > 0) {
        const emp = await this.prisma.employees.findUnique({ where: { employee_id: employeeId } });
        if (emp) await this.notifications.createEmail('LATE_PENALTY', { to: emp.email }).send();
      }
    }

    return { status: isLate ? 'late' : 'present', lateCountMonth };
  }

  async recordLogout(employeeId: string, timestamp: string, source = 'desktop') {
    const ts = new Date(timestamp);
    const dateStr = toDateOnly(ts);

    await this.prisma.attendance_events.create({
      data: { employee_id: employeeId, event_type: 'LOGOUT', timestamp: ts, source },
    });

    const summary = await this.prisma.attendance_daily_summary.findUnique({
      where: { employee_id_date: { employee_id: employeeId, date: parseDateOnly(dateStr) } },
    });
    const loginAt = summary?.login_at;
    const hours = loginAt ? hoursBetween(loginAt, ts) : 0;

    const { end } = await this.getShiftTimes(employeeId, dateStr);
    const earlyLeave = isEarlyLeave(ts, end);
    const status = logoutDayStatus(hours, earlyLeave, Boolean(summary?.is_late));

    await this.prisma.attendance_daily_summary.upsert({
      where: { employee_id_date: { employee_id: employeeId, date: parseDateOnly(dateStr) } },
      create: {
        employee_id: employeeId,
        date: parseDateOnly(dateStr),
        status,
        logout_at: ts,
        hours_worked: hours,
      },
      update: { status, logout_at: ts, hours_worked: hours },
    });

    const approvedLeave = await this.leaveRepo.findApprovedOverlapping(employeeId, parseDateOnly(dateStr));
    // Policy: early leave = short leave; every 3 short leaves in a month → 1 leave.
    if (earlyLeave && !approvedLeave) {
      await this.monthCounters.recordShortLeave(
        employeeId,
        dateStr,
        'EARLY_LEAVE',
        `logout:${employeeId}:${dateStr}`,
      );
    }

    if (hours >= MIN_FULL_DAY_HOURS) {
      await this.compensationFacade.evaluateOffDayWork(employeeId, dateStr, hours);
    }

    return { status, workingHours: hours, earlyLeave };
  }

  async getStatus(employeeId: string, date: string) {
    const summary = await this.prisma.attendance_daily_summary.findUnique({
      where: { employee_id_date: { employee_id: employeeId, date: parseDateOnly(date) } },
    });
    if (!summary) {
      return { employeeId, date, status: 'absent', workingHours: 0, lateCountMonth: 0 };
    }
    return {
      employeeId,
      date,
      status: summary.status,
      workingHours: Number(summary.hours_worked ?? 0),
      lateCountMonth: summary.late_count_month,
    };
  }

  async myHistory(employeeId: string, from?: string, to?: string) {
    return this.historyForEmployee(employeeId, from, to);
  }

  async teamMemberHistory(actorId: string, targetEmployeeId: string, from?: string, to?: string) {
    if (await this.teamService.isAppAdmin(actorId)) {
      return this.historyForEmployee(targetEmployeeId, from, to);
    }

    const actorTeams = await this.teamService.listActiveTeamsForEmployee(actorId);
    for (const team of actorTeams) {
      const visible = await this.teamService.listVisibleTeamMembers(team.teamId, actorId);
      if (visible.some((m) => m.employeeId === targetEmployeeId)) {
        return this.historyForEmployee(targetEmployeeId, from, to);
      }
    }
    throw new ForbiddenError('Not allowed to view attendance for this employee');
  }

  private async historyForEmployee(employeeId: string, from?: string, to?: string) {
    const where: { employee_id: string; date?: { gte?: Date; lte?: Date } } = { employee_id: employeeId };
    if (from || to) {
      where.date = {};
      if (from) where.date.gte = parseDateOnly(from);
      if (to) where.date.lte = parseDateOnly(to);
    }
    const rows = await this.prisma.attendance_daily_summary.findMany({
      where,
      orderBy: { date: 'desc' },
    });
    return {
      items: rows.map((r) => ({
        date: toDateOnly(r.date),
        status: r.status,
        workingHours: Number(r.hours_worked ?? 0),
        isLate: r.is_late,
      })),
    };
  }

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

  async markAbsents(dateStr: string) {
    const employees = await this.prisma.employees.findMany({ where: { status: 'active' } });
    const holidays = await this.holidayRepo.listInRange(parseDateOnly(dateStr), parseDateOnly(dateStr));
    if (isWeekend(dateStr) || holidays.length > 0) return { skipped: true };

    let marked = 0;
    let autoPresent = 0;
    for (const emp of employees) {
      if (await this.employeeSettings.isAutoWeekdays(emp.employee_id)) {
        if (await this.autoWeekdayAttendance.syncEmployeeDay(emp.employee_id, dateStr)) autoPresent++;
        continue;
      }

      const summary = await this.prisma.attendance_daily_summary.findUnique({
        where: { employee_id_date: { employee_id: emp.employee_id, date: parseDateOnly(dateStr) } },
      });
      if (summary?.login_at) continue;
      const leave = await this.leaveRepo.findApprovedOverlapping(emp.employee_id, parseDateOnly(dateStr));
      if (leave) continue;

      await this.prisma.attendance_daily_summary.upsert({
        where: { employee_id_date: { employee_id: emp.employee_id, date: parseDateOnly(dateStr) } },
        create: { employee_id: emp.employee_id, date: parseDateOnly(dateStr), status: 'absent' },
        update: { status: 'absent' },
      });
      marked++;
    }
    return { marked, autoPresent, date: dateStr };
  }
}
