<?php
if (session_status() === PHP_SESSION_NONE) {
    ini_set('session.use_strict_mode', '1');
    ini_set('session.use_only_cookies', '1');
    session_set_cookie_params([
        'lifetime' => 0,
        'path' => '/',
        'secure' => (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'),
        'httponly' => true,
        'samesite' => 'Lax'
    ]);
    session_start();
}

function csrfToken() {
    if (empty($_SESSION['csrf_token'])) {
        $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
    }
    return $_SESSION['csrf_token'];
}

function validateCsrfToken($token = null) {
    if ($token === null) {
        $token = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? ($_POST['csrf_token'] ?? '');
    }
    return is_string($token) && $token !== '' && isset($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token);
}

if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['logout'])) {
    if (!validateCsrfToken()) die('Nieprawidłowy token bezpieczeństwa.');
    $_SESSION = [];
    if (ini_get('session.use_cookies')) {
        $params = session_get_cookie_params();
        setcookie(session_name(), '', time() - 42000, $params['path'], $params['domain'] ?? '', $params['secure'], $params['httponly']);
    }
    session_destroy();
    header("Location: index.php");
    exit;
}

function cfg() { 
    static $c=null; 
    if($c===null) { $c=require __DIR__.'/config.php'; } 
    return $c; 
}

function readJsonFile($file, $default = []) {
    $dir = dirname($file);
    if (!is_dir($dir)) @mkdir($dir, 0777, true);
    $lock = @fopen($file . '.lock', 'c+');
    if ($lock === false) return $default;
    if (!flock($lock, LOCK_SH)) { fclose($lock); return $default; }
    $content = is_file($file) ? @file_get_contents($file) : '';
    flock($lock, LOCK_UN);
    fclose($lock);
    if ($content === false || trim($content) === '') return $default;
    $data = json_decode($content, true);
    return is_array($data) ? $data : $default;
}

function writeJsonUnlocked($file, $data) {
    $json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
    if ($json === false) return false;
    $tmp = $file . '.tmp.' . getmypid() . '.' . bin2hex(random_bytes(4));
    if (@file_put_contents($tmp, $json, LOCK_EX) === false) return false;
    $ok = @rename($tmp, $file);
    if (!$ok) @unlink($tmp);
    return $ok;
}

function writeJsonFile($file, $data) {
    $dir = dirname($file);
    if (!is_dir($dir)) @mkdir($dir, 0777, true);
    $lock = @fopen($file . '.lock', 'c+');
    if ($lock === false) return false;
    if (!flock($lock, LOCK_EX)) { fclose($lock); return false; }
    $ok = writeJsonUnlocked($file, $data);
    flock($lock, LOCK_UN);
    fclose($lock);
    return $ok;
}

function updateJsonFile($file, $callback, $default = []) {
    $dir = dirname($file);
    if (!is_dir($dir)) @mkdir($dir, 0777, true);
    $lock = @fopen($file . '.lock', 'c+');
    if ($lock === false) return null;
    if (!flock($lock, LOCK_EX)) { fclose($lock); return null; }
    $content = is_file($file) ? @file_get_contents($file) : '';
    $data = ($content !== false && trim($content) !== '') ? json_decode($content, true) : $default;
    if (!is_array($data)) $data = $default;
    $args = [&$data];
    $result = call_user_func_array($callback, $args);
    writeJsonUnlocked($file, $data);
    flock($lock, LOCK_UN);
    fclose($lock);
    return $result;
}

function migrateLegacyProgress($legacyId, $progressId) {
    if (!is_string($legacyId) || !preg_match('/^[0-9]{4}$/', $legacyId)) return;
    $progFile = __DIR__ . '/data/progress.json';
    updateJsonFile($progFile, function (&$prog) use ($legacyId, $progressId) {
        if (!isset($prog[$legacyId]) || !is_array($prog[$legacyId])) return;
        if (!isset($prog[$progressId]) || !is_array($prog[$progressId])) $prog[$progressId] = [];
        foreach ($prog[$legacyId] as $key => $value) {
            if (!isset($prog[$progressId][$key])) $prog[$progressId][$key] = $value;
        }
        unset($prog[$legacyId]);
    }, []);
}

