import { HttpStatus, Injectable } from '@nestjs/common';
import { formatInTimeZone, fromZonedTime } from 'date-fns-tz';
import * as QRCode from 'qrcode';
import { randomBytes } from 'crypto';
import { Between, In, IsNull, Not } from 'typeorm';
import { PermissionUserContext } from '../../common/access-control/permission-context.types';
import {
  assertToggle,
  resolveDataScope,
} from '../../common/access-control/permission-resolver.helpers';
import { PermissionResolverService } from '../../common/access-control/permission-resolver.service';
import { OrgTimezoneService } from '../../common/timezone/org-timezone.service';
import { DatabaseService, ErrorResponse, logger } from '../../config';
import {
  AttendanceStatusEnum,
  AttendanceTypeEnum,
} from '../../config/database/entities';
import { createGeneralError, getUtcStartEndOfDate } from '../../utils';
import { DevicesService } from '../devices/devices.service';
import { IpWhitelistService } from '../ip-whitelist/ip-whitelist.service';
import {
  calculateAttendanceHours,
  combineDateAndTime,
  determineAttendanceStatus,
} from './attendance-hours.helper';
import {
  AdminManualAttendanceDto,
  AttendanceFilterDto,
  AttendanceReportFilterDto,
  CreateQrPointDto,
  ScanQrAttendanceDto,
  TeacherCheckInDto,
  TeacherCheckOutDto,
  UpdateQrPointDto,
  attendanceReportResponseArraySchema,
  attendanceResponseSchema,
  attendancesResponseSchema,
  qrPointListResponseSchema,
  qrPointResponseSchema,
  qrScanResultResponseSchema,
  todayAttendancesResponseSchema,
} from './attendance.dto';

@Injectable()
export class AttendanceService {
  constructor(
    private readonly db: DatabaseService,
    private readonly permissionResolver: PermissionResolverService,
    private readonly orgTimezone: OrgTimezoneService,
    private readonly ipWhitelistService: IpWhitelistService,
    private readonly devicesService: DevicesService,
  ) {}

  /**
   * Returns day of week 0..6 (0=Sun, 1=Mon, ..., 6=Sat) in the given timezone
   */
  private getDayOfWeekInTimezone(date: Date, timeZone: string): number {
    return Number(formatInTimeZone(date, timeZone, 'i')) % 7;
  }

  private async getPermissions(ctx: PermissionUserContext) {
    return this.permissionResolver.resolveForUser(ctx);
  }

  /**
   * Generates a Data URL image string for a QR code string
   */
  private async generateQrDataUrl(code: string): Promise<string> {
    try {
      return await QRCode.toDataURL(code, {
        errorCorrectionLevel: 'H',
        margin: 2,
        width: 360,
        color: {
          dark: '#000000',
          light: '#ffffff',
        },
      });
    } catch (err) {
      logger.error('Error generating QR code data URL', err);
      return '';
    }
  }

