<?php
/**
 * license-php/lib/InstallerService.php
 * Comprehensive First-Run Installer & Database Bootstrap Engine.
 * Provides idempotent, non-destructive database initialization and server-side installation locking.
 */

class InstallerService {
    public const MIN_PHP_VERSION = '7.4.0';
    public const REQUIRED_EXTENSIONS = ['pdo', 'pdo_mysql', 'openssl', 'mbstring', 'json', 'curl'];

    /**
     * Determine if the application is already installed and locked.
     * Pure function: Authoritative deployment lock on filesystem only.
     * Zero side-effects, zero database connections, zero file writes.
     */
    public static function isInstalled(): bool {
        return file_exists(self::getLockFilePath());
    }

    /**
     * Check runtime environment requirements.
     */
    public static function checkRequirements(): array {
        $checks = [];
        $allPassed = true;

        // 1. PHP Version
        $phpVer = PHP_VERSION;
        $phpOk = version_compare($phpVer, self::MIN_PHP_VERSION, '>=');
        $checks[] = [
            'name'     => 'Versi PHP',
            'required' => '>= ' . self::MIN_PHP_VERSION,
            'current'  => $phpVer,
            'passed'   => $phpOk,
            'help'     => $phpOk ? 'Memenuhi syarat' : 'Upgrade versi PHP hosting Anda minimal ke ' . self::MIN_PHP_VERSION
        ];
        if (!$phpOk) $allPassed = false;

        // 2. Required Extensions
        foreach (self::REQUIRED_EXTENSIONS as $ext) {
            $extOk = extension_loaded($ext);
            $checks[] = [
                'name'     => 'Ekstensi PHP: ' . $ext,
                'required' => 'Aktif',
                'current'  => $extOk ? 'Tersedia' : 'Tidak Ada',
                'passed'   => $extOk,
                'help'     => $extOk ? 'Memenuhi syarat' : "Aktifkan ekstensi php_{$ext} di cPanel / php.ini"
            ];
            if (!$extOk) $allPassed = false;
        }

        // 3. Writable Directories
        $dataDir = dirname(__DIR__) . '/data';
        if (!is_dir($dataDir)) @mkdir($dataDir, 0755, true);
        $dataOk = is_dir($dataDir) && is_writable($dataDir);
        $checks[] = [
            'name'     => 'Direktori Runtime Data (license-php/data/)',
            'required' => 'Dapat ditulis (Writable)',
            'current'  => $dataOk ? 'Dapat ditulis' : 'Read-only / Terkunci',
            'passed'   => $dataOk,
            'help'     => $dataOk ? 'Memenuhi syarat' : 'Beri izin tulis (chmod 755/775) pada direktori license-php/data/'
        ];
        if (!$dataOk) $allPassed = false;

        $configDir = dirname(__DIR__) . '/config';
        $configOk = is_dir($configDir) && is_writable($configDir);
        $checks[] = [
            'name'     => 'Direktori Konfigurasi (license-php/config/)',
            'required' => 'Dapat ditulis (Writable)',
            'current'  => $configOk ? 'Dapat ditulis' : 'Read-only / Terkunci',
            'passed'   => $configOk,
            'help'     => $configOk ? 'Memenuhi syarat' : 'Beri izin tulis (chmod 755/775) pada direktori license-php/config/'
        ];
        if (!$configOk) $allPassed = false;

        // 4. Schema File Exists
        $schemaFile = self::getSchemaFilePath();
        $schemaOk = file_exists($schemaFile) && is_readable($schemaFile);
        $checks[] = [
            'name'     => 'File Skema Database (database/schema_client.sql)',
            'required' => 'Tersedia & Dapat dibaca',
            'current'  => $schemaOk ? 'Tersedia' : 'File tidak ditemukan',
            'passed'   => $schemaOk,
            'help'     => $schemaOk ? 'Memenuhi syarat' : 'Pastikan file database/schema_client.sql terunggah dengan lengkap'
        ];
        if (!$schemaOk) $allPassed = false;

        return [
            'all_passed' => $allPassed,
            'checks'     => $checks
        ];
    }

