<?php

namespace App\Modules\Attendance\Services;

use App\Modules\Academic\Models\ClassModel;
use App\Modules\Academic\Models\ScheduleModel;
use App\Modules\Academic\Models\StudentModel;
use App\Modules\Academic\Models\SubjectModel;
use App\Modules\Academic\Services\ScheduleResolverService;
use App\Modules\Attendance\Entities\AttendanceSession;
use App\Modules\Attendance\Models\AttendanceSessionModel;
use App\Modules\Attendance\Models\QrAttendanceTokenModel;
use App\Modules\Devices\Models\DeviceModel;
use App\Modules\Devices\Services\DeviceAuthService;
use App\Modules\Dashboard\Models\SchoolPresenceSettingsModel;
use App\Modules\Geo\Models\ZoneModel;
use App\Modules\Geo\Services\GeoFenceService;
use App\Modules\Notification\Models\TelegramAccountModel;
use App\Modules\Notification\Services\TelegramBotService;

/**
 * Attendance Check-in Service
 * 
 * Handles attendance check-in logic combining device auth, schedule resolution, and geofencing.
 * Sends Telegram notification to parents when status is PRESENT or LATE.
 */
class AttendanceCheckinService
{
    protected DeviceAuthService $deviceAuthService;
    protected ScheduleResolverService $scheduleResolverService;
    protected ScheduleModel $scheduleModel;
    protected StudentModel $studentModel;
    protected GeoFenceService $geoFenceService;
    protected ZoneModel $zoneModel;
    protected AttendanceSessionModel $attendanceModel;
    protected QrAttendanceTokenModel $qrTokenModel;
    protected DeviceModel $deviceModel;
    protected SchoolPresenceSettingsModel $presenceSettingsModel;

    /**
     * Device code untuk aplikasi mobile (absen masuk/pulang)
     */
    public const MOBILE_APP_DEVICE_CODE = 'MOBILE_APP';

    /**
     * Late tolerance in minutes (default: 10 minutes)
     */
    protected int $lateToleranceMinutes = 10;

    /**
     * Grace period in seconds after schedule end_time before session is considered closed (default: 30)
     */
    protected int $sessionCloseGraceSeconds = 30;

    public function __construct()
    {
        $this->deviceAuthService = new DeviceAuthService();
        $this->scheduleResolverService = new ScheduleResolverService();
        $this->scheduleModel = new ScheduleModel();
        $this->studentModel = new StudentModel();
        $this->geoFenceService = new GeoFenceService();
        $this->zoneModel = new ZoneModel();
        $this->attendanceModel = new AttendanceSessionModel();
        $this->qrTokenModel = new QrAttendanceTokenModel();
        $this->deviceModel = new DeviceModel();
        $this->presenceSettingsModel = new SchoolPresenceSettingsModel();
    }

