<?php
/**
 * FleekDash-specific rollback registry and version source.
 *
 * @package FleekDash\Core
 */

namespace FleekDash\Core;

defined('ABSPATH') || exit;

if (!function_exists('fleekdash_log_error')) {
    require_once FLEEKDASH_PLUGIN_DIR . 'includes/Core/debug-helpers.php';
}

class FleekDashRollbackRegistry
{
    public const PLUGIN_SLUG = 'fleekdash';
    public const PLUGIN_FILE = 'fleekdash/fleekdash.php';
    private const DEFAULT_LIMIT = 5;
    private const MAX_LIMIT = 20;
    private const ROOT_ALLOWED_FILES = ['fleekdash.php', 'uninstall.php', 'readme.txt'];
    private const ROOT_ALLOWED_DIRECTORIES = ['includes', 'assets', 'views', 'database', 'libs', 'integrations', 'public', 'config', 'vendor'];
    private const GLOBAL_EXCLUDE_DIRS = ['.git', '.github', '.vscode', '.cursor', '.idea', 'node_modules', 'tests', 'test', '__tests__', '.storybook', 'storybook-static', 'docs', 'documentation'];
    private const INCLUDES_EXCLUDE_DIRS = ['node_modules', 'test', 'tests', '__tests__', 'js', 'css'];
    private const ASSETS_EXCLUDE_DIRS = ['.vite', 'node_modules'];
    private const VENDOR_EXCLUDE_DIRS = ['tests', 'test', 'Test', 'Tests', 'docs', 'documentation', 'examples', 'example', '.git', '.github', 'bin'];
    private const VENDOR_EXCLUDE_FILE_PATTERNS = ['*.md', 'README*', 'LICENSE*', 'CHANGELOG*', 'phpunit.xml*', '.travis.yml', '.gitignore', '.gitattributes', '.editorconfig', 'composer.json', 'composer.lock', '.phpcs.xml*', '.php_cs*', '.php-cs-fixer*'];
    private const ASSETS_EXCLUDE_FILE_PATTERNS = ['*.map', '*.map.br', '*.map.gz', 'vite-dev-server.json', 'vite-dev-server.*.json'];

    public static function isFleekDashPluginSlug(string $slug): bool
    {
        return \sanitize_key($slug) === self::PLUGIN_SLUG;
    }

    public static function isFleekDashPluginFile(string $pluginFile): bool
    {
        return \strtolower(\trim($pluginFile)) === self::PLUGIN_FILE;
    }

    public static function shouldIncludeBackupRelativePath(string $relativePath): bool
    {
        $normalizedPath = \trim(\str_replace('\\', '/', $relativePath), '/');
        $shouldInclude = false;
        if ($normalizedPath !== '') {
            $pathParts = \array_values(\array_filter(\explode('/', $normalizedPath), static fn(string $part): bool => $part !== ''));
            if ($pathParts !== []) {
                $fileName = \basename($normalizedPath);
                if (\count($pathParts) === 1) {
                    $shouldInclude = \in_array($fileName, self::ROOT_ALLOWED_FILES, true);
                } else {
                    $topLevelDirectory = $pathParts[0];
                    if (self::isAllowedTopLevelDirectory($topLevelDirectory) && !self::containsExcludedDirectory($pathParts, self::GLOBAL_EXCLUDE_DIRS)) {
                        $shouldInclude = self::shouldIncludeAllowedTopLevelEntry($topLevelDirectory, $pathParts, $fileName);
                    }
                }
            }
        }

        return $shouldInclude;
    }

    public static function getBackupPath(string $backupFile): ?string
    {
        $fileName = \basename($backupFile);
        $backupPath = WP_CONTENT_DIR . '/fleekdash-backups/plugins/' . self::PLUGIN_SLUG . '/' . $fileName;

        return \is_file($backupPath) ? $backupPath : null;
    }

    public static function getDownloadUrlForVersion(string $version): string
    {
        $baseUrl = 'https://storage.ltdhunt.com';
        if (\defined('FLEEKDASH_R2_BASE_URL')) {
            $baseUrl = (string) \constant('FLEEKDASH_R2_BASE_URL');
        }

        return \rtrim($baseUrl, '/') . '/fleekdash/releases/fleekdash-' . $version . '.zip';
    }

    /**
     * @return list<array{
     *   version: string,
     *   download_url: string,
     *   size: string,
     *   release_channel: string,
     *   release_date: string
     * }>
     */
    public static function getRollbackVersions(?string $currentVersion, int $limit = self::DEFAULT_LIMIT): array
    {
        $versions = [];
        $endpointUrl = self::buildRollbackVersionsEndpointUrl($currentVersion, $limit);
        $headers = self::getSupabaseHeaders();
        $canRequest = $endpointUrl !== null && $headers !== [];
        if ($canRequest) {
            $response = \wp_remote_get($endpointUrl, [
                'headers' => $headers,
                'timeout' => 15,
            ]);
            $rawVersions = self::extractRawRollbackVersionsFromResponse($response);
            foreach ($rawVersions as $rawVersion) {
                $normalizedVersion = self::normalizeRollbackVersionRow($rawVersion);
                if ($normalizedVersion !== null) {
                    $versions[] = $normalizedVersion;
                }
            }
        }

        return $versions;
    }

