<?php
class BackupManager
{
    private static function candidateDirectories(): array
    {
        $runtimeUser = function_exists('posix_geteuid') ? (string)posix_geteuid() : substr(hash('sha256', PHP_VERSION . '|' . __FILE__), 0, 8);
        $tempBase = rtrim((string)sys_get_temp_dir(), '/\\') . '/cfc-autoescola-' . substr(hash('sha256', ROOT_PATH . '|' . $runtimeUser), 0, 16);
        return array_values(array_unique([
            // Preferimos pastas não públicas. A pasta backups na raiz fica
            // apenas como último recurso para hospedagens antigas.
            STORAGE_PATH . '/backups',
            UPLOAD_PATH . '/.system_backups',
            $tempBase . '/backups',
            ROOT_PATH . '/backups',
        ]));
    }

    private static function prepareDirectory(string $dir): bool
    {
        if (!is_dir($dir) && !@mkdir($dir, 0775, true) && !is_dir($dir)) {
            return false;
        }
        @chmod($dir, 0775);
        $probe = rtrim($dir, '/\\') . '/.write-test-' . bin2hex(random_bytes(4));
        if (@file_put_contents($probe, 'ok', LOCK_EX) === false) {
            @chmod($dir, 0777);
            if (@file_put_contents($probe, 'ok', LOCK_EX) === false) {
                return false;
            }
        }
        @unlink($probe);

        $normalized = str_replace('\\', '/', $dir);
        $root = str_replace('\\', '/', ROOT_PATH);
        if (str_starts_with($normalized, $root . '/')) {
            $deny = "<IfModule mod_authz_core.c>
Require all denied
</IfModule>
<IfModule !mod_authz_core.c>
Deny from all
</IfModule>
";
            @file_put_contents(rtrim($dir, '/\\') . '/.htaccess', $deny);
            if (!is_file(rtrim($dir, '/\\') . '/index.html')) {
                @file_put_contents(rtrim($dir, '/\\') . '/index.html', '');
            }
        }
        return true;
    }

    public static function backupDir(): string
    {
        foreach (self::candidateDirectories() as $dir) {
            if (self::prepareDirectory($dir)) {
                return $dir;
            }
        }
        throw new RuntimeException('Nenhuma pasta gravável foi encontrada para gerar o backup. Verifique storage, uploads ou a pasta temporária do PHP.');
    }

    private static function availableDirectories(): array
    {
        $dirs = [];
        try {
            $dirs[] = self::backupDir();
        } catch (Throwable $e) {
            // A listagem apenas ficará vazia quando nenhuma pasta estiver disponível.
        }
        foreach (self::candidateDirectories() as $dir) {
            if (is_dir($dir)) {
                $dirs[] = $dir;
            }
        }
        return array_values(array_unique($dirs));
    }

    public static function archiveMode(): string
    {
        if (class_exists('ZipArchive')) return 'ZIP';
        if (class_exists('PharData')) return 'TAR.GZ';
        return Database::driver() === 'sqlite' ? 'SQLite' : 'Indisponível';
    }
    public static function archiveAvailable(): bool { return class_exists('ZipArchive')||class_exists('PharData')||Database::driver()==='sqlite'; }

    public static function sqliteDatabasePath(): ?string
    {
        if (Database::driver() !== 'sqlite') {
            return null;
        }
        try {
            Database::pdo()->exec('PRAGMA wal_checkpoint(FULL)');
        } catch (Throwable $e) {
            // O banco pode não estar usando WAL; a leitura direta continua válida.
        }
        $source = config_value('db.sqlite_path', STORAGE_PATH . '/database.sqlite');
        $real = realpath($source);
        return $real && is_file($real) && self::isSqliteFile($real) ? $real : null;
    }
    public static function list(): array
    {
        $items = [];
        $seen = [];
        foreach (self::availableDirectories() as $dir) {
            foreach (['*.zip','*.tar.gz','*.tgz','*.sqlite','*.db'] as $pattern) {
                foreach (glob(rtrim($dir, '/\\') . '/' . $pattern) ?: [] as $file) {
                    if (!is_file($file)) continue;
                    $name = basename($file);
                    $key = $name . ':' . ((int)@filesize($file));
                    if (isset($seen[$key])) continue;
                    $seen[$key] = true;
                    $items[] = [
                        'name' => $name,
                        'size' => filesize($file) ?: 0,
                        'created_at' => date('Y-m-d H:i:s', filemtime($file) ?: time()),
                    ];
                }
            }
        }
        usort($items, static fn($a, $b) => strcmp($b['created_at'], $a['created_at']));
        return $items;
    }