    /**
     * Process attendance check-in
     *
     * Schedule resolution and attendance timestamp always use server current time;
     * payload "datetime" is ignored.
     *
     * @param array $payload Payload containing:
     *   - device_code: string
     *   - api_key: string
     *   - student_id: int
     *   - datetime: string (ignored; server time is used)
     *   - lat: float
     *   - lng: float
     *   - confidence: float|null
     * @return array Result with status and attendance session data
     */
    public function checkin(array $payload): array
    {
        // Extract payload (datetime from client is ignored; use server time)
        $deviceCode = $payload['device_code'] ?? '';
        $apiKey = $payload['api_key'] ?? '';
        $studentId = (int) ($payload['student_id'] ?? 0);
        $datetime = date('Y-m-d H:i:s');
        $lat = isset($payload['lat']) ? (float) $payload['lat'] : 0.0;
        $lng = isset($payload['lng']) ? (float) $payload['lng'] : 0.0;
        $confidence = isset($payload['confidence']) ? (float) $payload['confidence'] : null;

        // Step 1: Authenticate device
        $deviceData = $this->deviceAuthService->authenticate($deviceCode, $apiKey);
        
        if (!$deviceData) {
            return $this->createResult(
                AttendanceSession::STATUS_INVALID_DEVICE,
                $studentId,
                null,
                0,
                $datetime,
                $lat,
                $lng,
                $confidence
            );
        }

        $deviceId = $deviceData['device_id'];

        // Step 2: Geofence & validasi koordinat — cepat gagal sebelum query jadwal yang berat
        $zoneConfig = null;
        $zone = $this->zoneModel->findActiveByZoneCode('SMA1-SCHOOL');
        if (!$zone) {
            $zones = $this->zoneModel->findAllActive();
            $zone = !empty($zones) ? $zones[0] : null;
        }
        if ($zone) {
            $zoneConfig = [
                'latitude'      => $zone->latitude,
                'longitude'     => $zone->longitude,
                'radius_meters' => $zone->radius_meters,
            ];
        }

        // Jika koordinat null / 0 (belum diambil) langsung OUTSIDE_ZONE (tanpa insert)
        if ($lat === 0.0 && $lng === 0.0) {
            return $this->createResultWithoutInsert(
                AttendanceSession::STATUS_OUTSIDE_ZONE,
                $studentId,
                null,
                $deviceId,
                $datetime,
                $lat,
                $lng,
                $confidence
            );
        }

        if ($zoneConfig) {
            $isInsideZone = $this->geoFenceService->isInsideZone(
                $lat,
                $lng,
                $zoneConfig
            );

            if (!$isInsideZone) {
                return $this->createResultWithoutInsert(
                    AttendanceSession::STATUS_OUTSIDE_ZONE,
                    $studentId,
                    null,
                    $deviceId,
                    $datetime,
                    $lat,
                    $lng,
                    $confidence
                );
            }
        }

        // Step 3: Resolve active schedule (start_time <= now < end_time)
        $schedule = $this->scheduleResolverService->getActiveSchedule($studentId, $datetime);

        if (!$schedule) {
            // No active slot: check latest schedule for class today (may already be ended) for SESSION_CLOSED vs NO_SCHEDULE
            $student = $this->studentModel->find($studentId);
            $classId = $student && $student->class_id ? (int) $student->class_id : 0;
            $dayOfWeek = (int) date('N', strtotime($datetime));
            $time = date('H:i:s', strtotime($datetime));
            $latest = $classId > 0 ? $this->scheduleModel->getLatestScheduleForClassToday($classId, $dayOfWeek, $time) : null;
            if ($latest !== null) {
                $currentTimeTs = strtotime($datetime);
                $dateOnly      = date('Y-m-d', $currentTimeTs);
                $endTime       = $latest['end_time'] ?? '';
                $latestEndTs   = $endTime !== '' ? strtotime($dateOnly . ' ' . $endTime) : 0;
                if ($latestEndTs > 0 && $currentTimeTs > ($latestEndTs + $this->sessionCloseGraceSeconds)) {
                    return $this->createResultWithoutInsert(
                        AttendanceSession::STATUS_SESSION_CLOSED,
                        $studentId,
                        $latest['id'],
                        $deviceId,
                        $datetime,
                        $lat,
                        $lng,
                        $confidence
                    );
                }
            }
            return $this->createResultWithoutInsert(
                AttendanceSession::STATUS_NO_SCHEDULE,
                $studentId,
                null,
                $deviceId,
                $datetime,
                $lat,
                $lng,
                $confidence
            );
        }

        $scheduleId = $schedule['schedule_id'];

        // Step 3b: Auto-lock — if current server time is past schedule end_time, reject (uses resolved start_time/end_time from ScheduleResolverService)
        $currentTimeTs = strtotime($datetime);
        $dateOnly       = date('Y-m-d', $currentTimeTs);
        $endTime        = $schedule['end_time'] ?? '';
        if ($endTime !== '') {
            $scheduleEndTs = strtotime($dateOnly . ' ' . $endTime);
            if ($currentTimeTs > ($scheduleEndTs + $this->sessionCloseGraceSeconds)) {
                return $this->createResultWithoutInsert(
                    AttendanceSession::STATUS_SESSION_CLOSED,
                    $studentId,
                    $scheduleId,
                    $deviceId,
                    $datetime,
                    $lat,
                    $lng,
                    $confidence
                );
            }
        }

        // Step 4: Attendance window — only allow check-in within [start - open_before, start + close_after] minutes
        if (!$this->isWithinAttendanceWindow($datetime, $schedule['start_time'])) {
            return $this->createResultWithoutInsert(
                AttendanceSession::STATUS_ABSENCE_WINDOW_CLOSED,
                $studentId,
                $scheduleId,
                $deviceId,
                $datetime,
                $lat,
                $lng,
                $confidence
            );
        }

        // Step 6: Duplicate check — one attendance per (student_id, schedule_id, attendance_date); date from server
        $attendanceDate = date('Y-m-d');
        if ($this->attendanceModel->hasAttendanceFor($studentId, $scheduleId, $attendanceDate)) {
            return $this->createResultWithoutInsert(
                AttendanceSession::STATUS_ALREADY_CHECKED_IN,
                $studentId,
                $scheduleId,
                $deviceId,
                $datetime,
                $lat,
                $lng,
                $confidence
            );
        }

        // Step 7: Determine if LATE or PRESENT
        $status = $this->determineStatus($datetime, $schedule['start_time']);

        // Step 8: Save attendance session
        return $this->createResult(
            $status,
            $studentId,
            $scheduleId,
            $deviceId,
            $datetime,
            $lat,
            $lng,
            $confidence
        );
    }