    /**
     * @return array<string, string>
     */
    private static function getSupabaseHeaders(): array
    {
        $headers = [];
        $key = '';
        if (\defined('FLEEKDASH_SUPABASE_KEY')) {
            $key = (string) \constant('FLEEKDASH_SUPABASE_KEY');
        } elseif (\defined('FLEEKDASH_SUPABASE_ANON_KEY')) {
            $key = (string) \constant('FLEEKDASH_SUPABASE_ANON_KEY');
        }

        if ($key !== '') {
            $headers = [
                'apikey' => $key,
                'Authorization' => 'Bearer ' . $key,
                'Content-Type' => 'application/json',
            ];
        }

        return $headers;
    }

    /**
     * @param list<string> $pathParts
     * @param list<string> $excludedDirectories
     */
    private static function containsExcludedDirectory(array $pathParts, array $excludedDirectories): bool
    {
        foreach ($excludedDirectories as $excludedDirectory) {
            if (\in_array($excludedDirectory, $pathParts, true)) {
                return true;
            }
        }

        return false;
    }

    private static function isAllowedTopLevelDirectory(string $directory): bool
    {
        return \in_array($directory, self::ROOT_ALLOWED_DIRECTORIES, true);
    }

    /**
     * @param list<string> $pathParts
     */
    private static function shouldIncludeAllowedTopLevelEntry(string $topLevelDirectory, array $pathParts, string $fileName): bool
    {
        $shouldInclude = true;
        if ($topLevelDirectory === 'vendor') {
            $shouldInclude = self::shouldIncludeVendorEntry($pathParts, $fileName);
        } elseif ($topLevelDirectory === 'includes') {
            $shouldInclude = !self::containsExcludedDirectory($pathParts, self::INCLUDES_EXCLUDE_DIRS);
        } elseif ($topLevelDirectory === 'assets') {
            $shouldInclude = self::shouldIncludeAssetsEntry($pathParts, $fileName);
        }

        return $shouldInclude;
    }

    /**
     * @param list<string> $pathParts
     */
    private static function shouldIncludeVendorEntry(array $pathParts, string $fileName): bool
    {
        $containsVendorExcludedDir = self::containsExcludedDirectory($pathParts, self::VENDOR_EXCLUDE_DIRS);
        $isVendorExcludedFile = self::matchesAnyPattern($fileName, self::VENDOR_EXCLUDE_FILE_PATTERNS);

        return !$containsVendorExcludedDir && !$isVendorExcludedFile;
    }

    /**
     * @param list<string> $pathParts
     */
    private static function shouldIncludeAssetsEntry(array $pathParts, string $fileName): bool
    {
        $containsAssetsExcludedDir = self::containsExcludedDirectory($pathParts, self::ASSETS_EXCLUDE_DIRS);
        $isAssetsExcludedFile = self::matchesAnyPattern($fileName, self::ASSETS_EXCLUDE_FILE_PATTERNS);

        return !$containsAssetsExcludedDir && !$isAssetsExcludedFile;
    }

    /**
     * @param list<string> $patterns
     */
    private static function matchesAnyPattern(string $fileName, array $patterns): bool
    {
        foreach ($patterns as $pattern) {
            if (\fnmatch($pattern, $fileName)) {
                return true;
            }
        }

        return false;
    }

    /**
     * @param mixed $response
     * @return list<array<string, mixed>>
     */
    private static function extractRawRollbackVersionsFromResponse($response): array
    {
        $versions = [];
        if (\is_wp_error($response)) {
            fleekdash_log_error('FleekDash rollback versions request failed: ' . $response->get_error_message());
        } else {
            $statusCode = \wp_remote_retrieve_response_code($response);
            if ($statusCode < 200 || $statusCode >= 300) {
                fleekdash_log_error('FleekDash rollback versions request returned HTTP ' . $statusCode);
            } else {
                $decodedBody = \json_decode(\wp_remote_retrieve_body($response), true);
                $versions = \is_array($decodedBody['versions'] ?? null) ? $decodedBody['versions'] : [];
            }
        }

        return $versions;
    }

    /**
     * @param array<string, mixed> $rawVersion
     * @return array{
     *   version: string,
     *   download_url: string,
     *   size: string,
     *   release_channel: string,
     *   release_date: string
     * }|null
     */
    private static function normalizeRollbackVersionRow(array $rawVersion): ?array
    {
        $normalized = null;
        $version = isset($rawVersion['version']) ? (string) $rawVersion['version'] : '';
        if ($version !== '') {
            $normalized = [
                'version' => $version,
                'download_url' => isset($rawVersion['download_url'])
                    ? (string) $rawVersion['download_url']
                    : self::getDownloadUrlForVersion($version),
                'size' => '',
                'release_channel' => UpdatePreferences::resolveReleaseChannelFromPayload($rawVersion),
                'release_date' => isset($rawVersion['release_date']) ? (string) $rawVersion['release_date'] : '',
            ];
        }

        return $normalized;
    }

    private static function buildRollbackVersionsEndpointUrl(?string $currentVersion, int $limit): ?string
    {
        $url = null;
        if (\defined('FLEEKDASH_SUPABASE_URL')) {
            $safeLimit = \max(1, \min(self::MAX_LIMIT, $limit));
            $queryArgs = [
                'mode' => 'rollback_versions',
                'limit' => (string) $safeLimit,
            ];
            if (\is_string($currentVersion) && $currentVersion !== '') {
                $queryArgs['current_version'] = $currentVersion;
            }

            $url = \add_query_arg(
                $queryArgs,
                (string) \constant('FLEEKDASH_SUPABASE_URL') . '/functions/v1/updates-fleekdash-check'
            );
        }

        return $url;
    }
}
