<?php
/**
 * HDEncoder PHP Loader — Standalone Shared Hosting Edition
 *
 * Dieser Loader entschlüsselt HDEncoder-geschützte PHP-Dateien (v1 Source-Level)
 * OHNE die native C-Extension (hdencoder.so). Für Shared Hosting ohne Root-Zugang.
 *
 * Anforderungen: PHP 8.0+, openssl, sodium, curl
 * Unterstützt: v1 (Source-Level Verschlüsselung)
 * NICHT unterstützt: v2 (Opcode-Level) — dafür wird die native C-Extension benötigt.
 *
 * @copyright DGNVault — https://dgnshop.com
 */

/* Direkten Web-Zugriff blockieren — diese Datei darf nur per include geladen werden */
if (basename($_SERVER['SCRIPT_FILENAME'] ?? '') === basename(__FILE__)) {
    http_response_code(403);
    exit;
}

namespace HDEncoder\Runtime;

class Loader
{
    /* ── Konstanten ─────────────────────────────────────────── */

    private const MAGIC       = "\x89\x48\x44\x45\x0d\x0a\x1a\x0a";
    private const IV_LEN      = 12;
    private const TAG_LEN     = 16;
    private const KEY_LEN     = 32;
    private const HMAC_LEN    = 32;
    private const CIPHER      = 'aes-256-gcm';
    private const LICENSE_URL  = 'https://encodedgn.dgnvault.de/api/v1/lc';
    private const LICENSE_TTL  = 3600;
    private const CACHE_DIR    = 'hdencoder_cache';

    /* ── XOR-obfuskierter Master Key ──────────────────────────
     * Der Key ist in 4 Segmente aufgeteilt, jedes XOR'd mit einem
     * zufälligen Pad. Zur Laufzeit wird der echte Key rekonstruiert.
     * Generiert mit: scripts/generate_loader_key.php
     */
    private const K1 = '58I6QG1TwpiVjd54l8Yt2g=='; private const P1 = '1KBYeQtk+qygvece8vUbvw==';
    private const K2 = '92Tc+A7qqxI7fFiiYrjzcA=='; private const P2 = 'x1G4yTmPzSMPHTvDB4mVQQ==';
    private const K3 = '6DYfFtykHUIF7E+1XJ3MOg=='; private const P3 = 'iQMocuSUeHo1jymAOqWpCw==';
    private const K4 = 'nG9W/Xs911FLHvK+h6TTIA=='; private const P4 = 'qlcymEwN4DB5fMuO5sCwFQ==';

    /* ── State ────────────────────────────────────────────── */

    private static ?string $masterKey    = null;
    private static bool    $checked      = false;
    private static ?string $cacheDirPath = null;

    /* ================================================================
     * Haupt-Einstiegspunkt — wird vom Stub-Code aufgerufen
     * ================================================================ */

    public static function execute(string $encryptedData, string $filePath): mixed
    {
        if (!self::$checked) {
            self::checkRequirements();
            self::$checked = true;
        }

        /* Cache prüfen — vermeidet teures Argon2id (~90ms) bei jedem Request */
        $cacheKey = hash('sha256', $encryptedData);
        $cached   = self::getCachedFile($cacheKey);

        if ($cached === null) {
            /* Entschlüsseln */
            $result = self::decrypt($encryptedData);

            if ($result['version'] !== 1) {
                throw new \RuntimeException(
                    'HDEncoder: Diese Datei verwendet Opcode-Level Verschlüsselung (v2). ' .
                    'Dafür wird die native C-Extension (hdencoder.so) benötigt. ' .
                    'Auf Shared Hosting werden nur v1-Dateien (Source-Level) unterstützt.'
                );
            }

            /* Lizenz prüfen */
            $meta   = $result['meta'];
            $domain = $meta['domains'][0] ?? ($_SERVER['SERVER_NAME'] ?? $_SERVER['HTTP_HOST'] ?? 'localhost');
            self::verifyLicense($domain);

            /* Ablaufdatum prüfen */
            if (!empty($meta['expires_at'])) {
                $expires = strtotime($meta['expires_at']);
                if ($expires && $expires < time()) {
                    throw new \RuntimeException('HDEncoder: Lizenz abgelaufen am ' . $meta['expires_at']);
                }
            }

            /* Quellcode cachen */
            $source = $result['source'];
            $cached = self::putCachedFile($cacheKey, $source, $filePath);
        }

        return include $cached;
    }

    /* ================================================================
     * Crypto — portiert aus Encryptor.php (standalone, kein Laravel)
     * ================================================================ */

