<?php
/**
 * license-php/lib/LicenseEntitlement.php
 * Authoritative Server-Side Licensing & Entitlement Engine for Presensi Digital MySQL.
 * Governs DEMO vs FULL mode, operational limits, deterministic active/pending allocations, and promotion.
 */

require_once __DIR__ . '/ClientDatabase.php';
require_once __DIR__ . '/LicenseCrypto.php';

class LicenseEntitlement {
    public const DEMO_LIMIT_SISWA  = 20;
    public const DEMO_LIMIT_GURU   = 10;
    public const DEMO_LIMIT_TENDIK = 10;

    private static ?bool $isFullCached = null;
    private static ?array $licensePayloadCached = null;

    /**
     * Reset cached state in memory.
     */
    public static function clearCache(): void {
        self::$isFullCached = null;
        self::$licensePayloadCached = null;
    }

    /**
     * Determine whether the server is running under authoritative FULL license entitlement.
     * Evaluates cryptographic RSA-256 token in MySQL app_settings.system_license (or license.json).
     */

    public static function isFull(?PDO $pdo = null): bool {
        if (self::$isFullCached !== null) {
            return self::$isFullCached;
        }
        try {
            $db = $pdo ?? ClientDatabase::getConnection();
            $stmt = $db->prepare("SELECT setting_value FROM app_settings WHERE setting_key = 'system_license' LIMIT 1");
            $stmt->execute();
            $rawLicense = $stmt->fetchColumn();
            $parsed = $rawLicense ? json_decode($rawLicense, true) : null;

            // MySQL app_settings is the authoritative authority
            if (!$parsed || empty($parsed['token'])) {
                // If app_settings has no system_license, check if legacy file exists only if DB is not initialized
                return false;
            }

            $token = trim((string)$parsed['token']);
            if (substr_count($token, '.') !== 2) {
                self::$isFullCached = false;
                return false;
            }

            $pubKeyPath = dirname(__DIR__, 2) . '/keys/license_public.key';
            if (!file_exists($pubKeyPath)) {
                $altPaths = [
                    dirname(__DIR__) . '/keys/license_public.key',
                    dirname(__DIR__, 3) . '/keys/license_public.key',
                    dirname(__DIR__, 4) . '/keys/license_public.key'
                ];
                foreach ($altPaths as $ap) {
                    if (file_exists($ap)) {
                        $pubKeyPath = $ap;
                        break;
                    }
                }
            }

            if (!file_exists($pubKeyPath)) {
                self::$isFullCached = false;
                return false;
            }

            $pubKey = file_get_contents($pubKeyPath);
            $verified = LicenseCrypto::verifyToken($token, $pubKey);

            if (!$verified || empty($verified['valid'])) {
                self::$isFullCached = false;
                return false;
            }

            $payload = $verified['payload'] ?? [];
            if (!empty($payload['status']) && $payload['status'] !== 'active') {
                self::$isFullCached = false;
                return false;
            }

            // Expiration check
            if (!empty($payload['expires_at'])) {
                $expTime = strtotime($payload['expires_at']);
                if ($expTime !== false && time() > $expTime) {
                    self::$isFullCached = false;
                    return false;
                }
            }

            // Domain binding check (allowing localhost / 127.0.0.1)
            $currentHost = $_SERVER['HTTP_HOST'] ?? 'localhost';
            $cleanCurrent = LicenseCrypto::cleanDomain($currentHost);
            $tokenDomain = LicenseCrypto::cleanDomain($payload['domain'] ?? '');
            if ($tokenDomain && $cleanCurrent !== 'localhost' && $cleanCurrent !== '127.0.0.1') {
                if ($tokenDomain !== $cleanCurrent) {
                    self::$isFullCached = false;
                    return false;
                }
            }

            self::$isFullCached = true;
            self::$licensePayloadCached = $payload;
            return true;
        } catch (Throwable $e) {
            self::$isFullCached = false;
            return false;
        }
    }

    public static function isDemo(?PDO $pdo = null): bool {
        return !self::isFull($pdo);
    }

    public static function getLimit(string $entity): int {
        switch (strtolower(trim($entity))) {
            case 'siswa': return self::DEMO_LIMIT_SISWA;
            case 'guru': return self::DEMO_LIMIT_GURU;
            case 'tendik': return self::DEMO_LIMIT_TENDIK;
            default: return 1000000;
        }
    }

