<?php
/**
 * license-php/api/attendance.php
 * Authoritative Multi-Tenant Attendance Gateway (MySQL-First Architecture)
 * Powered by AttendanceRepository with ACID transactions & pessimistic locking.
 * Handles /api/attendance/scan, /api/attendance/self, /api/attendance/status, /api/attendance/leave, /api/attendance/logs
 */

require_once __DIR__ . '/../lib/Response.php';
require_once __DIR__ . '/../lib/LicenseCrypto.php';
require_once __DIR__ . '/../lib/LicenseEntitlement.php';
require_once __DIR__ . '/../lib/repositories/AttendanceRepository.php';
require_once __DIR__ . '/../lib/repositories/SiswaRepository.php';
require_once __DIR__ . '/../lib/repositories/GuruRepository.php';
require_once __DIR__ . '/../lib/repositories/TendikRepository.php';

date_default_timezone_set('Asia/Jakarta');

// Method Protection: Attendance records are immutable to deletion
if ($_SERVER['REQUEST_METHOD'] === 'DELETE') {
    Response::json(403, [
        'success' => false,
        'code'    => 'FORBIDDEN_DELETE',
        'message' => 'Akses ditolak: Penghapusan catatan presensi tidak diizinkan.'
    ]);
}

Response::handleCors();

$config = require __DIR__ . '/../config/config.php';
$uri = parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH);
$uri = rtrim(preg_replace('#^/license-php/router\.php#', '', $uri), '/');
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
$body = Response::getJsonBody();

// Detect tenant context from domain or local configuration
$host = $_SERVER['HTTP_X_FORWARDED_HOST'] ?? $_SERVER['HTTP_HOST'] ?? '';
$domain = LicenseCrypto::cleanDomain($host);

$tenantId = 'client_local';
$tenantsFile = dirname(__DIR__, 2) . '/data/tenants.json';
if (file_exists($tenantsFile)) {
    $tData = json_decode(file_get_contents($tenantsFile), true) ?: [];
    foreach ($tData as $tid => $t) {
        if (!$t) continue;
        if (LicenseCrypto::cleanDomain($t['domain'] ?? '') === $domain) {
            $tenantId = $tid;
            break;
        }
        if (!empty($t['aliases']) && is_array($t['aliases'])) {
            foreach ($t['aliases'] as $alias) {
                if (LicenseCrypto::cleanDomain($alias) === $domain) {
                    $tenantId = $tid;
                    break 2;
                }
            }
        }
    }
}

// Token Verification Helper
$pubKeyPath = $config['keys']['public'] ?? (dirname(__DIR__, 2) . '/keys/license_public.key');
$callerToken = $_COOKIE['presensi_auth'] ?? '';
if (!$callerToken && isset($_SERVER['HTTP_AUTHORIZATION'])) {
    $callerToken = trim((string)preg_replace('/^Bearer\s+/i', '', $_SERVER['HTTP_AUTHORIZATION']));
}
if (!$callerToken && isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])) {
    $callerToken = trim((string)preg_replace('/^Bearer\s+/i', '', $_SERVER['REDIRECT_HTTP_AUTHORIZATION']));
}
if (!$callerToken && function_exists('apache_request_headers')) {
    $headers = apache_request_headers();
    $authHeader = $headers['Authorization'] ?? ($headers['authorization'] ?? '');
    if ($authHeader) {
        $callerToken = trim((string)preg_replace('/^Bearer\s+/i', '', $authHeader));
    }
}

$callerPayload = null;
if ($callerToken && file_exists($pubKeyPath)) {
    $verified = LicenseCrypto::verifyToken($callerToken, file_get_contents($pubKeyPath));
    if ($verified['valid']) {
        $exp = $verified['payload']['exp'] ?? 0;
        if ($exp === 0 || $exp >= time()) {
            $callerPayload = $verified['payload'];
        }
    }
}