    /**
     * Absen masuk atau pulang dari aplikasi mobile (auth NISN+PIN).
     * Memakai jam dari Pengaturan Presensi dan zona sekolah.
     *
     * @param array $payload ['student_id' => int, 'type' => 'masuk'|'pulang', 'lat' => float, 'lng' => float]
     * @return array Same shape as checkin(): status, attendance_id, checkin_at, ...
     */
    public function checkinMasukPulang(array $payload): array
    {
        $studentId = (int) ($payload['student_id'] ?? 0);
        $type      = strtolower(trim((string) ($payload['type'] ?? '')));
        $lat       = isset($payload['lat']) ? (float) $payload['lat'] : 0.0;
        $lng       = isset($payload['lng']) ? (float) $payload['lng'] : 0.0;
        $datetime  = date('Y-m-d H:i:s');
        $dateOnly  = date('Y-m-d');

        if ($studentId < 1) {
            return $this->createResultWithoutInsert(
                AttendanceSession::STATUS_NO_SCHEDULE,
                $studentId,
                null,
                0,
                $datetime,
                $lat,
                $lng,
                null
            );
        }

        if ($type !== 'masuk' && $type !== 'pulang') {
            return $this->createResultWithoutInsert(
                AttendanceSession::STATUS_NO_SCHEDULE,
                $studentId,
                null,
                0,
                $datetime,
                $lat,
                $lng,
                null
            );
        }

        // Device aplikasi mobile (harus ada di dashboard)
        $device = $this->deviceModel->findActiveByDeviceCode(self::MOBILE_APP_DEVICE_CODE);
        if (! $device) {
            return $this->createResultWithoutInsert(
                AttendanceSession::STATUS_INVALID_DEVICE,
                $studentId,
                null,
                0,
                $datetime,
                $lat,
                $lng,
                null
            );
        }
        $deviceId = (int) $device->id;

        // Koordinat wajib ada dan di dalam zona sebelum cek jam / duplikasi
        if ($lat === 0.0 && $lng === 0.0) {
            return $this->createResultWithoutInsert(
                AttendanceSession::STATUS_OUTSIDE_ZONE,
                $studentId,
                null,
                $deviceId,
                $datetime,
                $lat,
                $lng,
                null
            );
        }

        // Zona sekolah (geofence)
        $zone = $this->zoneModel->findActiveByZoneCode('SMA1-SCHOOL');
        if (! $zone) {
            $zones = $this->zoneModel->findAllActive();
            $zone  = ! empty($zones) ? $zones[0] : null;
        }
        if ($zone) {
            $zoneConfig = [
                'latitude'      => $zone->latitude,
                'longitude'     => $zone->longitude,
                'radius_meters' => $zone->radius_meters,
            ];
            if (! $this->geoFenceService->isInsideZone($lat, $lng, $zoneConfig)) {
                return $this->createResultWithoutInsert(
                    AttendanceSession::STATUS_OUTSIDE_ZONE,
                    $studentId,
                    null,
                    $deviceId,
                    $datetime,
                    $lat,
                    $lng,
                    null
                );
            }
        }

        // Jam masuk/pulang dari pengaturan presensi
        $times = $this->presenceSettingsModel->getSettings();
        $timeStart = $type === 'masuk' ? ($times['time_masuk_start'] ?? '06:30:00') : ($times['time_pulang_start'] ?? '14:00:00');
        $timeEnd   = $type === 'masuk' ? ($times['time_masuk_end'] ?? '07:00:00') : ($times['time_pulang_end'] ?? '14:30:00');

        $nowTime = date('H:i:s', strtotime($datetime));
        if ($nowTime < $timeStart || $nowTime > $timeEnd) {
            return $this->createResultWithoutInsert(
                AttendanceSession::STATUS_ABSENCE_WINDOW_CLOSED,
                $studentId,
                null,
                $deviceId,
                $datetime,
                $lat,
                $lng,
                null
            );
        }

        // Sudah absen masuk/pulang hari ini?
        $db = \Config\Database::connect();
        $existing = $db->table('attendance_sessions')
            ->where('student_id', $studentId)
            ->where('attendance_date', $dateOnly)
            ->where('checkin_type', $type)
            ->limit(1)
            ->get()
            ->getRowArray();
        if ($existing) {
            return $this->createResultWithoutInsert(
                AttendanceSession::STATUS_ALREADY_CHECKED_IN,
                $studentId,
                null,
                $deviceId,
                $datetime,
                $lat,
                $lng,
                null
            );
        }

        // Simpan PRESENT
        $attendanceData = [
            'student_id'      => $studentId,
            'schedule_id'     => null,
            'checkin_type'    => $type,
            'attendance_date' => $dateOnly,
            'device_id'       => $deviceId,
            'checkin_at'      => $datetime,
            'latitude'        => $lat,
            'longitude'       => $lng,
            'confidence'      => null,
            'status'          => AttendanceSession::STATUS_PRESENT,
        ];
        $attendanceId = $this->attendanceModel->insert($attendanceData);

        $this->notifyParentsOfCheckin($studentId, null, $datetime, AttendanceSession::STATUS_PRESENT);

        return [
            'status'        => AttendanceSession::STATUS_PRESENT,
            'attendance_id' => $attendanceId,
            'student_id'    => $studentId,
            'schedule_id'   => null,
            'device_id'     => $deviceId,
            'checkin_at'    => $datetime,
            'latitude'      => $lat,
            'longitude'     => $lng,
            'confidence'    => null,
        ];
    }