    /**
     * Get authoritative mode, limits, and entity counts.
     */
    public static function getStatusSummary(?PDO $pdo = null): array {
        $db = $pdo ?? ClientDatabase::getConnection();
        $full = self::isFull($db);

        $counts = [];
        foreach (['siswa', 'guru', 'tendik'] as $ent) {
            $totalStmt = $db->query("SELECT COUNT(*) FROM {$ent}");
            $total = (int)($totalStmt ? $totalStmt->fetchColumn() : 0);

            if ($ent === 'guru') {
                $activeStmt = $db->query("SELECT COUNT(*) FROM guru WHERE status = 'aktif' AND LOWER(username) != 'admin' AND LOWER(jabatan) != 'administrator'");
            } else {
                $activeStmt = $db->query("SELECT COUNT(*) FROM {$ent} WHERE status = 'aktif'");
            }
            $active = (int)($activeStmt ? $activeStmt->fetchColumn() : 0);

            $pendingStmt = $db->query("SELECT COUNT(*) FROM {$ent} WHERE status = 'pending'");
            $pending = (int)($pendingStmt ? $pendingStmt->fetchColumn() : 0);

            $counts[$ent] = [
                'limit'        => $full ? null : self::getLimit($ent),
                'total'        => $total,
                'total_stored' => $total,
                'active'       => $active,
                'pending'      => $pending
            ];
        }

        return [
            'is_full'      => $full,
            'is_demo'      => !$full,
            'mode'         => $full ? 'FULL' : 'DEMO',
            'limits'       => [
                'siswa'  => self::DEMO_LIMIT_SISWA,
                'guru'   => self::DEMO_LIMIT_GURU,
                'tendik' => self::DEMO_LIMIT_TENDIK
            ],
            'entitlements' => $counts,
            'entities'     => $counts,
            'counts'       => $counts,
            'license'      => self::$licensePayloadCached
        ];
    }

    /**
     * Reconcile existing records in database to comply with active DEMO limits.
     * In DEMO mode, deterministically keeps the first N records active and sets remainder to pending.
     * In FULL mode, promotes any pending records to active.
     */
    public static function reconcileEntitlement(?PDO $pdo = null): array {
        $db = $pdo ?? ClientDatabase::getConnection();
        $isFull = self::isFull($db);

        if ($isFull) {
            return self::promoteAllPending($db);
        }

        // 1. Siswa
        $siswaActive = (int)$db->query("SELECT COUNT(*) FROM siswa WHERE status = 'aktif'")->fetchColumn();
        if ($siswaActive > self::DEMO_LIMIT_SISWA) {
            $activeIds = $db->query("SELECT id FROM siswa WHERE status = 'aktif' ORDER BY id ASC LIMIT " . self::DEMO_LIMIT_SISWA)->fetchAll(PDO::FETCH_COLUMN);
            if (!empty($activeIds)) {
                $inClause = implode(',', array_map('intval', $activeIds));
                $db->exec("UPDATE siswa SET status = 'pending' WHERE id NOT IN ({$inClause}) AND status = 'aktif'");
            }
        }

        // 2. Guru (Admin must ALWAYS remain active)
        $adminId = $db->query("SELECT id FROM guru WHERE username = 'admin' OR LOWER(jabatan) = 'administrator' LIMIT 1")->fetchColumn();
        $otherGuruLimit = $adminId ? (self::DEMO_LIMIT_GURU - 1) : self::DEMO_LIMIT_GURU;
        $activeGuruIds = [];
        if ($adminId) {
            $activeGuruIds[] = (int)$adminId;
        }
        if ($otherGuruLimit > 0) {
            $otherStmt = $db->query("SELECT id FROM guru WHERE id != " . (int)$adminId . " AND status = 'aktif' ORDER BY id ASC LIMIT " . $otherGuruLimit);
            while ($r = $otherStmt->fetchColumn()) {
                $activeGuruIds[] = (int)$r;
            }
        }
        if (!empty($activeGuruIds)) {
            $inClause = implode(',', $activeGuruIds);
            $db->exec("UPDATE guru SET status = 'pending' WHERE id NOT IN ({$inClause}) AND status = 'aktif'");
            $db->exec("UPDATE guru SET status = 'aktif' WHERE id IN ({$inClause})");
        }

        // 3. Tendik (Designated operator must remain active)
        $opUsernames = [];
        try {
            $opStmt = $db->query("SELECT setting_value FROM app_settings WHERE setting_key = 'attendance_operators' LIMIT 1");
            $rawOps = $opStmt ? $opStmt->fetchColumn() : null;
            if ($rawOps) {
                $opArr = json_decode($rawOps, true);
                if (is_array($opArr)) {
                    foreach ($opArr as $u) $opUsernames[] = strtolower(trim((string)$u));
                }
            }
        } catch (Throwable $e) {}

        $activeTendikIds = [];
        if (!empty($opUsernames)) {
            $ph = implode(',', array_fill(0, count($opUsernames), '?'));
            $opIdStmt = $db->prepare("SELECT id FROM tendik WHERE LOWER(username) IN ({$ph})");
            $opIdStmt->execute($opUsernames);
            while ($r = $opIdStmt->fetchColumn()) {
                $activeTendikIds[] = (int)$r;
            }
        }
        $remainSlots = max(0, self::DEMO_LIMIT_TENDIK - count($activeTendikIds));
        if ($remainSlots > 0) {
            $excludeClause = !empty($activeTendikIds) ? " AND id NOT IN (" . implode(',', $activeTendikIds) . ")" : "";
            $moreStmt = $db->query("SELECT id FROM tendik WHERE 1=1 {$excludeClause} AND status = 'aktif' ORDER BY id ASC LIMIT {$remainSlots}");
            while ($r = $moreStmt->fetchColumn()) {
                $activeTendikIds[] = (int)$r;
            }
        }
        if (!empty($activeTendikIds)) {
            $inClause = implode(',', $activeTendikIds);
            $db->exec("UPDATE tendik SET status = 'pending' WHERE id NOT IN ({$inClause}) AND status = 'aktif'");
            $db->exec("UPDATE tendik SET status = 'aktif' WHERE id IN ({$inClause})");
        }

        return self::getStatusSummary($db);
    }