// Helper: Resolve target entity by targetId (Strict Authoritative MySQL Lookup)
function resolveTargetEntity($targetId): ?array {
    $cleanId = strtolower(trim((string)$targetId));

    // 1. Check MySQL Repositories
    try {
        $sRepo = new SiswaRepository();
        $gRepo = new GuruRepository();
        $tRepo = new TendikRepository();

        $s = $sRepo->findByNisn($cleanId) ?: $sRepo->findByBarcode($cleanId);
        if ($s) {
            $isPending = (($s['status'] ?? 'aktif') !== 'aktif');
            return [
                'id'            => $s['nisn'],
                'username'      => $s['nisn'],
                'nama'          => $s['nama'],
                'kelas'         => $s['kelas'] ?? '-',
                'actualType'    => 'siswa',
                'status'        => $s['status'] ?? 'aktif',
                'isPendingDemo' => $isPending
            ];
        }

        $g = $gRepo->findByUsername($cleanId) ?: ($gRepo->findByNip($cleanId) ?: $gRepo->findByBarcode($cleanId));
        if ($g) {
            $isPending = (($g['status'] ?? 'aktif') !== 'aktif');
            return [
                'id'            => $g['username'],
                'username'      => $g['username'],
                'nama'          => $g['nama'],
                'kelas'         => $g['jabatan'] ?? 'Guru',
                'actualType'    => 'guru',
                'status'        => $g['status'] ?? 'aktif',
                'isPendingDemo' => $isPending
            ];
        }

        $t = $tRepo->findByUsername($cleanId) ?: ($tRepo->findByNip($cleanId) ?: $tRepo->findByBarcode($cleanId));
        if ($t) {
            $isPending = (($t['status'] ?? 'aktif') !== 'aktif');
            return [
                'id'            => $t['username'],
                'username'      => $t['username'],
                'nama'          => $t['nama'],
                'kelas'         => $t['jabatan'] ?? 'Tendik',
                'actualType'    => 'tendik',
                'status'        => $t['status'] ?? 'aktif',
                'isPendingDemo' => $isPending
            ];
        }
    } catch (Throwable $e) {
        error_log('[RESOLVE_TARGET_MYSQL NOTICE] ' . $e->getMessage());
    }

    // Fail-closed: Target does not exist in any MySQL master data table
    return null;
}

try {
    $attRepo = new AttendanceRepository();
    // Authoritative Server Time Integrity from MySQL Server
    $serverDT = $attRepo->getServerAuthoritativeDateTime();
    $today   = $serverDT['date'];
    $nowTime = $serverDT['time'];
} catch (Throwable $e) {
    Response::json(500, [
        'success' => false,
        'code'    => 'DB_CONNECTION_ERROR',
        'message' => 'Koneksi database presensi client gagal: ' . $e->getMessage()
    ]);
}

// --- ROUTE 1A: POST /api/attendance/self ---
if (strpos($uri, '/api/attendance/self') !== false) {
    // 1. Mandatory Authentication Gate
    if (!$callerPayload) {
        Response::json(401, [
            'success' => false,
            'code'    => 'UNAUTHENTICATED',
            'message' => 'Akses ditolak: Autentikasi diperlukan untuk presensi mandiri.'
        ]);
    }

    $callerRole = $callerPayload['role'] ?? 'user';
    $callerId   = $callerPayload['sub'] ?? ($callerPayload['username'] ?? '');

    // 2. Role Authorization: Only Guru and Tendik are permitted
    if ($callerRole === 'siswa') {
        Response::json(403, [
            'success' => false,
            'code'    => 'FORBIDDEN_SELF_ATTENDANCE',
            'message' => 'Akses ditolak: Siswa tidak diizinkan melakukan presensi mandiri.'
        ]);
    }

    if (!in_array($callerRole, ['guru', 'tendik'], true)) {
        Response::json(403, [
            'success' => false,
            'code'    => 'NOT_APPLICABLE',
            'message' => 'Akses ditolak: Presensi mandiri hanya tersedia untuk Guru dan Tendik.'
        ]);
    }

    // 3. Server derives own identity strictly from authenticated JWT
    $target = resolveTargetEntity($callerId);
    if (!$target) {
        Response::json(404, [
            'success' => false,
            'code'    => 'TARGET_NOT_FOUND',
            'message' => 'Data identitas pengajar/staf tidak terdaftar di database: ' . $callerId
        ]);
    }

    if (LicenseEntitlement::isDemo() && !empty($target['isPendingDemo'])) {
        Response::json(403, [
            'success' => false,
            'code'    => 'FORBIDDEN_DEMO_PENDING',
            'message' => 'Akses ditolak: Akun staf ini berada dalam status antrean Demo (menunggu aktivasi lisensi Full).'
        ]);
    }

    if (!in_array($target['actualType'], ['guru', 'tendik'], true)) {
        Response::json(403, [
            'success' => false,
            'code'    => 'FORBIDDEN',
            'message' => 'Akses ditolak: Entitas bukan pengajar atau staf terdaftar.'
        ]);
    }

    try {
        $result = $attRepo->recordScanWithTransaction([
            'tanggal'            => $today,
            'actor_type'         => $target['actualType'],
            'actor_identifier'   => $target['id'] ?? $callerId,
            'scanner_identifier' => $callerId,
            'nama'               => $target['nama'] ?? $callerId,
            'kelompok'           => $target['kelas'] ?? '-',
            'jam'                => $nowTime,
            'status'             => 'Hadir',
            'metode'             => 'Mandiri',
            'keterangan'         => 'Tepat Waktu'
        ]);

        if (!$result['success']) {
            Response::json(400, $result);
        }

        Response::json(200, $result);
    } catch (Throwable $e) {
        error_log('[ATTENDANCE_SELF_ERROR] ' . $e->getMessage());
        Response::json(500, [
            'success' => false,
            'code'    => 'TRANSACTION_ERROR',
            'message' => 'Gagal memproses presensi mandiri: ' . $e->getMessage()
        ]);
    }
}