    /**
     * Determine attendance status (PRESENT or LATE)
     * 
     * @param string $checkinDatetime Check-in datetime
     * @param string $scheduleStartTime Schedule start time (H:i:s format)
     * @return string Status (PRESENT or LATE)
     */
    protected function determineStatus(string $checkinDatetime, string $scheduleStartTime): string
    {
        $checkinTimestamp = strtotime($checkinDatetime);
        $checkinTime = date('H:i:s', $checkinTimestamp);

        // Parse schedule start time
        $scheduleParts = explode(':', $scheduleStartTime);
        $scheduleHour = (int) $scheduleParts[0];
        $scheduleMinute = (int) $scheduleParts[1];
        $scheduleSecond = isset($scheduleParts[2]) ? (int) $scheduleParts[2] : 0;

        // Create schedule datetime from checkin date
        $checkinDate = date('Y-m-d', $checkinTimestamp);
        $scheduleDatetime = sprintf(
            '%s %02d:%02d:%02d',
            $checkinDate,
            $scheduleHour,
            $scheduleMinute,
            $scheduleSecond
        );

        $scheduleTimestamp = strtotime($scheduleDatetime);
        $toleranceSeconds = $this->lateToleranceMinutes * 60;
        $lateThreshold = $scheduleTimestamp + $toleranceSeconds;

        // Check if check-in time exceeds late threshold
        if ($checkinTimestamp > $lateThreshold) {
            return AttendanceSession::STATUS_LATE;
        }

        return AttendanceSession::STATUS_PRESENT;
    }

    /**
     * Check if check-in time is within the configured attendance window.
     * Window: [schedule_start - open_before_minutes, schedule_start + close_after_minutes].
     *
     * @param string $checkinDatetime Check-in datetime (Y-m-d H:i:s)
     * @param string $scheduleStartTime Schedule start time (H:i:s)
     * @return bool True if within window
     */
    protected function isWithinAttendanceWindow(string $checkinDatetime, string $scheduleStartTime): bool
    {
        $config   = config('Attendance');
        $openMin  = $config->attendanceOpenBeforeMinutes ?? 5;
        $closeMin = $config->attendanceCloseAfterMinutes ?? 15;

        $checkinTs = strtotime($checkinDatetime);
        $date      = date('Y-m-d', $checkinTs);

        $scheduleParts = explode(':', $scheduleStartTime);
        $h             = (int) ($scheduleParts[0] ?? 0);
        $m             = (int) ($scheduleParts[1] ?? 0);
        $s             = (int) ($scheduleParts[2] ?? 0);

        $scheduleStartTs = strtotime(sprintf('%s %02d:%02d:%02d', $date, $h, $m, $s));
        $windowOpenTs    = $scheduleStartTs - ($openMin * 60);
        $windowCloseTs   = $scheduleStartTs + ($closeMin * 60);

        return $checkinTs >= $windowOpenTs && $checkinTs <= $windowCloseTs;
    }

