<?php
class PortalMedia
{
    public static function saveImage(array $file, string $folder, int $maxBytes = 5242880): array
    {
        return self::saveUploadedFile($file, $folder, ['image/jpeg','image/png','image/webp','image/gif'], ['jpg','jpeg','png','webp','gif'], $maxBytes);
    }

    public static function saveVideo(array $file, string $folder, int $maxBytes = 1073741824): array
    {
        return self::saveUploadedFile(
            $file,
            $folder,
            ['video/mp4','video/webm','video/ogg','video/quicktime','video/x-m4v','video/3gpp','video/x-matroska','application/mp4','application/octet-stream'],
            ['mp4','webm','ogg','mov','m4v','3gp','mkv'],
            $maxBytes
        );
    }

    public static function saveGroupAttachment(array $file, string $folder = 'group_attachments', int $maxBytes = 26214400): array
    {
        return self::saveUploadedFile(
            $file,
            $folder,
            ['image/jpeg','image/png','image/webp','image/gif','application/pdf'],
            ['jpg','jpeg','png','webp','gif','pdf'],
            $maxBytes,
            true
        );
    }

    public static function saveMessageAttachment(array $file, string $folder = 'message_attachments', int $maxBytes = 26214400): array
    {
        return self::saveUploadedFile(
            $file,
            $folder,
            [
                'image/jpeg','image/png','image/webp','image/gif','application/pdf',
                'audio/webm','audio/ogg','audio/mpeg','audio/mp4','audio/x-m4a',
                'audio/wav','audio/x-wav','audio/aac','audio/3gpp','video/webm',
                'application/ogg','application/octet-stream'
            ],
            ['jpg','jpeg','png','webp','gif','pdf','webm','ogg','oga','mp3','m4a','mp4','wav','aac','3gp'],
            $maxBytes,
            true
        );
    }

    public static function storeMessageAudioChunk(array $file, array $data, string $actorType, int $actorId): array
    {
        $uploadId = preg_replace('/[^a-zA-Z0-9_-]/', '', (string)($data['upload_id'] ?? ''));
        $chunkIndex = (int)($data['chunk_index'] ?? -1);
        $totalChunks = (int)($data['total_chunks'] ?? 0);
        $originalName = basename((string)($data['original_name'] ?? 'audio.webm'));
        $expectedSize = (int)($data['file_size'] ?? 0);
        $declaredMime = strtolower(trim((string)($data['mime_type'] ?? '')));
        $extension = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
        $actorType = normalize_internal_actor_type($actorType);

        if ($uploadId === '' || strlen($uploadId) < 8 || $chunkIndex < 0 || $totalChunks < 1 || $chunkIndex >= $totalChunks || $totalChunks > 128) {
            return ['ok' => false, 'message' => 'Dados do envio do áudio são inválidos.'];
        }
        if (!in_array($extension, ['webm','ogg','oga','mp3','m4a','mp4','wav','aac','3gp'], true)) {
            return ['ok' => false, 'message' => 'Formato de áudio não permitido.'];
        }
        if ($expectedSize <= 0 || $expectedSize > 26214400) {
            return ['ok' => false, 'message' => 'O áudio ultrapassa o limite de 25 MB.'];
        }
        $error = (int)($file['error'] ?? UPLOAD_ERR_NO_FILE);
        if ($error !== UPLOAD_ERR_OK) {
            return ['ok' => false, 'message' => self::uploadErrorMessage($error)];
        }
        $chunkSize = (int)($file['size'] ?? 0);
        if ($chunkSize <= 0 || $chunkSize > 1048576) {
            return ['ok' => false, 'message' => 'Parte do áudio inválida ou maior que 1 MB.'];
        }

        $dir = self::chunkDirectory('audio-' . $uploadId, true);
        if ($dir === null) {
            return ['ok' => false, 'message' => 'Não foi possível preparar o envio do áudio.'];
        }
        $metaPath = $dir . '/meta.json';
        if (is_file($metaPath)) {
            $existing = json_decode((string)@file_get_contents($metaPath), true);
            if (!is_array($existing)
                || (string)($existing['original_name'] ?? '') !== $originalName
                || (int)($existing['total_chunks'] ?? 0) !== $totalChunks
                || (int)($existing['expected_size'] ?? 0) !== $expectedSize
                || (string)($existing['actor_type'] ?? '') !== $actorType
                || (int)($existing['actor_id'] ?? 0) !== $actorId) {
                return ['ok' => false, 'message' => 'Os dados do áudio mudaram durante o envio. Recarregue a página e tente novamente.'];
            }
        }
        $meta = [
            'upload_id' => $uploadId,
            'original_name' => $originalName,
            'extension' => $extension,
            'declared_mime' => $declaredMime,
            'total_chunks' => $totalChunks,
            'expected_size' => $expectedSize,
            'actor_type' => $actorType,
            'actor_id' => $actorId,
            'created_at' => date('Y-m-d H:i:s'),
        ];
        if (@file_put_contents($metaPath, json_encode($meta, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT), LOCK_EX) === false) {
            return ['ok' => false, 'message' => 'Não foi possível registrar o envio do áudio.'];
        }

        $tmp = (string)($file['tmp_name'] ?? '');
        $destination = $dir . '/' . str_pad((string)$chunkIndex, 6, '0', STR_PAD_LEFT) . '.part';
        $moved = is_uploaded_file($tmp) ? @move_uploaded_file($tmp, $destination) : @rename($tmp, $destination);
        if (!$moved && !@copy($tmp, $destination)) {
            return ['ok' => false, 'message' => 'Não foi possível salvar uma parte do áudio.'];
        }
        @chmod($destination, 0664);
        return ['ok' => true, 'message' => 'Parte do áudio recebida.', 'received' => $chunkIndex + 1, 'total' => $totalChunks];
    }

