<?php
/**
 * license-php/api/master.php
 * Authoritative Master Data REST API for Presensi Digital (MySQL Only).
 * Backed strictly by SiswaRepository, GuruRepository, TendikRepository, and ClientDatabase.
 */

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

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();

// Parse /api/master/{entity}[/{id}[/{action}]]
if (!preg_match('#^/api/master/([a-zA-Z0-9_-]+)(?:/([^/]+)(?:/([^/]+))?)?$#', $uri, $matches)) {
    Response::json(404, ['success' => false, 'message' => 'Endpoint master tidak valid: ' . $uri]);
}

$entity = strtolower($matches[1]);
$targetId = isset($matches[2]) ? urldecode($matches[2]) : null;
$subAction = isset($matches[3]) ? strtolower(urldecode($matches[3])) : null;

// Token & Authorization Discovery (Multi-Source for cPanel/Apache/FastCGI/Proxy compatibility)
$token = $_COOKIE['presensi_auth'] ?? '';
if (!$token && isset($_SERVER['HTTP_COOKIE'])) {
    if (preg_match('/(?:^|;\s*)presensi_auth=([^;]+)/', $_SERVER['HTTP_COOKIE'], $cMatch)) {
        $token = urldecode($cMatch[1]);
    }
}
if (!$token && isset($_SERVER['HTTP_AUTHORIZATION'])) {
    $token = trim((string)preg_replace('/^Bearer\s+/i', '', $_SERVER['HTTP_AUTHORIZATION']));
}
if (!$token && isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])) {
    $token = trim((string)preg_replace('/^Bearer\s+/i', '', $_SERVER['REDIRECT_HTTP_AUTHORIZATION']));
}
if (!$token && function_exists('apache_request_headers')) {
    $arh = apache_request_headers();
    $authHdr = $arh['Authorization'] ?? $arh['authorization'] ?? null;
    if ($authHdr) {
        $token = trim((string)preg_replace('/^Bearer\s+/i', '', $authHdr));
    }
}
if (!$token && function_exists('getallheaders')) {
    $gah = getallheaders();
    $authHdr = $gah['Authorization'] ?? $gah['authorization'] ?? null;
    if ($authHdr) {
        $token = trim((string)preg_replace('/^Bearer\s+/i', '', $authHdr));
    }
}
if (!$token && !empty($body['token'])) {
    $token = trim((string)$body['token']);
}
if (!$token && !empty($_GET['token'])) {
    $token = trim((string)$_GET['token']);
}
if (!$token && !empty($_SERVER['HTTP_X_AUTH_TOKEN'])) {
    $token = trim((string)$_SERVER['HTTP_X_AUTH_TOKEN']);
}

$pubKeyPath = $config['keys']['public'] ?? (dirname(__DIR__, 2) . '/keys/license_public.key');
$userRole = null;
$userId = null;

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

// =============================================================================
// SEC-AUTH-03 Remediation: Comprehensive Authentication & Role-Based Authorization
// =============================================================================
// 1. All master data access (Read & Write) strictly requires a valid authenticated session,
// EXCEPT public configuration & holiday queries (GET /api/master/config and GET /api/master/libur)
// which are required by the landing/login view to render school branding and calendar.
$isPublicRead = ($method === 'GET' && in_array($entity, ['config', 'libur'], true));

if (!$isPublicRead && (!$userRole || !$userId)) {
    Response::json(401, [
        'success' => false,
        'code'    => 'UNAUTHENTICATED',
        'message' => 'Akses ditolak: Autentikasi diperlukan untuk mengakses API data master.'
    ]);
}

$isAdminRole = in_array($userRole, ['admin', 'developer', 'super_admin'], true);

// 2. Mutative methods (POST, PUT, DELETE) restricted exclusively to administrators
if ($method !== 'GET' && !$isAdminRole) {
    Response::json(403, [
        'success' => false,
        'code'    => 'FORBIDDEN',
        'message' => 'Akses ditolak: Operasi penulisan master data membutuhkan otorisasi administrator.'
    ]);
}

// 3. Read methods (GET) governed by Principle of Least Privilege
if ($method === 'GET') {
    if ($isAdminRole || $userRole === 'operator_absensi') {
        // Full read access granted to Admin and Operator (for attendance kiosk & roster management)
    } elseif ($userRole === 'guru') {
        // Teachers are permitted to read student rosters (for class attendance) and calendar/config
        if (in_array($entity, ['guru', 'tendik'], true)) {
            Response::json(403, [
                'success' => false,
                'code'    => 'FORBIDDEN',
                'message' => 'Akses ditolak: Guru tidak memiliki izin melihat seluruh data kepegawaian.'
            ]);
        }
    } else {
        // Students and standard Tendik are prohibited from bulk reading master rosters
        if (in_array($entity, ['siswa', 'guru', 'tendik'], true)) {
            Response::json(403, [
                'success' => false,
                'code'    => 'FORBIDDEN',
                'message' => 'Akses ditolak: Anda tidak memiliki wewenang mengakses daftar data master.'
            ]);
        }
    }
}

$db = ClientDatabase::getConnection();
$siswaRepo  = new SiswaRepository($db);
$guruRepo   = new GuruRepository($db);
$tendikRepo = new TendikRepository($db);

$isDemo = LicenseEntitlement::isDemo($db);
$demoLimitSiswa  = LicenseEntitlement::DEMO_LIMIT_SISWA;
$demoLimitGuru   = LicenseEntitlement::DEMO_LIMIT_GURU;
$demoLimitTendik = LicenseEntitlement::DEMO_LIMIT_TENDIK;