// --- ROUTE 1B: POST /api/attendance/scan ---
if (strpos($uri, '/api/attendance/scan') !== false) {
    // 1. Mandatory Authentication Gate
    if (!$callerPayload) {
        Response::json(401, [
            'success' => false,
            'code'    => 'UNAUTHENTICATED',
            'message' => 'Akses ditolak: Sesi login tidak valid, kedaluwarsa, atau token tidak ditemukan.'
        ]);
    }

    $targetId = trim((string)($body['targetId'] ?? ''));
    if (!$targetId) {
        Response::json(400, [
            'success' => false,
            'code'    => 'INVALID_REQUEST',
            'message' => 'targetId wajib diisi'
        ]);
    }

    $callerRole = $callerPayload['role'] ?? 'user';
    $callerId   = $callerPayload['sub'] ?? ($callerPayload['username'] ?? '');

    // 2. Students are strictly forbidden from scanning anyone (including self)
    if ($callerRole === 'siswa') {
        Response::json(403, [
            'success' => false,
            'code'    => 'FORBIDDEN_STUDENT_SCAN',
            'message' => 'Akses ditolak: Siswa tidak diizinkan melakukan pemindaian presensi.'
        ]);
    }

    // 3. Resolve target entity server-side from authoritative MySQL
    $target = resolveTargetEntity($targetId);
    if (!$target) {
        Response::json(404, [
            'success' => false,
            'code'    => 'TARGET_NOT_FOUND',
            'message' => 'Identitas atau barcode tidak terdaftar di database sekolah: ' . $targetId
        ]);
    }

    // Strict Demo Entitlement Gate: Pending records cannot perform operational attendance
    if (LicenseEntitlement::isDemo() && !empty($target['isPendingDemo'])) {
        Response::json(403, [
            'success' => false,
            'code'    => 'FORBIDDEN_DEMO_PENDING',
            'message' => 'Akses ditolak: Data ini berada dalam status antrean Demo (menunggu aktivasi lisensi Full) dan tidak dapat melakukan presensi operasional.'
        ]);
    }

    // 4. Role × Target Authorization Matrix
    $targetType = $target['actualType'] ?? 'siswa';

    if ($callerRole === 'admin' || $callerRole === 'operator_absensi') {
        // Admin & Operator Absensi are authorized to scan all target types
    } elseif ($callerRole === 'guru' || $callerRole === 'tendik') {
        // Guru & Tendik can ONLY scan Siswa
        if ($targetType !== 'siswa') {
            Response::json(403, [
                'success' => false,
                'code'    => 'FORBIDDEN_STAFF_SCAN',
                'message' => 'Akses ditolak: Guru dan Tendik hanya dapat memindai presensi Siswa.'
            ]);
        }
    } else {
        Response::json(403, [
            'success' => false,
            'code'    => 'FORBIDDEN',
            'message' => 'Akses ditolak: Peran pengguna tidak memiliki wewenang pemindaian presensi.'
        ]);
    }

    // 5. Audit scanner identity and record attendance
    $scannerIdentifier = $body['scanner_identifier'] ?? ($callerPayload['username'] ?? $callerId);

    try {
        $result = $attRepo->recordScanWithTransaction([
            'tanggal'            => $today,
            'actor_type'         => $targetType,
            'actor_identifier'   => $target['id'] ?? $targetId,
            'scanner_identifier' => $scannerIdentifier,
            'nama'               => $target['nama'] ?? ('ID: ' . $targetId),
            'kelompok'           => $target['kelas'] ?? '-',
            'jam'                => $nowTime,
            'status'             => 'Hadir',
            'metode'             => $body['metode'] ?? 'Barcode',
            'keterangan'         => 'Tepat Waktu'
        ]);

        if (!$result['success']) {
            Response::json(400, $result);
        }

        Response::json(200, $result);
    } catch (Throwable $e) {
        error_log('[ATTENDANCE_SCAN_ERROR] ' . $e->getMessage());
        Response::json(500, [
            'success' => false,
            'code'    => 'TRANSACTION_ERROR',
            'message' => 'Gagal memproses presensi: ' . $e->getMessage()
        ]);
    }
}