    public static function finalizeMessageAudioChunks(string $uploadId, string $actorType, int $actorId): array
    {
        @set_time_limit(120);
        $uploadId = preg_replace('/[^a-zA-Z0-9_-]/', '', $uploadId);
        $actorType = normalize_internal_actor_type($actorType);
        $dir = self::chunkDirectory('audio-' . $uploadId, false);
        if ($dir === null || !is_file($dir . '/meta.json')) {
            return ['ok' => false, 'message' => 'O envio do áudio expirou ou não foi encontrado.'];
        }
        $meta = json_decode((string)@file_get_contents($dir . '/meta.json'), true);
        if (!is_array($meta)
            || (string)($meta['actor_type'] ?? '') !== $actorType
            || (int)($meta['actor_id'] ?? 0) !== $actorId) {
            return ['ok' => false, 'message' => 'Este envio de áudio não pertence à sua conta.'];
        }
        $totalChunks = (int)($meta['total_chunks'] ?? 0);
        $expectedSize = (int)($meta['expected_size'] ?? 0);
        $extension = strtolower((string)($meta['extension'] ?? ''));
        $originalName = basename((string)($meta['original_name'] ?? ('audio.' . $extension)));
        if ($totalChunks < 1 || $totalChunks > 128 || $expectedSize <= 0 || $expectedSize > 26214400
            || !in_array($extension, ['webm','ogg','oga','mp3','m4a','mp4','wav','aac','3gp'], true)) {
            self::removeDirectory($dir);
            return ['ok' => false, 'message' => 'Os dados finais do áudio são inválidos.'];
        }

        $targetDir = self::prepareUploadFolder('message_attachments', true);
        if ($targetDir === null) {
            return ['ok' => false, 'message' => 'Não foi possível preparar a pasta final do áudio.'];
        }
        $filename = date('YmdHis') . '-' . bin2hex(random_bytes(8)) . '.' . $extension;
        $absolutePath = $targetDir . '/' . $filename;
        $out = @fopen($absolutePath, 'wb');
        if (!$out) {
            return ['ok' => false, 'message' => 'Não foi possível criar o arquivo final do áudio.'];
        }
        $written = 0;
        try {
            for ($i = 0; $i < $totalChunks; $i++) {
                $part = $dir . '/' . str_pad((string)$i, 6, '0', STR_PAD_LEFT) . '.part';
                if (!is_file($part)) {
                    throw new RuntimeException('Faltou uma parte do áudio. Tente enviar novamente.');
                }
                $in = @fopen($part, 'rb');
                if (!$in) {
                    throw new RuntimeException('Não foi possível ler uma parte do áudio.');
                }
                while (!feof($in)) {
                    $buffer = fread($in, 524288);
                    if ($buffer === false) {
                        fclose($in);
                        throw new RuntimeException('Falha ao montar o áudio.');
                    }
                    if ($buffer !== '') {
                        $length = strlen($buffer);
                        if (fwrite($out, $buffer) !== $length) {
                            fclose($in);
                            throw new RuntimeException('Falha ao gravar o áudio final.');
                        }
                        $written += $length;
                    }
                }
                fclose($in);
            }
            fclose($out);
            $out = null;
            if ($written !== $expectedSize) {
                throw new RuntimeException('O tamanho final do áudio não confere com o arquivo enviado.');
            }
            $mime = strtolower(self::detectMime($absolutePath));
            $allowedMimes = [
                'audio/webm','audio/ogg','audio/mpeg','audio/mp4','audio/x-m4a','audio/wav','audio/x-wav',
                'audio/aac','audio/3gpp','video/webm','application/ogg','application/octet-stream'
            ];
            if ($mime !== '' && !in_array($mime, $allowedMimes, true)) {
                throw new RuntimeException('O arquivo final não foi reconhecido como áudio.');
            }
            @chmod($absolutePath, 0664);
            self::removeDirectory($dir);
            self::cleanupPreparedMessageAudio();
            $token = bin2hex(random_bytes(24));
            $_SESSION['_prepared_message_audio'][$token] = [
                'actor_type' => $actorType,
                'actor_id' => $actorId,
                'expires_at' => time() + 1800,
                'path' => 'uploads/message_attachments/' . $filename,
                'original_name' => $originalName,
                'mime_type' => $mime !== '' ? $mime : ((string)($meta['declared_mime'] ?? '') ?: 'audio/webm'),
                'file_size' => $written,
            ];
            return ['ok' => true, 'message' => 'Áudio preparado para envio.', 'token' => $token];
        } catch (Throwable $e) {
            if (is_resource($out)) {
                fclose($out);
            }
            @unlink($absolutePath);
            return ['ok' => false, 'message' => $e->getMessage()];
        }
    }