try {
    switch ($entity) {
        case 'siswa':
            if ($method === 'GET') {
                if ($targetId) {
                    if ($subAction === 'photo') {
                        $photo = $siswaRepo->getPhotoByNisn($targetId);
                        if ($photo !== null) {
                            Response::json(200, ['success' => true, 'nisn' => $targetId, 'foto' => $photo]);
                        } else {
                            Response::json(404, ['success' => false, 'nisn' => $targetId, 'message' => 'Foto siswa tidak ditemukan']);
                        }
                    }

                    $row = $siswaRepo->findByNisn($targetId) ?: $siswaRepo->findByBarcode($targetId);
                    if ($row) {
                        if ($isDemo && ($row['status'] ?? 'aktif') !== 'aktif') {
                            Response::json(404, ['success' => false, 'code' => 'DEMO_PENDING_RECORD', 'message' => 'Siswa tidak ditemukan atau belum aktif dalam mode Demo']);
                        }
                        unset($row['password']);
                        Response::json(200, ['success' => true, 'data' => $row]);
                    }
                    Response::json(404, ['success' => false, 'message' => 'Siswa tidak ditemukan']);
                }
                $list = $siswaRepo->findAll(10000, 0, $isDemo);
                $sanitized = array_map(function($s) {
                    unset($s['password']);
                    return $s;
                }, $list);
                Response::json(200, [
                    'success'       => true,
                    'count'         => count($sanitized),
                    'total_stored'  => $siswaRepo->count(),
                    'pending_count' => $siswaRepo->countPending(),
                    'is_demo'       => $isDemo,
                    'demo_limit'    => $demoLimitSiswa,
                    'data'          => $sanitized
                ]);
            } elseif ($method === 'POST') {
                if ($targetId === 'batch') {
                    $items = $body['items'] ?? null;
                    if (!is_array($items) || empty($items)) {
                        Response::json(400, ['success' => false, 'code' => 'INVALID_PAYLOAD', 'message' => 'Payload items wajib berupa array tidak kosong']);
                    }
                    if (count($items) > 1500) {
                        Response::json(413, ['success' => false, 'code' => 'BATCH_TOO_LARGE', 'message' => 'Ukuran batch melebihi batas maksimal 1500 item']);
                    }

                    // Strict pre-flight validation for ALL rows
                    $validationErrors = [];
                    foreach ($items as $idx => $row) {
                        $rowIdx = $idx + 1;
                        $nisn = trim((string)($row['nisn'] ?? ''));
                        $nama = trim((string)($row['nama'] ?? ''));
                        if ($nisn === '') {
                            $validationErrors[] = ['row' => $rowIdx, 'field' => 'nisn', 'message' => "Baris {$rowIdx}: NISN wajib diisi"];
                        } elseif (strlen($nisn) > 32) {
                            $validationErrors[] = ['row' => $rowIdx, 'field' => 'nisn', 'message' => "Baris {$rowIdx}: NISN maksimal 32 karakter"];
                        }
                        if ($nama === '') {
                            $validationErrors[] = ['row' => $rowIdx, 'field' => 'nama', 'message' => "Baris {$rowIdx}: Nama siswa wajib diisi"];
                        } elseif (strlen($nama) > 128) {
                            $validationErrors[] = ['row' => $rowIdx, 'field' => 'nama', 'message' => "Baris {$rowIdx}: Nama siswa maksimal 128 karakter"];
                        }

                        // Validate tanggal_lahir if present and not empty / placeholder
                        $rawTgl = trim((string)($row['tanggal_lahir'] ?? ($row['tanggalLahir'] ?? '')));
                        if ($rawTgl !== '' && $rawTgl !== '-' && $rawTgl !== '0') {
                            $isValidDate = false;
                            if (preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $rawTgl, $m)) {
                                $year = (int)$m[1];
                                $month = (int)$m[2];
                                $day = (int)$m[3];
                                if (checkdate($month, $day, $year)) {
                                    $isValidDate = true;
                                }
                            }
                            if (!$isValidDate) {
                                $validationErrors[] = [
                                    'row' => $rowIdx,
                                    'field' => 'tanggal_lahir',
                                    'message' => "Baris {$rowIdx}: Tanggal Lahir tidak valid. Gunakan format YYYY-MM-DD."
                                ];
                            }
                        }

                        // Validate jenis_kelamin if present and not empty / placeholder
                        $rawJk = strtoupper(trim((string)($row['jenis_kelamin'] ?? ($row['jenisKelamin'] ?? ''))));
                        if ($rawJk !== '' && $rawJk !== '-' && $rawJk !== '0') {
                            $validJk = ['L', 'P', 'LAKI-LAKI', 'LAKI - LAKI', 'PRIA', 'PEREMPUAN', 'WANITA'];
                            if (!in_array($rawJk, $validJk, true)) {
                                $validationErrors[] = [
                                    'row' => $rowIdx,
                                    'field' => 'jenis_kelamin',
                                    'message' => "Baris {$rowIdx}: Jenis Kelamin tidak valid. Gunakan L atau P."
                                ];
                            }
                        }
                    }

                    if (!empty($validationErrors)) {
                        Response::json(422, [
                            'success' => false,
                            'code'    => 'VALIDATION_ERROR',
                            'summary' => [
                                'total_received' => count($items),
                                'inserted'       => 0,
                                'updated'        => 0,
                                'skipped'        => 0,
                                'failed'         => count($validationErrors)
                            ],
                            'details' => [
                                'errors' => $validationErrors
                            ],
                            'message' => 'Validasi batch gagal: terdapat data siswa yang tidak memenuhi kriteria skema.'
                        ]);
                    }

                    $strategy = $body['strategy'] ?? 'skip';
                    $db->beginTransaction();
                    try {
                        $res = $siswaRepo->createBatch($items, $strategy, $isDemo, $demoLimitSiswa);
                        $db->commit();
                        $updatedCount = $res['updated'] ?? 0;
                        if ($strategy === 'update' || $strategy === 'upsert') {
                            $msg = "Batch import berhasil: {$res['inserted']} siswa baru ditambahkan, {$updatedCount} data siswa diperbarui, {$res['skipped']} dilewati, 0 gagal.";
                        } else {
                            $msg = $isDemo
                                ? "Batch import berhasil: {$res['inserted']} total disimpan ke MySQL ({$res['active_inserted']} aktif di Demo, {$res['pending_inserted']} tersimpan menunggu lisensi Full), {$res['skipped']} dilewati (duplikat), 0 gagal."
                                : "Batch import berhasil: {$res['inserted']} ditambahkan, {$res['skipped']} dilewati (duplikat), 0 gagal.";
                        }
                        Response::json(200, [
                            'success' => true,
                            'code'    => 'BATCH_IMPORT_SUCCESS',
                            'summary' => [
                                'total_received' => count($items),
                                'inserted'       => $res['inserted'],
                                'updated'        => $updatedCount,
                                'skipped'        => $res['skipped'],
                                'failed'         => 0,
                                'active_demo'    => $res['active_inserted'] ?? $res['inserted'],
                                'pending_full'   => $res['pending_inserted'] ?? 0
                            ],
                            'details' => [
                                'skipped_identifiers' => $res['skipped_identifiers'],
                                'errors'              => []
                            ],
                            'message' => $msg
                        ]);
                    } catch (Throwable $e) {
                        if ($db->inTransaction()) {
                            $db->rollBack();
                        }
                        error_log('[BATCH IMPORT SISWA ERROR] ' . $e->getMessage());
                        Response::json(500, [
                            'success' => false,
                            'code'    => 'DATABASE_TRANSACTION_ERROR',
                            'message' => 'Terjadi kesalahan transaksi database saat mengimpor data siswa: ' . $e->getMessage()
                        ]);
                    }
                }

                if ($targetId === 'delete-multiple') {
                    $ids = $body['ids'] ?? ($body['nisnList'] ?? []);
                    if (!is_array($ids) || empty($ids)) {
                        Response::json(400, ['success' => false, 'message' => 'Daftar NISN tidak boleh kosong']);
                    }
                    $deleted = 0;
                    foreach ($ids as $nid) {
                        if ($siswaRepo->delete((string)$nid)) {
                            $deleted++;
                            if ($isDemo) {
                                LicenseEntitlement::promoteNextPending('siswa', $db);
                            }
                        }
                    }
                    Response::json(200, ['success' => true, 'message' => "{$deleted} siswa berhasil dihapus", 'deleted' => $deleted]);
                }

                $nisn = trim((string)($body['nisn'] ?? ''));
                $nama = trim((string)($body['nama'] ?? ''));
                if (!$nisn || !$nama) {
                    Response::json(400, ['success' => false, 'message' => 'NISN dan Nama siswa wajib diisi']);
                }
                if ($siswaRepo->findByNisn($nisn)) {
                    Response::json(409, ['success' => false, 'message' => 'NISN ' . $nisn . ' sudah terdaftar']);
                }

                // Password hashing
                $rawPass = trim((string)($body['password'] ?? '12345'));
                $pwHash = password_hash($rawPass, PASSWORD_BCRYPT);

                $activeCount = $siswaRepo->countActive();
                $assignStatus = 'aktif';
                $msg = 'Siswa berhasil ditambahkan';
                if ($isDemo && $activeCount >= $demoLimitSiswa) {
                    $assignStatus = 'pending';
                    $msg = 'Siswa berhasil ditambahkan (Kuota Demo 20 siswa telah tercapai. Data tersimpan aman di database dan akan aktif setelah lisensi FULL diaktifkan)';
                }

                $newId = $siswaRepo->create([
                    'nisn'          => $nisn,
                    'nis'           => $body['nis'] ?? null,
                    'nama'          => $nama,
                    'kelas'         => $body['kelas'] ?? '-',
                    'jurusan'       => $body['jurusan'] ?? null,
                    'jenis_kelamin' => $body['jenis_kelamin'] ?? ($body['jenisKelamin'] ?? null),
                    'tanggal_lahir' => !empty($body['tanggal_lahir']) ? trim((string)$body['tanggal_lahir']) : (!empty($body['tanggalLahir']) ? trim((string)$body['tanggalLahir']) : null),
                    'agama'         => !empty($body['agama']) ? trim((string)$body['agama']) : null,
                    'nama_ayah'     => !empty($body['nama_ayah']) ? trim((string)$body['nama_ayah']) : (!empty($body['namaAyah']) ? trim((string)$body['namaAyah']) : null),
                    'nama_ibu'      => !empty($body['nama_ibu']) ? trim((string)$body['nama_ibu']) : (!empty($body['namaIbu']) ? trim((string)$body['namaIbu']) : null),
                    'no_hp'         => $body['no_hp'] ?? ($body['noHp'] ?? null),
                    'alamat'        => $body['alamat'] ?? null,
                    'barcode'       => $body['barcode'] ?? $nisn,
                    'foto'          => $body['foto'] ?? null,
                    'password'      => $pwHash,
                    'status'        => $assignStatus
                ]);

                Response::json(201, ['success' => true, 'message' => $msg, 'id' => $newId, 'status' => $assignStatus]);
            } elseif ($method === 'PUT') {
                $nisn = $targetId ?: trim((string)($body['nisn'] ?? ($body['oldNisn'] ?? '')));
                if (!$nisn) {
                    Response::json(400, ['success' => false, 'message' => 'Identifier NISN wajib disertakan']);
                }
                $existing = $siswaRepo->findByNisn($nisn);
                if (!$existing) {
                    Response::json(404, ['success' => false, 'message' => 'Siswa tidak ditemukan']);
                }

                $updateData = [];
                if (isset($body['nama'])) $updateData['nama'] = trim((string)$body['nama']);
                if (isset($body['nis'])) $updateData['nis'] = trim((string)$body['nis']);
                if (isset($body['kelas'])) $updateData['kelas'] = trim((string)$body['kelas']);
                if (isset($body['jurusan'])) $updateData['jurusan'] = trim((string)$body['jurusan']);
                if (isset($body['jenis_kelamin']) || isset($body['jenisKelamin'])) {
                    $updateData['jenis_kelamin'] = $body['jenis_kelamin'] ?? $body['jenisKelamin'];
                }
                if (isset($body['tanggal_lahir']) || isset($body['tanggalLahir'])) {
                    $val = $body['tanggal_lahir'] ?? $body['tanggalLahir'];
                    $updateData['tanggal_lahir'] = !empty($val) ? trim((string)$val) : null;
                }
                if (isset($body['agama'])) {
                    $updateData['agama'] = !empty($body['agama']) ? trim((string)$body['agama']) : null;
                }
                if (isset($body['nama_ayah']) || isset($body['namaAyah'])) {
                    $val = $body['nama_ayah'] ?? $body['namaAyah'];
                    $updateData['nama_ayah'] = !empty($val) ? trim((string)$val) : null;
                }
                if (isset($body['nama_ibu']) || isset($body['namaIbu'])) {
                    $val = $body['nama_ibu'] ?? $body['namaIbu'];
                    $updateData['nama_ibu'] = !empty($val) ? trim((string)$val) : null;
                }
                if (isset($body['no_hp']) || isset($body['noHp'])) {
                    $updateData['no_hp'] = $body['no_hp'] ?? $body['noHp'];
                }
                if (isset($body['alamat'])) $updateData['alamat'] = $body['alamat'];
                if (isset($body['barcode'])) $updateData['barcode'] = $body['barcode'];
                if (isset($body['foto'])) $updateData['foto'] = $body['foto'];
                if ($isDemo && ($existing['status'] ?? 'aktif') === 'pending') {
                    $updateData['status'] = 'pending';
                } elseif (isset($body['status'])) {
                    $updateData['status'] = $body['status'];
                }
                if (!empty($body['password'])) {
                    $rawP = trim((string)$body['password']);
                    $updateData['password'] = password_hash($rawP, PASSWORD_BCRYPT);
                }

                $ok = $siswaRepo->update($nisn, $updateData);
                Response::json(200, ['success' => $ok, 'message' => $ok ? 'Data siswa berhasil diperbarui' : 'Tidak ada perubahan data']);
            } elseif ($method === 'DELETE') {
                $nisn = $targetId ?: trim((string)($body['nisn'] ?? ''));
                if (!$nisn) {
                    Response::json(400, ['success' => false, 'message' => 'NISN wajib diisi untuk penghapusan']);
                }
                $ok = $siswaRepo->delete($nisn);
                if ($ok && $isDemo) {
                    LicenseEntitlement::promoteNextPending('siswa', $db);
                }
                Response::json(200, ['success' => $ok, 'message' => $ok ? 'Siswa berhasil dihapus' : 'Siswa tidak ditemukan']);
            }
            break;

        case 'guru':
            if ($method === 'GET') {
                if ($targetId) {
                    $row = $guruRepo->findByUsername($targetId) ?: ($guruRepo->findByNip($targetId) ?: $guruRepo->findByBarcode($targetId));
                    if ($row) {
                        if ($isDemo && ($row['status'] ?? 'aktif') !== 'aktif') {
                            Response::json(404, ['success' => false, 'code' => 'DEMO_PENDING_RECORD', 'message' => 'Guru tidak ditemukan atau belum aktif dalam mode Demo']);
                        }
                        unset($row['password']);
                        Response::json(200, ['success' => true, 'data' => $row]);
                    }
                    Response::json(404, ['success' => false, 'message' => 'Guru tidak ditemukan']);
                }
                $list = $guruRepo->findAll(10000, 0, $isDemo);
                $sanitized = array_map(function($g) {
                    unset($g['password']);
                    return $g;
                }, $list);
                Response::json(200, [
                    'success'       => true,
                    'count'         => count($sanitized),
                    'total_stored'  => $guruRepo->count(),
                    'pending_count' => $guruRepo->countPending(),
                    'is_demo'       => $isDemo,
                    'demo_limit'    => $demoLimitGuru,
                    'data'          => $sanitized
                ]);
            } elseif ($method === 'POST') {
                if ($targetId === 'batch') {
                    $items = $body['items'] ?? null;
                    if (!is_array($items) || empty($items)) {
                        Response::json(400, ['success' => false, 'code' => 'INVALID_PAYLOAD', 'message' => 'Payload items wajib berupa array tidak kosong']);
                    }
                    if (count($items) > 1500) {
                        Response::json(413, ['success' => false, 'code' => 'BATCH_TOO_LARGE', 'message' => 'Ukuran batch melebihi batas maksimal 1500 item']);
                    }

                    // Strict pre-flight validation for ALL rows
                    $validationErrors = [];
                    foreach ($items as $idx => $row) {
                        $rowIdx = $idx + 1;
                        $username = strtolower(trim((string)($row['username'] ?? '')));
                        $nama = trim((string)($row['nama'] ?? ''));
                        if ($username === '') {
                            $validationErrors[] = ['row' => $rowIdx, 'field' => 'username', 'message' => 'Username guru wajib diisi'];
                        } elseif (in_array($username, ['admin', 'developer', 'super_admin'], true)) {
                            $validationErrors[] = ['row' => $rowIdx, 'field' => 'username', 'message' => 'Username "' . $username . '" merupakan akun sistem terproteksi'];
                        } elseif (strlen($username) > 64) {
                            $validationErrors[] = ['row' => $rowIdx, 'field' => 'username', 'message' => 'Username guru maksimal 64 karakter'];
                        }
                        if ($nama === '') {
                            $validationErrors[] = ['row' => $rowIdx, 'field' => 'nama', 'message' => 'Nama guru wajib diisi'];
                        } elseif (strlen($nama) > 128) {
                            $validationErrors[] = ['row' => $rowIdx, 'field' => 'nama', 'message' => 'Nama guru maksimal 128 karakter'];
                        }
                    }

                    if (!empty($validationErrors)) {
                        Response::json(422, [
                            'success' => false,
                            'code'    => 'VALIDATION_ERROR',
                            'summary' => [
                                'total_received' => count($items),
                                'inserted'       => 0,
                                'updated'        => 0,
                                'skipped'        => 0,
                                'failed'         => count($validationErrors)
                            ],
                            'details' => [
                                'errors' => $validationErrors
                            ],
                            'message' => 'Validasi batch gagal: terdapat data guru yang tidak memenuhi kriteria skema.'
                        ]);
                    }

                    $strategy = $body['strategy'] ?? 'skip';
                    $db->beginTransaction();
                    try {
                        $res = $guruRepo->createBatch($items, $strategy, $isDemo, $demoLimitGuru);
                        $db->commit();
                        $msg = $isDemo
                            ? "Batch import berhasil: {$res['inserted']} total disimpan ke MySQL ({$res['active_inserted']} aktif di Demo, {$res['pending_inserted']} tersimpan menunggu lisensi Full), {$res['skipped']} dilewati (duplikat), 0 gagal."
                            : "Batch import berhasil: {$res['inserted']} ditambahkan, {$res['skipped']} dilewati (duplikat), 0 gagal.";
                        Response::json(200, [
                            'success' => true,
                            'code'    => 'BATCH_IMPORT_SUCCESS',
                            'summary' => [
                                'total_received' => count($items),
                                'inserted'       => $res['inserted'],
                                'updated'        => 0,
                                'skipped'        => $res['skipped'],
                                'failed'         => 0,
                                'active_demo'    => $res['active_inserted'] ?? $res['inserted'],
                                'pending_full'   => $res['pending_inserted'] ?? 0
                            ],
                            'details' => [
                                'skipped_identifiers' => $res['skipped_identifiers'],
                                'errors'              => []
                            ],
                            'message' => $msg
                        ]);
                    } catch (Throwable $e) {
                        if ($db->inTransaction()) {
                            $db->rollBack();
                        }
                        error_log('[BATCH IMPORT GURU ERROR] ' . $e->getMessage());
                        Response::json(500, [
                            'success' => false,
                            'code'    => 'DATABASE_TRANSACTION_ERROR',
                            'message' => 'Terjadi kesalahan transaksi database saat mengimpor data guru: ' . $e->getMessage()
                        ]);
                    }
                }

                if ($targetId === 'delete-multiple') {
                    $ids = $body['usernames'] ?? ($body['ids'] ?? []);
                    if (!is_array($ids) || empty($ids)) {
                        Response::json(400, ['success' => false, 'message' => 'Daftar username tidak boleh kosong']);
                    }
                    $deleted = 0;
                    foreach ($ids as $u) {
                        if ($guruRepo->delete((string)$u)) {
                            $deleted++;
                            if ($isDemo) {
                                LicenseEntitlement::promoteNextPending('guru', $db);
                            }
                        }
                    }
                    Response::json(200, ['success' => true, 'message' => "{$deleted} guru berhasil dihapus", 'deleted' => $deleted]);
                }

                $username = strtolower(trim((string)($body['username'] ?? '')));
                $nama = trim((string)($body['nama'] ?? ''));
                if (!$username || !$nama) {
                    Response::json(400, ['success' => false, 'message' => 'Username dan Nama guru wajib diisi']);
                }
                if ($guruRepo->findByUsername($username)) {
                    Response::json(409, ['success' => false, 'message' => 'Username ' . $username . ' sudah terdaftar']);
                }

                $rawPass = trim((string)($body['password'] ?? '12345'));
                $pwHash = password_hash($rawPass, PASSWORD_BCRYPT);

                $activeCount = $guruRepo->countActive();
                $assignStatus = 'aktif';
                $msg = 'Guru berhasil ditambahkan';
                if ($isDemo && $activeCount >= $demoLimitGuru) {
                    $assignStatus = 'pending';
                    $msg = 'Guru berhasil ditambahkan (Kuota Demo 10 guru telah tercapai. Data tersimpan aman di database dan akan aktif setelah lisensi FULL diaktifkan)';
                }

                $newId = $guruRepo->create([
                    'username'      => $username,
                    'nip'           => !empty($body['nip']) && $body['nip'] !== '-' ? trim((string)$body['nip']) : null,
                    'nama'          => $nama,
                    'jabatan'       => $body['jabatan'] ?? (!empty($body['kelas']) ? 'Wali Kelas ' . $body['kelas'] : 'Guru'),
                    'mapel'         => $body['mapel'] ?? null,
                    'jenis_kelamin' => $body['jenis_kelamin'] ?? ($body['jenisKelamin'] ?? null),
                    'no_hp'         => $body['no_hp'] ?? ($body['noHp'] ?? null),
                    'barcode'       => $body['barcode'] ?? $username,
                    'foto'          => $body['foto'] ?? null,
                    'password'      => $pwHash,
                    'status'        => $assignStatus
                ]);

                Response::json(201, ['success' => true, 'message' => $msg, 'id' => $newId, 'status' => $assignStatus]);
            } elseif ($method === 'PUT') {
                $username = strtolower($targetId ?: trim((string)($body['username'] ?? ($body['oldUsername'] ?? ''))));
                if (!$username) {
                    Response::json(400, ['success' => false, 'message' => 'Username guru wajib disertakan']);
                }
                $existing = $guruRepo->findByUsername($username);
                if (!$existing) {
                    Response::json(404, ['success' => false, 'message' => 'Guru tidak ditemukan']);
                }

                $updateData = [];
                if (isset($body['nama'])) $updateData['nama'] = trim((string)$body['nama']);
                if (isset($body['nip'])) $updateData['nip'] = !empty($body['nip']) && $body['nip'] !== '-' ? trim((string)$body['nip']) : null;
                if (isset($body['jabatan'])) $updateData['jabatan'] = $body['jabatan'];
                elseif (!empty($body['kelas'])) $updateData['jabatan'] = 'Wali Kelas ' . $body['kelas'];
                if (isset($body['mapel'])) $updateData['mapel'] = $body['mapel'];
                if (isset($body['jenis_kelamin']) || isset($body['jenisKelamin'])) {
                    $updateData['jenis_kelamin'] = $body['jenis_kelamin'] ?? $body['jenisKelamin'];
                }
                if (isset($body['no_hp']) || isset($body['noHp'])) {
                    $updateData['no_hp'] = $body['no_hp'] ?? $body['noHp'];
                }
                if (isset($body['barcode'])) $updateData['barcode'] = $body['barcode'];
                if (isset($body['foto'])) $updateData['foto'] = $body['foto'];
                if ($isDemo && ($existing['status'] ?? 'aktif') === 'pending') {
                    $updateData['status'] = 'pending';
                } elseif (isset($body['status'])) {
                    $updateData['status'] = $body['status'];
                }
                if (!empty($body['password'])) {
                    $rawP = trim((string)$body['password']);
                    $updateData['password'] = password_hash($rawP, PASSWORD_BCRYPT);
                }

                $ok = $guruRepo->update($username, $updateData);
                Response::json(200, ['success' => $ok, 'message' => $ok ? 'Data guru berhasil diperbarui' : 'Tidak ada perubahan data']);
            } elseif ($method === 'DELETE') {
                $username = strtolower($targetId ?: trim((string)($body['username'] ?? '')));
                if (!$username) {
                    Response::json(400, ['success' => false, 'message' => 'Username wajib disertakan untuk penghapusan']);
                }
                $ok = $guruRepo->delete($username);
                if ($ok && $isDemo) {
                    LicenseEntitlement::promoteNextPending('guru', $db);
                }
                Response::json(200, ['success' => $ok, 'message' => $ok ? 'Guru berhasil dihapus' : 'Guru tidak ditemukan']);
            }
            break;

        case 'tendik':
            if ($method === 'GET') {
                $opSet = [];
                try {
                    $opStmt = $db->query("SELECT setting_value FROM app_settings WHERE setting_key = 'attendance_operators' LIMIT 1");
                    $rawOp = $opStmt ? $opStmt->fetchColumn() : null;
                    if ($rawOp) {
                        $opArr = json_decode($rawOp, true);
                        if (is_array($opArr)) {
                            foreach ($opArr as $u) $opSet[strtolower(trim($u))] = true;
                        }
                    }
                } catch (Throwable $e) {}

                if ($targetId) {
                    $row = $tendikRepo->findByUsername($targetId) ?: ($tendikRepo->findByNip($targetId) ?: $tendikRepo->findByBarcode($targetId));
                    if ($row) {
                        unset($row['password']);
                        $uLower = strtolower(trim($row['username'] ?? ''));
                        $row['role'] = (isset($opSet[$uLower]) || strpos(strtolower($row['jabatan'] ?? ''), 'operator') !== false)
                            ? 'operator_absensi'
                            : 'tendik';
                        Response::json(200, ['success' => true, 'data' => $row]);
                    }
                    Response::json(404, ['success' => false, 'message' => 'Tendik tidak ditemukan']);
                }
                $list = $tendikRepo->findAll(10000);
                $sanitized = array_map(function($t) use ($opSet) {
                    unset($t['password']);
                    $uLower = strtolower(trim($t['username'] ?? ''));
                    $t['role'] = (isset($opSet[$uLower]) || strpos(strtolower($t['jabatan'] ?? ''), 'operator') !== false)
                        ? 'operator_absensi'
                        : 'tendik';
                    return $t;
                }, $list);
                Response::json(200, ['success' => true, 'count' => count($sanitized), 'data' => $sanitized]);
            } elseif ($method === 'POST') {
                if ($targetId === 'batch') {
                    $items = $body['items'] ?? null;
                    if (!is_array($items) || empty($items)) {
                        Response::json(400, ['success' => false, 'code' => 'INVALID_PAYLOAD', 'message' => 'Payload items wajib berupa array tidak kosong']);
                    }
                    if (count($items) > 1500) {
                        Response::json(413, ['success' => false, 'code' => 'BATCH_TOO_LARGE', 'message' => 'Ukuran batch melebihi batas maksimal 1500 item']);
                    }

                    // Strict pre-flight validation for ALL rows
                    $validationErrors = [];
                    foreach ($items as $idx => $row) {
                        $rowIdx = $idx + 1;
                        $username = strtolower(trim((string)($row['username'] ?? '')));
                        $nama = trim((string)($row['nama'] ?? ''));
                        if ($username === '') {
                            $validationErrors[] = ['row' => $rowIdx, 'field' => 'username', 'message' => 'Username tendik wajib diisi'];
                        } elseif (in_array($username, ['admin', 'developer', 'super_admin'], true)) {
                            $validationErrors[] = ['row' => $rowIdx, 'field' => 'username', 'message' => 'Username "' . $username . '" merupakan akun sistem terproteksi'];
                        } elseif (strlen($username) > 64) {
                            $validationErrors[] = ['row' => $rowIdx, 'field' => 'username', 'message' => 'Username tendik maksimal 64 karakter'];
                        }
                        if ($nama === '') {
                            $validationErrors[] = ['row' => $rowIdx, 'field' => 'nama', 'message' => 'Nama tendik wajib diisi'];
                        } elseif (strlen($nama) > 128) {
                            $validationErrors[] = ['row' => $rowIdx, 'field' => 'nama', 'message' => 'Nama tendik maksimal 128 karakter'];
                        }
                    }

                    if (!empty($validationErrors)) {
                        Response::json(422, [
                            'success' => false,
                            'code'    => 'VALIDATION_ERROR',
                            'summary' => [
                                'total_received' => count($items),
                                'inserted'       => 0,
                                'updated'        => 0,
                                'skipped'        => 0,
                                'failed'         => count($validationErrors)
                            ],
                            'details' => [
                                'errors' => $validationErrors
                            ],
                            'message' => 'Validasi batch gagal: terdapat data tendik yang tidak memenuhi kriteria skema.'
                        ]);
                    }

                    $strategy = $body['strategy'] ?? 'skip';
                    $db->beginTransaction();
                    try {
                        $res = $tendikRepo->createBatch($items, $strategy, $isDemo, $demoLimitTendik);
                        $db->commit();
                        $msg = $isDemo
                            ? "Batch import berhasil: {$res['inserted']} total disimpan ke MySQL ({$res['active_inserted']} aktif di Demo, {$res['pending_inserted']} tersimpan menunggu lisensi Full), {$res['skipped']} dilewati (duplikat), 0 gagal."
                            : "Batch import berhasil: {$res['inserted']} ditambahkan, {$res['skipped']} dilewati (duplikat), 0 gagal.";
                        Response::json(200, [
                            'success' => true,
                            'code'    => 'BATCH_IMPORT_SUCCESS',
                            'summary' => [
                                'total_received' => count($items),
                                'inserted'       => $res['inserted'],
                                'updated'        => 0,
                                'skipped'        => $res['skipped'],
                                'failed'         => 0,
                                'active_demo'    => $res['active_inserted'] ?? $res['inserted'],
                                'pending_full'   => $res['pending_inserted'] ?? 0
                            ],
                            'details' => [
                                'skipped_identifiers' => $res['skipped_identifiers'],
                                'errors'              => []
                            ],
                            'message' => $msg
                        ]);
                    } catch (Throwable $e) {
                        if ($db->inTransaction()) {
                            $db->rollBack();
                        }
                        error_log('[BATCH IMPORT TENDIK ERROR] ' . $e->getMessage());
                        Response::json(500, [
                            'success' => false,
                            'code'    => 'DATABASE_TRANSACTION_ERROR',
                            'message' => 'Terjadi kesalahan transaksi database saat mengimpor data tendik: ' . $e->getMessage()
                        ]);
                    }
                }

                if ($targetId === 'delete-multiple') {
                    $ids = $body['usernames'] ?? ($body['ids'] ?? []);
                    if (!is_array($ids) || empty($ids)) {
                        Response::json(400, ['success' => false, 'message' => 'Daftar username tidak boleh kosong']);
                    }
                    $deleted = 0;
                    foreach ($ids as $u) {
                        if ($tendikRepo->delete((string)$u)) {
                            $deleted++;
                            if ($isDemo) {
                                LicenseEntitlement::promoteNextPending('tendik', $db);
                            }
                        }
                    }
                    Response::json(200, ['success' => true, 'message' => "{$deleted} tendik berhasil dihapus", 'deleted' => $deleted]);
                }

                $username = strtolower(trim((string)($body['username'] ?? '')));
                $nama = trim((string)($body['nama'] ?? ''));
                if (!$username || !$nama) {
                    Response::json(400, ['success' => false, 'message' => 'Username dan Nama tendik wajib diisi']);
                }
                if ($tendikRepo->findByUsername($username)) {
                    Response::json(409, ['success' => false, 'message' => 'Username ' . $username . ' sudah terdaftar']);
                }

                $rawPass = trim((string)($body['password'] ?? '12345'));
                $pwHash = password_hash($rawPass, PASSWORD_BCRYPT);

                $activeCount = $tendikRepo->countActive();
                $assignStatus = 'aktif';
                $msg = 'Data tendik berhasil ditambahkan';
                if ($isDemo && $activeCount >= $demoLimitTendik) {
                    $assignStatus = 'pending';
                    $msg = 'Data tendik berhasil ditambahkan (Kuota Demo 10 tendik telah tercapai. Data tersimpan aman di database dan akan aktif setelah lisensi FULL diaktifkan)';
                }

                $newId = $tendikRepo->create([
                    'username'      => $username,
                    'nip'           => !empty($body['nip']) && $body['nip'] !== '-' ? trim((string)$body['nip']) : null,
                    'nama'          => $nama,
                    'jabatan'       => !empty($body['jabatan']) ? trim((string)$body['jabatan']) : 'Tendik',
                    'divisi'        => $body['divisi'] ?? null,
                    'jenis_kelamin' => $body['jenis_kelamin'] ?? ($body['jenisKelamin'] ?? null),
                    'no_hp'         => $body['no_hp'] ?? ($body['noHp'] ?? null),
                    'barcode'       => $body['barcode'] ?? $username,
                    'foto'          => $body['foto'] ?? null,
                    'password'      => $pwHash,
                    'status'        => $assignStatus
                ]);

                Response::json(201, ['success' => true, 'message' => $msg, 'id' => $newId, 'status' => $assignStatus]);
            } elseif ($method === 'PUT') {
                $username = strtolower($targetId ?: trim((string)($body['username'] ?? ($body['oldUsername'] ?? ''))));
                if (!$username) {
                    Response::json(400, ['success' => false, 'message' => 'Username tendik wajib disertakan']);
                }
                $existing = $tendikRepo->findByUsername($username);
                if (!$existing) {
                    Response::json(404, ['success' => false, 'message' => 'Tendik tidak ditemukan']);
                }

                $updateData = [];
                if (isset($body['nama'])) $updateData['nama'] = trim((string)$body['nama']);
                if (isset($body['nip'])) $updateData['nip'] = !empty($body['nip']) && $body['nip'] !== '-' ? trim((string)$body['nip']) : null;
                if (isset($body['jabatan'])) $updateData['jabatan'] = trim((string)$body['jabatan']);
                if (isset($body['divisi'])) $updateData['divisi'] = $body['divisi'];
                if (isset($body['jenis_kelamin']) || isset($body['jenisKelamin'])) {
                    $updateData['jenis_kelamin'] = $body['jenis_kelamin'] ?? $body['jenisKelamin'];
                }
                if (isset($body['no_hp']) || isset($body['noHp'])) {
                    $updateData['no_hp'] = $body['no_hp'] ?? $body['noHp'];
                }
                if (isset($body['barcode'])) $updateData['barcode'] = $body['barcode'];
                if (isset($body['foto'])) $updateData['foto'] = $body['foto'];
                if ($isDemo && ($existing['status'] ?? 'aktif') === 'pending') {
                    $updateData['status'] = 'pending';
                } elseif (isset($body['status'])) {
                    $updateData['status'] = $body['status'];
                }
                if (!empty($body['password'])) {
                    $rawP = trim((string)$body['password']);
                    $updateData['password'] = password_hash($rawP, PASSWORD_BCRYPT);
                }

                $ok = empty($updateData) ? true : $tendikRepo->update($username, $updateData);

                // Authoritative Role Configuration via app_settings.attendance_operators
                // Keeps master identity (jabatan, nama, nip) 100% pure and untouched!
                if (isset($body['role'])) {
                    $roleVal = strtolower(trim((string)$body['role']));
                    try {
                        $stmt = $db->query("SELECT setting_value FROM app_settings WHERE setting_key = 'attendance_operators' LIMIT 1");
                        $rawOps = $stmt ? $stmt->fetchColumn() : null;
                        $opList = $rawOps ? json_decode($rawOps, true) : [];
                        if (!is_array($opList)) $opList = [];
                        $cleanUser = strtolower($username);

                        if ($roleVal === 'operator_absensi' || $roleVal === 'operator') {
                            if (!in_array($cleanUser, array_map('strtolower', $opList), true)) {
                                $opList[] = $cleanUser;
                                $saveStmt = $db->prepare("INSERT INTO app_settings (setting_key, setting_value, description) 
                                    VALUES ('attendance_operators', :val, 'Daftar username operator absensi') 
                                    ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value), updated_at = CURRENT_TIMESTAMP");
                                $saveStmt->execute([':val' => json_encode(array_values($opList))]);
                            }
                        } elseif ($roleVal === 'tendik') {
                            $newOpList = array_values(array_filter($opList, function($u) use ($cleanUser) {
                                return strtolower(trim($u)) !== $cleanUser;
                            }));
                            $saveStmt = $db->prepare("INSERT INTO app_settings (setting_key, setting_value, description) 
                                VALUES ('attendance_operators', :val, 'Daftar username operator absensi') 
                                ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value), updated_at = CURRENT_TIMESTAMP");
                            $saveStmt->execute([':val' => json_encode($newOpList)]);
                        }
                    } catch (Throwable $e) {
                        error_log('[ROLE_UPDATE_ERROR] ' . $e->getMessage());
                    }
                }

                Response::json(200, ['success' => true, 'message' => 'Data tendik berhasil diperbarui']);
            } elseif ($method === 'DELETE') {
                $username = strtolower($targetId ?: trim((string)($body['username'] ?? '')));
                if (!$username) {
                    Response::json(400, ['success' => false, 'message' => 'Username wajib disertakan untuk penghapusan']);
                }
                $ok = $tendikRepo->delete($username);
                if ($ok && $isDemo) {
                    LicenseEntitlement::promoteNextPending('tendik', $db);
                }
                Response::json(200, ['success' => $ok, 'message' => $ok ? 'Tendik berhasil dihapus' : 'Tendik tidak ditemukan']);
            }
            break;

        case 'config':
            if ($method === 'GET') {
                $stmt = $db->prepare("SELECT setting_value FROM app_settings WHERE setting_key = 'school_config' LIMIT 1");
                $stmt->execute();
                $val = $stmt->fetchColumn();
                $cfg = $val ? json_decode($val, true) : [];
                Response::json(200, ['success' => true, 'data' => $cfg ?: new stdClass()]);
            } elseif ($method === 'POST') {
                // Merge with existing configuration to avoid overwriting unedited fields
                $stmt = $db->prepare("SELECT setting_value FROM app_settings WHERE setting_key = 'school_config' LIMIT 1");
                $stmt->execute();
                $existingVal = $stmt->fetchColumn();
                $existingCfg = $existingVal ? json_decode($existingVal, true) : [];
                if (!is_array($existingCfg)) $existingCfg = [];

                $mergedCfg = array_merge($existingCfg, $body);
                $payload = json_encode($mergedCfg, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
                $stmt = $db->prepare("
                    INSERT INTO app_settings (setting_key, setting_value, description, updated_at)
                    VALUES ('school_config', :val, 'Authoritative School Application Configuration', CURRENT_TIMESTAMP)
                    ON DUPLICATE KEY UPDATE setting_value = :val2, updated_at = CURRENT_TIMESTAMP
                ");
                $stmt->execute([':val' => $payload, ':val2' => $payload]);
                Response::json(200, ['success' => true, 'message' => 'Pengaturan aplikasi berhasil disimpan ke MySQL', 'data' => $mergedCfg]);
            }
            break;

        case 'libur':
            if ($method === 'GET') {
                $stmt = $db->prepare("SELECT setting_value FROM app_settings WHERE setting_key = 'holiday_calendar' LIMIT 1");
                $stmt->execute();
                $val = $stmt->fetchColumn();
                $holidays = $val ? json_decode($val, true) : [];
                Response::json(200, ['success' => true, 'data' => is_array($holidays) ? $holidays : []]);
            } elseif ($method === 'POST') {
                $payload = json_encode($body, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
                $stmt = $db->prepare("
                    INSERT INTO app_settings (setting_key, setting_value, description, updated_at)
                    VALUES ('holiday_calendar', :val, 'Authoritative School Academic Holidays', CURRENT_TIMESTAMP)
                    ON DUPLICATE KEY UPDATE setting_value = :val2, updated_at = CURRENT_TIMESTAMP
                ");
                $stmt->execute([':val' => $payload, ':val2' => $payload]);
                Response::json(200, ['success' => true, 'message' => 'Daftar hari libur berhasil disimpan ke MySQL']);
            }
            break;

        default:
            Response::json(404, ['success' => false, 'message' => 'Entity master tidak dikenal: ' . $entity]);
    }
} catch (Throwable $e) {
    error_log('[MASTER_API_ERROR] ' . $e->getMessage());
    Response::json(500, ['success' => false, 'message' => 'Kesalahan server master data: ' . $e->getMessage()]);
}
