<?php
declare(strict_types=1);

/**
 * Infraestrutura central de imagens do painel.
 * - valida o conteúdo real da imagem (não confia em extensão/MIME do navegador)
 * - limita tamanho e dimensões para evitar arquivos maliciosos ou estouro de memória
 * - grava de forma atômica e cria proteção contra execução de scripts nos uploads
 * - gera URLs absolutas consistentes atrás de proxy HTTPS
 */

function media_panel_root(): string
{
    return dirname(__DIR__);
}

function media_clean_relative_path(string $path): string
{
    $path = trim(str_replace('\\', '/', $path));
    $path = preg_replace('#/+#', '/', $path) ?: '';
    $path = ltrim($path, '/');
    if ($path === '' || str_contains($path, "\0") || str_contains($path, '..')) return '';
    return preg_match('#^[A-Za-z0-9_./-]+$#', $path) ? $path : '';
}

function media_absolute_path(string $relativePath): string
{
    $clean = media_clean_relative_path($relativePath);
    return $clean === '' ? '' : media_panel_root() . '/' . $clean;
}

function media_forwarded_first(string $value): string
{
    $part = trim(explode(',', $value)[0] ?? '');
    return preg_replace('/[\r\n]/', '', $part) ?: '';
}

function media_public_base_url(): string
{
    $forwardedProto = strtolower(media_forwarded_first((string)($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '')));
    $https = (!empty($_SERVER['HTTPS']) && strtolower((string)$_SERVER['HTTPS']) !== 'off') || $forwardedProto === 'https';
    $scheme = $https ? 'https' : 'http';

    $host = media_forwarded_first((string)($_SERVER['HTTP_X_FORWARDED_HOST'] ?? $_SERVER['HTTP_HOST'] ?? ''));
    if ($host === '' || !preg_match('/^[A-Za-z0-9.-]+(?::\d{1,5})?$/', $host)) {
        $host = 'localhost';
    }

    $script = str_replace('\\', '/', (string)($_SERVER['SCRIPT_NAME'] ?? ''));
    $prefix = media_forwarded_first((string)($_SERVER['HTTP_X_FORWARDED_PREFIX'] ?? ''));
    $basePath = preg_replace('#/(api|controllers|telas)(/.*)?$#', '', $script) ?: '';
    if ($basePath === $script) {
        $basePath = preg_replace('#/[^/]+\.php$#', '', $script) ?: '';
    }
    $basePath = rtrim('/' . trim($prefix . '/' . ltrim($basePath, '/'), '/'), '/');
    return $scheme . '://' . $host . ($basePath === '/' ? '' : $basePath);
}

function media_monotonic_version(int $previous = 0): int
{
    $now = (int)floor(microtime(true) * 1000);
    return max($now, $previous + 1);
}

function media_allowed_mimes(bool $allowGif = false): array
{
    $allowed = [
        'image/jpeg' => 'jpg',
        'image/png' => 'png',
        'image/webp' => 'webp',
    ];
    if ($allowGif) $allowed['image/gif'] = 'gif';
    return $allowed;
}

function media_detect_image(string $filePath, bool $allowGif = false): array
{
    if ($filePath === '' || !is_file($filePath) || !is_readable($filePath)) {
        throw new RuntimeException('Arquivo de imagem inválido.');
    }

    $size = (int)@filesize($filePath);
    if ($size <= 0) throw new RuntimeException('A imagem está vazia.');

    $info = @getimagesize($filePath);
    if (!is_array($info)) throw new RuntimeException('O arquivo enviado não é uma imagem válida.');

    $width = (int)($info[0] ?? 0);
    $height = (int)($info[1] ?? 0);
    $mimeFromImage = strtolower((string)($info['mime'] ?? ''));
    $mimeFromFinfo = '';
    if (class_exists('finfo')) {
        try {
            $finfo = new finfo(FILEINFO_MIME_TYPE);
            $mimeFromFinfo = strtolower((string)$finfo->file($filePath));
        } catch (Throwable $e) {}
    }

    $allowed = media_allowed_mimes($allowGif);
    $mime = isset($allowed[$mimeFromFinfo]) ? $mimeFromFinfo : $mimeFromImage;
    if (!isset($allowed[$mime])) {
        throw new RuntimeException($allowGif
            ? 'Formato inválido. Use JPG, PNG, WEBP ou GIF.'
            : 'Formato inválido. Use JPG, PNG ou WEBP.');
    }
    if ($mimeFromFinfo !== '' && $mimeFromImage !== '' && $mimeFromFinfo !== $mimeFromImage) {
        throw new RuntimeException('O conteúdo da imagem não corresponde ao formato informado.');
    }

    if ($width < 16 || $height < 16) throw new RuntimeException('A imagem é pequena demais.');
    if ($width > 12000 || $height > 12000 || ($width * $height) > 40000000) {
        throw new RuntimeException('A resolução da imagem é muito alta. Limite: 40 megapixels.');
    }

    return [
        'mime' => $mime,
        'extension' => $allowed[$mime],
        'width' => $width,
        'height' => $height,
        'size' => $size,
        'sha256' => hash_file('sha256', $filePath) ?: '',
    ];
}