function getProgressId() {
    if (!empty($_SESSION['progress_id']) && preg_match('/^[a-f0-9]{32}$/', $_SESSION['progress_id'])) {
        return $_SESSION['progress_id'];
    }
    $cookieId = $_COOKIE['progress_id'] ?? '';
    if (is_string($cookieId) && preg_match('/^[a-f0-9]{32}$/', $cookieId)) {
        $_SESSION['progress_id'] = $cookieId;
        return $cookieId;
    }
    $progressId = bin2hex(random_bytes(16));
    $_SESSION['progress_id'] = $progressId;
    setcookie('progress_id', $progressId, [
        'expires' => time() + 31536000,
        'path' => '/',
        'secure' => (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'),
        'httponly' => true,
        'samesite' => 'Lax'
    ]);
    return $progressId;
}

function checkAuth() {
    $cfg = cfg();
    $ip = $_SERVER['REMOTE_ADDR'];
    $authFile = __DIR__ . '/data/auth.json';
    if (!is_dir(__DIR__ . '/data')) mkdir(__DIR__ . '/data', 0777, true);
    
    $authDb = readJsonFile($authFile, []);
    
    if (isset($authDb[$ip]) && isset($authDb[$ip]['locked_until']) && $authDb[$ip]['locked_until'] < time()) {
        updateJsonFile($authFile, function (&$data) use ($ip) { unset($data[$ip]); }, []);
        unset($authDb[$ip]);
    }

    if (isset($_SESSION['logged_in']) && $_SESSION['logged_in'] === true) {
        csrfToken();
        $progressId = getProgressId();
        if (isset($_COOKIE['user_pin'])) {
            migrateLegacyProgress((string)$_COOKIE['user_pin'], $progressId);
            setcookie('user_pin', '', time() - 3600, '/');
        }
        session_write_close(); 
        return;
    }
    
    $isLocked = isset($authDb[$ip]['locked_until']) && $authDb[$ip]['locked_until'] > time();
    $lockTimeLeft = $isLocked ? ceil(($authDb[$ip]['locked_until'] - time()) / 60) : 0;
    
    if (isset($_POST['pin'])) {
        if ($isLocked) {
            $error = "Zbyt wiele prób! Spróbuj ponownie za $lockTimeLeft min.";
        } else {
            $pin = (string)$_POST['pin'];
            $isUser = in_array($pin, $cfg['pins'] ?? [], true);
            $isAdmin = in_array($pin, $cfg['admin_pins'] ?? [], true);
            
            if ($isUser || $isAdmin) {
                session_regenerate_id(true);
                $_SESSION['logged_in'] = true;
                $_SESSION['role'] = $isAdmin ? 'admin' : 'user';
                csrfToken();
                $progressId = getProgressId();
                
                // Twarde ciasteczko z PINEM na potrzeby zapisywania czasu wideo (ważne rok)
                // PIN nie jest już zapisywany w ciasteczku. Używany jest losowy identyfikator progress_id.
                if (isset($_COOKIE['user_pin'])) {
                    migrateLegacyProgress((string)$_COOKIE['user_pin'], $progressId);
                    setcookie('user_pin', '', time() - 3600, '/');
                }
                
                updateJsonFile($authFile, function (&$data) use ($ip) { unset($data[$ip]); }, []);
                header("Location: " . $_SERVER['REQUEST_URI']);
                exit;
            } else {
                $authDb = readJsonFile($authFile, []);
                updateJsonFile($authFile, function (&$data) use ($ip) {
                    if (!isset($data[$ip]) || !is_array($data[$ip])) $data[$ip] = [];
                    $data[$ip]['attempts'] = ($data[$ip]['attempts'] ?? 0) + 1;
                    if ($data[$ip]['attempts'] >= 5) $data[$ip]['locked_until'] = time() + 900;
                }, []);
                $authDb = readJsonFile($authFile, []);
                $error = "Nieprawidłowy PIN!";
                $isLocked = isset($authDb[$ip]['locked_until']) && $authDb[$ip]['locked_until'] > time();
            }
        }
    }
    
    echo "<!doctype html><html lang='pl'><head><meta charset='utf-8'><title>Logowanie</title><link rel='stylesheet' href='assets/style.css'></head>";
    echo "<body class='pin-body'><div class='pin-box'><h2>🔒 Autoryzacja</h2>";
    if (isset($error)) echo "<p style='color:#ff4d4d; margin-bottom:15px;'>$error</p>";
    if ($isLocked) {
        echo "<p style='color:#ffc107;'>Dostęp zablokowany. Odczekaj czas kary.</p>";
    } else {
        echo "<form method='post'><input type='password' name='pin' pattern='[0-9]{4}' placeholder='****' maxlength='4' autofocus><br><button type='submit' class='btn btn-blue'>Odblokuj</button></form>";
    }
    echo "</div></body></html>";
    exit;
}

function requireAdmin() {
    if (($_SESSION['role'] ?? '') !== 'admin') die('Odmowa dostępu.');
}

function sanitizePath($path) {
    $parts = preg_split('#[\\\\/]+#', (string)$path);
    $safe = [];
    foreach ($parts as $p) {
        if ($p === '' || $p === '.' || $p === '..') continue;
        $safe[] = $p;
    }
    return implode('/', $safe);
}

function resolveAllowedFile($folderKey, $subPath, $fileName) {
    $cfg = cfg();
    if (!isset($cfg['allowedFolders'][$folderKey])) return false;
    $subPath = sanitizePath($subPath);
    $fileName = basename((string)$fileName);
    if ($fileName === '' || $fileName === '.' || $fileName === '..') return false;
    $baseCandidate = '/mnt/mega' . $cfg['allowedFolders'][$folderKey];
    $baseReal = realpath($baseCandidate);
    if ($baseReal === false || !is_dir($baseReal)) return false;
    $targetCandidate = rtrim($baseReal . '/' . $subPath, '/') . '/' . $fileName;
    $targetReal = realpath($targetCandidate);
    if ($targetReal === false || !is_file($targetReal)) return false;
    $prefix = rtrim($baseReal, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
    if (strpos($targetReal, $prefix) !== 0) return false;
    return [
        'folder' => $folderKey,
        'path' => $subPath,
        'file' => $fileName,
        'base' => $baseReal,
        'full' => $targetReal,
        'mega' => rtrim($cfg['allowedFolders'][$folderKey] . '/' . $subPath, '/') . '/' . $fileName
    ];
}


function lowerText($value) {
    return function_exists('mb_strtolower') ? mb_strtolower((string)$value, 'UTF-8') : strtolower((string)$value);
}

function formatSize($bytes) {
    if ($bytes === 0) return '0 B';
    $units = ['B', 'KB', 'MB', 'GB', 'TB'];
    $pow = floor(log($bytes) / log(1024));
    $pow = min($pow, count($units) - 1);
    return round($bytes / pow(1024, $pow), 2) . ' ' . $units[$pow];
}

function getDbPath() {
    $dir = __DIR__ . '/data';
    if (!is_dir($dir)) mkdir($dir, 0777, true); 
    return $dir . '/db.json';
}

function runPeriodicCleanup() {
    $now = time();
    $lastCleanFile = __DIR__ . '/data/last_clean.txt';
    $lockFile = __DIR__ . '/data/last_clean.lock';
    $lockHandle = @fopen($lockFile, 'c+');
    if ($lockHandle === false || !flock($lockHandle, LOCK_EX | LOCK_NB)) {
        if (is_resource($lockHandle)) fclose($lockHandle);
        return;
    }
    $lastClean = file_exists($lastCleanFile) ? (int)file_get_contents($lastCleanFile) : 0;
    if ($now - $lastClean <= 1800) {
        flock($lockHandle, LOCK_UN);
        fclose($lockHandle);
        return;
    }
    file_put_contents($lastCleanFile, (string)$now, LOCK_EX);

    // ŚMIECIARKA 1: Kasowanie wygasłych linków 24H
    updateJsonFile(getDbPath(), function (&$db) use ($now) {
        if (!isset($db['downloads']) || !is_array($db['downloads'])) $db['downloads'] = [];
        if (!isset($db['shares']) || !is_array($db['shares'])) $db['shares'] = [];
        foreach($db['shares'] as $hash => $data) {
            if(!isset($data['expires']) || $now > $data['expires']) unset($db['shares'][$hash]);
        }
    }, ['downloads'=>[], 'shares'=>[]]);

    // Niedokończone atomowe zapisy JSON po awaryjnie przerwanym procesie.
    foreach (glob(__DIR__ . '/data/*.json.tmp.*') ?: [] as $f) {
        if ($now - filemtime($f) > 3600) @unlink($f);
    }

    // ŚMIECIARKA 2: Kasowanie starych śmieci z yt_temp (starsze niż 30 min)
    $tempDir = __DIR__ . '/data/yt_temp';
    if (is_dir($tempDir)) {
        foreach(glob($tempDir . '/*') ?: [] as $f) {
            $activeMarker = is_dir($f) ? $f . '/.active' : '';
            if ($activeMarker !== '' && is_file($activeMarker) && $now - filemtime($activeMarker) < 86400) continue;
            if($now - filemtime($f) > 1800) { is_dir($f) ? @exec("rm -rf " . escapeshellarg($f)) : @unlink($f); }
        }
    }

    // ŚMIECIARKA 2B: Kasowanie porzuconych plików konwersji i starej historii zadań.
    $convertTempDir = __DIR__ . '/data/convert_temp';
    if (is_dir($convertTempDir)) {
        foreach (glob($convertTempDir . '/*') ?: [] as $f) {
            $activeMarker = is_dir($f) ? $f . '/.active' : '';
            if ($activeMarker !== '' && is_file($activeMarker) && $now - filemtime($activeMarker) < 86400) continue;
            if ($now - filemtime($f) > 86400) { is_dir($f) ? @exec("rm -rf " . escapeshellarg($f)) : @unlink($f); }
        }
    }

    $convertJobsDir = __DIR__ . '/data/convert_jobs';
    if (is_dir($convertJobsDir)) {
        foreach (glob($convertJobsDir . '/job_*.json') ?: [] as $jobFile) {
            $job = readJsonFile($jobFile, []);
            $status = $job['status'] ?? '';
            $updatedAt = (int)($job['updated_at'] ?? @filemtime($jobFile));
            if (!in_array($status, ['done', 'error', 'skipped'], true) || $now - $updatedAt <= 604800) continue;
            $base = substr($jobFile, 0, -5);
            @unlink($jobFile);
            @unlink($jobFile . '.lock');
            @unlink($base . '.log');
        }
    }

    // ŚMIECIARKA 3: Kasowanie niepotrzebnych miniaturek sprite-sheet (starsze niż 24h)
    // Gotowe miniaturki .jpg są zachowywane do ponownego użycia. Usuwane są tylko niedokończone pliki i logi.
    $thumbDir = __DIR__ . '/data/thumbnails';
    if (is_dir($thumbDir)) {
        $thumbGarbage = array_merge(
            glob($thumbDir . '/*.tmp') ?: [],
            glob($thumbDir . '/*.part.jpg') ?: [],
            glob($thumbDir . '/*.log') ?: [],
            glob($thumbDir . '/*.failed') ?: [],
            glob($thumbDir . '/*.json.tmp.*') ?: []
        );
        foreach($thumbGarbage as $f) {
            if($now - filemtime($f) > 86400) @unlink($f);
        }

        // Limit cache miniaturek: maksymalnie 1 GB. Usuwane są najdawniej używane pliki JPG.
        $thumbFiles = glob($thumbDir . '/*.jpg') ?: [];
        $totalSize = 0;
        $fileData = [];
        foreach ($thumbFiles as $f) {
            $size = @filesize($f);
            if ($size === false) continue;
            $totalSize += $size;
            $fileData[] = ['path' => $f, 'size' => $size, 'time' => @filemtime($f) ?: 0];
        }
        $cacheLimit = 1024 * 1024 * 1024;
        if ($totalSize > $cacheLimit) {
            usort($fileData, function($a, $b) { return $a['time'] <=> $b['time']; });
            foreach ($fileData as $entry) {
                if ($totalSize <= $cacheLimit) break;
                if (@unlink($entry['path'])) $totalSize -= $entry['size'];
            }
        }
    }

    flock($lockHandle, LOCK_UN);
    fclose($lockHandle);
}

function getDb() {
    runPeriodicCleanup();
    $db = readJsonFile(getDbPath(), ['downloads'=>[], 'shares'=>[]]);
    if (!isset($db['downloads']) || !is_array($db['downloads'])) $db['downloads'] = [];
    if (!isset($db['shares']) || !is_array($db['shares'])) $db['shares'] = [];
    return $db;
}

function saveDb($data) {
    return writeJsonFile(getDbPath(), $data);
}

function createShare($folderKey, $subPath, $fileName, $ttl = 86400) {
    $resolved = resolveAllowedFile($folderKey, $subPath, $fileName);
    if ($resolved === false) return false;
    $hash = bin2hex(random_bytes(16));
    updateJsonFile(getDbPath(), function (&$db) use ($hash, $resolved, $ttl) {
        if (!isset($db['downloads']) || !is_array($db['downloads'])) $db['downloads'] = [];
        if (!isset($db['shares']) || !is_array($db['shares'])) $db['shares'] = [];
        $db['shares'][$hash] = [
            'folder' => $resolved['folder'],
            'path' => $resolved['path'],
            'file' => $resolved['file'],
            'expires' => time() + $ttl
        ];
    }, ['downloads'=>[], 'shares'=>[]]);
    return $hash;
}

function recordDownload($megaPath) {
    updateJsonFile(getDbPath(), function (&$db) use ($megaPath) {
        if (!isset($db['downloads']) || !is_array($db['downloads'])) $db['downloads'] = [];
        if (!isset($db['shares']) || !is_array($db['shares'])) $db['shares'] = [];
        $db['downloads'][$megaPath] = ($db['downloads'][$megaPath] ?? 0) + 1;
    }, ['downloads'=>[], 'shares'=>[]]);
}

function progressKey($folderKey, $subPath, $fileName) {
    return $folderKey . '|' . sanitizePath($subPath) . '|' . basename((string)$fileName);
}

function getProgressTime($folderKey, $subPath, $fileName) {
    $progressId = getProgressId();
    $progFile = __DIR__ . '/data/progress.json';
    $prog = readJsonFile($progFile, []);
    $key = progressKey($folderKey, $subPath, $fileName);
    if (isset($prog[$progressId][$key])) return (float)($prog[$progressId][$key]['time'] ?? 0);
    // Zgodność ze starym formatem, gdzie kluczem była wyłącznie nazwa pliku.
    $oldKey = basename((string)$fileName);
    return isset($prog[$progressId][$oldKey]) ? (float)($prog[$progressId][$oldKey]['time'] ?? 0) : 0;
}

function saveProgressTime($folderKey, $subPath, $fileName, $time) {
    $progressId = getProgressId();
    $key = progressKey($folderKey, $subPath, $fileName);
    $progFile = __DIR__ . '/data/progress.json';
    updateJsonFile($progFile, function (&$prog) use ($progressId, $key, $time) {
        if (!isset($prog[$progressId]) || !is_array($prog[$progressId])) $prog[$progressId] = [];
        $prog[$progressId][$key] = ['time' => max(0, (float)$time), 'ts' => time()];
    }, []);
}

function listMegaFolder($basePath, $subPath = '', $sort = 'name_asc') {
    $fullPath = rtrim('/mnt/mega' . $basePath . '/' . $subPath, '/');
    $items = ['folders' => [], 'files' => []];
    $db = getDb();
    
    if (is_dir($fullPath)) {
        $files = scandir($fullPath);
        foreach ($files as $file) {
            if ($file === '.' || $file === '..') continue;
            if (strpos($file, '.') === 0) continue; 
            
            $physicalPath = $fullPath . '/' . $file;
            $megaPath = rtrim($basePath . '/' . $subPath, '/') . '/' . $file;
            $stat = @stat($physicalPath);
            $isDir = $stat ? (($stat['mode'] & 0170000) === 0040000) : false;
            
            $itemData = [
                'name' => $file,
                'ext' => strtolower(pathinfo($file, PATHINFO_EXTENSION)),
                'size' => $isDir ? '-' : ($stat ? formatSize($stat['size']) : '-'),
                'bytes' => $isDir ? 0 : ($stat ? $stat['size'] : 0),
                'date' => $stat ? date('d.m.Y H:i', $stat['mtime']) : '-',
                'timestamp' => $stat ? $stat['mtime'] : 0,
                'downloads' => $db['downloads'][$megaPath] ?? 0
            ];
            
            if ($isDir) $items['folders'][] = $itemData;
            else $items['files'][] = $itemData;
        }
    }

    $sortFunc = function($key, $dir, $isNumeric = false) {
        return function($a, $b) use ($key, $dir, $isNumeric) {
            if ($isNumeric) {
                $valA = $a[$key];
                $valB = $b[$key];
            } else {
                $valA = lowerText($a[$key]);
                $valB = lowerText($b[$key]);
            }
            if ($valA == $valB) return 0;
            $res = ($valA < $valB) ? -1 : 1;
            return $dir === 'asc' ? $res : -$res;
        };
    };

    switch($sort) {
        case 'name_desc': usort($items['folders'], $sortFunc('name', 'desc')); usort($items['files'], $sortFunc('name', 'desc')); break;
        case 'type_asc': usort($items['folders'], $sortFunc('name', 'asc')); usort($items['files'], $sortFunc('ext', 'asc')); break;
        case 'type_desc': usort($items['folders'], $sortFunc('name', 'asc')); usort($items['files'], $sortFunc('ext', 'desc')); break;
        case 'date_asc': usort($items['folders'], $sortFunc('timestamp', 'asc', true)); usort($items['files'], $sortFunc('timestamp', 'asc', true)); break;
        case 'date_desc': usort($items['folders'], $sortFunc('timestamp', 'desc', true)); usort($items['files'], $sortFunc('timestamp', 'desc', true)); break;
        case 'size_asc': usort($items['folders'], $sortFunc('name', 'asc')); usort($items['files'], $sortFunc('bytes', 'asc', true)); break;
        case 'size_desc': usort($items['folders'], $sortFunc('name', 'asc')); usort($items['files'], $sortFunc('bytes', 'desc', true)); break;
        case 'name_asc': default: usort($items['folders'], $sortFunc('name', 'asc')); usort($items['files'], $sortFunc('name', 'asc')); break;
    }
    return $items;
}

function invalidateSearchIndex() {
    @unlink(__DIR__ . '/data/search_index.json');
}

function searchAllMegaFolders($query) {
    $cfg = cfg();
    $results = [];
    $query = lowerText($query);
    $indexFile = __DIR__ . '/data/search_index.json';
    $index = [];
    if (is_file($indexFile) && time() - filemtime($indexFile) < 600) {
        $index = readJsonFile($indexFile, []);
    }
    if (empty($index)) {
        $visited = [];
        foreach ($cfg['allowedFolders'] as $folderKey => $basePath) {
            scanDirRecursive('/mnt/mega' . $basePath, $folderKey, '', '', $index, $visited, 0, 30, 50000);
        }
        writeJsonFile($indexFile, $index);
    }
    foreach ($index as $item) {
        if (strpos(lowerText($item['name']), $query) !== false) {
            $results[] = $item;
            if (count($results) >= 100) break;
        }
    }
    return $results;
}

function scanDirRecursive($physicalBasePath, $folderKey, $currentSubPath, $query, &$results, &$visited = [], $depth = 0, $maxDepth = 30, $maxItems = 50000) {
    if ($depth > $maxDepth || count($results) >= $maxItems) return;
    $dirToScan = rtrim($physicalBasePath . '/' . $currentSubPath, '/');
    $realDir = realpath($dirToScan);
    if ($realDir === false || !is_dir($realDir) || isset($visited[$realDir])) return;
    $visited[$realDir] = true;
    $items = @scandir($realDir);
    if ($items === false) return;
    foreach ($items as $item) {
        if ($item === '.' || $item === '..') continue;
        if (strpos($item, '.') === 0) continue;
        if (count($results) >= $maxItems) break;
        $fullItemPath = $realDir . '/' . $item;
        if (is_link($fullItemPath)) continue;
        $itemPath = ltrim($currentSubPath . '/' . $item, '/');
        $isDir = is_dir($fullItemPath);
        if ($query === '' || strpos(lowerText($item), $query) !== false) {
            $results[] = ['name' => $item, 'folder' => $folderKey, 'path' => $currentSubPath, 'is_dir' => $isDir];
        }
        if ($isDir) scanDirRecursive($physicalBasePath, $folderKey, $itemPath, $query, $results, $visited, $depth + 1, $maxDepth, $maxItems);
    }
}
