<?php
namespace Core\System\License;

class LicenseMiddleware {
    private const VERIFY_TTL_NORMAL     = 43200;   // 12 hours  — verify twice/day max
    private const VERIFY_TTL_SUSPICIOUS = 7200;    // 2 hours   — after a failed verify
    private const VERIFY_JITTER_MAX     = 1800;    // ±30 min spread across sites

    private const PING_TTL              = 86400;   // 24 hours  — ping once/day max
    private const PING_JITTER_MAX       = 3600;    // ±1 hour spread across sites
    private const PING_GRACE            = 604800;  // 7 days    — site stays up if server down
    private const MAX_GRACE_VERIFY      = 604800;  // 7 days    — same grace for verify

    public static function handle(): void {
        if (self::shouldSkip()) {
            return;
        }

        if (!class_exists(LicenseClient::class) || !class_exists(LicenseStorage::class) || !class_exists(Fingerprint::class)) {
            self::clearAndBlock('tamper_missing_license_classes');
        }

        if (!function_exists('db')) {
            self::clearAndBlock('tamper_missing_db_function');
        }

        $cache = LicenseStorage::getCache();
        if (!is_array($cache) || empty($cache['status'])) {
            self::clearAndBlock('missing_license_cache');
        }

        $licenseKey = self::getLicenseKey();
        if ($licenseKey === '') {
            $licenseKey = trim((string)($cache['license_key'] ?? ''));
        }
        if ($licenseKey === '') {
            self::clearAndBlock('missing_license_key');
        }

        self::pingKillSwitch($licenseKey, $cache);

        if (($cache['status'] ?? '') !== 'active') {
            self::clearAndBlock('cache_status_not_active');
        }

        $now = time();
        $ttl = (int)($cache['ttl'] ?? self::VERIFY_TTL_NORMAL);
        $checkedAt = (int)($cache['checked_at'] ?? 0);

        // Jitter on verify: deterministic per-domain so load is spread across VERIFY_JITTER_MAX window.
        $domain = $_SERVER['HTTP_HOST'] ?? 'default';
        $verifyJitter = abs((int)(crc32($domain . '_v') % self::VERIFY_JITTER_MAX));
        $effectiveVerifyTtl = $ttl + $verifyJitter;
        $needVerify = ($checkedAt <= 0) || (($now - $checkedAt) >= $effectiveVerifyTtl);

        if ($needVerify) {
            self::verifyNow($licenseKey, $cache);
        }
    }