// --- ROUTE 2: PATCH /api/attendance/status ---
if (strpos($uri, '/api/attendance/status') !== false) {
    $targetId = trim((string)($body['targetId'] ?? ''));
    $status   = trim((string)($body['status'] ?? ''));

    if (!$targetId || !$status) {
        Response::json(400, ['success' => false, 'code' => 'INVALID_REQUEST', 'message' => 'targetId dan status wajib diisi']);
    }

    // Authorization: Only admin, operator, or guru can modify attendance status
    if (!$callerPayload) {
        Response::json(401, [
            'success' => false,
            'code'    => 'UNAUTHENTICATED',
            'message' => 'Akses ditolak: Autentikasi diperlukan untuk mengubah status presensi.'
        ]);
    }
    $role = $callerPayload['role'] ?? '';
    if (!in_array($role, ['admin', 'operator_absensi', 'guru', 'developer'], true)) {
        Response::json(403, [
            'success' => false,
            'code'    => 'FORBIDDEN',
            'message' => 'Akses ditolak: Hanya Admin, Operator, atau Guru yang dapat mengubah status presensi.'
        ]);
    }

    $target = resolveTargetEntity($targetId);
    if (!$target) {
        Response::json(404, [
            'success' => false,
            'code'    => 'TARGET_NOT_FOUND',
            'message' => 'Identitas tidak terdaftar di database sekolah: ' . $targetId
        ]);
    }

    if (LicenseEntitlement::isDemo() && !empty($target['isPendingDemo'])) {
        Response::json(403, [
            'success' => false,
            'code'    => 'FORBIDDEN_DEMO_PENDING',
            'message' => 'Akses ditolak: Status presensi hanya dapat diubah untuk entitas yang aktif di Demo.'
        ]);
    }

    // Role-based boundary: Guru can only modify student attendance status
    if ($role === 'guru' && ($target['actualType'] ?? '') !== 'siswa') {
        Response::json(403, [
            'success' => false,
            'code'    => 'FORBIDDEN_STAFF_STATUS_CHANGE',
            'message' => 'Akses ditolak: Guru hanya dapat mengubah status presensi Siswa.'
        ]);
    }

    try {
        $ok = $attRepo->updateStatus($today, $targetId, $status, $target['actualType'], [
            'nama'     => $target['nama'],
            'kelompok' => $target['kelas']
        ]);

        Response::json(200, [
            'success' => true,
            'message' => "Status presensi untuk {$targetId} berhasil diperbarui menjadi {$status}."
        ]);
    } catch (Throwable $e) {
        Response::json(500, ['success' => false, 'message' => 'Gagal memperbarui status: ' . $e->getMessage()]);
    }
}