function media_upload_error_message(int $error): string
{
    return match ($error) {
        UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE => 'A imagem excede o limite permitido pelo servidor.',
        UPLOAD_ERR_PARTIAL => 'O upload da imagem foi interrompido. Tente novamente.',
        UPLOAD_ERR_NO_TMP_DIR => 'O servidor está sem pasta temporária para upload.',
        UPLOAD_ERR_CANT_WRITE => 'O servidor não conseguiu gravar a imagem.',
        UPLOAD_ERR_EXTENSION => 'O upload foi bloqueado por uma extensão do servidor.',
        default => 'Falha no upload da imagem.',
    };
}

function media_ensure_upload_directory(string $relativeDir): string
{
    $relativeDir = trim(media_clean_relative_path($relativeDir), '/');
    if ($relativeDir === '' || !str_starts_with($relativeDir, 'api/uploads/')) {
        throw new RuntimeException('Pasta de upload inválida.');
    }

    $absolute = media_panel_root() . '/' . $relativeDir;
    if (!is_dir($absolute) && !@mkdir($absolute, 0775, true) && !is_dir($absolute)) {
        throw new RuntimeException('Não foi possível criar a pasta de upload.');
    }
    if (!is_writable($absolute)) {
        @chmod($absolute, 0775);
    }
    if (!is_writable($absolute)) {
        throw new RuntimeException('A pasta de upload não possui permissão de escrita.');
    }

    $uploadsRoot = media_panel_root() . '/api/uploads';
    if (!is_dir($uploadsRoot)) @mkdir($uploadsRoot, 0775, true);
    $htaccess = $uploadsRoot . '/.htaccess';
    if (!is_file($htaccess)) {
        $rules = "Options -Indexes\nRemoveHandler .php .phtml .php3 .php4 .php5 .php7 .php8 .phar .cgi .pl .py .sh\n<FilesMatch \"\\.(php|phtml|phar|cgi|pl|py|sh|shtml)$\">\n  Require all denied\n</FilesMatch>\n";
        @file_put_contents($htaccess, $rules, LOCK_EX);
    }
    $index = $uploadsRoot . '/index.php';
    if (!is_file($index)) @file_put_contents($index, "<?php http_response_code(404);\n", LOCK_EX);

    return $absolute;
}

function media_atomic_copy(string $source, string $destination): void
{
    $tmp = $destination . '.part-' . bin2hex(random_bytes(4));
    $in = @fopen($source, 'rb');
    $out = @fopen($tmp, 'xb');
    if (!$in || !$out) {
        if (is_resource($in)) fclose($in);
        if (is_resource($out)) fclose($out);
        @unlink($tmp);
        throw new RuntimeException('Não foi possível preparar a gravação da imagem.');
    }
    try {
        if (stream_copy_to_stream($in, $out) === false) throw new RuntimeException('Não foi possível gravar a imagem.');
        fflush($out);
    } finally {
        fclose($in);
        fclose($out);
    }
    @chmod($tmp, 0664);
    if (!@rename($tmp, $destination)) {
        @unlink($tmp);
        throw new RuntimeException('Não foi possível finalizar a gravação da imagem.');
    }
    @chmod($destination, 0664);
}