    private static function getMasterKey(): string
    {
        if (self::$masterKey !== null) {
            return self::$masterKey;
        }

        /* Key aus XOR-Segmenten rekonstruieren */
        $hex = '';
        $pairs = [[self::K1, self::P1], [self::K2, self::P2], [self::K3, self::P3], [self::K4, self::P4]];
        foreach ($pairs as [$k, $p]) {
            $kb = base64_decode($k);
            $pb = base64_decode($p);
            $hex .= $kb ^ $pb;
        }

        self::$masterKey = $hex;
        return $hex;
    }

    private static function decrypt(string $raw): array
    {
        $pkg = self::unpackage($raw);
        $key = self::deriveKey($pkg['salt']);

        $metaJson = self::aesDecrypt($pkg['meta_encrypted'], $pkg['meta_iv'], $key);
        $meta     = json_decode($metaJson, true);
        if (!is_array($meta)) {
            throw new \RuntimeException('HDEncoder: Metadata ungültig');
        }

        $source = self::aesDecrypt($pkg['payload_encrypted'], $pkg['payload_iv'], $key);

        /* Key sofort aus dem Speicher löschen */
        sodium_memzero($key);

        return ['version' => $pkg['version'], 'meta' => $meta, 'source' => $source];
    }

    private static function unpackage(string $packed): array
    {
        /* HMAC prüfen (letzte 32 Bytes) */
        $hmac = substr($packed, -self::HMAC_LEN);
        $body = substr($packed, 0, -self::HMAC_LEN);

        $expected = hash_hmac('sha256', $body, self::getMasterKey(), true);
        if (!hash_equals($expected, $hmac)) {
            throw new \RuntimeException('HDEncoder: Integritätsprüfung fehlgeschlagen (HMAC)');
        }

        /* Binary-Format parsen */
        $magic = substr($body, 0, 8);
        if ($magic !== self::MAGIC) {
            throw new \RuntimeException('HDEncoder: Ungültiges Dateiformat');
        }

        $pos     = 8;
        $version = ord($body[$pos++]);
        $saltLen = ord($body[$pos++]);
        $salt    = substr($body, $pos, $saltLen); $pos += $saltLen;

        $metaIv  = substr($body, $pos, self::IV_LEN); $pos += self::IV_LEN;
        $metaLen = unpack('N', substr($body, $pos, 4))[1]; $pos += 4;
        $metaEnc = substr($body, $pos, $metaLen); $pos += $metaLen;

        $payIv   = substr($body, $pos, self::IV_LEN); $pos += self::IV_LEN;
        $payLen  = unpack('N', substr($body, $pos, 4))[1]; $pos += 4;
        $payEnc  = substr($body, $pos, $payLen);

        return [
            'version'           => $version,
            'salt'              => $salt,
            'meta_iv'           => $metaIv,
            'meta_encrypted'    => $metaEnc,
            'payload_iv'        => $payIv,
            'payload_encrypted' => $payEnc,
        ];
    }

    private static function deriveKey(string $salt): string
    {
        return sodium_crypto_pwhash(
            self::KEY_LEN,
            self::getMasterKey(),
            $salt,
            SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE,
            SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE,
            SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13
        );
    }

    private static function aesDecrypt(string $data, string $iv, string $key): string
    {
        $tag       = substr($data, 0, self::TAG_LEN);
        $encrypted = substr($data, self::TAG_LEN);
        $decrypted = openssl_decrypt($encrypted, self::CIPHER, $key, OPENSSL_RAW_DATA, $iv, $tag);

        if ($decrypted === false) {
            throw new \RuntimeException('HDEncoder: Entschlüsselung fehlgeschlagen — ' . openssl_error_string());
        }
        return $decrypted;
    }

    /* ================================================================
     * Lizenz-Check via API
     * ================================================================ */

    private static function verifyLicense(string $domain): void
    {
        /* Leere Domain = kein Domain-Lock → überspringen */
        if (empty($domain) || $domain === 'localhost') {
            return;
        }

        $cacheDir  = self::getCacheDir();
        $cacheFile = $cacheDir . '/lic_' . md5($domain);

        /* Cache prüfen (1 Stunde gültig) */
        if (file_exists($cacheFile) && (time() - filemtime($cacheFile)) < self::LICENSE_TTL) {
            $cached = file_get_contents($cacheFile);
            if ($cached === '1') return;
            throw new \RuntimeException('HDEncoder: Lizenz ungültig für Domain ' . $domain);
        }

        /* API-Check */
        $payload   = json_encode(['domain' => $domain]);
        $signature = hash_hmac('sha256', $payload, self::getMasterKey());

        $ch = curl_init(self::LICENSE_URL);
        curl_setopt_array($ch, [
            CURLOPT_POST           => true,
            CURLOPT_POSTFIELDS     => $payload,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT        => 10,
            CURLOPT_CONNECTTIMEOUT => 5,
            CURLOPT_HTTPHEADER     => [
                'Content-Type: application/json',
                'X-Signature: ' . $signature,
            ],
        ]);

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

        /* Bei Netzwerk-Fehler: Fail-open (kleine Radios sollen nicht offline gehen) */
        if ($response === false || $httpCode !== 200) {
            /* Alten Cache nutzen wenn vorhanden */
            if (file_exists($cacheFile)) {
                return;
            }
            error_log('HDEncoder: Lizenz-Check fehlgeschlagen (' . ($error ?: "HTTP $httpCode") . ') — Fail-open');
            return;
        }

        $data  = json_decode($response, true);
        $valid = ($data['valid'] ?? false) === true;

        /* Ergebnis cachen */
        @file_put_contents($cacheFile, $valid ? '1' : '0', LOCK_EX);

        if (!$valid) {
            throw new \RuntimeException('HDEncoder: Lizenz ungültig für Domain ' . $domain);
        }
    }