    /**
     * Atomic Promotion of ALL pending records when upgrading from DEMO to FULL.
     */
    public static function promoteAllPending(PDO $db): array {
        $inTx = $db->inTransaction();
        if (!$inTx) $db->beginTransaction();
        try {
            $siswaPromoted = (int)$db->exec("UPDATE siswa SET status = 'aktif' WHERE status = 'pending'");
            $guruPromoted  = (int)$db->exec("UPDATE guru SET status = 'aktif' WHERE status = 'pending'");
            $tendikPromoted = (int)$db->exec("UPDATE tendik SET status = 'aktif' WHERE status = 'pending'");

            if (!$inTx) $db->commit();
            self::clearCache();

            return [
                'success'         => true,
                'siswa'           => $siswaPromoted,
                'guru'            => $guruPromoted,
                'tendik'          => $tendikPromoted,
                'promoted_siswa'  => $siswaPromoted,
                'promoted_guru'   => $guruPromoted,
                'promoted_tendik' => $tendikPromoted,
                'total_promoted'  => ($siswaPromoted + $guruPromoted + $tendikPromoted)
            ];
        } catch (Throwable $e) {
            if (!$inTx && $db->inTransaction()) $db->rollBack();
            throw $e;
        }
    }

    /**
     * Promote the next eligible pending record when an active record is deleted in DEMO mode.
     */
    public static function promoteNextPending(string $entity, PDO $db): ?string {
        if (self::isFull($db)) {
            return null;
        }
        $tbl = strtolower(trim($entity));
        if (!in_array($tbl, ['siswa', 'guru', 'tendik'], true)) {
            return null;
        }

        $limit = self::getLimit($tbl);
        $currentActive = (int)$db->query("SELECT COUNT(*) FROM {$tbl} WHERE status = 'aktif'")->fetchColumn();
        if ($currentActive >= $limit) {
            return null;
        }

        $identCol = ($tbl === 'siswa') ? 'nisn' : 'username';
        $stmt = $db->query("SELECT id, {$identCol} AS ident FROM {$tbl} WHERE status = 'pending' ORDER BY id ASC LIMIT 1");
        $pending = $stmt ? $stmt->fetch(PDO::FETCH_ASSOC) : null;
        if ($pending) {
            $up = $db->prepare("UPDATE {$tbl} SET status = 'aktif' WHERE id = :id");
            $up->execute([':id' => $pending['id']]);
            return (string)$pending['ident'];
        }
        return null;
    }
}