function media_store_uploaded_image(
    array $file,
    string $relativeDir,
    string $prefix,
    int $maxBytes = 5242880,
    bool $allowGif = false
): array {
    $error = (int)($file['error'] ?? UPLOAD_ERR_NO_FILE);
    if ($error === UPLOAD_ERR_NO_FILE) return [];
    if ($error !== UPLOAD_ERR_OK) throw new RuntimeException(media_upload_error_message($error));

    $tmp = (string)($file['tmp_name'] ?? '');
    $reportedSize = (int)($file['size'] ?? 0);
    if ($tmp === '' || !is_file($tmp)) throw new RuntimeException('Arquivo temporário de upload não encontrado.');
    if (PHP_SAPI !== 'cli' && !is_uploaded_file($tmp)) throw new RuntimeException('Origem do upload inválida.');

    $realSize = (int)@filesize($tmp);
    $size = max($reportedSize, $realSize);
    if ($size <= 0 || $size > $maxBytes) {
        throw new RuntimeException('A imagem deve ter no máximo ' . number_format($maxBytes / 1048576, 0, ',', '.') . ' MB.');
    }

    $meta = media_detect_image($tmp, $allowGif);
    if ((int)$meta['size'] > $maxBytes) throw new RuntimeException('A imagem excede o limite permitido.');

    $absoluteDir = media_ensure_upload_directory($relativeDir);
    $safePrefix = preg_replace('/[^A-Za-z0-9_-]/', '', $prefix) ?: 'image';
    $version = media_monotonic_version();
    $name = $safePrefix . '_' . $version . '_' . substr((string)$meta['sha256'], 0, 12) . '.' . $meta['extension'];
    $destination = $absoluteDir . '/' . $name;
    media_atomic_copy($tmp, $destination);

    $relativePath = trim(media_clean_relative_path($relativeDir), '/') . '/' . $name;
    return $meta + [
        'path' => $relativePath,
        'updated_at' => $version,
    ];
}

function media_store_bytes_image(
    string $bytes,
    string $relativeDir,
    string $prefix,
    int $maxBytes = 5242880,
    bool $allowGif = false
): array {
    $length = strlen($bytes);
    if ($length <= 0 || $length > $maxBytes) throw new RuntimeException('A imagem remota excede o limite permitido.');
    $temp = tempnam(sys_get_temp_dir(), 'img_');
    if ($temp === false) throw new RuntimeException('Não foi possível preparar a imagem remota.');
    try {
        if (@file_put_contents($temp, $bytes, LOCK_EX) === false) throw new RuntimeException('Não foi possível validar a imagem remota.');
        $meta = media_detect_image($temp, $allowGif);
        $absoluteDir = media_ensure_upload_directory($relativeDir);
        $safePrefix = preg_replace('/[^A-Za-z0-9_-]/', '', $prefix) ?: 'image';
        $version = media_monotonic_version();
        $name = $safePrefix . '_' . $version . '_' . substr((string)$meta['sha256'], 0, 12) . '.' . $meta['extension'];
        $destination = $absoluteDir . '/' . $name;
        media_atomic_copy($temp, $destination);
        return $meta + [
            'path' => trim(media_clean_relative_path($relativeDir), '/') . '/' . $name,
            'updated_at' => $version,
        ];
    } finally {
        @unlink($temp);
    }
}

function media_path_is_managed(string $relativePath): bool
{
    $clean = media_clean_relative_path($relativePath);
    return $clean !== '' && (
        str_starts_with($clean, 'api/uploads/') ||
        str_starts_with($clean, 'api/img/revendas/')
    );
}

function media_delete_relative_file(string $relativePath): void
{
    $clean = media_clean_relative_path($relativePath);
    if ($clean === '' || !media_path_is_managed($clean)) return;
    $full = media_absolute_path($clean);
    if ($full !== '' && is_file($full)) @unlink($full);
}

function media_asset_url(string $relativePath, int $version = 0, bool $requireFile = true): string
{
    $path = trim(str_replace('\\', '/', $relativePath));
    if ($path === '') return '';
    if (preg_match('#^https?://#i', $path)) return $path;
    $clean = media_clean_relative_path($path);
    if ($clean === '') return '';
    if ($requireFile && !is_file(media_absolute_path($clean))) return '';
    $url = rtrim(media_public_base_url(), '/') . '/' . $clean;
    return $version > 0 ? $url . '?v=' . $version : $url;
}

function media_mime_for_path(string $file): string
{
    try {
        return (string)(media_detect_image($file, true)['mime'] ?? 'application/octet-stream');
    } catch (Throwable $e) {
        return 'application/octet-stream';
    }
}