    public static function claimMessageAudio(string $token, string $actorType, int $actorId): array
    {
        self::cleanupPreparedMessageAudio();
        $token = preg_replace('/[^a-f0-9]/', '', strtolower($token));
        $entry = $_SESSION['_prepared_message_audio'][$token] ?? null;
        if (!is_array($entry)) {
            return ['ok' => false, 'message' => 'O áudio preparado expirou. Grave ou escolha o áudio novamente.'];
        }
        if ((string)($entry['actor_type'] ?? '') !== normalize_internal_actor_type($actorType)
            || (int)($entry['actor_id'] ?? 0) !== $actorId) {
            return ['ok' => false, 'message' => 'Este áudio não pertence à sua conta.'];
        }
        unset($_SESSION['_prepared_message_audio'][$token]);
        $relative = trim((string)($entry['path'] ?? ''));
        $absolute = ROOT_PATH . '/' . ltrim($relative, '/');
        $realRoot = realpath(UPLOAD_PATH . '/message_attachments');
        $realPath = realpath($absolute);
        if (!$realRoot || !$realPath || !str_starts_with($realPath, $realRoot . DIRECTORY_SEPARATOR) || !is_file($realPath)) {
            return ['ok' => false, 'message' => 'O arquivo de áudio preparado não foi encontrado.'];
        }
        return [
            'ok' => true,
            'path' => $relative,
            'original_name' => basename((string)($entry['original_name'] ?? 'audio.webm')),
            'mime_type' => (string)($entry['mime_type'] ?? 'audio/webm'),
            'file_size' => (int)($entry['file_size'] ?? filesize($realPath)),
        ];
    }