    /**
     * Check-in via QR token (guru generate QR, siswa scan).
     * Payload: student_id, qr_token; optional lat, lng.
     * Returns same shape as checkin(): status, attendance_id, checkin_at, etc.
     */
    public function checkinByQr(array $payload): array
    {
        $studentId = (int) ($payload['student_id'] ?? 0);
        $qrToken   = trim((string) ($payload['qr_token'] ?? ''));
        $datetime  = date('Y-m-d H:i:s');
        $lat       = (float) ($payload['lat'] ?? 0);
        $lng       = (float) ($payload['lng'] ?? 0);

        if ($studentId < 1 || $qrToken === '') {
            return $this->createResultWithoutInsert(
                'INVALID_QR_TOKEN',
                $studentId,
                null,
                0,
                $datetime,
                $lat,
                $lng,
                null
            );
        }

        $tokenData = $this->qrTokenModel->validateToken($qrToken);
        if (!$tokenData) {
            return $this->createResultWithoutInsert(
                'INVALID_QR_TOKEN',
                $studentId,
                null,
                0,
                $datetime,
                $lat,
                $lng,
                null
            );
        }

        $scheduleId = $tokenData['schedule_id'];
        $schedule   = $this->scheduleModel->getScheduleWithSlot($scheduleId);
        if (!$schedule) {
            return $this->createResultWithoutInsert(
                AttendanceSession::STATUS_NO_SCHEDULE,
                $studentId,
                $scheduleId,
                0,
                $datetime,
                $lat,
                $lng,
                null
            );
        }

        $student = $this->studentModel->find($studentId);
        if (!$student) {
            return $this->createResultWithoutInsert(
                'INVALID_QR_TOKEN',
                $studentId,
                $scheduleId,
                0,
                $datetime,
                $lat,
                $lng,
                null
            );
        }

        $scheduleClassId = (int) $schedule['class_id'];
        $studentClassId  = (int) ($student->class_id ?? 0);
        if ($studentClassId !== $scheduleClassId) {
            return $this->createResultWithoutInsert(
                'STUDENT_NOT_IN_CLASS',
                $studentId,
                $scheduleId,
                0,
                $datetime,
                $lat,
                $lng,
                null
            );
        }

        $attendanceDate = date('Y-m-d');
        if ($this->attendanceModel->hasAttendanceFor($studentId, $scheduleId, $attendanceDate)) {
            return $this->createResultWithoutInsert(
                AttendanceSession::STATUS_ALREADY_CHECKED_IN,
                $studentId,
                $scheduleId,
                0,
                $datetime,
                $lat,
                $lng,
                null
            );
        }

        $mobileDevice = $this->deviceModel->findActiveByDeviceCode('MOBILE_APP');
        $deviceId     = $mobileDevice ? (int) $mobileDevice->id : 0;
        if ($deviceId < 1) {
            return $this->createResultWithoutInsert(
                AttendanceSession::STATUS_INVALID_DEVICE,
                $studentId,
                $scheduleId,
                0,
                $datetime,
                $lat,
                $lng,
                null
            );
        }

        $status = $this->determineStatus($datetime, $schedule['start_time']);
        return $this->createResult(
            $status,
            $studentId,
            $scheduleId,
            $deviceId,
            $datetime,
            $lat,
            $lng,
            1.0
        );
    }

