<?php
/**
 * license-php/lib/repositories/AttendanceRepository.php
 * Robust transactional Repository for `attendance` table in `presensi_client_db`.
 * Guarantees pessimistic locking (SELECT ... FOR UPDATE) inside atomic ACID transactions
 * to prevent double-scan race conditions.
 */

require_once dirname(__DIR__) . '/ClientDatabase.php';

class AttendanceRepository {
    private PDO $db;

    public function __construct(?PDO $db = null) {
        $this->db = $db ?? ClientDatabase::getConnection();
    }

    /**
     * Read-only lookup for existing attendance record today.
     */
    public function findTodayAttendance(string $tanggal, string $actorType, string $actorIdentifier): ?array {
        $stmt = $this->db->prepare("
            SELECT * FROM attendance
            WHERE tanggal = :tanggal
              AND actor_type = :actor_type
              AND actor_identifier = :actor_identifier
            LIMIT 1
        ");
        $stmt->execute([
            ':tanggal'          => $tanggal,
            ':actor_type'       => $actorType,
            ':actor_identifier' => $actorIdentifier
        ]);
        $row = $stmt->fetch();
        return $row ?: null;
    }

    /**
     * Atomically executes check-in or check-out transition with pessimistic row lock.
     * Prevents duplicate scans when multiple concurrent requests arrive simultaneously.
     *
     * @param array $params
     * @return array [
     *   'success'   => bool,
     *   'code'      => 'SCAN_SUCCESS' | 'ALREADY_COMPLETED' | 'TRANSACTION_FAILED',
     *   'type'      => 'datang' | 'pulang',
     *   'nama'      => string,
     *   'kelas'     => string,
     *   'jam'       => string,
     *   'jamDatang' => string,
     *   'jamPulang' => string,
     *   'tipeUser'  => string,
     *   'message'   => string,
     *   'data'      => array
     * ]
     */
    public function recordScanWithTransaction(array $params, ?string $serverTimeOverride = null): array {
        $maxRetries = 3;
        $attempt = 0;

        while ($attempt < $maxRetries) {
            $attempt++;
            try {
                return $this->executeScanTransaction($params, $serverTimeOverride);
            } catch (PDOException $e) {
                $isDeadlock = false;
                if ($e->getCode() === '40001' || 
                    (isset($e->errorInfo[1]) && (int)$e->errorInfo[1] === 1213) || 
                    strpos($e->getMessage(), '1213') !== false ||
                    strpos($e->getMessage(), 'Deadlock') !== false) {
                    $isDeadlock = true;
                }

                if ($isDeadlock && $attempt < $maxRetries) {
                    // Small backoff with jitter: 15ms - 45ms multiplied by attempt
                    usleep(random_int(15000, 45000) * $attempt);
                    continue;
                }

                if ($isDeadlock) {
                    error_log('[ATTENDANCE_DEADLOCK_RETRY_EXHAUSTED] Max retries reached for ' . ($params['actor_identifier'] ?? 'unknown'));
                    return [
                        'success' => false,
                        'code'    => 'DEADLOCK_RETRY_EXHAUSTED',
                        'message' => 'Sistem sedang memproses transaksi bersamaan. Silakan coba pindai kembali.'
                    ];
                }

                throw $e;
            }
        }

        return [
            'success' => false,
            'code'    => 'TRANSACTION_RETRY_EXHAUSTED',
            'message' => 'Transaksi presensi tidak dapat diselesaikan setelah beberapa kali percobaan.'
        ];
    }

    /**
     * Authoritative Server Date and Time from MySQL Server.
     */
    public function getServerAuthoritativeDateTime(): array {
        $stmt = $this->db->query("SELECT CURRENT_DATE() AS s_date, CURRENT_TIME() AS s_time_full, DATE_FORMAT(CURRENT_TIME(), '%H:%i') AS s_time");
        $row = $stmt ? $stmt->fetch(PDO::FETCH_ASSOC) : null;
        return [
            'date'      => $row['s_date'] ?? date('Y-m-d'),
            'time'      => $row['s_time'] ?? date('H:i'),
            'time_full' => $row['s_time_full'] ?? date('H:i:s')
        ];
    }

    /**
     * Authoritative School Configuration lookup from `app_settings`.
     */
    public function getSchoolConfig(): ?array {
        $stmt = $this->db->prepare("SELECT setting_value FROM app_settings WHERE setting_key = 'school_config' LIMIT 1");
        $stmt->execute();
        $val = $stmt->fetchColumn();
        return $val ? json_decode($val, true) : null;
    }

    /**
     * Helper to validate and normalize time string to HH:MM:SS.
     * Returns string 'HH:MM:SS' if valid, or null if invalid.
     */
    public static function normalizeTime(string $timeStr): ?string {
        $t = trim($timeStr);
        if ($t === '') return null;
        if (preg_match('/^(\d{1,2}):(\d{2})(?::(\d{2}))?$/', $t, $m)) {
            $hour = (int)$m[1];
            $minute = (int)$m[2];
            $second = isset($m[3]) ? (int)$m[3] : 0;
            if ($hour >= 0 && $hour <= 23 && $minute >= 0 && $minute <= 59 && $second >= 0 && $second <= 59) {
                return sprintf('%02d:%02d:%02d', $hour, $minute, $second);
            }
        }
        return null;
    }

    /**
     * Strict Attendance Time Window Validator.
     * Enforces that current server time is within the Admin configured window.
     * Inclusive boundary precision [HH:MM:SS].
     */
    public function validateAttendanceTimeWindow(string $type, string $currentTime, ?array $schoolConfig): array {
        $typeName = ($type === 'pulang') ? 'pulang' : 'datang';

        // Rule 13: FAIL CLOSED if configuration not available
        if (empty($schoolConfig)) {
            return [
                'valid'   => false,
                'code'    => 'ATTENDANCE_TIME_NOT_CONFIGURED',
                'message' => "Presensi ditolak. Pengaturan jam presensi {$typeName} belum dikonfigurasi oleh Administrator."
            ];
        }

        if ($typeName === 'datang') {
            $rawStart = $schoolConfig['jam_masuk_mulai'] ?? null;
            $rawEnd   = $schoolConfig['jam_masuk_akhir'] ?? null;
        } else {
            $rawStart = $schoolConfig['jam_pulang_mulai'] ?? null;
            $rawEnd   = $schoolConfig['jam_pulang_akhir'] ?? null;
        }

        // Rule 13: FAIL CLOSED if specific window setting is empty
        if ($rawStart === null || trim((string)$rawStart) === '' || $rawEnd === null || trim((string)$rawEnd) === '') {
            return [
                'valid'   => false,
                'code'    => 'ATTENDANCE_TIME_NOT_CONFIGURED',
                'message' => "Presensi ditolak. Pengaturan jam presensi {$typeName} belum dikonfigurasi oleh Administrator."
            ];
        }

        // Rule 14: Invalid Configuration format
        $normStart = self::normalizeTime((string)$rawStart);
        $normEnd   = self::normalizeTime((string)$rawEnd);
        if (!$normStart || !$normEnd) {
            return [
                'valid'   => false,
                'code'    => 'ATTENDANCE_TIME_CONFIGURATION_INVALID',
                'message' => "Presensi ditolak. Format konfigurasi jam presensi {$typeName} tidak valid."
            ];
        }

        // Rule 15: Start time > End time is invalid
        if ($normStart > $normEnd) {
            return [
                'valid'   => false,
                'code'    => 'ATTENDANCE_TIME_CONFIGURATION_INVALID',
                'message' => "Presensi ditolak. Format konfigurasi jam presensi {$typeName} tidak valid: jam mulai lebih besar dari jam akhir."
            ];
        }

        // Rule 6: Boundary evaluation (inclusive) with precision HH:MM:SS
        $normCurrent = self::normalizeTime($currentTime);
        if (!$normCurrent) {
            $normCurrent = date('H:i:s');
        }

        if ($normCurrent < $normStart || $normCurrent > $normEnd) {
            $dispStart = substr($normStart, 0, 5);
            $dispEnd   = substr($normEnd, 0, 5);
            return [
                'valid'   => false,
                'code'    => 'ATTENDANCE_OUTSIDE_TIME_WINDOW',
                'message' => "Presensi ditolak. Presensi {$typeName} hanya diperbolehkan pukul {$dispStart}–{$dispEnd}."
            ];
        }

        return [
            'valid' => true,
            'start' => $normStart,
            'end'   => $normEnd
        ];
    }

    /**
     * Internal transactional core for attendance scan.
     */
    private function executeScanTransaction(array $params, ?string $serverTimeOverride = null): array {
        // Server Time Integrity: Authoritative timestamp strictly determined by MySQL Server
        $serverDT          = $this->getServerAuthoritativeDateTime();
        $tanggal           = $serverDT['date'];
        $nowTime           = $serverDT['time'];
        $currentTimeFull   = $serverTimeOverride ?? $serverDT['time_full'];
        $actorType         = $params['actor_type'] ?? 'siswa';
        $actorIdentifier   = trim((string)($params['actor_identifier'] ?? ''));
        $scannerIdentifier = !empty($params['scanner_identifier']) ? trim((string)$params['scanner_identifier']) : null;
        $nama              = trim((string)($params['nama'] ?? ''));
        $kelompok          = !empty($params['kelompok']) ? trim((string)$params['kelompok']) : '-';
        $status            = !empty($params['status']) ? trim((string)$params['status']) : 'Hadir';
        $metode            = !empty($params['metode']) ? trim((string)$params['metode']) : 'Barcode';
        $keterangan        = !empty($params['keterangan']) ? trim((string)$params['keterangan']) : 'Tepat Waktu';

        if (!$actorIdentifier) {
            throw new InvalidArgumentException('actor_identifier wajib diisi');
        }

        $this->db->beginTransaction();

        try {
            // Acquire exclusive row lock for this specific actor today
            $selectStmt = $this->db->prepare("
                SELECT * FROM attendance
                WHERE tanggal = :tanggal
                  AND actor_type = :actor_type
                  AND actor_identifier = :actor_identifier
                LIMIT 1
                FOR UPDATE
            ");
            $selectStmt->execute([
                ':tanggal'          => $tanggal,
                ':actor_type'       => $actorType,
                ':actor_identifier' => $actorIdentifier
            ]);
            $existing = $selectStmt->fetch();

            // 1. Determine attendance type: datang vs pulang
            $attendanceType = 'datang';
            if ($existing) {
                // Record exists: evaluate check-out or already completed
                $existingPulang = trim((string)($existing['jam_pulang'] ?? ''));

                if ($existingPulang !== '' && $existingPulang !== '-') {
                    // Both datang and pulang already recorded
                    $this->db->commit();
                    return [
                        'success'   => true,
                        'code'      => 'ALREADY_COMPLETED',
                        'type'      => 'pulang',
                        'nama'      => $existing['nama'] ?? $nama,
                        'kelas'     => $existing['kelompok'] ?? $kelompok,
                        'jam'       => $nowTime,
                        'jamDatang' => $existing['jam_masuk'] ?? $nowTime,
                        'jamPulang' => $existingPulang,
                        'tipeUser'  => ucfirst($actorType),
                        'message'   => ($existing['nama'] ?? $nama) . " sudah menyelesaikan absensi datang & pulang hari ini.",
                        'data'      => $existing
                    ];
                }
                $attendanceType = 'pulang';
            }

            // 2. Strict Attendance Time Window Enforcement (PRIOR TO ANY DATABASE WRITE)
            $schoolConfig = $this->getSchoolConfig();
            $timeCheck = $this->validateAttendanceTimeWindow($attendanceType, $currentTimeFull, $schoolConfig);

            if (!$timeCheck['valid']) {
                // FAIL CLOSED: Rollback transaction immediately. ZERO records written to MySQL.
                $this->db->rollBack();
                return [
                    'success' => false,
                    'code'    => $timeCheck['code'],
                    'type'    => $attendanceType,
                    'message' => $timeCheck['message']
                ];
            }

            if ($existing) {
                // Check-out (Pulang)
                $updateStmt = $this->db->prepare("
                    UPDATE attendance
                    SET jam_pulang = :jam_pulang,
                        scan_pulang = :scan_pulang,
                        scanner_identifier = COALESCE(:scanner_identifier, scanner_identifier),
                        updated_at = CURRENT_TIMESTAMP
                    WHERE id = :id
                ");
                $updateStmt->execute([
                    ':jam_pulang'         => $nowTime,
                    ':scan_pulang'        => $nowTime,
                    ':scanner_identifier' => $scannerIdentifier,
                    ':id'                 => $existing['id']
                ]);

                $existing['jam_pulang'] = $nowTime;
                $existing['scan_pulang'] = $nowTime;
                if ($scannerIdentifier) {
                    $existing['scanner_identifier'] = $scannerIdentifier;
                }

                $this->db->commit();

                return [
                    'success'   => true,
                    'code'      => 'SCAN_SUCCESS',
                    'type'      => 'pulang',
                    'nama'      => $existing['nama'] ?? $nama,
                    'kelas'     => $existing['kelompok'] ?? $kelompok,
                    'jam'       => $nowTime,
                    'jamDatang' => $existing['jam_masuk'] ?? '-',
                    'jamPulang' => $nowTime,
                    'tipeUser'  => ucfirst($actorType),
                    'message'   => "Presensi Pulang " . strtoupper($actorType) . " Berhasil",
                    'data'      => $existing
                ];
            }

            // Record does NOT exist: Check-in (Datang)
            $insertStmt = $this->db->prepare("
                INSERT INTO attendance (
                    tanggal, actor_type, actor_identifier, scanner_identifier, nama,
                    kelompok, jam_masuk, jam_pulang, scan_masuk, scan_pulang,
                    status, terlambat, metode, keterangan
                ) VALUES (
                    :tanggal, :actor_type, :actor_identifier, :scanner_identifier, :nama,
                    :kelompok, :jam_masuk, '-', :scan_masuk, '-',
                    :status, 0, :metode, :keterangan
                )
            ");
            $insertStmt->execute([
                ':tanggal'            => $tanggal,
                ':actor_type'         => $actorType,
                ':actor_identifier'   => $actorIdentifier,
                ':scanner_identifier' => $scannerIdentifier,
                ':nama'               => $nama,
                ':kelompok'           => $kelompok,
                ':jam_masuk'          => $nowTime,
                ':scan_masuk'         => $nowTime,
                ':status'             => $status,
                ':metode'             => $metode,
                ':keterangan'         => $keterangan
            ]);

            $newId = (int)$this->db->lastInsertId();

            $savedRecord = [
                'id'                 => $newId,
                'tanggal'            => $tanggal,
                'actor_type'         => $actorType,
                'actor_identifier'   => $actorIdentifier,
                'scanner_identifier' => $scannerIdentifier,
                'nama'               => $nama,
                'kelompok'           => $kelompok,
                'jam_masuk'          => $nowTime,
                'jam_pulang'         => '-',
                'scan_masuk'         => $nowTime,
                'scan_pulang'        => '-',
                'status'             => $status,
                'terlambat'          => 0,
                'metode'             => $metode,
                'keterangan'         => $keterangan
            ];

            $this->db->commit();

            return [
                'success'   => true,
                'code'      => 'SCAN_SUCCESS',
                'type'      => 'datang',
                'nama'      => $nama,
                'kelas'     => $kelompok,
                'jam'       => $nowTime,
                'jamDatang' => $nowTime,
                'jamPulang' => '-',
                'tipeUser'  => ucfirst($actorType),
                'message'   => "Presensi Masuk " . strtoupper($actorType) . " Berhasil",
                'data'      => $savedRecord
            ];
        } catch (Throwable $e) {
            if ($this->db->inTransaction()) {
                $this->db->rollBack();
            }
            error_log('[ATTENDANCE TRANSACTION ERROR] ' . $e->getMessage());
            throw $e;
        }
    }

    public function updateStatus(string $tanggal, string $actorIdentifier, string $status, ?string $actorType = null, ?array $fallbackInfo = null): bool {
        $serverDT = $this->getServerAuthoritativeDateTime();
        $serverTime = $serverDT['time'];
        if (empty($tanggal)) {
            $tanggal = $serverDT['date'];
        }
        $stmt = $this->db->prepare("
            SELECT id FROM attendance
            WHERE tanggal = :tanggal AND actor_identifier = :actor_identifier
            LIMIT 1
        ");
        $stmt->execute([
            ':tanggal'          => $tanggal,
            ':actor_identifier' => $actorIdentifier
        ]);
        $row = $stmt->fetch();

        if ($row) {
            $update = $this->db->prepare("
                UPDATE attendance
                SET status = :status,
                    keterangan = 'Status diubah oleh Admin/Guru',
                    updated_at = CURRENT_TIMESTAMP
                WHERE id = :id
            ");
            return $update->execute([
                ':status' => $status,
                ':id'     => $row['id']
            ]);
        }

        if ($fallbackInfo) {
            $insert = $this->db->prepare("
                INSERT INTO attendance (
                    tanggal, actor_type, actor_identifier, nama, kelompok,
                    jam_masuk, jam_pulang, scan_masuk, scan_pulang, status,
                    keterangan, metode
                ) VALUES (
                    :tanggal, :actor_type, :actor_identifier, :nama, :kelompok,
                    :jam_masuk, '-', :scan_masuk, '-', :status,
                    'Status diubah oleh Admin/Guru', 'Manual'
                )
            ");
            return $insert->execute([
                ':tanggal'          => $tanggal,
                ':actor_type'       => $actorType ?? 'siswa',
                ':actor_identifier' => $actorIdentifier,
                ':nama'             => $fallbackInfo['nama'] ?? ('ID: ' . $actorIdentifier),
                ':kelompok'         => $fallbackInfo['kelompok'] ?? '-',
                ':jam_masuk'        => $serverTime,
                ':scan_masuk'       => $serverTime,
                ':status'           => $status
            ]);
        }

        return false;
    }

    /**
     * Record leave submission (Izin / Sakit).
     */
    public function createLeave(array $data): int {
        $sql = "INSERT INTO attendance (
            tanggal, actor_type, actor_identifier, nama, kelompok,
            jam_masuk, jam_pulang, scan_masuk, scan_pulang, status,
            terlambat, metode, keterangan
        ) VALUES (
            :tanggal, :actor_type, :actor_identifier, :nama, :kelompok,
            :jam_masuk, '-', :scan_masuk, '-', :status,
            0, 'Pengajuan Mandiri', :keterangan
        )";
        $serverDT  = $this->getServerAuthoritativeDateTime();
        $nowTime   = $serverDT['time'];
        $leaveDate = !empty($data['tanggal']) ? $data['tanggal'] : $serverDT['date'];
        $stmt = $this->db->prepare($sql);
        $stmt->execute([
            ':tanggal'          => $leaveDate,
            ':actor_type'       => $data['actor_type'] ?? 'siswa',
            ':actor_identifier' => trim((string)$data['actor_identifier']),
            ':nama'             => trim((string)($data['nama'] ?? 'Pengguna')),
            ':kelompok'         => !empty($data['kelompok']) ? trim((string)$data['kelompok']) : '-',
            ':jam_masuk'        => $nowTime,
            ':scan_masuk'       => $nowTime,
            ':status'           => !empty($data['status']) ? trim((string)$data['status']) : 'Izin',
            ':keterangan'       => !empty($data['keterangan']) ? trim((string)$data['keterangan']) : 'Pengajuan Mandiri'
        ]);
        return (int)$this->db->lastInsertId();
    }

    /**
     * Find logs with optional filtering.
     */
    public function findLogs(array $filters = [], int $limit = 100, int $offset = 0): array {
        $sql = "SELECT * FROM attendance WHERE 1=1";
        $params = [];

        if (!empty($filters['tanggal'])) {
            $sql .= " AND tanggal = :tanggal";
            $params[':tanggal'] = $filters['tanggal'];
        }
        if (!empty($filters['actor_type'])) {
            $sql .= " AND actor_type = :actor_type";
            $params[':actor_type'] = $filters['actor_type'];
        }
        if (!empty($filters['actor_identifier'])) {
            $sql .= " AND actor_identifier = :actor_identifier";
            $params[':actor_identifier'] = $filters['actor_identifier'];
        }
        if (!empty($filters['status'])) {
            $sql .= " AND status = :status";
            $params[':status'] = $filters['status'];
        }

        $sql .= " ORDER BY tanggal DESC, id DESC LIMIT :limit OFFSET :offset";
        $stmt = $this->db->prepare($sql);

        foreach ($params as $k => $v) {
            $stmt->bindValue($k, $v);
        }
        $stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
        $stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
        $stmt->execute();

        return $stmt->fetchAll();
    }

    /**
     * Count logs with optional filtering.
     */
    public function countLogs(array $filters = []): int {
        $sql = "SELECT COUNT(*) AS total FROM attendance WHERE 1=1";
        $params = [];

        if (!empty($filters['tanggal'])) {
            $sql .= " AND tanggal = :tanggal";
            $params[':tanggal'] = $filters['tanggal'];
        }
        if (!empty($filters['actor_type'])) {
            $sql .= " AND actor_type = :actor_type";
            $params[':actor_type'] = $filters['actor_type'];
        }
        if (!empty($filters['actor_identifier'])) {
            $sql .= " AND actor_identifier = :actor_identifier";
            $params[':actor_identifier'] = $filters['actor_identifier'];
        }
        if (!empty($filters['status'])) {
            $sql .= " AND status = :status";
            $params[':status'] = $filters['status'];
        }

        $stmt = $this->db->prepare($sql);
        $stmt->execute($params);
        $res = $stmt->fetch();
        return (int)($res['total'] ?? 0);
    }
}