// --- ROUTE 3: POST /api/attendance/leave ---
if (strpos($uri, '/api/attendance/leave') !== false) {
    // 1. Mandatory Authentication Gate
    if (!$callerPayload) {
        Response::json(401, [
            'success' => false,
            'code'    => 'UNAUTHENTICATED',
            'message' => 'Akses ditolak: Autentikasi diperlukan untuk mengajukan izin atau sakit.'
        ]);
    }

    $callerRole = $callerPayload['role'] ?? 'user';
    $callerId   = $callerPayload['sub'] ?? ($callerPayload['username'] ?? '');

    // 2. Mandatory Requester Role Authorization Gate
    if (!in_array($callerRole, ['siswa', 'guru', 'tendik'], true)) {
        Response::json(403, [
            'success' => false,
            'code'    => 'FORBIDDEN_LEAVE_REQUESTER_ROLE',
            'message' => 'Akses ditolak: Peran ' . htmlspecialchars($callerRole) . ' tidak diizinkan membuat pengajuan izin atau sakit.'
        ]);
    }

    // 3. Ownership & Anti-Spoofing Gate
    $requestedId = trim((string)($body['userId'] ?? ($body['targetId'] ?? ($body['requester_id'] ?? ''))));
    if ($requestedId !== '' && strtolower($requestedId) !== strtolower($callerId)) {
        Response::json(403, [
            'success' => false,
            'code'    => 'FORBIDDEN_IDENTITY_SPOOFING',
            'message' => 'Akses ditolak: Anda tidak dapat membuat pengajuan atas nama pengguna lain.'
        ]);
    }

    // 4. Resolve requester identity server-side
    $target = resolveTargetEntity($callerId);
    if (!$target) {
        Response::json(404, [
            'success' => false,
            'code'    => 'TARGET_NOT_FOUND',
            'message' => 'Identitas pemohon izin/sakit tidak terdaftar di database sekolah: ' . $callerId
        ]);
    }

    if (LicenseEntitlement::isDemo() && !empty($target['isPendingDemo'])) {
        Response::json(403, [
            'success' => false,
            'code'    => 'FORBIDDEN_DEMO_PENDING',
            'message' => 'Akses ditolak: Pengajuan izin/sakit hanya berlaku untuk data aktif operasional.'
        ]);
    }

    // 5. Input Validation
    $rawStatus  = trim((string)($body['status'] ?? ''));
    $rawAlasan  = trim((string)($body['alasan'] ?? ($body['keterangan'] ?? '')));
    $rawTanggal = trim((string)($body['tanggal'] ?? ''));

    $statusNorm = ucfirst(strtolower($rawStatus));
    if (!in_array($statusNorm, ['Izin', 'Sakit'], true)) {
        Response::json(400, [
            'success' => false,
            'code'    => 'INVALID_REQUEST',
            'message' => 'Jenis pengajuan tidak valid. Pilihan yang diizinkan hanya "Izin" atau "Sakit".'
        ]);
    }

    if (mb_strlen($rawAlasan) < 3) {
        Response::json(400, [
            'success' => false,
            'code'    => 'INVALID_REQUEST',
            'message' => 'Alasan / keterangan pengajuan wajib diisi (minimal 3 karakter).'
        ]);
    }

    $targetTanggal = $today;
    if ($rawTanggal !== '') {
        if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $rawTanggal)) {
            Response::json(400, [
                'success' => false,
                'code'    => 'INVALID_DATE',
                'message' => 'Format tanggal tidak valid. Gunakan format YYYY-MM-DD.'
            ]);
        }
        $targetTanggal = $rawTanggal;
    }

    // 6. Duplicate / Conflict Check
    $existing = $attRepo->findTodayAttendance($targetTanggal, $target['actualType'], $target['id']);
    if ($existing) {
        $existingStatus = $existing['status'] ?? '';
        if (in_array(strtolower($existingStatus), ['izin', 'sakit'], true)) {
            Response::json(409, [
                'success' => false,
                'code'    => 'DUPLICATE_SUBMISSION',
                'message' => "Pengajuan {$existingStatus} untuk tanggal {$targetTanggal} sudah tercatat sebelumnya."
            ]);
        } else {
            Response::json(409, [
                'success' => false,
                'code'    => 'CONFLICT_EXISTING_RECORD',
                'message' => "Presensi kehadiran ({}) untuk tanggal {$targetTanggal} sudah tercatat."
            ]);
        }
    }

    // 7. Record Leave
    try {
        $insertId = $attRepo->createLeave([
            'tanggal'          => $targetTanggal,
            'actor_type'       => $target['actualType'] ?? 'siswa',
            'actor_identifier' => $target['id'] ?? $callerId,
            'nama'             => $target['nama'] ?? $callerId,
            'kelompok'         => $target['kelas'] ?? '-',
            'status'           => $statusNorm,
            'keterangan'       => $rawAlasan
        ]);

        Response::json(200, [
            'success' => true,
            'code'    => 'LEAVE_RECORDED',
            'id'      => $insertId,
            'data'    => [
                'tanggal'    => $targetTanggal,
                'actor_type' => $target['actualType'] ?? 'siswa',
                'id'         => $target['id'] ?? $callerId,
                'nama'       => $target['nama'] ?? $callerId,
                'kelompok'   => $target['kelas'] ?? '-',
                'status'     => $statusNorm,
                'keterangan' => $rawAlasan
            ],
            'message' => "Pengajuan {$statusNorm} mandiri berhasil dikirim dan tersimpan di database."
        ]);
    } catch (Throwable $e) {
        error_log('[ATTENDANCE_LEAVE_ERROR] ' . $e->getMessage());
        Response::json(500, [
            'success' => false,
            'code'    => 'TRANSACTION_ERROR',
            'message' => 'Gagal mencatat pengajuan: ' . $e->getMessage()
        ]);
    }
}