    /**
     * Create result and save attendance session
     * 
     * @param string $status Status
     * @param int $studentId Student ID
     * @param int|null $scheduleId Schedule ID
     * @param int $deviceId Device ID
     * @param string $datetime Check-in datetime
     * @param float $lat Latitude
     * @param float $lng Longitude
     * @param float|null $confidence Confidence score
     * @return array Result array
     */
    protected function createResult(
        string $status,
        int $studentId,
        ?int $scheduleId,
        int $deviceId,
        string $datetime,
        float $lat,
        float $lng,
        ?float $confidence
    ): array {
        // Save attendance session (attendance_date = server date for unique constraint)
        $attendanceData = [
            'student_id'      => $studentId,
            'schedule_id'     => $scheduleId,
            'attendance_date' => date('Y-m-d'),
            'device_id'       => $deviceId,
            'checkin_at'      => $datetime,
            'latitude'        => $lat,
            'longitude'       => $lng,
            'confidence'      => $confidence,
            'status'          => $status,
        ];

        $attendanceId = $this->attendanceModel->insert($attendanceData);

        $result = [
            'status' => $status,
            'attendance_id' => $attendanceId,
            'student_id' => $studentId,
            'schedule_id' => $scheduleId,
            'device_id' => $deviceId,
            'checkin_at' => $datetime,
            'latitude' => $lat,
            'longitude' => $lng,
            'confidence' => $confidence,
        ];

        // Notify parents via Telegram when PRESENT or LATE (do not break flow on failure)
        if ($status === AttendanceSession::STATUS_PRESENT || $status === AttendanceSession::STATUS_LATE) {
            $this->notifyParentsOfCheckin($studentId, $scheduleId, $datetime, $status);
        }

        return $result;
    }

    /**
     * Build result array without inserting (for ALREADY_CHECKED_IN, ABSENCE_WINDOW_CLOSED).
     */
    protected function createResultWithoutInsert(
        string $status,
        int $studentId,
        ?int $scheduleId,
        int $deviceId,
        string $datetime,
        float $lat,
        float $lng,
        ?float $confidence
    ): array {
        return [
            'status'        => $status,
            'attendance_id' => null,
            'student_id'    => $studentId,
            'schedule_id'   => $scheduleId,
            'device_id'     => $deviceId,
            'checkin_at'    => $datetime,
            'latitude'      => $lat,
            'longitude'     => $lng,
            'confidence'    => $confidence,
        ];
    }

    /**
     * Send Telegram notification to student's linked parents
     */
    protected function notifyParentsOfCheckin(int $studentId, ?int $scheduleId, string $checkinAt, string $status): void
    {
        try {
            $studentModel = new StudentModel();
            $classModel = new ClassModel();
            $scheduleModel = new ScheduleModel();
            $subjectModel = new SubjectModel();
            $telegramAccountModel = new TelegramAccountModel();
            $telegramBot = new TelegramBotService();

            $student = $studentModel->find($studentId);
            if (!$student) {
                return;
            }

            $studentName = htmlspecialchars((string) ($student->name ?? '-'), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
            $className = '-';
            if ($student->class_id) {
                $class = $classModel->find($student->class_id);
                $className = $class ? $class->name : '-';
            }

            $subjectName = '-';
            if ($scheduleId) {
                $schedule = $scheduleModel->find($scheduleId);
                if ($schedule && $schedule->subject_id) {
                    $subject = $subjectModel->find($schedule->subject_id);
                    $subjectName = $subject ? htmlspecialchars((string) $subject->name, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') : '-';
                }
            }

            $statusLabel = $status === AttendanceSession::STATUS_PRESENT ? 'Hadir' : 'Terlambat';
            $timeStr = date('H:i', strtotime($checkinAt)) . ' WIB';
            $emojiStatus = $status === AttendanceSession::STATUS_PRESENT ? '✅' : '⏰';

            $message  = "<b>{$emojiStatus} Absensi SMAN 1 Garut</b>\n\n";
            $message .= "Nama: <b>{$studentName}</b>\n";
            $message .= "Kelas: " . htmlspecialchars($className, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . "\n";
            $message .= "Mapel: " . htmlspecialchars($subjectName, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . "\n";
            $message .= "Status: {$emojiStatus} <b>{$statusLabel}</b>\n";
            $message .= "Waktu: <i>{$timeStr}</i>";

            $telegramUserIds = $telegramAccountModel->getTelegramUserIdsByStudentId($studentId);
            foreach ($telegramUserIds as $telegramUserId) {
                try {
                    $telegramBot->sendMessage($telegramUserId, $message);
                } catch (\Throwable $e) {
                    log_message('error', 'AttendanceCheckinService notifyParents: sendMessage failed for telegram_user_id=' . $telegramUserId . ' - ' . $e->getMessage());
                }
            }
        } catch (\Throwable $e) {
            log_message('error', 'AttendanceCheckinService notifyParents: ' . $e->getMessage());
        }
    }
}