    /**
     * Test database connection with provided credentials.
     */
    public static function testConnection(string $host, int $port, string $name, string $user, string $password): array {
        $host = trim($host);
        $name = trim($name);
        $user = trim($user);

        if ($host === '' || $name === '' || $user === '') {
            return [
                'success' => false,
                'message' => 'Host, Nama Database, dan Username database wajib diisi.'
            ];
        }

        if ($port <= 0 || $port > 65535) {
            return [
                'success' => false,
                'message' => 'Port database tidak valid (harus antara 1 dan 65535).'
            ];
        }

        $dsn = "mysql:host={$host};port={$port};dbname={$name};charset=utf8mb4";
        $options = [
            PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_TIMEOUT            => 5,
            PDO::ATTR_EMULATE_PREPARES   => false
        ];

        try {
            $pdo = new PDO($dsn, $user, $password, $options);
            $verStmt = $pdo->query("SELECT VERSION() AS ver");
            $dbVer = $verStmt ? $verStmt->fetchColumn() : 'Unknown';

            return [
                'success' => true,
                'message' => 'Koneksi ke server database MySQL berhasil!',
                'server_version' => $dbVer
            ];
        } catch (PDOException $e) {
            $errCode = $e->getCode();
            $rawMsg = $e->getMessage();

            // Provide clear, actionable, sanitized user-friendly error messages
            $safeMsg = 'Koneksi database gagal.';
            if (strpos($rawMsg, 'Access denied') !== false) {
                $safeMsg = 'Akses ditolak: Username atau password database salah.';
            } elseif (strpos($rawMsg, 'Unknown database') !== false) {
                $safeMsg = "Database '{$name}' tidak ditemukan. Pastikan database sudah dibuat di cPanel/MySQL.";
            } elseif (strpos($rawMsg, 'Connection refused') !== false || strpos($rawMsg, 'Can\'t connect') !== false) {
                $safeMsg = "Gagal terhubung ke host '{$host}:{$port}'. Pastikan server MySQL berjalan dan port dapat diakses.";
            } else {
                $safeMsg = 'Koneksi database gagal: ' . preg_replace('/\[.*?\]/', '', $rawMsg);
            }

            return [
                'success' => false,
                'message' => trim($safeMsg)
            ];
        }
    }

    /**
     * Save database credentials to server-side configuration file safely.
     */
    public static function saveConfiguration(string $host, int $port, string $name, string $user, string $password): bool {
        $configDir = dirname(__DIR__) . '/config';
        if (!is_dir($configDir)) @mkdir($configDir, 0755, true);

        // 1. Write license-php/config/db_config.php (Protected PHP return file)
        $dbPhpFile = $configDir . '/db_config.php';
        $phpCode = "<?php\n"
            . "/**\n"
            . " * Authoritative MySQL Database Configuration\n"
            . " * Generated automatically by Presensi Digital Web Installer\n"
            . " * Date: " . date('Y-m-d H:i:s') . "\n"
            . " */\n"
            . "return [\n"
            . "    'driver'   => 'mysql',\n"
            . "    'host'     => " . var_export($host, true) . ",\n"
            . "    'port'     => " . (int)$port . ",\n"
            . "    'name'     => " . var_export($name, true) . ",\n"
            . "    'user'     => " . var_export($user, true) . ",\n"
            . "    'password' => " . var_export($password, true) . ",\n"
            . "    'charset'  => 'utf8mb4',\n"
            . "];\n";

        $resPhp = @file_put_contents($dbPhpFile, $phpCode, LOCK_EX);

        // 2. Also write/update license-php/.env for compatibility
        $envFile = dirname(__DIR__) . '/.env';
        $envLines = [
            "# Presensi Digital Database Environment",
            "DB_HOST=" . $host,
            "DB_PORT=" . (int)$port,
            "DB_NAME=" . $name,
            "DB_USER=" . $user,
            "DB_PASSWORD=\"" . addcslashes($password, "\"\\$") . "\"",
            "APP_ENV=production"
        ];
        @file_put_contents($envFile, implode("\n", $envLines) . "\n", LOCK_EX);

        return ($resPhp !== false);
    }