    private static function shouldSkip(): bool {
        $uri = (string)($_SERVER['REQUEST_URI'] ?? '');

        if ($uri === '') {
            return false;
        }

        $u = strtolower($uri);

        if (strpos($u, '/public/') !== false) return true;
        if (strpos($u, '/uploads/') !== false) return true;
        if (strpos($u, '/assets/') !== false) return true;
        if (strpos($u, '/license/recover') !== false) return true;

        $qpos = strpos($u, '?');
        if ($qpos !== false) {
            $u = substr($u, 0, $qpos);
        }

        $ext = strtolower((string)pathinfo($u, PATHINFO_EXTENSION));
        if ($ext !== '') {
            $skipExt = [
                'jpg','jpeg','png','gif','webp','svg','ico',
                'css','js','map',
                'woff','woff2','ttf','eot','otf',
                'mp4','webm','mp3','wav','ogg',
                'pdf','zip','rar'
            ];
            if (in_array($ext, $skipExt, true)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Kill-switch ping:
     * - If response missing/empty -> treat as offline (grace applies)
     * - If response says block -> block immediately
     * - If response contains invalid_signature marker -> block immediately (no grace)
     */
    private static function pingKillSwitch(string $licenseKey, array $cache): void {
        $now = time();
        $pingAt = (int)($cache['ping_at'] ?? 0);

        if ($pingAt > 0) {
            // Jitter: deterministic per-domain offset — same site always gets same spread value.
            // This distributes 100 sites evenly across a PING_JITTER_MAX window
            // so they never all hit the license server at the same second.
            $domain  = $_SERVER['HTTP_HOST'] ?? 'default';
            $jitter  = abs((int)(crc32($domain) % self::PING_JITTER_MAX));
            $effectiveTtl = self::PING_TTL + $jitter;
            if (($now - $pingAt) < $effectiveTtl) {
                return;
            }
        }

        $domain = $_SERVER['HTTP_HOST'] ?? '';
        $fp = Fingerprint::generate();

        $client = new LicenseClient();
        $res = $client->ping($licenseKey, $domain, $fp);

        // Offline / no response
        if (!is_array($res) || empty($res['action'])) {
            $lastOk = (int)($cache['ping_last_ok_at'] ?? 0);
            if ($lastOk > 0 && ($now - $lastOk) <= self::PING_GRACE) {
                $cache['ping_at'] = $now;
                LicenseStorage::storeCache($cache);
                return;
            }
            self::clearAndBlock('ping_offline_grace_exceeded');
        }

        // If signature invalid => fail-closed immediately (no grace)
        if (($res['message'] ?? '') === 'invalid_signature') {
            self::clearAndBlock('invalid_signature_ping');
        }

        $cache['ping_at'] = $now;

        if (($res['action'] ?? '') === 'block') {
            self::clearAndBlock('server_block_ping');
        }

        $cache['ping_last_ok_at'] = $now;
        LicenseStorage::storeCache($cache);
    }

    /**
     * Verify:
     * - Offline -> grace applies as before
     * - Invalid signature -> block immediately (no grace)
     */
    private static function verifyNow(string $licenseKey, array $cache): void {
        $now = time();

        $client = new LicenseClient();
        $res = $client->verify($licenseKey);

        // Offline / no response => keep grace logic
        if (!is_array($res) || empty($res['status'])) {
            $lastOk = (int)($cache['last_ok_at'] ?? 0);
            if ($lastOk > 0 && ($now - $lastOk) <= self::MAX_GRACE_VERIFY) {
                $cache['checked_at'] = $now;
                $cache['ttl'] = self::VERIFY_TTL_SUSPICIOUS;
                $cache['failures'] = (int)($cache['failures'] ?? 0) + 1;
                LicenseStorage::storeCache($cache);
                return;
            }
            self::clearAndBlock('verify_offline_grace_exceeded');
        }

        // Invalid signature => fail-closed immediately (no grace)
        if (($res['message'] ?? '') === 'invalid_signature') {
            self::clearAndBlock('invalid_signature_verify');
        }

        $cache['checked_at'] = $now;
        $cache['last_ok_at'] = $now;
        $cache['status'] = (string)($res['status'] ?? 'invalid');
        $cache['action'] = (string)($res['action'] ?? 'block');
        $cache['type'] = (string)($res['type'] ?? '');
        $cache['type_original'] = (string)($res['type_original'] ?? '');
        $cache['expires'] = $res['expires'] ?? null;
        $cache['domain_bound'] = $res['domain_bound'] ?? null;
        $cache['max_domains'] = (int)($res['max_domains'] ?? 0);
        $cache['domains_used'] = (int)($res['domains_used'] ?? 0);
        $cache['message'] = (string)($res['message'] ?? '');
        $cache['failures'] = 0;

        if (($cache['action'] ?? '') === 'block') {
            self::clearAndBlock('server_block_verify');
        }
        if (($cache['status'] ?? '') !== 'active') {
            self::clearAndBlock('status_not_active_verify');
        }

        $cache['ttl'] = self::VERIFY_TTL_NORMAL;
        LicenseStorage::storeCache($cache);
    }

    private static function getLicenseKey(): string {
        try {
            $pdo = db();
            $stmt = $pdo->prepare("SELECT setting_value FROM settings WHERE setting_key = ? LIMIT 1");
            $stmt->execute(['license_key']);
            $val = $stmt->fetchColumn();
            return trim((string)$val);
        } catch (\Throwable $e) {
            return '';
        }
    }

    private static function clearAndBlock(string $reason = 'unknown', bool $clearCache = true): void {
        // Best-effort report to license server (violations_logs)
        try { (new ViolationsReporter())->report($reason); } catch (\Throwable $e) {}

        if ($clearCache) {
            try { LicenseStorage::clear(); } catch (\Throwable $e) {}
        }

        self::block($reason);
    }

    private static function basePath(): string {
        $script = (string)($_SERVER['SCRIPT_NAME'] ?? '');
        $dir = str_replace('\\', '/', dirname($script));
        if ($dir === '/' || $dir === '.' || $dir === '') {
            return '';
        }
        return rtrim($dir, '/');
    }

    private static function block(string $reason = ''): void {
        $base = self::basePath();
        $q = $reason !== '' ? ('?reason=' . rawurlencode($reason)) : '';
        header('Location: ' . $base . '/license/recover' . $q, true, 302);
        exit;
    }
}