function media_output_image_file(string $file, int $updatedAt = 0, string $fallbackFile = ''): never
{
    if (!is_file($file) && $fallbackFile !== '' && is_file($fallbackFile)) $file = $fallbackFile;
    if (!is_file($file) || !is_readable($file)) {
        http_response_code(404);
        header('Cache-Control: no-store');
        exit;
    }

    $mtime = (int)(@filemtime($file) ?: time());
    $size = (int)(@filesize($file) ?: 0);
    $version = $updatedAt > 0 ? $updatedAt : $mtime;
    $etag = '"' . sha1($file . '|' . $mtime . '|' . $size . '|' . $version) . '"';
    $ifNoneMatch = trim((string)($_SERVER['HTTP_IF_NONE_MATCH'] ?? ''));
    if ($ifNoneMatch !== '' && $ifNoneMatch === $etag) {
        http_response_code(304);
        header('ETag: ' . $etag);
        exit;
    }

    header('Content-Type: ' . media_mime_for_path($file));
    header('Content-Length: ' . $size);
    header('X-Content-Type-Options: nosniff');
    header('Cross-Origin-Resource-Policy: cross-origin');
    header('ETag: ' . $etag);
    header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $mtime) . ' GMT');
    header('Cache-Control: public, max-age=86400, stale-while-revalidate=604800');
    readfile($file);
    exit;
}

function media_is_public_ip(string $ip): bool
{
    return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false;
}

function media_validate_remote_host(string $host): void
{
    $host = strtolower(trim($host));
    if ($host === '' || $host === 'localhost' || str_ends_with($host, '.local')) {
        throw new RuntimeException('Host remoto não permitido.');
    }
    $ips = [];
    if (filter_var($host, FILTER_VALIDATE_IP)) {
        $ips[] = $host;
    } else {
        $records = @dns_get_record($host, DNS_A | DNS_AAAA) ?: [];
        foreach ($records as $record) {
            if (!empty($record['ip'])) $ips[] = (string)$record['ip'];
            if (!empty($record['ipv6'])) $ips[] = (string)$record['ipv6'];
        }
    }
    if (!$ips) throw new RuntimeException('Não foi possível resolver o endereço da imagem.');
    foreach (array_unique($ips) as $ip) {
        if (!media_is_public_ip($ip)) throw new RuntimeException('Endereço remoto privado ou reservado não permitido.');
    }
}

function media_fetch_remote_image(string $url, int $maxBytes = 5242880): string
{
    $url = trim($url);
    if ($url === '' || strlen($url) > 2048) throw new RuntimeException('URL da imagem inválida.');
    $parts = parse_url($url);
    $scheme = strtolower((string)($parts['scheme'] ?? ''));
    $host = (string)($parts['host'] ?? '');
    if (!in_array($scheme, ['http', 'https'], true) || $host === '' || isset($parts['user']) || isset($parts['pass'])) {
        throw new RuntimeException('Use uma URL HTTP ou HTTPS válida.');
    }
    media_validate_remote_host($host);

    if (!function_exists('curl_init')) {
        throw new RuntimeException('O servidor precisa da extensão cURL para importar logos por URL.');
    }

    $bytes = '';
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_MAXREDIRS => 3,
        CURLOPT_CONNECTTIMEOUT => 8,
        CURLOPT_TIMEOUT => 20,
        CURLOPT_USERAGENT => 'TheFullPanelImage/2.0',
        CURLOPT_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS,
        CURLOPT_REDIR_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS,
        CURLOPT_SSL_VERIFYPEER => true,
        CURLOPT_SSL_VERIFYHOST => 2,
        CURLOPT_HEADER => false,
        CURLOPT_WRITEFUNCTION => static function ($ch, string $chunk) use (&$bytes, $maxBytes): int {
            if (strlen($bytes) + strlen($chunk) > $maxBytes) return 0;
            $bytes .= $chunk;
            return strlen($chunk);
        },
    ]);
    $ok = curl_exec($ch);
    $status = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    $error = curl_error($ch);
    curl_close($ch);
    if ($ok === false || $status < 200 || $status >= 300 || $bytes === '') {
        throw new RuntimeException($error !== '' ? 'Falha ao baixar a imagem remota.' : 'A URL da imagem retornou erro HTTP.');
    }
    return $bytes;
}