    public static function pathFor(string $name): ?string
    {
        $safe = basename($name);
        if ($safe === '' || !preg_match('/\.(zip|tar\.gz|tgz|sqlite|db)$/i', $safe)) return null;
        foreach (self::availableDirectories() as $dir) {
            $path = rtrim($dir, '/\\') . '/' . $safe;
            $realDir = realpath($dir);
            $realPath = realpath($path);
            if ($realDir && $realPath && str_starts_with($realPath, $realDir . DIRECTORY_SEPARATOR) && is_file($realPath)) {
                return $realPath;
            }
        }
        return null;
    }
    public static function exists(string $name): bool { $p=self::pathFor($name);return $p&&is_file($p); }
    public static function delete(string $name): array { $p=self::pathFor($name);if(!$p)return ['ok'=>false,'message'=>'Backup não encontrado.'];return @unlink($p)?['ok'=>true,'message'=>'Backup apagado.']:['ok'=>false,'message'=>'Não foi possível apagar o backup.']; }

    public static function create(string $prefix = 'backup-cfc'): array
    {
        @set_time_limit(0);
        @ignore_user_abort(true);
        $temp = null;
        $target = null;

        try {
            $dir = self::backupDir();
            if (!is_writable($dir)) {
                return ['ok' => false, 'message' => 'A pasta escolhida para o backup não possui permissão de escrita: ' . $dir];
            }

            $stamp = date('Ymd-His') . '-' . bin2hex(random_bytes(2));
            $temp = $dir . '/.tmp-' . $stamp . '-' . bin2hex(random_bytes(4));
            if (!mkdir($temp, 0775, true) && !is_dir($temp)) {
                return ['ok' => false, 'message' => 'Não foi possível criar a pasta temporária do backup.'];
            }

            $manifest = [
                'type' => 'cfc-autoescola-backup',
                'version' => 13,
                'created_at' => date('Y-m-d H:i:s'),
                'php_version' => PHP_VERSION,
                'items' => [],
            ];

            if (Database::driver() === 'sqlite') {
                $source = config_value('db.sqlite_path', STORAGE_PATH . '/database.sqlite');
                if (!is_file($source)) {
                    throw new RuntimeException('Banco SQLite não encontrado em storage/database.sqlite.');
                }
                try {
                    Database::pdo()->exec('PRAGMA wal_checkpoint(FULL)');
                } catch (Throwable $e) {
                    // Continua com a cópia normal quando a hospedagem não usa WAL.
                }
                $dbTemp = $temp . '/database.sqlite';
                if (!copy($source, $dbTemp) || !self::isSqliteFile($dbTemp)) {
                    throw new RuntimeException('Não foi possível criar uma cópia SQLite válida.');
                }
            }

            $errors = [];
            $filename = '';

            if (class_exists('ZipArchive')) {
                try {
                    $filename = $prefix . '-' . $stamp . '.zip';
                    $target = $dir . '/' . $filename;
                    @unlink($target);
                    $zip = new ZipArchive();
                    $opened = $zip->open($target, ZipArchive::CREATE | ZipArchive::OVERWRITE);
                    if ($opened !== true) {
                        throw new RuntimeException('código ' . $opened);
                    }
                    if (is_file($temp . '/database.sqlite')) {
                        $zip->addFile($temp . '/database.sqlite', 'storage/database.sqlite');
                        $manifest['items'][] = 'storage/database.sqlite';
                    }
                    self::addDirectoryToZip($zip, UPLOAD_PATH, 'uploads', $manifest);
                    self::addDirectoryToZip($zip, STORAGE_PATH . '/ebooks', 'storage/ebooks', $manifest);
                    self::addDirectoryToZip($zip, STORAGE_PATH . '/professional_materials', 'storage/professional_materials', $manifest);
                    self::addDirectoryToZip($zip, STORAGE_PATH . '/training_contents', 'storage/training_contents', $manifest);
                    self::addDirectoryToZip($zip, STORAGE_PATH . '/training_proofs', 'storage/training_proofs', $manifest);
                    self::addDirectoryToZip($zip, STORAGE_PATH . '/covers', 'storage/covers', $manifest);
                    $zip->addFromString('backup_manifest.json', json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
                    if (!$zip->close() || !is_file($target) || (filesize($target) ?: 0) <= 0) {
                        throw new RuntimeException('o ZIP não pôde ser finalizado');
                    }
                } catch (Throwable $e) {
                    $errors[] = 'ZIP: ' . $e->getMessage();
                    if ($target) {
                        @unlink($target);
                    }
                    $target = null;
                    $filename = '';
                }
            }

            if ($target === null && class_exists('PharData')) {
                try {
                    $filename = $prefix . '-' . $stamp . '.tar.gz';
                    $target = $dir . '/' . $filename;
                    $tarPath = $dir . '/' . $prefix . '-' . $stamp . '.tar';
                    @unlink($tarPath);
                    @unlink($target);
                    $tar = new PharData($tarPath);
                    if (is_file($temp . '/database.sqlite')) {
                        $tar->addFile($temp . '/database.sqlite', 'storage/database.sqlite');
                        $manifest['items'][] = 'storage/database.sqlite';
                    }
                    self::addDirectoryToPhar($tar, UPLOAD_PATH, 'uploads', $manifest);
                    self::addDirectoryToPhar($tar, STORAGE_PATH . '/ebooks', 'storage/ebooks', $manifest);
                    self::addDirectoryToPhar($tar, STORAGE_PATH . '/professional_materials', 'storage/professional_materials', $manifest);
                    self::addDirectoryToPhar($tar, STORAGE_PATH . '/training_contents', 'storage/training_contents', $manifest);
                    self::addDirectoryToPhar($tar, STORAGE_PATH . '/training_proofs', 'storage/training_proofs', $manifest);
                    self::addDirectoryToPhar($tar, STORAGE_PATH . '/covers', 'storage/covers', $manifest);
                    $tar->addFromString('backup_manifest.json', json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
                    unset($tar);
                    $tar = new PharData($tarPath);
                    $tar->compress(Phar::GZ);
                    unset($tar);
                    @unlink($tarPath);
                    if (!is_file($target) && is_file($tarPath . '.gz')) {
                        rename($tarPath . '.gz', $target);
                    }
                    if (!is_file($target) || (filesize($target) ?: 0) <= 0) {
                        throw new RuntimeException('o TAR.GZ não pôde ser finalizado');
                    }
                } catch (Throwable $e) {
                    $errors[] = 'TAR.GZ: ' . $e->getMessage();
                    if (isset($tarPath)) {
                        @unlink($tarPath);
                        @unlink($tarPath . '.gz');
                    }
                    if ($target) {
                        @unlink($target);
                    }
                    $target = null;
                    $filename = '';
                }
            }

            // Último recurso: pelo menos o banco SQLite é preservado, mesmo em
            // hospedagens sem ZIP ou sem compactação TAR.GZ funcional.
            if ($target === null && Database::driver() === 'sqlite' && is_file($temp . '/database.sqlite')) {
                $filename = $prefix . '-' . $stamp . '.sqlite';
                $target = $dir . '/' . $filename;
                if (!copy($temp . '/database.sqlite', $target) || !self::isSqliteFile($target)) {
                    @unlink($target);
                    $target = null;
                    $errors[] = 'SQLite: não foi possível salvar a cópia do banco';
                }
            }

            self::removeDirectory($temp);
            $temp = null;

            if ($target === null || !is_file($target) || (filesize($target) ?: 0) <= 0) {
                return ['ok' => false, 'message' => 'Não foi possível gerar o backup. ' . implode(' | ', $errors)];
            }

            $format = str_ends_with(strtolower($filename), '.zip')
                ? 'ZIP'
                : (str_ends_with(strtolower($filename), '.gz') ? 'TAR.GZ' : 'SQLite');
            return [
                'ok' => true,
                'message' => 'Backup criado com sucesso no formato ' . $format . '.',
                'name' => $filename,
                'path' => $target,
            ];
        } catch (Throwable $e) {
            if ($temp) {
                self::removeDirectory($temp);
            }
            if ($target) {
                @unlink($target);
            }
            return ['ok' => false, 'message' => 'Erro ao gerar backup: ' . $e->getMessage()];
        }
    }

    public static function restoreFromSaved(string $name): array
    {
        $p=self::pathFor($name);if(!$p)return ['ok'=>false,'message'=>'Backup salvo não encontrado.'];return self::restoreFromUpload(['tmp_name'=>$p,'error'=>UPLOAD_ERR_OK,'name'=>basename($p),'size'=>filesize($p)?:1]);
    }
    public static function restoreFromUpload(array $file): array
    {
        @set_time_limit(0);
        $error = (int)($file['error'] ?? UPLOAD_ERR_NO_FILE);
        if ($error !== UPLOAD_ERR_OK) {
            return ['ok' => false, 'message' => self::uploadErrorMessage($error)];
        }

        $tmp = (string)($file['tmp_name'] ?? '');
        $name = (string)($file['name'] ?? '');
        if ($tmp === '' || !is_file($tmp) || (filesize($tmp) ?: 0) <= 0) {
            return ['ok' => false, 'message' => 'O arquivo temporário do backup não foi encontrado ou está vazio.'];
        }

        $lower = strtolower($name);
        if (str_ends_with($lower, '.sqlite') || str_ends_with($lower, '.db')) {
            return self::restoreSqliteFile($tmp);
        }

        $temp = STORAGE_PATH . '/restore_' . bin2hex(random_bytes(8));
        if (!mkdir($temp, 0775, true) && !is_dir($temp)) {
            return ['ok' => false, 'message' => 'Não foi possível criar a pasta temporária da restauração.'];
        }

        try {
            // Extraímos primeiro e só depois criamos o backup de segurança.
            // Isso evita uma falha do PharData em algumas hospedagens, onde abrir
            // dois arquivos TAR.GZ no mesmo processo podia gerar arquivos vazios.
            if (str_ends_with($lower, '.zip')) {
                if (!class_exists('ZipArchive')) {
                    throw new RuntimeException('A extensão ZIP não está habilitada.');
                }
                $zip = new ZipArchive();
                if ($zip->open($tmp) !== true) {
                    throw new RuntimeException('Não foi possível abrir o ZIP.');
                }
                if (!self::zipEntriesAreSafe($zip)) {
                    $zip->close();
                    throw new RuntimeException('O arquivo contém caminhos inseguros.');
                }
                if (!$zip->extractTo($temp)) {
                    $zip->close();
                    throw new RuntimeException('Não foi possível extrair o ZIP.');
                }
                $zip->close();
            } elseif (str_ends_with($lower, '.tar.gz') || str_ends_with($lower, '.tgz')) {
                if (!class_exists('PharData')) {
                    throw new RuntimeException('PharData não está disponível para abrir TAR.GZ.');
                }
                $phar = new PharData($tmp);
                foreach (new RecursiveIteratorIterator($phar) as $item) {
                    $entry = str_replace('\\', '/', (string)$item->getPathName());
                    if (str_contains($entry, '../')) {
                        throw new RuntimeException('O arquivo contém caminhos inseguros.');
                    }
                }
                if (!$phar->extractTo($temp, null, true)) {
                    throw new RuntimeException('Não foi possível extrair o TAR.GZ.');
                }
                unset($phar);
            } else {
                throw new RuntimeException('Formato inválido. Use .zip, .tar.gz, .sqlite ou .db.');
            }

            $db = self::findFileByBasename($temp, 'database.sqlite');
            $mediaSources = [
                'uploads' => self::findDirectoryBySuffix($temp, 'uploads'),
                'storage/ebooks' => self::findDirectoryBySuffix($temp, 'storage/ebooks'),
                'storage/professional_materials' => self::findDirectoryBySuffix($temp, 'storage/professional_materials'),
                'storage/training_contents' => self::findDirectoryBySuffix($temp, 'storage/training_contents'),
                'storage/training_proofs' => self::findDirectoryBySuffix($temp, 'storage/training_proofs'),
                'storage/covers' => self::findDirectoryBySuffix($temp, 'storage/covers'),
            ];
            if (!$db && !array_filter($mediaSources)) {
                throw new RuntimeException('Nenhum item restaurável foi encontrado neste arquivo.');
            }
            if ($db && Database::driver() === 'sqlite' && !self::isSqliteFile($db)) {
                throw new RuntimeException('O banco encontrado no backup não é um SQLite válido.');
            }

            $before = self::create('antes-restaurar');
            if (!($before['ok'] ?? false)) {
                throw new RuntimeException('A restauração foi interrompida porque o backup de segurança falhou: ' . ($before['message'] ?? 'erro desconhecido'));
            }

            $restored = [];
            if ($db && Database::driver() === 'sqlite') {
                Database::resetConnection();
                $target = config_value('db.sqlite_path', STORAGE_PATH . '/database.sqlite');
                if (!is_dir(dirname($target)) && !mkdir(dirname($target), 0775, true) && !is_dir(dirname($target))) {
                    throw new RuntimeException('Não foi possível preparar a pasta do banco.');
                }
                if (!copy($db, $target)) {
                    throw new RuntimeException('Não foi possível substituir o banco.');
                }
                Database::resetConnection();
                Database::migrate();
                $restored[] = 'banco de dados';
            }

            $targets = [
                'uploads' => UPLOAD_PATH,
                'storage/ebooks' => STORAGE_PATH . '/ebooks',
                'storage/professional_materials' => STORAGE_PATH . '/professional_materials',
                'storage/training_contents' => STORAGE_PATH . '/training_contents',
                'storage/training_proofs' => STORAGE_PATH . '/training_proofs',
                'storage/covers' => STORAGE_PATH . '/covers',
            ];
            foreach ($mediaSources as $inside => $source) {
                if ($source && is_dir($source)) {
                    self::copyDirectory($source, $targets[$inside]);
                    $restored[] = $inside;
                }
            }

            self::removeDirectory($temp);
            return ['ok' => true, 'message' => 'Backup restaurado: ' . implode(', ', $restored) . '.'];
        } catch (Throwable $e) {
            self::removeDirectory($temp);
            return ['ok' => false, 'message' => 'Erro ao restaurar: ' . $e->getMessage()];
        }
    }

    private static function restoreSqliteFile(string $tmp): array
    {
        if(Database::driver()!=='sqlite')return ['ok'=>false,'message'=>'Restauração direta de SQLite só funciona no modo SQLite.'];if(!self::isSqliteFile($tmp))return ['ok'=>false,'message'=>'O arquivo não é um banco SQLite válido.'];$before=self::create('antes-restaurar');if(!($before['ok']??false))return ['ok'=>false,'message'=>'Backup de segurança falhou: '.($before['message']??'')];Database::resetConnection();$target=config_value('db.sqlite_path',STORAGE_PATH.'/database.sqlite');if(!copy($tmp,$target))return ['ok'=>false,'message'=>'Não foi possível copiar o banco.'];Database::resetConnection();Database::migrate();return ['ok'=>true,'message'=>'Banco SQLite restaurado com sucesso.'];
    }
    private static function isSqliteFile(string $path): bool { $h=@fopen($path,'rb');if(!$h)return false;$head=fread($h,16);fclose($h);return $head==="SQLite format 3\000"; }
    private static function addDirectoryToZip(ZipArchive $zip,string $src,string $inside,array &$manifest): void { if(!is_dir($src))return;$it=new RecursiveIteratorIterator(new RecursiveDirectoryIterator($src,FilesystemIterator::SKIP_DOTS),RecursiveIteratorIterator::SELF_FIRST);foreach($it as $item){$p=$item->getPathname();if(self::isInsideBackupDir($p)||self::isTransientUploadPath($p))continue;$rel=ltrim(str_replace('\\','/',substr($p,strlen(rtrim($src,'/\\')))),'/');if($rel==='')continue;$z=trim($inside.'/'.$rel,'/');if($item->isDir())$zip->addEmptyDir($z);elseif($item->isFile()&&$zip->addFile($p,$z))$manifest['items'][]=$z;} }
    private static function addDirectoryToPhar(PharData $tar,string $src,string $inside,array &$manifest): void { if(!is_dir($src))return;$it=new RecursiveIteratorIterator(new RecursiveDirectoryIterator($src,FilesystemIterator::SKIP_DOTS));foreach($it as $item){if(!$item->isFile()||self::isInsideBackupDir($item->getPathname())||self::isTransientUploadPath($item->getPathname()))continue;$rel=ltrim(str_replace('\\','/',substr($item->getPathname(),strlen(rtrim($src,'/\\')))),'/');$z=trim($inside.'/'.$rel,'/');$tar->addFile($item->getPathname(),$z);$manifest['items'][]=$z;} }
    private static function isTransientUploadPath(string $path): bool
    {
        $normalized = '/' . ltrim(str_replace('\\', '/', $path), '/');
        return str_contains($normalized, '/uploads/.chunks/') || str_contains($normalized, '/uploads/.system_backups/');
    }

    private static function isInsideBackupDir(string $path): bool
    {
        $real = realpath($path);
        if (!$real) return false;
        foreach (self::availableDirectories() as $dir) {
            $backup = realpath($dir);
            if ($backup && ($real === $backup || str_starts_with($real, $backup . DIRECTORY_SEPARATOR))) {
                return true;
            }
        }
        return false;
    }
    private static function uploadErrorMessage(int $e): string { return match($e){UPLOAD_ERR_INI_SIZE,UPLOAD_ERR_FORM_SIZE=>'O backup ultrapassou o limite de upload da hospedagem.',UPLOAD_ERR_PARTIAL=>'O backup foi enviado parcialmente.',UPLOAD_ERR_NO_FILE=>'Selecione um arquivo de backup.',UPLOAD_ERR_NO_TMP_DIR=>'A hospedagem está sem pasta temporária.',UPLOAD_ERR_CANT_WRITE=>'A hospedagem não conseguiu gravar o upload.',UPLOAD_ERR_EXTENSION=>'Uma extensão do PHP bloqueou o upload.',default=>'Falha no envio do backup.'}; }
    private static function zipEntriesAreSafe(ZipArchive $z): bool { for($i=0;$i<$z->numFiles;$i++){$n=str_replace('\\','/',(string)$z->getNameIndex($i));if(str_starts_with($n,'/')||str_contains($n,'../')||preg_match('/^[A-Za-z]:\//',$n))return false;}return true; }
    private static function findFileByBasename(string $root,string $name): ?string { if(!is_dir($root))return null;$it=new RecursiveIteratorIterator(new RecursiveDirectoryIterator($root,FilesystemIterator::SKIP_DOTS));foreach($it as $i)if($i->isFile()&&$i->getFilename()===$name)return $i->getPathname();return null; }
    private static function findDirectoryBySuffix(string $root,string $suffix): ?string { if(!is_dir($root))return null;$suffix=trim(str_replace('\\','/',$suffix),'/');if(is_dir($root.'/'.$suffix))return $root.'/'.$suffix;$it=new RecursiveIteratorIterator(new RecursiveDirectoryIterator($root,FilesystemIterator::SKIP_DOTS),RecursiveIteratorIterator::SELF_FIRST);foreach($it as $i)if($i->isDir()&&str_ends_with(trim(str_replace('\\','/',$i->getPathname()),'/'),$suffix))return $i->getPathname();return null; }
    private static function copyDirectory(string $src,string $dst): void { if(!is_dir($dst))mkdir($dst,0775,true);$it=new RecursiveIteratorIterator(new RecursiveDirectoryIterator($src,FilesystemIterator::SKIP_DOTS),RecursiveIteratorIterator::SELF_FIRST);foreach($it as $i){$rel=ltrim(str_replace('\\','/',substr($i->getPathname(),strlen($src))),'/');$to=rtrim($dst,'/\\').'/'.$rel;if($i->isDir()){if(!is_dir($to))mkdir($to,0775,true);}else{if(!is_dir(dirname($to)))mkdir(dirname($to),0775,true);if(!copy($i->getPathname(),$to))throw new RuntimeException('Falha ao copiar '.$rel);}} }
    private static function removeDirectory(string $dir): void { if(!is_dir($dir))return;$it=new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir,FilesystemIterator::SKIP_DOTS),RecursiveIteratorIterator::CHILD_FIRST);foreach($it as $i)$i->isDir()?@rmdir($i->getPathname()):@unlink($i->getPathname());@rmdir($dir); }
    public static function humanSize(int|float $b): string { $u=['B','KB','MB','GB'];$i=0;while($b>=1024&&$i<count($u)-1){$b/=1024;$i++;}return number_format((float)$b,$i?2:0,',','.').' '.$u[$i]; }
}