    private static function cleanupPreparedMessageAudio(): void
    {
        $items = $_SESSION['_prepared_message_audio'] ?? [];
        if (!is_array($items)) {
            $_SESSION['_prepared_message_audio'] = [];
            return;
        }
        $now = time();
        foreach ($items as $token => $entry) {
            if (!is_array($entry) || (int)($entry['expires_at'] ?? 0) < $now) {
                self::delete((string)($entry['path'] ?? ''));
                unset($_SESSION['_prepared_message_audio'][$token]);
            }
        }
    }

    public static function storeVideoChunk(array $file, array $data): array
    {
        $uploadId = preg_replace('/[^a-zA-Z0-9_-]/', '', (string)($data['upload_id'] ?? ''));
        $chunkIndex = (int)($data['chunk_index'] ?? -1);
        $totalChunks = (int)($data['total_chunks'] ?? 0);
        $originalName = basename((string)($data['original_name'] ?? 'video.mp4'));
        $expectedSize = (int)($data['file_size'] ?? 0);
        $extension = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));

        if ($uploadId === '' || strlen($uploadId) < 8 || $chunkIndex < 0 || $totalChunks < 1 || $chunkIndex >= $totalChunks || $totalChunks > 2048) {
            return ['ok' => false, 'message' => 'Dados do envio em partes são inválidos.'];
        }
        if (!in_array($extension, ['mp4','webm','ogg','mov','m4v','3gp','mkv'], true)) {
            return ['ok' => false, 'message' => 'Formato de vídeo não permitido.'];
        }
        if ($expectedSize <= 0 || $expectedSize > 1073741824) {
            return ['ok' => false, 'message' => 'O vídeo ultrapassa o limite de 1 GB.'];
        }

        $error = (int)($file['error'] ?? UPLOAD_ERR_NO_FILE);
        if ($error !== UPLOAD_ERR_OK) {
            return ['ok' => false, 'message' => self::uploadErrorMessage($error)];
        }
        $chunkSize = (int)($file['size'] ?? 0);
        if ($chunkSize <= 0 || $chunkSize > 3145728) {
            return ['ok' => false, 'message' => 'Parte do vídeo inválida ou maior que 3 MB.'];
        }

        $dir = self::chunkDirectory($uploadId, true);
        if ($dir === null) {
            return ['ok' => false, 'message' => 'Não foi possível preparar a pasta temporária do vídeo.'];
        }
        $metaPath = $dir . '/meta.json';
        if (is_file($metaPath)) {
            $existingMeta = json_decode((string)@file_get_contents($metaPath), true);
            if (!is_array($existingMeta)
                || (string)($existingMeta['original_name'] ?? '') !== $originalName
                || (int)($existingMeta['total_chunks'] ?? 0) !== $totalChunks
                || (int)($existingMeta['expected_size'] ?? 0) !== $expectedSize) {
                return ['ok' => false, 'message' => 'Os dados do vídeo mudaram durante o envio. Recarregue a página e tente novamente.'];
            }
        }
        $meta = [
            'upload_id' => $uploadId,
            'original_name' => $originalName,
            'extension' => $extension,
            'total_chunks' => $totalChunks,
            'expected_size' => $expectedSize,
            'created_at' => date('Y-m-d H:i:s'),
        ];
        if (@file_put_contents($metaPath, json_encode($meta, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT), LOCK_EX) === false) {
            return ['ok' => false, 'message' => 'Não foi possível registrar o envio do vídeo.'];
        }

        $tmp = (string)($file['tmp_name'] ?? '');
        $destination = $dir . '/' . str_pad((string)$chunkIndex, 6, '0', STR_PAD_LEFT) . '.part';
        $moved = is_uploaded_file($tmp) ? @move_uploaded_file($tmp, $destination) : @rename($tmp, $destination);
        if (!$moved && !@copy($tmp, $destination)) {
            return ['ok' => false, 'message' => 'Não foi possível salvar uma parte do vídeo.'];
        }
        @chmod($destination, 0664);
        return ['ok' => true, 'message' => 'Parte recebida.', 'received' => $chunkIndex + 1, 'total' => $totalChunks];
    }

    public static function finalizeVideoChunks(string $uploadId, string $folder = 'video_lessons'): array
    {
        @set_time_limit(0);
        @ignore_user_abort(true);
        $uploadId = preg_replace('/[^a-zA-Z0-9_-]/', '', $uploadId);
        $dir = self::chunkDirectory($uploadId, false);
        if ($dir === null || !is_file($dir . '/meta.json')) {
            return ['ok' => false, 'message' => 'O envio em partes expirou ou não foi encontrado.'];
        }
        $meta = json_decode((string)file_get_contents($dir . '/meta.json'), true);
        if (!is_array($meta)) {
            self::removeDirectory($dir);
            return ['ok' => false, 'message' => 'Os dados temporários do vídeo estão inválidos.'];
        }
        $totalChunks = (int)($meta['total_chunks'] ?? 0);
        $expectedSize = (int)($meta['expected_size'] ?? 0);
        $extension = strtolower((string)($meta['extension'] ?? ''));
        $originalName = basename((string)($meta['original_name'] ?? ('video.' . $extension)));
        if ($totalChunks < 1 || $expectedSize <= 0 || $expectedSize > 1073741824 || !in_array($extension, ['mp4','webm','ogg','mov','m4v','3gp','mkv'], true)) {
            self::removeDirectory($dir);
            return ['ok' => false, 'message' => 'Metadados do vídeo inválidos.'];
        }

        $targetDir = self::prepareUploadFolder($folder, true);
        if ($targetDir === null) {
            return ['ok' => false, 'message' => 'Não foi possível preparar a pasta final do vídeo.'];
        }
        $filename = date('YmdHis') . '-' . bin2hex(random_bytes(8)) . '.' . $extension;
        $absolutePath = $targetDir . '/' . $filename;
        $out = @fopen($absolutePath, 'wb');
        if (!$out) {
            return ['ok' => false, 'message' => 'Não foi possível criar o arquivo final do vídeo.'];
        }
        $written = 0;
        try {
            for ($i = 0; $i < $totalChunks; $i++) {
                $part = $dir . '/' . str_pad((string)$i, 6, '0', STR_PAD_LEFT) . '.part';
                if (!is_file($part)) {
                    throw new RuntimeException('Faltou a parte ' . ($i + 1) . ' do vídeo.');
                }
                $in = fopen($part, 'rb');
                if (!$in) {
                    throw new RuntimeException('Não foi possível ler uma parte do vídeo.');
                }
                while (!feof($in)) {
                    $buffer = fread($in, 1048576);
                    if ($buffer === false) {
                        fclose($in);
                        throw new RuntimeException('Falha ao montar o vídeo.');
                    }
                    if ($buffer !== '') {
                        $length = strlen($buffer);
                        if (fwrite($out, $buffer) !== $length) {
                            fclose($in);
                            throw new RuntimeException('Falha ao gravar o vídeo final.');
                        }
                        $written += $length;
                    }
                }
                fclose($in);
            }
            fclose($out);
            $out = null;
            if ($written <= 0 || abs($written - $expectedSize) > 1024) {
                throw new RuntimeException('O tamanho final do vídeo não confere com o arquivo enviado.');
            }
            $mime = self::detectMime($absolutePath);
            $allowedMimes = ['video/mp4','video/webm','video/ogg','video/quicktime','video/x-m4v','video/3gpp','video/x-matroska','application/mp4','application/octet-stream'];
            if ($mime !== '' && !in_array($mime, $allowedMimes, true)) {
                throw new RuntimeException('O arquivo final não foi reconhecido como vídeo.');
            }
            @chmod($absolutePath, 0664);
            self::removeDirectory($dir);
            return [
                'ok' => true,
                'path' => 'uploads/' . trim($folder, '/') . '/' . $filename,
                'original_name' => $originalName,
                'mime_type' => $mime !== '' ? $mime : 'application/octet-stream',
                'file_size' => $written,
            ];
        } catch (Throwable $e) {
            if (is_resource($out)) {
                fclose($out);
            }
            @unlink($absolutePath);
            return ['ok' => false, 'message' => $e->getMessage()];
        }
    }

    public static function delete(?string $relativePath): void
    {
        $relativePath = trim((string)$relativePath);
        if ($relativePath === '' || !str_starts_with($relativePath, 'uploads/')) {
            return;
        }
        $absolute = ROOT_PATH . '/' . ltrim($relativePath, '/');
        $realRoot = realpath(UPLOAD_PATH);
        $realFile = realpath($absolute);
        if ($realRoot && $realFile && str_starts_with($realFile, $realRoot . DIRECTORY_SEPARATOR) && is_file($realFile)) {
            @unlink($realFile);
        }
    }

    private static function saveUploadedFile(array $file, string $folder, array $allowedMimes, array $allowedExtensions, int $maxBytes, bool $protectFolder = false): array
    {
        $error = (int)($file['error'] ?? UPLOAD_ERR_NO_FILE);
        if ($error !== UPLOAD_ERR_OK) {
            return ['ok' => false, 'message' => self::uploadErrorMessage($error)];
        }
        if (empty($file['tmp_name']) || (!is_uploaded_file($file['tmp_name']) && PHP_SAPI !== 'cli')) {
            return ['ok' => false, 'message' => 'O arquivo temporário do upload não foi encontrado.'];
        }
        $size = (int)($file['size'] ?? 0);
        if ($size <= 0 || $size > $maxBytes) {
            return ['ok' => false, 'message' => 'O arquivo ultrapassa o limite permitido de ' . round($maxBytes / 1048576) . ' MB.'];
        }
        $original = basename((string)($file['name'] ?? 'arquivo'));
        $extension = strtolower(pathinfo($original, PATHINFO_EXTENSION));
        $mime = self::detectMime((string)$file['tmp_name']);
        if (!in_array($extension, $allowedExtensions, true) || ($mime !== '' && !in_array($mime, $allowedMimes, true))) {
            return ['ok' => false, 'message' => 'Formato de arquivo não permitido.'];
        }
        $absoluteFolder = self::prepareUploadFolder($folder, $protectFolder || $folder === 'video_lessons');
        if ($absoluteFolder === null) {
            return ['ok' => false, 'message' => 'Não foi possível criar a pasta de armazenamento.'];
        }
        $filename = date('YmdHis') . '-' . bin2hex(random_bytes(8)) . '.' . $extension;
        $absolutePath = $absoluteFolder . '/' . $filename;
        $moved = move_uploaded_file($file['tmp_name'], $absolutePath);
        if (!$moved && PHP_SAPI === 'cli') {
            $moved = @rename($file['tmp_name'], $absolutePath) || @copy($file['tmp_name'], $absolutePath);
        }
        if (!$moved) {
            return ['ok' => false, 'message' => 'Não foi possível salvar o arquivo enviado. Verifique a permissão da pasta uploads.'];
        }
        @chmod($absolutePath, 0664);
        return [
            'ok' => true,
            'path' => 'uploads/' . trim($folder, '/') . '/' . $filename,
            'original_name' => $original,
            'mime_type' => $mime !== '' ? $mime : (string)($file['type'] ?? ''),
            'file_size' => $size,
        ];
    }

    private static function prepareUploadFolder(string $folder, bool $protect): ?string
    {
        $safeFolder = trim(preg_replace('/[^a-zA-Z0-9_\/-]/', '', $folder), '/');
        if ($safeFolder === '') {
            return null;
        }
        $absoluteFolder = UPLOAD_PATH . '/' . $safeFolder;
        if (!is_dir($absoluteFolder) && !@mkdir($absoluteFolder, 0775, true) && !is_dir($absoluteFolder)) {
            return null;
        }
        @chmod($absoluteFolder, 0775);
        $probe = $absoluteFolder . '/.write-test-' . bin2hex(random_bytes(3));
        if (@file_put_contents($probe, 'ok') === false) {
            @chmod($absoluteFolder, 0777);
            if (@file_put_contents($probe, 'ok') === false) {
                return null;
            }
        }
        @unlink($probe);
        if ($protect) {
            $denyRules = "<IfModule mod_authz_core.c>\nRequire all denied\n</IfModule>\n<IfModule !mod_authz_core.c>\nDeny from all\n</IfModule>\n";
            @file_put_contents($absoluteFolder . '/.htaccess', $denyRules);
            if (!is_file($absoluteFolder . '/index.html')) {
                @file_put_contents($absoluteFolder . '/index.html', '');
            }
        }
        return $absoluteFolder;
    }

    private static function chunkDirectory(string $uploadId, bool $create): ?string
    {
        if ($uploadId === '') {
            return null;
        }
        $sessionKey = hash('sha256', session_id() . '|' . CSRF::token());
        $chunksRoot = UPLOAD_PATH . '/.chunks';
        if ($create) {
            self::cleanupStaleChunkUploads($chunksRoot);
        }
        $base = $chunksRoot . '/' . substr($sessionKey, 0, 24);
        $dir = $base . '/' . $uploadId;
        if ($create && !is_dir($dir) && !@mkdir($dir, 0775, true) && !is_dir($dir)) {
            return null;
        }
        if ($create && is_dir($chunksRoot)) {
            $denyRules = "<IfModule mod_authz_core.c>
Require all denied
</IfModule>
<IfModule !mod_authz_core.c>
Deny from all
</IfModule>
";
            @file_put_contents($chunksRoot . '/.htaccess', $denyRules);
            if (!is_file($chunksRoot . '/index.html')) {
                @file_put_contents($chunksRoot . '/index.html', '');
            }
        }
        return is_dir($dir) ? $dir : null;
    }

    private static function cleanupStaleChunkUploads(string $chunksRoot): void
    {
        if (!is_dir($chunksRoot)) {
            return;
        }
        $cutoff = time() - 86400;
        try {
            $sessions = new DirectoryIterator($chunksRoot);
            foreach ($sessions as $sessionDir) {
                if ($sessionDir->isDot() || !$sessionDir->isDir()) {
                    continue;
                }
                $sessionPath = $sessionDir->getPathname();
                foreach (new DirectoryIterator($sessionPath) as $uploadDir) {
                    if ($uploadDir->isDot() || !$uploadDir->isDir()) {
                        continue;
                    }
                    $path = $uploadDir->getPathname();
                    $mtime = @filemtime($path) ?: 0;
                    if ($mtime < $cutoff) {
                        self::removeDirectory($path);
                    }
                }
                $remaining = @scandir($sessionPath);
                if (is_array($remaining) && count(array_diff($remaining, ['.', '..'])) === 0) {
                    @rmdir($sessionPath);
                }
            }
        } catch (Throwable $e) {
            // A limpeza é apenas preventiva e não pode interromper o upload.
        }
    }

    private static function detectMime(string $path): string
    {
        if (function_exists('finfo_open') && is_file($path)) {
            $finfo = finfo_open(FILEINFO_MIME_TYPE);
            if ($finfo) {
                $mime = (string)finfo_file($finfo, $path);
                finfo_close($finfo);
                return $mime;
            }
        }
        return '';
    }

    private static function uploadErrorMessage(int $error): string
    {
        return match ($error) {
            UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE => 'O áudio ou arquivo ultrapassou o limite direto da hospedagem. Tente novamente; os áudios são enviados automaticamente em partes.',
            UPLOAD_ERR_PARTIAL => 'O arquivo foi enviado somente pela metade. Tente novamente em uma conexão estável.',
            UPLOAD_ERR_NO_FILE => 'Selecione um arquivo para enviar.',
            UPLOAD_ERR_NO_TMP_DIR => 'A hospedagem está sem pasta temporária para upload.',
            UPLOAD_ERR_CANT_WRITE => 'A hospedagem não conseguiu gravar o arquivo no disco.',
            UPLOAD_ERR_EXTENSION => 'Uma extensão do PHP bloqueou o envio do arquivo.',
            default => 'Falha no envio do arquivo. Código: ' . $error,
        };
    }

    private static function removeDirectory(string $dir): void
    {
        if (!is_dir($dir)) {
            return;
        }
        $items = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS),
            RecursiveIteratorIterator::CHILD_FIRST
        );
        foreach ($items as $item) {
            $item->isDir() ? @rmdir($item->getPathname()) : @unlink($item->getPathname());
        }
        @rmdir($dir);
    }
}