// --- ROUTE 4: GET /api/attendance/logs ---
if (strpos($uri, '/api/attendance/logs') !== false) {
    // 1. Mandatory Authentication Gate
    if (!$callerPayload) {
        Response::json(401, [
            'success' => false,
            'code'    => 'UNAUTHENTICATED',
            'message' => 'Akses ditolak: Autentikasi diperlukan untuk melihat riwayat log presensi.'
        ]);
    }

    $callerRole = $callerPayload['role'] ?? 'user';
    $callerId   = $callerPayload['sub'] ?? ($callerPayload['username'] ?? '');

    // 2. Ownership & Authorization Enforcement
    $requestedTargetId = trim((string)($_GET['targetId'] ?? ($_GET['actor_identifier'] ?? '')));
    $filters = [];

    if ($callerRole === 'siswa' || $callerRole === 'tendik') {
        // Students and Tendik can ONLY view their own records
        if ($requestedTargetId !== '' && strtolower($requestedTargetId) !== strtolower($callerId)) {
            Response::json(403, [
                'success' => false,
                'code'    => 'FORBIDDEN_CROSS_USER_ACCESS',
                'message' => 'Akses ditolak: Anda hanya dapat melihat riwayat presensi milik Anda sendiri.'
            ]);
        }
        $filters['actor_identifier'] = $callerId;
    } elseif ($callerRole === 'guru') {
        // Teachers can view students or their own records, but not other staff/guru records
        if ($requestedTargetId !== '') {
            $target = resolveTargetEntity($requestedTargetId);
            if ($target && ($target['actualType'] ?? '') !== 'siswa' && strtolower($requestedTargetId) !== strtolower($callerId)) {
                Response::json(403, [
                    'success' => false,
                    'code'    => 'FORBIDDEN_STAFF_LOG_ACCESS',
                    'message' => 'Akses ditolak: Guru hanya dapat melihat catatan siswa atau diri sendiri.'
                ]);
            }
            $filters['actor_identifier'] = $requestedTargetId;
        }
    } elseif (in_array($callerRole, ['admin', 'operator_absensi', 'developer', 'super_admin'], true)) {
        // Administrative roles can view all or filter by requestedTargetId
        if ($requestedTargetId !== '') {
            $filters['actor_identifier'] = $requestedTargetId;
        }
    } else {
        Response::json(403, [
            'success' => false,
            'code'    => 'FORBIDDEN',
            'message' => 'Akses ditolak: Peran pengguna tidak diizinkan mengakses log presensi.'
        ]);
    }

    if (!empty($_GET['tanggal'])) {
        $filters['tanggal'] = trim((string)$_GET['tanggal']);
    }

    try {
        $logs = $attRepo->findLogs($filters, 500, 0);

        // Map database records to authoritative client API contract
        $formatted = array_map(function($row) use ($tenantId) {
            return [
                'id'         => $row['actor_identifier'],
                'tanggal'    => $row['tanggal'],
                'nisn'       => ($row['actor_type'] === 'siswa') ? $row['actor_identifier'] : null,
                'username'   => $row['actor_identifier'],
                'nama'       => $row['nama'],
                'kelas'      => $row['kelompok'],
                'jamMasuk'   => $row['jam_masuk'],
                'jamPulang'  => $row['jam_pulang'],
                'keterangan' => $row['keterangan'],
                'status'     => $row['status'],
                'metode'     => $row['metode'] ?? '-',
                'tenant_id'  => $tenantId,
                'tipe'       => $row['actor_type']
            ];
        }, $logs);

        Response::json(200, [
            'success'   => true,
            'tenant_id' => $tenantId,
            'count'     => count($formatted),
            'data'      => $formatted
        ]);
    } catch (Throwable $e) {
        Response::json(500, ['success' => false, 'message' => 'Gagal mengambil log presensi: ' . $e->getMessage()]);
    }
}

Response::json(404, ['success' => false, 'message' => 'Endpoint attendance tidak ditemukan: ' . $uri]);