    /**
     * Initialize database schema idempotently and non-destructively.
     */
    public static function initializeSchema(PDO $pdo): array {
        $schemaPath = self::getSchemaFilePath();
        if (!file_exists($schemaPath)) {
            throw new RuntimeException('File skema database tidak ditemukan: database/schema_client.sql');
        }

        $sql = file_get_contents($schemaPath);
        if (!$sql) {
            throw new RuntimeException('File skema database kosong.');
        }

        // Split SQL statements safely
        // Invariant: Non-destructive CREATE TABLE IF NOT EXISTS statements only
        $queries = array_filter(array_map('trim', explode(';', $sql)), function($q) {
            return $q !== '' && stripos($q, 'SET ') !== 0;
        });

        $created = [];
        $existing = [];

        // Check pre-existing tables
        $tablesBefore = self::getExistingTables($pdo);

        $pdo->exec("SET FOREIGN_KEY_CHECKS = 0;");
        foreach ($queries as $query) {
            if (empty($query)) continue;
            $pdo->exec($query);
        }
        $pdo->exec("SET FOREIGN_KEY_CHECKS = 1;");

        $tablesAfter = self::getExistingTables($pdo);
        $requiredTables = ['siswa', 'guru', 'tendik', 'attendance', 'app_settings'];

        foreach ($requiredTables as $t) {
            if (in_array($t, $tablesBefore, true)) {
                $existing[] = $t;
            } elseif (in_array($t, $tablesAfter, true)) {
                $created[] = $t;
            }
        }

        // Ensure default app_settings row for operators exists
        $stmt = $pdo->prepare("
            INSERT INTO app_settings (setting_key, setting_value, description, updated_at)
            VALUES ('attendance_operators', '[]', 'Daftar username operator absensi', CURRENT_TIMESTAMP)
            ON DUPLICATE KEY UPDATE updated_at = CURRENT_TIMESTAMP
        ");
        $stmt->execute();

        $validation = self::validateExistingInstallation($pdo);

        return [
            'success'                 => true,
            'created_tables'          => $created,
            'existing_tables'         => $existing,
            'total_tables'            => count($tablesAfter),
            'has_existing_admin'      => !empty($validation['has_admin']),
            'existing_admin_username' => !empty($validation['admin']['username']) ? $validation['admin']['username'] : null,
            'message'                 => 'Inisialisasi skema database berhasil.'
        ];
    }

    /**
     * Validate whether the database represents a valid, coherent existing installation.
     */
    public static function validateExistingInstallation(PDO $pdo): array {
        $tables = self::getExistingTables($pdo);
        $requiredTables = ['siswa', 'guru', 'tendik', 'attendance', 'app_settings'];
        $missingTables = array_diff($requiredTables, $tables);

        if (!empty($missingTables)) {
            return [
                'is_valid'  => false,
                'code'      => 'INCOMPLETE_SCHEMA_STATE',
                'message'   => 'Struktur database tidak lengkap untuk instalasi existing. Tabel belum lengkap: ' . implode(', ', $missingTables),
                'has_admin' => false,
                'admin'     => null
            ];
        }

        // Check if an authoritative Administrator already exists in MySQL
        $stmt = $pdo->prepare("
            SELECT id, username, nama, jabatan, password, status
            FROM guru
            WHERE LOWER(jabatan) = 'administrator'
            ORDER BY id ASC
            LIMIT 1
        ");
        $stmt->execute();
        $admin = $stmt->fetch(PDO::FETCH_ASSOC);

        if (!$admin) {
            return [
                'is_valid'  => true,
                'has_admin' => false,
                'admin'     => null
            ];
        }

        if (empty($admin['username']) || empty($admin['password'])) {
            return [
                'is_valid'  => false,
                'code'      => 'INCONSISTENT_ADMIN_RECORD',
                'message'   => 'Ditemukan akun Administrator di database tetapi data kredensial tidak konsisten.',
                'has_admin' => true,
                'admin'     => null
            ];
        }

        return [
            'is_valid'  => true,
            'has_admin' => true,
            'admin'     => [
                'id'       => (int)$admin['id'],
                'username' => $admin['username'],
                'nama'     => $admin['nama'],
                'status'   => $admin['status']
            ]
        ];
    }

    /**
     * Create or verify the initial Administrator account.
     * Supports both Fresh Database (provisioning) and Existing Database (non-destructive adoption).
     */
    public static function createFirstAdmin(PDO $pdo, string $username, string $password, string $confirmPassword): array {
        $username = strtolower(trim($username));
        $password = trim($password);
        $confirmPassword = trim($confirmPassword);

        // Begin atomic transaction for administrator provisioning/adoption
        $pdo->beginTransaction();
        try {
            // Validate existing installation state with exclusive lock
            $validation = self::validateExistingInstallation($pdo);

            if (!$validation['is_valid']) {
                if ($pdo->inTransaction()) $pdo->rollBack();
                return [
                    'success' => false,
                    'code'    => $validation['code'] ?? 'INCONSISTENT_INSTALLATION_STATE',
                    'message' => $validation['message'] ?? 'Status database tidak konsisten untuk instalasi.',
                    'created' => false
                ];
            }

            // STATE B: Valid Existing Installation with Pre-existing Administrator
            if (!empty($validation['has_admin']) && !empty($validation['admin'])) {
                if ($pdo->inTransaction()) $pdo->rollBack();
                $existingUser = $validation['admin']['username'];
                return [
                    'success'         => true,
                    'code'            => 'ADMIN_ALREADY_EXISTS',
                    'message'         => "Akun Administrator existing ('{$existingUser}') terdeteksi di database MySQL dan dipertahankan. Password dan data existing tidak diubah.",
                    'username'        => $existingUser,
                    'created'         => false,
                    'already_existed' => true
                ];
            }

            // STATE A: Fresh Installation - Provisioning First Administrator
            if (!preg_match('/^[a-z0-9_]{3,32}$/', $username)) {
                if ($pdo->inTransaction()) $pdo->rollBack();
                return [
                    'success' => false,
                    'code'    => 'INVALID_USERNAME',
                    'message' => 'Username admin hanya boleh berisi huruf kecil, angka, dan underscore (3-32 karakter).',
                    'created' => false
                ];
            }

            // Check if username is taken by non-admin staff in guru table
            $stmtUserCheck = $pdo->prepare("SELECT id, username, jabatan FROM guru WHERE username = :u LIMIT 1 FOR UPDATE");
            $stmtUserCheck->execute([':u' => $username]);
            $takenUser = $stmtUserCheck->fetch(PDO::FETCH_ASSOC);
            if ($takenUser) {
                if ($pdo->inTransaction()) $pdo->rollBack();
                return [
                    'success' => false,
                    'code'    => 'USERNAME_TAKEN',
                    'message' => "Username '{$username}' sudah digunakan oleh staf/guru lain di database. Silakan pilih username lain.",
                    'created' => false
                ];
            }

            if (strlen($password) < 8) {
                if ($pdo->inTransaction()) $pdo->rollBack();
                return [
                    'success' => false,
                    'code'    => 'PASSWORD_TOO_SHORT',
                    'message' => 'Password minimal 8 karakter.',
                    'created' => false
                ];
            }

            if (!preg_match('/[A-Z]/', $password) || !preg_match('/[a-z]/', $password) || !preg_match('/[0-9]/', $password)) {
                if ($pdo->inTransaction()) $pdo->rollBack();
                return [
                    'success' => false,
                    'code'    => 'PASSWORD_COMPLEXITY_FAILED',
                    'message' => 'Password harus mengandung huruf besar, huruf kecil, dan angka.',
                    'created' => false
                ];
            }

            if ($password !== $confirmPassword) {
                if ($pdo->inTransaction()) $pdo->rollBack();
                return [
                    'success' => false,
                    'code'    => 'PASSWORD_MISMATCH',
                    'message' => 'Konfirmasi password tidak cocok.',
                    'created' => false
                ];
            }

            // Hash password using Bcrypt with cost 12
            $hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);

            // Insert into MySQL guru table (Primary Identity Authority)
            $insert = $pdo->prepare("
                INSERT INTO guru (username, nip, nama, jabatan, password, status)
                VALUES (:username, '-', 'Administrator Utama', 'Administrator', :password, 'aktif')
            ");
            $insert->execute([
                ':username' => $username,
                ':password' => $hash
            ]);

            // Read-back verification: Ensure record actually exists and password matches
            $verifyStmt = $pdo->prepare("SELECT id, username, password, jabatan, status FROM guru WHERE username = :u AND jabatan = 'Administrator' LIMIT 1");
            $verifyStmt->execute([':u' => $username]);
            $verifiedRow = $verifyStmt->fetch(PDO::FETCH_ASSOC);

            if (!$verifiedRow || empty($verifiedRow['password']) || !password_verify($password, $verifiedRow['password'])) {
                if ($pdo->inTransaction()) $pdo->rollBack();
                return [
                    'success' => false,
                    'code'    => 'ADMIN_VERIFICATION_FAILED',
                    'message' => 'Gagal memverifikasi penyimpanan akun administrator di database.',
                    'created' => false
                ];
            }

            // Commit atomic transaction: Administrator successfully provisioned
            if ($pdo->inTransaction()) $pdo->commit();
        } catch (Throwable $e) {
            if ($pdo->inTransaction()) $pdo->rollBack();
            throw $e;
        }

        return [
            'success'  => true,
            'code'     => 'ADMIN_CREATED',
            'message'  => 'Akun Administrator berhasil dibuat.',
            'username' => $username,
            'created'  => true
        ];
    }

    /**
     * Lock the installation permanently.
     */
    public static function lockInstallation(PDO $pdo, array $metadata = []): bool {
        $lockFile = self::getLockFilePath();
        $payload = array_merge([
            'installed'    => true,
            'timestamp'    => date('c'),
            'php_version'  => PHP_VERSION,
            'server_ip'    => $_SERVER['SERVER_ADDR'] ?? '127.0.0.1',
            'client_ip'    => $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1'
        ], $metadata);

        // 1. Filesystem lock
        $resLock = @file_put_contents($lockFile, json_encode($payload, JSON_PRETTY_PRINT), LOCK_EX);

        // 2. MySQL app_settings lock
        try {
            $stmt = $pdo->prepare("
                INSERT INTO app_settings (setting_key, setting_value, description, updated_at)
                VALUES ('system_installed', :val, 'Authoritative Installation Lock Metadata', CURRENT_TIMESTAMP)
                ON DUPLICATE KEY UPDATE setting_value = :val2, updated_at = CURRENT_TIMESTAMP
            ");
            $jsonPayload = json_encode($payload);
            $stmt->execute([':val' => $jsonPayload, ':val2' => $jsonPayload]);
        } catch (Throwable $e) {
            error_log('[INSTALLER_LOCK_ERROR] ' . $e->getMessage());
        }

        return ($resLock !== false);
    }

    private static function getLockFilePath(): string {
        return dirname(__DIR__) . '/data/installed.lock';
    }

    private static function getSchemaFilePath(): string {
        return dirname(__DIR__, 2) . '/database/schema_client.sql';
    }

    private static function loadCurrentConfig(): array {
        $configFile = dirname(__DIR__) . '/config/config.php';
        return file_exists($configFile) ? (require $configFile) : [];
    }

    private static function getExistingTables(PDO $pdo): array {
        $stmt = $pdo->query("SHOW TABLES");
        return $stmt ? $stmt->fetchAll(PDO::FETCH_COLUMN) : [];
    }
    /**
     * Activate license against centralized master server (presensi.kangyos.com).
     * Cryptographically verifies the RSA-256 token and persists valid license state.
     */
    public static function activateCentralLicense(string $rawKey, string $domain, string $product = 'presensi-smansa-pro', string $masterUrl = 'https://presensi.kangyos.com'): array {
        require_once __DIR__ . '/LicenseCrypto.php';

        $cleanKey = strtoupper(trim($rawKey));
        if (empty($cleanKey)) {
            return ['success' => false, 'code' => 'INVALID_KEY', 'message' => 'Kunci lisensi wajib diisi'];
        }

        $cleanDomain = LicenseCrypto::cleanDomain($domain);
        if (empty($cleanDomain)) {
            $cleanDomain = LicenseCrypto::cleanDomain($_SERVER['HTTP_HOST'] ?? 'localhost');
        }

        // 1. Server-side HTTPS request to centralized master server
        $endpoint = rtrim($masterUrl, '/') . '/api/license/activate';
        $postData = json_encode([
            'license_key' => $cleanKey,
            'domain'      => $cleanDomain,
            'product'     => $product
        ]);

        $ch = curl_init($endpoint);
        curl_setopt_array($ch, [
            CURLOPT_POST           => true,
            CURLOPT_POSTFIELDS     => $postData,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPHEADER     => ['Content-Type: application/json', 'Accept: application/json'],
            CURLOPT_TIMEOUT        => 12,
            CURLOPT_CONNECTTIMEOUT => 6,
            CURLOPT_SSL_VERIFYPEER => true
        ]);

        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $curlErr  = curl_error($ch);

        if ($response === false || !empty($curlErr)) {
            return [
                'success' => false,
                'code'    => 'CENTRAL_SERVER_UNREACHABLE',
                'message' => 'Gagal menghubungi server lisensi pusat (' . (parse_url($masterUrl, PHP_URL_HOST) ?: 'presensi.kangyos.com') . '). Pastikan server memiliki koneksi internet.'
            ];
        }

        $resData = json_decode($response, true);
        if (!$resData || !is_array($resData)) {
            return [
                'success' => false,
                'code'    => 'INVALID_CENTRAL_RESPONSE',
                'message' => 'Respon tidak valid dari server lisensi pusat (HTTP ' . $httpCode . ')'
            ];
        }

        if ($httpCode !== 200 || empty($resData['success'])) {
            $msg = $resData['message'] ?? 'Aktivasi lisensi ditolak oleh server pusat.';
            return [
                'success' => false,
                'code'    => $resData['status'] ?? 'ACTIVATION_REJECTED',
                'message' => $msg
            ];
        }

        // 2. Cryptographic token signature verification
        $token = $resData['token'] ?? '';
        if (empty($token) || substr_count($token, '.') !== 2) {
            return [
                'success' => false,
                'code'    => 'MISSING_TOKEN',
                'message' => 'Token lisensi tidak ditemukan dalam respon server pusat.'
            ];
        }

        $config = self::loadCurrentConfig();
        $pubKeyPath = $config['keys']['public'] ?? (dirname(__DIR__, 2) . '/keys/license_public.key');
        if (!file_exists($pubKeyPath)) {
            $altPaths = [
                dirname(__DIR__, 2) . '/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)) {
            return [
                'success' => false,
                'code'    => 'PUBLIC_KEY_MISSING',
                'message' => 'Kunci publik verifikasi lisensi klien tidak ditemukan.'
            ];
        }

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

        if (!$verified || empty($verified['valid'])) {
            return [
                'success' => false,
                'code'    => 'INVALID_SIGNATURE',
                'message' => 'Verifikasi tanda tangan digital token lisensi gagal (RSA-256 signature mismatch).'
            ];
        }

        // 3. Claims verification
        $payload = $verified['payload'] ?? [];
        if (!empty($payload['product']) && strtolower($payload['product']) !== strtolower($product)) {
            return [
                'success' => false,
                'code'    => 'PRODUCT_MISMATCH',
                'message' => "Produk lisensi tidak sesuai (diharapkan: {$product}, didapat: {$payload['product']})."
            ];
        }

        if (!empty($payload['status']) && $payload['status'] !== 'active') {
            return [
                'success' => false,
                'code'    => 'LICENSE_INACTIVE',
                'message' => "Status lisensi pada token tidak aktif ({$payload['status']})."
            ];
        }

        // Domain binding check
        if (!empty($payload['domain'])) {
            $tokenDomain = LicenseCrypto::cleanDomain($payload['domain']);
            if ($tokenDomain !== $cleanDomain && $cleanDomain !== 'localhost' && $cleanDomain !== '127.0.0.1') {
                return [
                    'success' => false,
                    'code'    => 'DOMAIN_MISMATCH',
                    'message' => "Lisensi terikat pada domain {$tokenDomain}, bukan {$cleanDomain}."
                ];
            }
        }

        // Expiration check
        if (!empty($payload['expires_at'])) {
            $expTime = strtotime($payload['expires_at']);
            if ($expTime !== false && time() > $expTime) {
                return [
                    'success' => false,
                    'code'    => 'LICENSE_EXPIRED',
                    'message' => 'Masa berlaku lisensi telah berakhir pada ' . $payload['expires_at'] . '.'
                ];
            }
        }

        // 4. Server-side persistence
        $licenseData = [
            'domain'       => $payload['domain'] ?? $cleanDomain,
            'key'          => $cleanKey,
            'plan'         => $payload['plan'] ?? 'PRO',
            'type'         => $payload['license_type'] ?? 'Subscription',
            'expires_at'   => $payload['expires_at'] ?? null,
            'token'        => $token,
            'status'       => 'active',
            'customer'     => $payload['customer'] ?? ($resData['license']['customer_name'] ?? 'Client'),
            'entitlements' => $resData['license']['entitlements'] ?? [],
            'activated_at' => date('c')
        ];

        // 4a. Persist to data/license.json
        $dataDir = dirname(__DIR__) . '/data';
        if (!is_dir($dataDir)) @mkdir($dataDir, 0755, true);
        file_put_contents($dataDir . '/license.json', json_encode($licenseData, JSON_PRETTY_PRINT), LOCK_EX);

        // 4b. Persist to MySQL app_settings if database is available
        try {
            require_once __DIR__ . '/ClientDatabase.php';
            $pdo = ClientDatabase::getConnection();
            $stmt = $pdo->prepare("
                INSERT INTO app_settings (setting_key, setting_value, description, updated_at)
                VALUES ('system_license', :val, 'Authoritative Central License Metadata & Token', CURRENT_TIMESTAMP)
                ON DUPLICATE KEY UPDATE setting_value = :val2, updated_at = CURRENT_TIMESTAMP
            ");
            $jsonLicense = json_encode($licenseData);
            $stmt->execute([':val' => $jsonLicense, ':val2' => $jsonLicense]);
        } catch (Throwable $e) {
            error_log('[INSTALLER_LICENSE_DB_WARN] ' . $e->getMessage());
        }

        return [
            'success' => true,
            'status'  => 'active',
            'message' => 'Lisensi berhasil diaktivasi dan diverifikasi secara kriptografis oleh server pusat.',
            'license' => $licenseData,
            'token'   => $token
        ];
    }

}
