<?php

declare(strict_types=1);

namespace PpPhp\Core\Projects;

/** Rules shared by project export, inspection, and import. */
final class ProjectPackageFormat
{
    public const ID = 'ppphp-project';
    public const VERSION = 1;
    public const EXTENSION = 'ppphp';
    public const MAX_ARCHIVE_BYTES = 100 * 1024 * 1024;
    public const MAX_EXPANDED_BYTES = 250 * 1024 * 1024;
    public const MAX_ENTRIES = 5000;
    public const MAX_COMPRESSION_RATIO = 200;

    private const EXECUTABLE_EXTENSIONS = [
        'php', 'php3', 'php4', 'php5', 'php7', 'php8', 'phtml', 'phar',
        'cgi', 'pl', 'py', 'rb', 'sh', 'bash', 'zsh', 'exe', 'dll', 'so',
        'js', 'mjs', 'cjs', 'html', 'htm', 'svg',
    ];

    private const SECRET_KEYS = [
        'pass', 'password', 'passwd', 'secret', 'token', 'api_token', 'api-key',
        'api_key', 'apikey', 'private_key', 'private-key', 'client_secret',
        'client-secret', 'access_token', 'refresh_token', 'smtp_password',
    ];

    public static function normalizeEntryPath(string $path): string
    {
        if (str_contains($path, "\0")) {
            throw new \RuntimeException('The package contains a path with a null byte.');
        }
        $path = str_replace('\\', '/', trim($path));
        if (preg_match('/[\x00-\x1F\x7F:]/', $path)) {
            throw new \RuntimeException('The package contains a path with unsafe characters.');
        }
        $path = preg_replace('~/+~', '/', $path) ?? $path;
        if ($path === '' || str_starts_with($path, '/') || preg_match('/^[A-Za-z]:\//', $path)) {
            throw new \RuntimeException('The package contains an absolute or empty path.');
        }
        $parts = explode('/', rtrim($path, '/'));
        if (in_array('..', $parts, true) || in_array('.', $parts, true)) {
            throw new \RuntimeException('The package contains a path traversal segment.');
        }
        return implode('/', $parts);
    }

    public static function isAllowedEntry(string $path): bool
    {
        $path = self::normalizeEntryPath($path);
        if ($path === 'manifest.json' || $path === 'project/connection.json') return true;
        if (preg_match('~^project/config/[A-Za-z0-9][A-Za-z0-9._-]*\.json$~', $path)) return true;
        if (str_starts_with($path, 'project/custom/') && strlen($path) > strlen('project/custom/')) return true;
        return $path === 'project/database/app.sqlite';
    }

    public static function isExecutableEntry(string $path): bool
    {
        $name = strtolower(basename($path));
        if (in_array($name, ['.htaccess', '.user.ini', 'web.config'], true)) return true;
        return in_array(strtolower(pathinfo($name, PATHINFO_EXTENSION)), self::EXECUTABLE_EXTENSIONS, true);
    }

    public static function sanitizeJson(mixed $value, int &$removed = 0, ?string $key = null): mixed
    {
        if ($key !== null && self::isSecretKey($key)) {
            if ($value !== '' && $value !== null && $value !== []) $removed++;
            return is_array($value) ? [] : '';
        }
        if (!is_array($value)) return $value;
        $clean = [];
        foreach ($value as $childKey => $childValue) {
            $clean[$childKey] = self::sanitizeJson($childValue, $removed, (string)$childKey);
        }
        return $clean;
    }

    public static function safeSlug(string $slug): string
    {
        $slug = strtolower(trim($slug));
        if (!preg_match('/^[a-z0-9]+(?:-[a-z0-9]+)*$/', $slug)) {
            throw new \InvalidArgumentException('The project slug may contain lowercase letters, numbers, and single hyphens only.');
        }
        return $slug;
    }

    public static function formatBytes(int $bytes): string
    {
        if ($bytes < 1024) return $bytes . ' B';
        if ($bytes < 1024 * 1024) return round($bytes / 1024, 1) . ' KB';
        return round($bytes / 1024 / 1024, 1) . ' MB';
    }

    private static function isSecretKey(string $key): bool
    {
        $key = strtolower(trim($key));
        return in_array($key, self::SECRET_KEYS, true)
            || (bool)preg_match('/(?:^|[_-])(password|passwd|secret|private[_-]?key|access[_-]?token|refresh[_-]?token)$/', $key);
    }
}