  /**
   * Haversine formula to compute great-circle distance between two GPS coordinates in meters
   */
  private calculateHaversineDistanceMeters(
    lat1: number,
    lon1: number,
    lat2: number,
    lon2: number,
  ): number {
    const R = 6371e3; // Earth's radius in meters
    const phi1 = (lat1 * Math.PI) / 180;
    const phi2 = (lat2 * Math.PI) / 180;
    const deltaPhi = ((lat2 - lat1) * Math.PI) / 180;
    const deltaLambda = ((lon2 - lon1) * Math.PI) / 180;

    const a =
      Math.sin(deltaPhi / 2) * Math.sin(deltaPhi / 2) +
      Math.cos(phi1) *
        Math.cos(phi2) *
        Math.sin(deltaLambda / 2) *
        Math.sin(deltaLambda / 2);
    const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));

    return Math.round(R * c);
  }

  /**
   * Validates both IP Whitelist and Location Geofencing based on configured attendance_settings.
   * Returns distance in meters if location was validated, and coordinates.
   */
  private async validateAttendanceRequirements(
    ownerId: string,
    clientIp: string,
    coords?: { latitude?: number; longitude?: number },
  ): Promise<{
    distanceMeters: number | null;
    latitude: number | null;
    longitude: number | null;
  }> {
    const settings = await this.db.attendance_settings.findFirst({
      where: { owner_id: ownerId },
    });

    // 1. IP Whitelist Check (if enforce_ip_whitelist is active)
    if (settings?.enforce_ip_whitelist) {
      const isIpAllowed = await this.ipWhitelistService.isIpAllowed(
        ownerId,
        clientIp,
      );
      if (!isIpAllowed) {
        throw new ErrorResponse(
          'Network Restricted: You must be connected to the institution authorized network (Wi-Fi/LAN) to record attendance.',
          HttpStatus.FORBIDDEN,
        );
      }
    }

    // 2. Geolocation Geofence Check (if enforce_location is active)
    let distanceMeters: number | null = null;
    const userLat = coords?.latitude ?? null;
    const userLng = coords?.longitude ?? null;

    if (settings?.enforce_location) {
      if (
        userLat === null ||
        userLat === undefined ||
        userLng === null ||
        userLng === undefined
      ) {
        throw new ErrorResponse(
          'Location Required: GPS location access is required to verify your presence on campus. Please enable location services in your browser.',
          HttpStatus.BAD_REQUEST,
        );
      }

      if (
        settings.latitude === null ||
        settings.latitude === undefined ||
        settings.longitude === null ||
        settings.longitude === undefined
      ) {
        logger.warn(
          `Location validation enabled for owner ${ownerId} but coordinates not configured.`,
        );
      } else {
        const adminLat = Number(settings.latitude);
        const adminLng = Number(settings.longitude);
        const radius = Number(settings.radius_meters) || 100;

        distanceMeters = this.calculateHaversineDistanceMeters(
          userLat,
          userLng,
          adminLat,
          adminLng,
        );

        if (distanceMeters > radius) {
          throw new ErrorResponse(
            `Location Restricted: You are ${distanceMeters}m away from ${settings.location_name || 'the campus'} (Allowed radius: ${radius}m). Please mark attendance on-site.`,
            HttpStatus.FORBIDDEN,
          );
        }
      }
    }

    // If coordinates provided without strict enforcement, record distance if admin location is known
    if (
      distanceMeters === null &&
      userLat !== null &&
      userLng !== null &&
      settings?.latitude !== null &&
      settings?.latitude !== undefined &&
      settings?.longitude !== null &&
      settings?.longitude !== undefined
    ) {
      distanceMeters = this.calculateHaversineDistanceMeters(
        userLat,
        userLng,
        Number(settings.latitude),
        Number(settings.longitude),
      );
    }

    return {
      distanceMeters,
      latitude: userLat,
      longitude: userLng,
    };
  }

  // =========================================================================
  // QR CODE POINT MANAGEMENT (Admin)
  // =========================================================================

  /**
   * Retrieves or creates the primary static QR code point for the school
   */
  async getPrimaryQrPoint(ownerId: string) {
    try {
      let primaryPoint = await this.db.qr_attendance_points.findFirst({
        where: { owner_id: ownerId, is_active: true },
        orderBy: { created_at: 'asc' },
      });

      if (!primaryPoint) {
        const uniqueCode = `QR-ATT-${randomBytes(12).toString('hex').toUpperCase()}`;
        primaryPoint = await this.db.qr_attendance_points.create({
          data: {
            owner_id: ownerId,
            label: 'Main Campus Attendance QR',
            code: uniqueCode,
            is_active: true,
          },
        });
      }

      const qrDataUrl = await this.generateQrDataUrl(primaryPoint.code);
      const parsed = qrPointResponseSchema.safeParse({
        ...primaryPoint,
        qr_data_url: qrDataUrl,
      });

      return {
        status: HttpStatus.OK,
        message: 'Primary attendance QR point retrieved successfully',
        data: parsed.data,
      };
    } catch (error) {
      logger.error('Error in getPrimaryQrPoint', error);
      if (error instanceof ErrorResponse) throw error;
      throw new ErrorResponse(error, HttpStatus.INTERNAL_SERVER_ERROR);
    }
  }

  /**
   * Lists all QR points for the organization with QR image URLs
   */
  async listQrPoints(ownerId: string) {
    try {
      // Ensure at least one primary QR exists
      await this.getPrimaryQrPoint(ownerId);

      const points = await this.db.qr_attendance_points.findMany({
        where: { owner_id: ownerId },
        orderBy: { created_at: 'desc' },
      });

      const withImages = await Promise.all(
        points.map(async (point: any) => ({
          ...point,
          qr_data_url: await this.generateQrDataUrl(point.code),
        })),
      );

      const parsed = qrPointListResponseSchema.safeParse(withImages);
      if (parsed.error) {
        throw new ErrorResponse(parsed.error.message, HttpStatus.INTERNAL_SERVER_ERROR);
      }

      return {
        status: HttpStatus.OK,
        message: 'QR points retrieved successfully',
        data: parsed.data,
      };
    } catch (error) {
      logger.error('Error in listQrPoints', error);
      if (error instanceof ErrorResponse) throw error;
      throw new ErrorResponse(error, HttpStatus.INTERNAL_SERVER_ERROR);
    }
  }

  /**
   * Admin creates a new QR point (e.g. for Secondary Gate, Science Lab, etc.)
   */
  async createQrPoint(ownerId: string, dto: CreateQrPointDto) {
    try {
      const uniqueCode = `QR-ATT-${randomBytes(12).toString('hex').toUpperCase()}`;
      const newPoint = await this.db.qr_attendance_points.create({
        data: {
          owner_id: ownerId,
          label: dto.label.trim(),
          code: uniqueCode,
          is_active: dto.is_active ?? true,
        },
      });

      const qrDataUrl = await this.generateQrDataUrl(newPoint.code);
      const parsed = qrPointResponseSchema.safeParse({
        ...newPoint,
        qr_data_url: qrDataUrl,
      });

      return {
        status: HttpStatus.CREATED,
        message: 'New QR attendance point created successfully',
        data: parsed.data,
      };
    } catch (error) {
      logger.error('Error in createQrPoint', error);
      if (error instanceof ErrorResponse) throw error;
      throw new ErrorResponse(error, HttpStatus.INTERNAL_SERVER_ERROR);
    }
  }

  /**
   * Admin updates a QR point
   */
  async updateQrPoint(ownerId: string, id: string, dto: UpdateQrPointDto) {
    try {
      const point = await this.db.qr_attendance_points.findFirst({
        where: { id, owner_id: ownerId },
      });

      if (!point) {
        throw new ErrorResponse('QR point not found', HttpStatus.NOT_FOUND);
      }

      const updated = await this.db.qr_attendance_points.update({
        where: { id },
        data: {
          ...(dto.label !== undefined ? { label: dto.label.trim() } : {}),
          ...(dto.is_active !== undefined ? { is_active: dto.is_active } : {}),
        },
      });

      const qrDataUrl = await this.generateQrDataUrl(updated.code);
      const parsed = qrPointResponseSchema.safeParse({
        ...updated,
        qr_data_url: qrDataUrl,
      });

      return {
        status: HttpStatus.OK,
        message: 'QR attendance point updated successfully',
        data: parsed.data,
      };
    } catch (error) {
      logger.error('Error in updateQrPoint', error);
      if (error instanceof ErrorResponse) throw error;
      throw new ErrorResponse(error, HttpStatus.INTERNAL_SERVER_ERROR);
    }
  }

  /**
   * Admin deletes a QR point
   */
  async deleteQrPoint(ownerId: string, id: string) {
    try {
      const point = await this.db.qr_attendance_points.findFirst({
        where: { id, owner_id: ownerId },
      });

      if (!point) {
        throw new ErrorResponse('QR point not found', HttpStatus.NOT_FOUND);
      }

      await this.db.qr_attendance_points.delete({ where: { id } });

      return {
        status: HttpStatus.OK,
        message: 'QR attendance point removed successfully',
      };
    } catch (error) {
      logger.error('Error in deleteQrPoint', error);
      if (error instanceof ErrorResponse) throw error;
      throw new ErrorResponse(error, HttpStatus.INTERNAL_SERVER_ERROR);
    }
  }

  // =========================================================================
  // TEACHER QR CODE SCAN ATTENDANCE
  // =========================================================================

  /**
   * Teacher scans static QR code:
   * - Validates network / IP whitelist
   * - Validates teacher device lock & anti-proxy
   * - Validates QR code authenticity & active state
   * - Validates schedule & verifies the working day is not over
   * - Automatically records CHECK-IN if not yet checked in, or CHECK-OUT if already checked in
   */
  async scanQr(
    memberId: string,
    ownerId: string,
    body: ScanQrAttendanceDto,
    clientIp: string,
  ) {
    try {
      const now = new Date();

      // 1. Validate Network IP & GPS Geofence requirements
      const geoCheck = await this.validateAttendanceRequirements(
        ownerId,
        clientIp,
        { latitude: body.latitude, longitude: body.longitude },
      );

      // 2. Validate Device Lock & Anti-Proxy
      const deviceCheck = await this.devicesService.validateOrBindDevice(
        memberId,
        ownerId,
        body.device_id,
        body.device_name,
        clientIp,
      );
      if (!deviceCheck.valid) {
        throw new ErrorResponse(
          deviceCheck.message || 'Device validation failed.',
          HttpStatus.FORBIDDEN,
        );
      }

      // 3. Verify Static QR Code
      const qrPoint = await this.db.qr_attendance_points.findFirst({
        where: {
          owner_id: ownerId,
          code: body.qr_code.trim(),
          is_active: true,
        },
      });

      if (!qrPoint) {
        throw new ErrorResponse(
          'Invalid or inactive QR code. Please scan an authorized school QR code.',
          HttpStatus.BAD_REQUEST,
        );
      }

      // 4. Timezone & Schedule validation
      const timeZone = await this.orgTimezone.getTimezone(ownerId);
      const todayStr = formatInTimeZone(now, timeZone, 'yyyy-MM-dd');

      const dayOfWeek = this.getDayOfWeekInTimezone(now, timeZone);
      const daySchedule = await this.db.work_day_schedules.findFirst({
        where: {
          owner_id: ownerId,
          day_of_week: dayOfWeek,
        },
      });

      if (daySchedule && !daySchedule.is_active) {
        throw new ErrorResponse(
          'Today is configured as a non-working day/weekend. Attendance is not permitted.',
          HttpStatus.BAD_REQUEST,
        );
      }

      const startTimeStr = daySchedule?.start_time || '09:00';
      const endTimeStr = daySchedule?.end_time || '17:00';
      const gracePeriodMinutes = daySchedule?.grace_period_minutes ?? 15;

      const scheduledDayEnd = combineDateAndTime(todayStr, endTimeStr, timeZone);

      // 5. Look for existing attendance record today
      const existingAttendance = await this.db.attendances.findFirst({
        where: {
          member_id: memberId,
          owner_id: ownerId,
          date: todayStr,
        },
      });

      // SCENARIO 1: Teacher has NOT checked in today -> ACTION: CHECK-IN
      if (!existingAttendance || !existingAttendance.check_in) {
        // If the working day is already over, check-in is not permitted
        if (now > scheduledDayEnd) {
          throw new ErrorResponse(
            `Working day has already ended at ${endTimeStr}. Check-in is closed for today.`,
            HttpStatus.BAD_REQUEST,
          );
        }

        const calculatedStatus = determineAttendanceStatus(
          now,
          startTimeStr,
          gracePeriodMinutes,
          timeZone,
        );

        let savedAttendance;
        if (existingAttendance) {
          savedAttendance = await this.db.attendances.update({
            where: { id: existingAttendance.id },
            data: {
              check_in: now,
              status: calculatedStatus as AttendanceStatusEnum,
              check_in_ip: clientIp,
              device_id: body.device_id.trim(),
              attendance_method: 'qr',
              qr_point_id: qrPoint.id,
              check_in_latitude: geoCheck.latitude,
              check_in_longitude: geoCheck.longitude,
              check_in_distance_meters: geoCheck.distanceMeters,
              ...(body.note ? { note: body.note } : {}),
            },
          });
        } else {
          savedAttendance = await this.db.attendances.create({
            data: {
              member_id: memberId,
              owner_id: ownerId,
              date: todayStr,
              check_in: now,
              status: calculatedStatus as AttendanceStatusEnum,
              check_in_ip: clientIp,
              device_id: body.device_id.trim(),
              attendance_method: 'qr',
              qr_point_id: qrPoint.id,
              type: AttendanceTypeEnum.in_office,
              check_in_latitude: geoCheck.latitude,
              check_in_longitude: geoCheck.longitude,
              check_in_distance_meters: geoCheck.distanceMeters,
              ...(body.note ? { note: body.note } : {}),
            },
          });
        }

        return {
          status: HttpStatus.OK,
          message: `Check-in recorded successfully via QR code (${calculatedStatus.toUpperCase()})`,
          data: qrScanResultResponseSchema.safeParse({
            action: 'CHECK_IN',
            status: calculatedStatus,
            check_in: now.toISOString(),
            check_out: null,
            work_hours: 0,
            break_hours: 0,
            net_hours: 0,
            overtime_hours: 0,
            attendance: attendanceResponseSchema.safeParse(savedAttendance).data,
            message: `Checked in successfully at ${formatInTimeZone(now, timeZone, 'hh:mm a')}`,
          }).data,
        };
      }

      // SCENARIO 2: Teacher has checked in, but has NOT checked out -> ACTION: CHECK-OUT
      if (existingAttendance.check_in && !existingAttendance.check_out) {
        const checkInTime = new Date(existingAttendance.check_in).getTime();
        const diffSeconds = Math.floor((now.getTime() - checkInTime) / 1000);
        if (diffSeconds < 60) {
          throw new ErrorResponse(
            `You just clocked in ${diffSeconds}s ago. Please wait at least 1 minute before checking out.`,
            HttpStatus.BAD_REQUEST,
          );
        }

        const scheduleConfig = {
          start_time: startTimeStr,
          end_time: endTimeStr,
          break_start: daySchedule?.break_start || null,
          break_end: daySchedule?.break_end || null,
        };

        const calculatedHours = calculateAttendanceHours(
          new Date(existingAttendance.check_in),
          now,
          scheduleConfig,
          timeZone,
        );

        const updated = await this.db.attendances.update({
          where: { id: existingAttendance.id },
          data: {
            check_out: now,
            work_hours: calculatedHours.work_hours,
            break_hours: calculatedHours.break_hours,
            net_hours: calculatedHours.net_hours,
            overtime_hours: calculatedHours.overtime_hours,
            check_out_ip: clientIp,
            attendance_method: 'qr',
            qr_point_id: qrPoint.id,
            check_out_latitude: geoCheck.latitude,
            check_out_longitude: geoCheck.longitude,
            check_out_distance_meters: geoCheck.distanceMeters,
            ...(body.note ? { note: body.note } : {}),
          },
        });

        return {
          status: HttpStatus.OK,
          message: `Check-out recorded successfully via QR code at ${formatInTimeZone(now, timeZone, 'hh:mm a')}`,
          data: qrScanResultResponseSchema.safeParse({
            action: 'CHECK_OUT',
            status: existingAttendance.status,
            check_in: new Date(existingAttendance.check_in).toISOString(),
            check_out: now.toISOString(),
            work_hours: calculatedHours.work_hours,
            break_hours: calculatedHours.break_hours,
            net_hours: calculatedHours.net_hours,
            overtime_hours: calculatedHours.overtime_hours,
            attendance: attendanceResponseSchema.safeParse(updated).data,
            message: `Checked out successfully. Total Work: ${calculatedHours.work_hours} hrs, Net: ${calculatedHours.net_hours} hrs`,
          }).data,
        };
      }

      // SCENARIO 3: Teacher already has both check_in and check_out -> ALREADY_COMPLETED
      return {
        status: HttpStatus.OK,
        message: 'Attendance for today is already completed (both Check-In and Check-Out are recorded).',
        data: qrScanResultResponseSchema.safeParse({
          action: 'ALREADY_COMPLETED',
          status: existingAttendance.status,
          check_in: new Date(existingAttendance.check_in).toISOString(),
          check_out: new Date(existingAttendance.check_out).toISOString(),
          work_hours: existingAttendance.work_hours,
          break_hours: existingAttendance.break_hours,
          net_hours: existingAttendance.net_hours,
          overtime_hours: existingAttendance.overtime_hours,
          attendance: attendanceResponseSchema.safeParse(existingAttendance).data,
          message: 'Attendance already completed for today.',
        }).data,
      };
    } catch (error) {
      logger.error('Error in scanQr', error);
      if (error instanceof ErrorResponse) throw error;
      throw new ErrorResponse(error, HttpStatus.INTERNAL_SERVER_ERROR);
    }
  }

  // =========================================================================
  // STANDARD ATTENDANCE METHODS (Check-In, Check-Out, Admin Entry, Filters)
  // =========================================================================

  /**
   * Teacher Manual Check-In (network validated + device locked + anti-proxy enforced)
   */
  async checkIn(
    memberId: string,
    ownerId: string,
    body: TeacherCheckInDto,
    clientIp: string,
  ) {
    try {
      const now = new Date();

      // 1. Validate Network IP & GPS Geofence requirements
      const geoCheck = await this.validateAttendanceRequirements(
        ownerId,
        clientIp,
        { latitude: body.latitude, longitude: body.longitude },
      );

      // 2. Validate Device Lock & Anti-Proxy
      const deviceCheck = await this.devicesService.validateOrBindDevice(
        memberId,
        ownerId,
        body.device_id,
        body.device_name,
        clientIp,
      );
      if (!deviceCheck.valid) {
        throw new ErrorResponse(
          deviceCheck.message || 'Device validation failed.',
          HttpStatus.FORBIDDEN,
        );
      }

      // 3. Timezone & Schedule validation
      const timeZone = await this.orgTimezone.getTimezone(ownerId);
      const todayStr = formatInTimeZone(now, timeZone, 'yyyy-MM-dd');

      const dayOfWeek = this.getDayOfWeekInTimezone(now, timeZone);
      const daySchedule = await this.db.work_day_schedules.findFirst({
        where: {
          owner_id: ownerId,
          day_of_week: dayOfWeek,
        },
      });

      if (daySchedule && !daySchedule.is_active) {
        throw new ErrorResponse(
          'Today is configured as a non-working day/weekend. Check-in is not permitted.',
          HttpStatus.BAD_REQUEST,
        );
      }

      const startTimeStr = daySchedule?.start_time || '09:00';
      const endTimeStr = daySchedule?.end_time || '17:00';
      const gracePeriodMinutes = daySchedule?.grace_period_minutes ?? 15;

      const scheduledDayEnd = combineDateAndTime(todayStr, endTimeStr, timeZone);
      if (now > scheduledDayEnd) {
        throw new ErrorResponse(
          `Working day has already ended at ${endTimeStr}. Check-in is closed for today.`,
          HttpStatus.BAD_REQUEST,
        );
      }

      // 4. Check existing attendance record today
      const existingAttendance = await this.db.attendances.findFirst({
        where: {
          member_id: memberId,
          owner_id: ownerId,
          date: todayStr,
        },
      });

      if (existingAttendance && existingAttendance.check_in) {
        const checkInFormatted = formatInTimeZone(
          existingAttendance.check_in,
          timeZone,
          'hh:mm a',
        );
        throw new ErrorResponse(
          `You have already checked in today at ${checkInFormatted}.`,
          HttpStatus.BAD_REQUEST,
        );
      }

      // 5. Determine Present vs Late status
      const calculatedStatus = determineAttendanceStatus(
        now,
        startTimeStr,
        gracePeriodMinutes,
        timeZone,
      );

      let savedAttendance;
      if (existingAttendance) {
        savedAttendance = await this.db.attendances.update({
          where: { id: existingAttendance.id },
          data: {
            check_in: now,
            status: calculatedStatus as AttendanceStatusEnum,
            check_in_ip: clientIp,
            device_id: body.device_id.trim(),
            attendance_method: 'manual',
            check_in_latitude: geoCheck.latitude,
            check_in_longitude: geoCheck.longitude,
            check_in_distance_meters: geoCheck.distanceMeters,
            ...(body.note ? { note: body.note } : {}),
          },
        });
      } else {
        savedAttendance = await this.db.attendances.create({
          data: {
            member_id: memberId,
            owner_id: ownerId,
            date: todayStr,
            check_in: now,
            status: calculatedStatus as AttendanceStatusEnum,
            check_in_ip: clientIp,
            device_id: body.device_id.trim(),
            attendance_method: 'manual',
            type: AttendanceTypeEnum.in_office,
            check_in_latitude: geoCheck.latitude,
            check_in_longitude: geoCheck.longitude,
            check_in_distance_meters: geoCheck.distanceMeters,
            ...(body.note ? { note: body.note } : {}),
          },
        });
      }

      return {
        status: HttpStatus.OK,
        message: `Check-in successful (${calculatedStatus.toUpperCase()})`,
        data: attendanceResponseSchema.safeParse(savedAttendance).data,
      };
    } catch (error) {
      logger.error('Error in checkIn', error);
      if (error instanceof ErrorResponse) throw error;
      throw new ErrorResponse(error, HttpStatus.INTERNAL_SERVER_ERROR);
    }
  }

  /**
   * Teacher Manual Check-Out
   */
  async checkOut(
    memberId: string,
    ownerId: string,
    body: TeacherCheckOutDto,
    clientIp: string,
  ) {
    try {
      const now = new Date();

      // 1. Validate Network IP & GPS Geofence requirements
      const geoCheck = await this.validateAttendanceRequirements(
        ownerId,
        clientIp,
        { latitude: body.latitude, longitude: body.longitude },
      );

      // 2. Validate Device Lock & Anti-Proxy
      const deviceCheck = await this.devicesService.validateOrBindDevice(
        memberId,
        ownerId,
        body.device_id,
        null,
        clientIp,
      );
      if (!deviceCheck.valid) {
        throw new ErrorResponse(
          deviceCheck.message || 'Device validation failed.',
          HttpStatus.FORBIDDEN,
        );
      }

      // 3. Timezone & find today's check-in
      const timeZone = await this.orgTimezone.getTimezone(ownerId);
      const todayStr = formatInTimeZone(now, timeZone, 'yyyy-MM-dd');

      const attendance = await this.db.attendances.findFirst({
        where: {
          member_id: memberId,
          owner_id: ownerId,
          date: todayStr,
          check_in: Not(IsNull()),
        },
      });

      if (!attendance || !attendance.check_in) {
        throw new ErrorResponse(
          'No check-in found for today. You must check in before checking out.',
          HttpStatus.BAD_REQUEST,
        );
      }

      if (attendance.check_out) {
        const checkOutFormatted = formatInTimeZone(
          attendance.check_out,
          timeZone,
          'hh:mm a',
        );
        throw new ErrorResponse(
          `You have already checked out today at ${checkOutFormatted}.`,
          HttpStatus.BAD_REQUEST,
        );
      }

      const checkInTime = new Date(attendance.check_in).getTime();
      const diffSeconds = Math.floor((now.getTime() - checkInTime) / 1000);
      if (diffSeconds < 60) {
        throw new ErrorResponse(
          `You just clocked in ${diffSeconds}s ago. Please wait at least 1 minute before checking out.`,
          HttpStatus.BAD_REQUEST,
        );
      }

      // 4. Fetch today's schedule for break & overtime calculations
      const dayOfWeek = this.getDayOfWeekInTimezone(now, timeZone);
      const daySchedule = await this.db.work_day_schedules.findFirst({
        where: {
          owner_id: ownerId,
          day_of_week: dayOfWeek,
        },
      });

      const scheduleConfig = {
        start_time: daySchedule?.start_time || '09:00',
        end_time: daySchedule?.end_time || '17:00',
        break_start: daySchedule?.break_start || null,
        break_end: daySchedule?.break_end || null,
      };

      const calculatedHours = calculateAttendanceHours(
        new Date(attendance.check_in),
        now,
        scheduleConfig,
        timeZone,
      );

      const updated = await this.db.attendances.update({
        where: { id: attendance.id },
        data: {
          check_out: now,
          work_hours: calculatedHours.work_hours,
          break_hours: calculatedHours.break_hours,
          net_hours: calculatedHours.net_hours,
          overtime_hours: calculatedHours.overtime_hours,
          check_out_ip: clientIp,
          check_out_latitude: geoCheck.latitude,
          check_out_longitude: geoCheck.longitude,
          check_out_distance_meters: geoCheck.distanceMeters,
          ...(body.note ? { note: body.note } : {}),
        },
      });

      return {
        status: HttpStatus.OK,
        message: 'Check-out successful',
        data: attendanceResponseSchema.safeParse(updated).data,
      };
    } catch (error) {
      logger.error('Error in checkOut', error);
      if (error instanceof ErrorResponse) throw error;
      throw new ErrorResponse(error, HttpStatus.INTERNAL_SERVER_ERROR);
    }
  }

  /**
   * Admin Manual Attendance Entry / Adjustment
   */
  async createOrUpdateAttendance(
    data: AdminManualAttendanceDto,
    owner_id: string,
    ctx: PermissionUserContext,
  ) {
    try {
      const permissions = await this.getPermissions(ctx);
      assertToggle(permissions, 'attendance.manage');

      const timeZone = await this.orgTimezone.getTimezone(owner_id);
      const dateStr = formatInTimeZone(new Date(data.date), timeZone, 'yyyy-MM-dd');

      const existing = await this.db.attendances.findFirst({
        where: {
          member_id: data.member_id,
          owner_id,
          date: dateStr,
        },
      });

      let work_hours = data.work_hours ?? 0;
      let break_hours = data.break_hours ?? 0;
      let net_hours = data.net_hours ?? 0;
      let overtime_hours = data.overtime_hours ?? 0;

      if (data.check_in && data.check_out && !data.work_hours) {
        const dayOfWeek = this.getDayOfWeekInTimezone(new Date(data.date), timeZone);
        const daySchedule = await this.db.work_day_schedules.findFirst({
          where: { owner_id, day_of_week: dayOfWeek },
        });

        const calculated = calculateAttendanceHours(
          new Date(data.check_in),
          new Date(data.check_out),
          {
            start_time: daySchedule?.start_time || '09:00',
            end_time: daySchedule?.end_time || '17:00',
            break_start: daySchedule?.break_start,
            break_end: daySchedule?.break_end,
          },
          timeZone,
        );
        work_hours = calculated.work_hours;
        break_hours = calculated.break_hours;
        net_hours = calculated.net_hours;
        overtime_hours = calculated.overtime_hours;
      }

      let result;
      if (existing) {
        result = await this.db.attendances.update({
          where: { id: existing.id },
          data: {
            check_in: data.check_in ?? existing.check_in,
            check_out: data.check_out ?? existing.check_out,
            status: (data.status as AttendanceStatusEnum) ?? existing.status,
            work_hours,
            break_hours,
            net_hours,
            overtime_hours,
            note: data.note ?? existing.note,
          },
        });
      } else {
        result = await this.db.attendances.create({
          data: {
            member_id: data.member_id,
            owner_id,
            date: dateStr,
            check_in: data.check_in ?? null,
            check_out: data.check_out ?? null,
            status: (data.status as AttendanceStatusEnum) ?? AttendanceStatusEnum.present,
            work_hours,
            break_hours,
            net_hours,
            overtime_hours,
            note: data.note ?? 'NT/APL',
            type: AttendanceTypeEnum.in_office,
          },
        });
      }

      return {
        status: HttpStatus.OK,
        message: 'Attendance record saved successfully',
        data: attendanceResponseSchema.safeParse(result).data,
      };
    } catch (error) {
      logger.error('Error in createOrUpdateAttendance', error);
      if (error instanceof ErrorResponse) throw error;
      throw new ErrorResponse(error, HttpStatus.INTERNAL_SERVER_ERROR);
    }
  }

  async getAttendances(
    query: AttendanceFilterDto,
    owner_id: string,
    ctx: PermissionUserContext,
  ) {
    try {
      const permissions = await this.getPermissions(ctx);
      assertToggle(permissions, 'attendance.access');
      const scopeFilter = resolveDataScope(
        permissions,
        'attendance.view',
        query.data_scope,
        ctx,
      );
      const isMember = scopeFilter === 'own';
      const member_id = isMember ? ctx.sub : query.member_id;

      const timeZone = await this.orgTimezone.getTimezone(owner_id);

      const where: any = { owner_id };
      if (member_id) where.member_id = member_id;

      if (query.date) {
        const dateStr = formatInTimeZone(query.date, timeZone, 'yyyy-MM-dd');
        where.date = dateStr;
      } else if (query.from && query.to) {
        const fromStr = formatInTimeZone(query.from, timeZone, 'yyyy-MM-dd');
        const toStr = formatInTimeZone(query.to, timeZone, 'yyyy-MM-dd');
        where.date = Between(fromStr, toStr);
      } else if (query.month_year) {
        const mDate = new Date(query.month_year);
        const year = mDate.getFullYear();
        const month = mDate.getMonth();
        const startOfMonth = new Date(year, month, 1);
        const endOfMonth = new Date(year, month + 1, 0);
        const fromStr = formatInTimeZone(startOfMonth, timeZone, 'yyyy-MM-dd');
        const toStr = formatInTimeZone(endOfMonth, timeZone, 'yyyy-MM-dd');
        where.date = Between(fromStr, toStr);
      }

      if (query.status && query.status !== 'all') {
        where.status = query.status;
      }

      const page = query.page ?? 1;
      const limit = query.limit ?? 10;
      const skip = (page - 1) * limit;

      const [attendances, total] = await Promise.all([
        this.db.attendances.findMany({
          where,
          include: { member: { include: { department: true } } },
          orderBy: { date: 'desc' },
          skip,
          take: limit,
        }),
        this.db.attendances.count({ where }),
      ]);

      const processedAttendances = attendances.map((att: any) => ({
        ...att,
        date: att.date
          ? (att.date instanceof Date
              ? formatInTimeZone(att.date, timeZone, 'yyyy-MM-dd')
              : String(att.date).substring(0, 10))
          : (att.check_in
              ? formatInTimeZone(new Date(att.check_in), timeZone, 'yyyy-MM-dd')
              : formatInTimeZone(new Date(att.created_at), timeZone, 'yyyy-MM-dd')),
      }));

      const { data, error } = attendancesResponseSchema.safeParse(processedAttendances);
      if (error) {
        return createGeneralError(error.message, 'root', HttpStatus.INTERNAL_SERVER_ERROR);
      }

      return {
        status: HttpStatus.OK,
        message: 'Attendances retrieved successfully',
        data,
        pagination: {
          total,
          page,
          limit,
          totalPages: Math.ceil(total / limit),
        },
      };
    } catch (error) {
      logger.error('Error in getAttendances', error);
      if (error instanceof ErrorResponse) throw error;
      throw new ErrorResponse(error, HttpStatus.INTERNAL_SERVER_ERROR);
    }
  }

  async getTodayAttendances(
    owner_id: string,
    query: AttendanceFilterDto,
    ctx: PermissionUserContext,
  ) {
    try {
      const permissions = await this.getPermissions(ctx);
      assertToggle(permissions, 'attendance.access');

      const timeZone = await this.orgTimezone.getTimezone(owner_id);

      const dateArg: any = query.date;
      let targetDateStr: string;
      if (dateArg instanceof Date) {
        targetDateStr = formatInTimeZone(dateArg, timeZone, 'yyyy-MM-dd');
      } else if (typeof dateArg === 'string' && dateArg.trim()) {
        targetDateStr = dateArg.includes('T')
          ? formatInTimeZone(new Date(dateArg), timeZone, 'yyyy-MM-dd')
          : dateArg.trim().substring(0, 10);
      } else {
        targetDateStr = formatInTimeZone(new Date(), timeZone, 'yyyy-MM-dd');
      }

      const whereMembers: any = { owner_id, status: 'active' };
      if (query.department_id && query.department_id !== 'all') {
        whereMembers.department_id = query.department_id;
      }

      const allActiveMembers = await this.db.members.findMany({
        where: whereMembers,
        include: { department: true },
        orderBy: { first_name: 'asc' },
      });

      const todayAttendances = await this.db.attendances.findMany({
        where: {
          owner_id,
          date: targetDateStr,
        },
      });

      const attendanceMap = new Map<string, any>(
        todayAttendances.map((a: any) => [a.member_id, a]),
      );

      let items = allActiveMembers.map((member: any) => {
        const att: any = attendanceMap.get(member.id);
        const isSubmitted = !!(att && att.check_in);
        return {
          id: att?.id || member.id,
          member_id: member.id,
          member: {
            id: member.id,
            first_name: member.first_name,
            last_name: member.last_name || '',
            nickname: member.nickname || '',
            employee_id: member.employee_id || '',
            role: member.role || 'member',
            image: member.image || null,
            department: member.department
              ? { id: member.department.id, name: member.department.name }
              : null,
          },
          date: targetDateStr,
          check_in: att?.check_in || null,
          check_out: att?.check_out || null,
          work_hours: att?.work_hours ? Number(att.work_hours) : null,
          break_hours: att?.break_hours
            ? Number(att.break_hours)
            : att?.total_break_hours
              ? Number(att.total_break_hours)
              : null,
          total_break_hours: att?.break_hours
            ? Number(att.break_hours)
            : att?.total_break_hours
              ? Number(att.total_break_hours)
              : null,
          net_hours: att?.net_hours ? Number(att.net_hours) : null,
          overtime_hours: att?.overtime_hours
            ? Number(att.overtime_hours)
            : null,
          status: att?.status || (isSubmitted ? 'present' : 'absent'),
          type: att?.type || 'in_office',
          check_in_ip: att?.check_in_ip || null,
          check_out_ip: att?.check_out_ip || null,
          device_id: att?.device_id || null,
          attendance_method: att?.attendance_method || null,
          note: att?.note || '',
          is_submitted: isSubmitted,
          created_at: att?.created_at || new Date(),
          updated_at: att?.updated_at || new Date(),
        };
      });

      // Filter by search term (name / employee_id)
      if (query.search && query.search.trim()) {
        const s = query.search.trim().toLowerCase();
        items = items.filter(
          (item: any) =>
            item.member.first_name.toLowerCase().includes(s) ||
            item.member.last_name.toLowerCase().includes(s) ||
            item.member.employee_id.toLowerCase().includes(s),
        );
      }

      // Filter by status if requested
      if (query.status && query.status !== 'all') {
        items = items.filter((item: any) => item.status === query.status);
      }

      const totalMembers = allActiveMembers.length;
      const presentCount = items.filter(
        (i) => i.status === 'present' || i.status === 'late',
      ).length;
      const absentCount = items.filter((i) => i.status === 'absent').length;
      const leaveCount = items.filter((i) => i.status === 'on_leave').length;

      const page = Number(query.page) || 1;
      const limit = Number(query.limit) || 10;
      const paginatedItems = items.slice((page - 1) * limit, page * limit);

      return {
        status: HttpStatus.OK,
        message: "Today's attendances retrieved successfully",
        data: paginatedItems,
        details: {
          summary: {
            member_count: totalMembers,
            present_count: presentCount,
            absent_count: absentCount,
            leave_count: leaveCount,
          },
          pagination: {
            total: items.length,
            current_page: page,
            items_per_page: limit,
            total_pages: Math.ceil(items.length / limit) || 1,
          },
        },
      };
    } catch (error) {
      logger.error('Error in getTodayAttendances', error);
      if (error instanceof ErrorResponse) throw error;
      throw new ErrorResponse(error, HttpStatus.INTERNAL_SERVER_ERROR);
    }
  }

  async getTodayAttendancesByMember(
    memberId: string,
    owner_id: string,
    dateArg: any,
    ctx: PermissionUserContext,
  ) {
    try {
      const timeZone = await this.orgTimezone.getTimezone(owner_id);
      let targetDateStr: string;
      if (dateArg instanceof Date) {
        targetDateStr = formatInTimeZone(dateArg, timeZone, 'yyyy-MM-dd');
      } else if (typeof dateArg === 'string' && dateArg.trim()) {
        targetDateStr = dateArg.includes('T')
          ? formatInTimeZone(new Date(dateArg), timeZone, 'yyyy-MM-dd')
          : dateArg.trim().substring(0, 10);
      } else {
        targetDateStr = formatInTimeZone(new Date(), timeZone, 'yyyy-MM-dd');
      }

      const attendance = await this.db.attendances.findFirst({
        where: {
          member_id: memberId,
          owner_id,
          date: targetDateStr,
        },
        include: { member: { include: { department: true } } },
      });

      return {
        status: HttpStatus.OK,
        message: 'Member attendance for today retrieved successfully',
        data: attendance ? attendanceResponseSchema.safeParse(attendance).data : null,
      };
    } catch (error) {
      logger.error('Error in getTodayAttendancesByMember', error);
      if (error instanceof ErrorResponse) throw error;
      throw new ErrorResponse(error, HttpStatus.INTERNAL_SERVER_ERROR);
    }
  }

  async getAttendancesByMember(
    memberId: string,
    query: AttendanceFilterDto,
    owner_id: string,
    ctx: PermissionUserContext,
  ) {
    query.member_id = memberId;
    return this.getAttendances(query, owner_id, ctx);
  }

  async getAttendanceById(
    attendanceId: string,
    owner_id: string,
    ctx: PermissionUserContext,
  ) {
    try {
      const attendance = await this.db.attendances.findFirst({
        where: { id: attendanceId, owner_id },
        include: { member: { include: { department: true } } },
      });

      if (!attendance) {
        throw new ErrorResponse('Attendance record not found', HttpStatus.NOT_FOUND);
      }

      return {
        status: HttpStatus.OK,
        message: 'Attendance record retrieved successfully',
        data: attendanceResponseSchema.safeParse(attendance).data,
      };
    } catch (error) {
      logger.error('Error in getAttendanceById', error);
      if (error instanceof ErrorResponse) throw error;
      throw new ErrorResponse(error, HttpStatus.INTERNAL_SERVER_ERROR);
    }
  }

  async getAttendanceReport(
    query: AttendanceReportFilterDto,
    owner_id: string,
    ctx: PermissionUserContext,
  ) {
    try {
      const permissions = await this.getPermissions(ctx);
      assertToggle(permissions, 'attendance.access');

      const timeZone = await this.orgTimezone.getTimezone(owner_id);

      const now = new Date();
      const start = query.start_date
        ? new Date(query.start_date)
        : new Date(now.getFullYear(), now.getMonth(), 1);
      const end = query.end_date ? new Date(query.end_date) : now;

      const fromStr = formatInTimeZone(start, timeZone, 'yyyy-MM-dd');
      const toStr = formatInTimeZone(end, timeZone, 'yyyy-MM-dd');

      const whereMembers: any = { owner_id, status: 'active' };
      if (query.department_id) {
        whereMembers.department_id = query.department_id;
      }

      const members = await this.db.members.findMany({
        where: whereMembers,
        include: { department: true },
      });

      const attendances = await this.db.attendances.findMany({
        where: {
          owner_id,
          date: Between(fromStr, toStr),
        },
      });

      const report = members.map((member: any) => {
        const memberAtts = attendances.filter((a: any) => a.member_id === member.id);

        const presentCount = memberAtts.filter(
          (a: any) => a.status === AttendanceStatusEnum.present,
        ).length;
        const lateCount = memberAtts.filter(
          (a: any) => a.status === AttendanceStatusEnum.late,
        ).length;
        const halfDayCount = memberAtts.filter(
          (a: any) => a.status === AttendanceStatusEnum.half_day,
        ).length;
        const leaveCount = memberAtts.filter(
          (a: any) => a.status === AttendanceStatusEnum.on_leave,
        ).length;
        const absentCount = memberAtts.filter(
          (a: any) => a.status === AttendanceStatusEnum.absent,
        ).length;

        const totalWorkingHours = Number(
          memberAtts
            .reduce((acc: number, a: any) => acc + (a.work_hours || 0), 0)
            .toFixed(2),
        );

        const totalNetHours = Number(
          memberAtts
            .reduce((acc: number, a: any) => acc + (a.net_hours || 0), 0)
            .toFixed(2),
        );

        const totalOvertime = Number(
          memberAtts
            .reduce((acc: number, a: any) => acc + (a.overtime_hours || 0), 0)
            .toFixed(2),
        );

        const totalDays = memberAtts.length || 1;
        const attendedDays = presentCount + lateCount + halfDayCount;
        const attendancePercentage = Number(
          ((attendedDays / totalDays) * 100).toFixed(1),
        );

        return {
          id: member.id,
          first_name: member.first_name,
          last_name: member.last_name,
          nickname: member.nickname,
          employee_id: member.employee_id,
          role: member.role,
          image: member.image,
          department: member.department,
          attendance_count: attendedDays,
          late_count: lateCount,
          half_day_count: halfDayCount,
          leave_count: leaveCount,
          absent_count: absentCount,
          total_working_days: totalDays,
          total_working_hours: totalWorkingHours,
          total_net_hours: totalNetHours,
          total_overtime: totalOvertime,
          attendance_percentage: attendancePercentage,
          attendances: attendanceResponseSchema.array().safeParse(memberAtts).data || [],
        };
      });

      return {
        status: HttpStatus.OK,
        message: 'Attendance report generated successfully',
        data: attendanceReportResponseArraySchema.safeParse(report).data,
      };
    } catch (error) {
      logger.error('Error in getAttendanceReport', error);
      if (error instanceof ErrorResponse) throw error;
      throw new ErrorResponse(error, HttpStatus.INTERNAL_SERVER_ERROR);
    }
  }

  async createPreviousAttendanceData(owner_id: string, ctx: PermissionUserContext) {
    return {
      status: HttpStatus.OK,
      message: 'Attendance data synchronization up to date.',
    };
  }
}