    /* ================================================================
     * File-Cache — vermeidet Argon2id pro Request
     * ================================================================ */

    private static function getCacheDir(): string
    {
        if (self::$cacheDirPath !== null) {
            return self::$cacheDirPath;
        }

        $dir = getenv('HDENCODER_CACHE_DIR');
        if (!$dir) {
            $dir = sys_get_temp_dir() . '/' . self::CACHE_DIR;
        }

        if (!is_dir($dir)) {
            @mkdir($dir, 0700, true);
            /* .htaccess schützt vor direktem Web-Zugriff */
            @file_put_contents($dir . '/.htaccess', "Deny from all\n", LOCK_EX);
        }

        self::$cacheDirPath = $dir;
        return $dir;
    }

    private static function getCachedFile(string $cacheKey): ?string
    {
        $path = self::getCacheDir() . '/' . $cacheKey . '.php';
        if (file_exists($path)) {
            return $path;
        }
        return null;
    }

    private static function putCachedFile(string $cacheKey, string $source, string $filePath): string
    {
        $path = self::getCacheDir() . '/' . $cacheKey . '.php';

        /* PHP Opening-Tag entfernen (für sauberes include) */
        $code = $source;
        $code = preg_replace('/^\s*<\?(php)?\s*/i', '<?php ', $code, 1);

        /* __FILE__ und __DIR__ auf den echten Deployment-Pfad korrigieren */
        $realFile = var_export($filePath, true);
        $realDir  = var_export(dirname($filePath), true);
        $code = str_replace('__FILE__', $realFile, $code);
        $code = str_replace('__DIR__', $realDir, $code);

        @file_put_contents($path, $code, LOCK_EX);
        @chmod($path, 0600);

        return $path;
    }

    /* ================================================================
     * Anforderungen prüfen
     * ================================================================ */

    private static function checkRequirements(): void
    {
        if (PHP_VERSION_ID < 80000) {
            throw new \RuntimeException('HDEncoder: PHP 8.0+ erforderlich (aktuell: ' . PHP_VERSION . ')');
        }
        if (!extension_loaded('openssl')) {
            throw new \RuntimeException('HDEncoder: PHP-Extension "openssl" nicht geladen');
        }
        if (!extension_loaded('sodium')) {
            throw new \RuntimeException('HDEncoder: PHP-Extension "sodium" nicht geladen');
        }
        if (!extension_loaded('curl')) {
            throw new \RuntimeException('HDEncoder: PHP-Extension "curl" nicht geladen');
        }
        if (!in_array('aes-256-gcm', openssl_get_cipher_methods(), true)) {
            throw new \RuntimeException('HDEncoder: OpenSSL unterstützt kein AES-256-GCM');
        }

        /* Speicher-Warnung: Argon2id braucht 64MB */
        $memLimit = ini_get('memory_limit');
        if ($memLimit !== '-1') {
            $bytes = self::parseMemoryLimit($memLimit);
            if ($bytes > 0 && $bytes < 128 * 1024 * 1024) {
                error_log('HDEncoder: memory_limit ist ' . $memLimit . ' — empfohlen: mindestens 128M (Argon2id benötigt 64MB)');
            }
        }
    }

    private static function parseMemoryLimit(string $val): int
    {
        $val  = trim($val);
        $num  = (int)$val;
        $unit = strtolower(substr($val, -1));
        return match ($unit) {
            'g' => $num * 1024 * 1024 * 1024,
            'm' => $num * 1024 * 1024,
            'k' => $num * 1024,
            default => $num,
        };
    }

    /**
     * Cache leeren — kann manuell aufgerufen werden.
     */
    public static function clearCache(): void
    {
        $dir = self::getCacheDir();
        if (is_dir($dir)) {
            $files = glob($dir . '/*.php');
            if ($files) {
                foreach ($files as $f) @unlink($f);
            }
            $lics = glob($dir . '/lic_*');
            if ($lics) {
                foreach ($lics as $f) @unlink($f);
            }
        }
    }
}
