"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AttendanceService = void 0;
const common_1 = require("@nestjs/common");
const date_fns_tz_1 = require("date-fns-tz");
const QRCode = __importStar(require("qrcode"));
const crypto_1 = require("crypto");
const typeorm_1 = require("typeorm");
const permission_resolver_helpers_1 = require("../../common/access-control/permission-resolver.helpers");
const permission_resolver_service_1 = require("../../common/access-control/permission-resolver.service");
const org_timezone_service_1 = require("../../common/timezone/org-timezone.service");
const config_1 = require("../../config");
const entities_1 = require("../../config/database/entities");
const utils_1 = require("../../utils");
const devices_service_1 = require("../devices/devices.service");
const ip_whitelist_service_1 = require("../ip-whitelist/ip-whitelist.service");
const attendance_hours_helper_1 = require("./attendance-hours.helper");
const attendance_dto_1 = require("./attendance.dto");
let AttendanceService = class AttendanceService {
    constructor(db, permissionResolver, orgTimezone, ipWhitelistService, devicesService) {
        this.db = db;
        this.permissionResolver = permissionResolver;
        this.orgTimezone = orgTimezone;
        this.ipWhitelistService = ipWhitelistService;
        this.devicesService = devicesService;
    }
    getDayOfWeekInTimezone(date, timeZone) {
        return Number((0, date_fns_tz_1.formatInTimeZone)(date, timeZone, 'i')) % 7;
    }
    async getPermissions(ctx) {
        return this.permissionResolver.resolveForUser(ctx);
    }
    async generateQrDataUrl(code) {
        try {
            return await QRCode.toDataURL(code, {
                errorCorrectionLevel: 'H',
                margin: 2,
                width: 360,
                color: {
                    dark: '#000000',
                    light: '#ffffff',
                },
            });
        }
        catch (err) {
            config_1.logger.error('Error generating QR code data URL', err);
            return '';
        }
    }
    calculateHaversineDistanceMeters(lat1, lon1, lat2, lon2) {
        const R = 6371e3;
        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);
    }
    async validateAttendanceRequirements(ownerId, clientIp, coords) {
        const settings = await this.db.attendance_settings.findFirst({
            where: { owner_id: ownerId },
        });
        if (settings?.enforce_ip_whitelist) {
            const isIpAllowed = await this.ipWhitelistService.isIpAllowed(ownerId, clientIp);
            if (!isIpAllowed) {
                throw new config_1.ErrorResponse('Network Restricted: You must be connected to the institution authorized network (Wi-Fi/LAN) to record attendance.', common_1.HttpStatus.FORBIDDEN);
            }
        }
        let distanceMeters = 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 config_1.ErrorResponse('Location Required: GPS location access is required to verify your presence on campus. Please enable location services in your browser.', common_1.HttpStatus.BAD_REQUEST);
            }
            if (settings.latitude === null ||
                settings.latitude === undefined ||
                settings.longitude === null ||
                settings.longitude === undefined) {
                config_1.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 config_1.ErrorResponse(`Location Restricted: You are ${distanceMeters}m away from ${settings.location_name || 'the campus'} (Allowed radius: ${radius}m). Please mark attendance on-site.`, common_1.HttpStatus.FORBIDDEN);
                }
            }
        }
        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,
        };
    }
    async getPrimaryQrPoint(ownerId) {
        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-${(0, crypto_1.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 = attendance_dto_1.qrPointResponseSchema.safeParse({
                ...primaryPoint,
                qr_data_url: qrDataUrl,
            });
            return {
                status: common_1.HttpStatus.OK,
                message: 'Primary attendance QR point retrieved successfully',
                data: parsed.data,
            };
        }
        catch (error) {
            config_1.logger.error('Error in getPrimaryQrPoint', error);
            if (error instanceof config_1.ErrorResponse)
                throw error;
            throw new config_1.ErrorResponse(error, common_1.HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }
    async listQrPoints(ownerId) {
        try {
            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) => ({
                ...point,
                qr_data_url: await this.generateQrDataUrl(point.code),
            })));
            const parsed = attendance_dto_1.qrPointListResponseSchema.safeParse(withImages);
            if (parsed.error) {
                throw new config_1.ErrorResponse(parsed.error.message, common_1.HttpStatus.INTERNAL_SERVER_ERROR);
            }
            return {
                status: common_1.HttpStatus.OK,
                message: 'QR points retrieved successfully',
                data: parsed.data,
            };
        }
        catch (error) {
            config_1.logger.error('Error in listQrPoints', error);
            if (error instanceof config_1.ErrorResponse)
                throw error;
            throw new config_1.ErrorResponse(error, common_1.HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }
    async createQrPoint(ownerId, dto) {
        try {
            const uniqueCode = `QR-ATT-${(0, crypto_1.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 = attendance_dto_1.qrPointResponseSchema.safeParse({
                ...newPoint,
                qr_data_url: qrDataUrl,
            });
            return {
                status: common_1.HttpStatus.CREATED,
                message: 'New QR attendance point created successfully',
                data: parsed.data,
            };
        }
        catch (error) {
            config_1.logger.error('Error in createQrPoint', error);
            if (error instanceof config_1.ErrorResponse)
                throw error;
            throw new config_1.ErrorResponse(error, common_1.HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }
    async updateQrPoint(ownerId, id, dto) {
        try {
            const point = await this.db.qr_attendance_points.findFirst({
                where: { id, owner_id: ownerId },
            });
            if (!point) {
                throw new config_1.ErrorResponse('QR point not found', common_1.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 = attendance_dto_1.qrPointResponseSchema.safeParse({
                ...updated,
                qr_data_url: qrDataUrl,
            });
            return {
                status: common_1.HttpStatus.OK,
                message: 'QR attendance point updated successfully',
                data: parsed.data,
            };
        }
        catch (error) {
            config_1.logger.error('Error in updateQrPoint', error);
            if (error instanceof config_1.ErrorResponse)
                throw error;
            throw new config_1.ErrorResponse(error, common_1.HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }
    async deleteQrPoint(ownerId, id) {
        try {
            const point = await this.db.qr_attendance_points.findFirst({
                where: { id, owner_id: ownerId },
            });
            if (!point) {
                throw new config_1.ErrorResponse('QR point not found', common_1.HttpStatus.NOT_FOUND);
            }
            await this.db.qr_attendance_points.delete({ where: { id } });
            return {
                status: common_1.HttpStatus.OK,
                message: 'QR attendance point removed successfully',
            };
        }
        catch (error) {
            config_1.logger.error('Error in deleteQrPoint', error);
            if (error instanceof config_1.ErrorResponse)
                throw error;
            throw new config_1.ErrorResponse(error, common_1.HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }
    async scanQr(memberId, ownerId, body, clientIp) {
        try {
            const now = new Date();
            const geoCheck = await this.validateAttendanceRequirements(ownerId, clientIp, { latitude: body.latitude, longitude: body.longitude });
            const deviceCheck = await this.devicesService.validateOrBindDevice(memberId, ownerId, body.device_id, body.device_name, clientIp);
            if (!deviceCheck.valid) {
                throw new config_1.ErrorResponse(deviceCheck.message || 'Device validation failed.', common_1.HttpStatus.FORBIDDEN);
            }
            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 config_1.ErrorResponse('Invalid or inactive QR code. Please scan an authorized school QR code.', common_1.HttpStatus.BAD_REQUEST);
            }
            const timeZone = await this.orgTimezone.getTimezone(ownerId);
            const todayStr = (0, date_fns_tz_1.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 config_1.ErrorResponse('Today is configured as a non-working day/weekend. Attendance is not permitted.', common_1.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 = (0, attendance_hours_helper_1.combineDateAndTime)(todayStr, endTimeStr, timeZone);
            const existingAttendance = await this.db.attendances.findFirst({
                where: {
                    member_id: memberId,
                    owner_id: ownerId,
                    date: todayStr,
                },
            });
            if (!existingAttendance || !existingAttendance.check_in) {
                if (now > scheduledDayEnd) {
                    throw new config_1.ErrorResponse(`Working day has already ended at ${endTimeStr}. Check-in is closed for today.`, common_1.HttpStatus.BAD_REQUEST);
                }
                const calculatedStatus = (0, attendance_hours_helper_1.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,
                            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,
                            check_in_ip: clientIp,
                            device_id: body.device_id.trim(),
                            attendance_method: 'qr',
                            qr_point_id: qrPoint.id,
                            type: entities_1.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: common_1.HttpStatus.OK,
                    message: `Check-in recorded successfully via QR code (${calculatedStatus.toUpperCase()})`,
                    data: attendance_dto_1.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: attendance_dto_1.attendanceResponseSchema.safeParse(savedAttendance).data,
                        message: `Checked in successfully at ${(0, date_fns_tz_1.formatInTimeZone)(now, timeZone, 'hh:mm a')}`,
                    }).data,
                };
            }
            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 config_1.ErrorResponse(`You just clocked in ${diffSeconds}s ago. Please wait at least 1 minute before checking out.`, common_1.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 = (0, attendance_hours_helper_1.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: common_1.HttpStatus.OK,
                    message: `Check-out recorded successfully via QR code at ${(0, date_fns_tz_1.formatInTimeZone)(now, timeZone, 'hh:mm a')}`,
                    data: attendance_dto_1.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: attendance_dto_1.attendanceResponseSchema.safeParse(updated).data,
                        message: `Checked out successfully. Total Work: ${calculatedHours.work_hours} hrs, Net: ${calculatedHours.net_hours} hrs`,
                    }).data,
                };
            }
            return {
                status: common_1.HttpStatus.OK,
                message: 'Attendance for today is already completed (both Check-In and Check-Out are recorded).',
                data: attendance_dto_1.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: attendance_dto_1.attendanceResponseSchema.safeParse(existingAttendance).data,
                    message: 'Attendance already completed for today.',
                }).data,
            };
        }
        catch (error) {
            config_1.logger.error('Error in scanQr', error);
            if (error instanceof config_1.ErrorResponse)
                throw error;
            throw new config_1.ErrorResponse(error, common_1.HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }
    async checkIn(memberId, ownerId, body, clientIp) {
        try {
            const now = new Date();
            const geoCheck = await this.validateAttendanceRequirements(ownerId, clientIp, { latitude: body.latitude, longitude: body.longitude });
            const deviceCheck = await this.devicesService.validateOrBindDevice(memberId, ownerId, body.device_id, body.device_name, clientIp);
            if (!deviceCheck.valid) {
                throw new config_1.ErrorResponse(deviceCheck.message || 'Device validation failed.', common_1.HttpStatus.FORBIDDEN);
            }
            const timeZone = await this.orgTimezone.getTimezone(ownerId);
            const todayStr = (0, date_fns_tz_1.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 config_1.ErrorResponse('Today is configured as a non-working day/weekend. Check-in is not permitted.', common_1.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 = (0, attendance_hours_helper_1.combineDateAndTime)(todayStr, endTimeStr, timeZone);
            if (now > scheduledDayEnd) {
                throw new config_1.ErrorResponse(`Working day has already ended at ${endTimeStr}. Check-in is closed for today.`, common_1.HttpStatus.BAD_REQUEST);
            }
            const existingAttendance = await this.db.attendances.findFirst({
                where: {
                    member_id: memberId,
                    owner_id: ownerId,
                    date: todayStr,
                },
            });
            if (existingAttendance && existingAttendance.check_in) {
                const checkInFormatted = (0, date_fns_tz_1.formatInTimeZone)(existingAttendance.check_in, timeZone, 'hh:mm a');
                throw new config_1.ErrorResponse(`You have already checked in today at ${checkInFormatted}.`, common_1.HttpStatus.BAD_REQUEST);
            }
            const calculatedStatus = (0, attendance_hours_helper_1.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,
                        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,
                        check_in_ip: clientIp,
                        device_id: body.device_id.trim(),
                        attendance_method: 'manual',
                        type: entities_1.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: common_1.HttpStatus.OK,
                message: `Check-in successful (${calculatedStatus.toUpperCase()})`,
                data: attendance_dto_1.attendanceResponseSchema.safeParse(savedAttendance).data,
            };
        }
        catch (error) {
            config_1.logger.error('Error in checkIn', error);
            if (error instanceof config_1.ErrorResponse)
                throw error;
            throw new config_1.ErrorResponse(error, common_1.HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }
    async checkOut(memberId, ownerId, body, clientIp) {
        try {
            const now = new Date();
            const geoCheck = await this.validateAttendanceRequirements(ownerId, clientIp, { latitude: body.latitude, longitude: body.longitude });
            const deviceCheck = await this.devicesService.validateOrBindDevice(memberId, ownerId, body.device_id, null, clientIp);
            if (!deviceCheck.valid) {
                throw new config_1.ErrorResponse(deviceCheck.message || 'Device validation failed.', common_1.HttpStatus.FORBIDDEN);
            }
            const timeZone = await this.orgTimezone.getTimezone(ownerId);
            const todayStr = (0, date_fns_tz_1.formatInTimeZone)(now, timeZone, 'yyyy-MM-dd');
            const attendance = await this.db.attendances.findFirst({
                where: {
                    member_id: memberId,
                    owner_id: ownerId,
                    date: todayStr,
                    check_in: (0, typeorm_1.Not)((0, typeorm_1.IsNull)()),
                },
            });
            if (!attendance || !attendance.check_in) {
                throw new config_1.ErrorResponse('No check-in found for today. You must check in before checking out.', common_1.HttpStatus.BAD_REQUEST);
            }
            if (attendance.check_out) {
                const checkOutFormatted = (0, date_fns_tz_1.formatInTimeZone)(attendance.check_out, timeZone, 'hh:mm a');
                throw new config_1.ErrorResponse(`You have already checked out today at ${checkOutFormatted}.`, common_1.HttpStatus.BAD_REQUEST);
            }
            const checkInTime = new Date(attendance.check_in).getTime();
            const diffSeconds = Math.floor((now.getTime() - checkInTime) / 1000);
            if (diffSeconds < 60) {
                throw new config_1.ErrorResponse(`You just clocked in ${diffSeconds}s ago. Please wait at least 1 minute before checking out.`, common_1.HttpStatus.BAD_REQUEST);
            }
            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 = (0, attendance_hours_helper_1.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: common_1.HttpStatus.OK,
                message: 'Check-out successful',
                data: attendance_dto_1.attendanceResponseSchema.safeParse(updated).data,
            };
        }
        catch (error) {
            config_1.logger.error('Error in checkOut', error);
            if (error instanceof config_1.ErrorResponse)
                throw error;
            throw new config_1.ErrorResponse(error, common_1.HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }
    async createOrUpdateAttendance(data, owner_id, ctx) {
        try {
            const permissions = await this.getPermissions(ctx);
            (0, permission_resolver_helpers_1.assertToggle)(permissions, 'attendance.manage');
            const timeZone = await this.orgTimezone.getTimezone(owner_id);
            const dateStr = (0, date_fns_tz_1.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 = (0, attendance_hours_helper_1.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 ?? 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 ?? entities_1.AttendanceStatusEnum.present,
                        work_hours,
                        break_hours,
                        net_hours,
                        overtime_hours,
                        note: data.note ?? 'NT/APL',
                        type: entities_1.AttendanceTypeEnum.in_office,
                    },
                });
            }
            return {
                status: common_1.HttpStatus.OK,
                message: 'Attendance record saved successfully',
                data: attendance_dto_1.attendanceResponseSchema.safeParse(result).data,
            };
        }
        catch (error) {
            config_1.logger.error('Error in createOrUpdateAttendance', error);
            if (error instanceof config_1.ErrorResponse)
                throw error;
            throw new config_1.ErrorResponse(error, common_1.HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }
    async getAttendances(query, owner_id, ctx) {
        try {
            const permissions = await this.getPermissions(ctx);
            (0, permission_resolver_helpers_1.assertToggle)(permissions, 'attendance.access');
            const scopeFilter = (0, permission_resolver_helpers_1.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 = { owner_id };
            if (member_id)
                where.member_id = member_id;
            if (query.date) {
                const dateStr = (0, date_fns_tz_1.formatInTimeZone)(query.date, timeZone, 'yyyy-MM-dd');
                where.date = dateStr;
            }
            else if (query.from && query.to) {
                const fromStr = (0, date_fns_tz_1.formatInTimeZone)(query.from, timeZone, 'yyyy-MM-dd');
                const toStr = (0, date_fns_tz_1.formatInTimeZone)(query.to, timeZone, 'yyyy-MM-dd');
                where.date = (0, typeorm_1.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 = (0, date_fns_tz_1.formatInTimeZone)(startOfMonth, timeZone, 'yyyy-MM-dd');
                const toStr = (0, date_fns_tz_1.formatInTimeZone)(endOfMonth, timeZone, 'yyyy-MM-dd');
                where.date = (0, typeorm_1.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) => ({
                ...att,
                date: att.date
                    ? (att.date instanceof Date
                        ? (0, date_fns_tz_1.formatInTimeZone)(att.date, timeZone, 'yyyy-MM-dd')
                        : String(att.date).substring(0, 10))
                    : (att.check_in
                        ? (0, date_fns_tz_1.formatInTimeZone)(new Date(att.check_in), timeZone, 'yyyy-MM-dd')
                        : (0, date_fns_tz_1.formatInTimeZone)(new Date(att.created_at), timeZone, 'yyyy-MM-dd')),
            }));
            const { data, error } = attendance_dto_1.attendancesResponseSchema.safeParse(processedAttendances);
            if (error) {
                return (0, utils_1.createGeneralError)(error.message, 'root', common_1.HttpStatus.INTERNAL_SERVER_ERROR);
            }
            return {
                status: common_1.HttpStatus.OK,
                message: 'Attendances retrieved successfully',
                data,
                pagination: {
                    total,
                    page,
                    limit,
                    totalPages: Math.ceil(total / limit),
                },
            };
        }
        catch (error) {
            config_1.logger.error('Error in getAttendances', error);
            if (error instanceof config_1.ErrorResponse)
                throw error;
            throw new config_1.ErrorResponse(error, common_1.HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }
    async getTodayAttendances(owner_id, query, ctx) {
        try {
            const permissions = await this.getPermissions(ctx);
            (0, permission_resolver_helpers_1.assertToggle)(permissions, 'attendance.access');
            const timeZone = await this.orgTimezone.getTimezone(owner_id);
            const dateArg = query.date;
            let targetDateStr;
            if (dateArg instanceof Date) {
                targetDateStr = (0, date_fns_tz_1.formatInTimeZone)(dateArg, timeZone, 'yyyy-MM-dd');
            }
            else if (typeof dateArg === 'string' && dateArg.trim()) {
                targetDateStr = dateArg.includes('T')
                    ? (0, date_fns_tz_1.formatInTimeZone)(new Date(dateArg), timeZone, 'yyyy-MM-dd')
                    : dateArg.trim().substring(0, 10);
            }
            else {
                targetDateStr = (0, date_fns_tz_1.formatInTimeZone)(new Date(), timeZone, 'yyyy-MM-dd');
            }
            const whereMembers = { 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(todayAttendances.map((a) => [a.member_id, a]));
            let items = allActiveMembers.map((member) => {
                const att = 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(),
                };
            });
            if (query.search && query.search.trim()) {
                const s = query.search.trim().toLowerCase();
                items = items.filter((item) => item.member.first_name.toLowerCase().includes(s) ||
                    item.member.last_name.toLowerCase().includes(s) ||
                    item.member.employee_id.toLowerCase().includes(s));
            }
            if (query.status && query.status !== 'all') {
                items = items.filter((item) => 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: common_1.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) {
            config_1.logger.error('Error in getTodayAttendances', error);
            if (error instanceof config_1.ErrorResponse)
                throw error;
            throw new config_1.ErrorResponse(error, common_1.HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }
    async getTodayAttendancesByMember(memberId, owner_id, dateArg, ctx) {
        try {
            const timeZone = await this.orgTimezone.getTimezone(owner_id);
            let targetDateStr;
            if (dateArg instanceof Date) {
                targetDateStr = (0, date_fns_tz_1.formatInTimeZone)(dateArg, timeZone, 'yyyy-MM-dd');
            }
            else if (typeof dateArg === 'string' && dateArg.trim()) {
                targetDateStr = dateArg.includes('T')
                    ? (0, date_fns_tz_1.formatInTimeZone)(new Date(dateArg), timeZone, 'yyyy-MM-dd')
                    : dateArg.trim().substring(0, 10);
            }
            else {
                targetDateStr = (0, date_fns_tz_1.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: common_1.HttpStatus.OK,
                message: 'Member attendance for today retrieved successfully',
                data: attendance ? attendance_dto_1.attendanceResponseSchema.safeParse(attendance).data : null,
            };
        }
        catch (error) {
            config_1.logger.error('Error in getTodayAttendancesByMember', error);
            if (error instanceof config_1.ErrorResponse)
                throw error;
            throw new config_1.ErrorResponse(error, common_1.HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }
    async getAttendancesByMember(memberId, query, owner_id, ctx) {
        query.member_id = memberId;
        return this.getAttendances(query, owner_id, ctx);
    }
    async getAttendanceById(attendanceId, owner_id, ctx) {
        try {
            const attendance = await this.db.attendances.findFirst({
                where: { id: attendanceId, owner_id },
                include: { member: { include: { department: true } } },
            });
            if (!attendance) {
                throw new config_1.ErrorResponse('Attendance record not found', common_1.HttpStatus.NOT_FOUND);
            }
            return {
                status: common_1.HttpStatus.OK,
                message: 'Attendance record retrieved successfully',
                data: attendance_dto_1.attendanceResponseSchema.safeParse(attendance).data,
            };
        }
        catch (error) {
            config_1.logger.error('Error in getAttendanceById', error);
            if (error instanceof config_1.ErrorResponse)
                throw error;
            throw new config_1.ErrorResponse(error, common_1.HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }
    async getAttendanceReport(query, owner_id, ctx) {
        try {
            const permissions = await this.getPermissions(ctx);
            (0, permission_resolver_helpers_1.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 = (0, date_fns_tz_1.formatInTimeZone)(start, timeZone, 'yyyy-MM-dd');
            const toStr = (0, date_fns_tz_1.formatInTimeZone)(end, timeZone, 'yyyy-MM-dd');
            const whereMembers = { 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: (0, typeorm_1.Between)(fromStr, toStr),
                },
            });
            const report = members.map((member) => {
                const memberAtts = attendances.filter((a) => a.member_id === member.id);
                const presentCount = memberAtts.filter((a) => a.status === entities_1.AttendanceStatusEnum.present).length;
                const lateCount = memberAtts.filter((a) => a.status === entities_1.AttendanceStatusEnum.late).length;
                const halfDayCount = memberAtts.filter((a) => a.status === entities_1.AttendanceStatusEnum.half_day).length;
                const leaveCount = memberAtts.filter((a) => a.status === entities_1.AttendanceStatusEnum.on_leave).length;
                const absentCount = memberAtts.filter((a) => a.status === entities_1.AttendanceStatusEnum.absent).length;
                const totalWorkingHours = Number(memberAtts
                    .reduce((acc, a) => acc + (a.work_hours || 0), 0)
                    .toFixed(2));
                const totalNetHours = Number(memberAtts
                    .reduce((acc, a) => acc + (a.net_hours || 0), 0)
                    .toFixed(2));
                const totalOvertime = Number(memberAtts
                    .reduce((acc, a) => 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: attendance_dto_1.attendanceResponseSchema.array().safeParse(memberAtts).data || [],
                };
            });
            return {
                status: common_1.HttpStatus.OK,
                message: 'Attendance report generated successfully',
                data: attendance_dto_1.attendanceReportResponseArraySchema.safeParse(report).data,
            };
        }
        catch (error) {
            config_1.logger.error('Error in getAttendanceReport', error);
            if (error instanceof config_1.ErrorResponse)
                throw error;
            throw new config_1.ErrorResponse(error, common_1.HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }
    async createPreviousAttendanceData(owner_id, ctx) {
        return {
            status: common_1.HttpStatus.OK,
            message: 'Attendance data synchronization up to date.',
        };
    }
};
exports.AttendanceService = AttendanceService;
exports.AttendanceService = AttendanceService = __decorate([
    (0, common_1.Injectable)(),
    __metadata("design:paramtypes", [config_1.DatabaseService,
        permission_resolver_service_1.PermissionResolverService,
        org_timezone_service_1.OrgTimezoneService,
        ip_whitelist_service_1.IpWhitelistService,
        devices_service_1.DevicesService])
], AttendanceService);
//# sourceMappingURL=attendance.service.js.map