<?php

declare(strict_types=1);

namespace ShieldX\Security;

defined('ABSPATH') || exit;

final class VirtualPatchRepository
{
    private const OPTION = 'shieldx_virtual_patch_repository';
    private const FEED_OPTION = 'shieldx_virtual_patch_feed_state';
    private const CATALOG_OPTION = 'shieldx_virtual_patch_catalog_state';
    private const REPOSITORY_INDEX_OPTION = 'shieldx_virtual_patch_repository_index';
    private const REPOSITORY_OPTION_PREFIX = 'shieldx_virtual_patch_repo_';
    private const MIGRATION_OPTION = 'shieldx_virtual_patch_repo_migration';
    private const COMPILED_TRANSIENT = 'shieldx_virtual_patch_compiled_rules';
    private const MAX_HISTORY = 1000;

    /**
     * @param array<string,mixed> $bundle
     * @return array{installed:int,updated:int,revoked:int,superseded:int}
     */
    public function applyBundle(array $bundle): array
    {
        $records = $this->records();
        $summary = $this->applyBundleToRecords($records, $bundle, 'legacy');
        $this->saveRecords($records);
        $this->saveFeedState([
            'sequence' => (int) ($bundle['sequence'] ?? 0),
            'version' => (string) ($bundle['version'] ?? ''),
            'issued_at' => (string) ($bundle['issued_at'] ?? ''),
            'expires_at' => (string) ($bundle['expires_at'] ?? ''),
            'verified_at' => gmdate('c'),
            'last_error' => '',
        ]);

        return $summary;
    }

    /**
     * @param array<string,mixed> $bundle
     * @return array{installed:int,updated:int,revoked:int,superseded:int}
     */
    public function applyRepositoryBundle(array $bundle, array $catalogEntry = []): array
    {
        $this->migrateLegacyRecords();
        $repositoryId = (string) ($bundle['repository_id'] ?? '');
        if ($repositoryId === '') {
            return ['installed' => 0, 'updated' => 0, 'revoked' => 0, 'superseded' => 0];
        }
        $repository = $this->repositoryData($repositoryId);
        $records = isset($repository['records']) && is_array($repository['records']) ? $repository['records'] : [];
        $summary = $this->applyBundleToRecords($records, $bundle, $repositoryId);
        $this->saveRepository($repositoryId, [
            'repository_id' => $repositoryId,
            'component' => isset($bundle['component']) && is_array($bundle['component']) ? $bundle['component'] : [],
            'records' => $records,
            'state' => [
                'sequence' => (int) ($bundle['sequence'] ?? 0),
                'version' => (string) ($bundle['version'] ?? ''),
                'issued_at' => (string) ($bundle['issued_at'] ?? ''),
                'expires_at' => (string) ($bundle['expires_at'] ?? ''),
                'verified_at' => gmdate('c'),
                'sha256' => (string) ($catalogEntry['sha256'] ?? ''),
                'bytes' => (int) ($catalogEntry['bytes'] ?? 0),
                'last_error' => '',
            ],
        ]);

        return $summary;
    }

    /** @param array<string,mixed> $catalog */
    public function applyCatalog(array $catalog, string $payloadHash = ''): void
    {
        $this->migrateLegacyRecords();
        $repositories = [];
        foreach ((array) ($catalog['repositories'] ?? []) as $entry) {
            if (is_array($entry) && (string) ($entry['repository_id'] ?? '') !== '') {
                $repositories[(string) $entry['repository_id']] = $entry;
            }
        }
        $this->saveCatalogState([
            'sequence' => (int) ($catalog['sequence'] ?? 0),
            'version' => (string) ($catalog['version'] ?? ''),
            'issued_at' => (string) ($catalog['issued_at'] ?? ''),
            'expires_at' => (string) ($catalog['expires_at'] ?? ''),
            'verified_at' => gmdate('c'),
            'sha256' => preg_match('/^[a-f0-9]{64}$/', $payloadHash) === 1 ? $payloadHash : '',
            'last_error' => '',
            'repositories' => $repositories,
        ]);
    }

    /** @return array<string,mixed> */
    public function catalogState(): array
    {
        $value = function_exists('get_option') ? get_option(self::CATALOG_OPTION, []) : [];

        return is_array($value) ? $value : [];
    }

    /** @return array<string,array<string,mixed>> */
    public function catalogEntries(): array
    {
        $state = $this->catalogState();
        $entries = $state['repositories'] ?? [];

        return is_array($entries) ? $entries : [];
    }

    /** @return array<string,mixed> */
    public function feedState(string $repositoryId = ''): array
    {
        if ($repositoryId !== '') {
            $repository = $this->repositoryData($repositoryId);
            $state = $repository['state'] ?? [];

            return is_array($state) ? $state : [];
        }
        $value = function_exists('get_option') ? get_option(self::FEED_OPTION, []) : [];

        return is_array($value) ? $value : [];
    }

    /**
     * @param array<string,array<string,mixed>> $records
     * @param array<string,mixed> $bundle
     * @return array{installed:int,updated:int,revoked:int,superseded:int}
     */
    private function applyBundleToRecords(array &$records, array $bundle, string $repositoryId): array
    {
        $seen = [];
        $summary = ['installed' => 0, 'updated' => 0, 'revoked' => 0, 'superseded' => 0];
        $now = gmdate('c');

        foreach ((array) ($bundle['definitions'] ?? []) as $definition) {
            if (!is_array($definition)) {
                continue;
            }
            $id = (string) ($definition['id'] ?? '');
            if ($id === '') {
                continue;
            }
            $seen[$id] = true;
            $existing = isset($records[$id]) && is_array($records[$id]) ? $records[$id] : [];
            $oldRevision = (int) ($existing['definition']['revision'] ?? 0);
            $newRevision = (int) ($definition['revision'] ?? 0);
            if ($oldRevision > $newRevision) {
                continue;
            }

            $isNew = $existing === [];
            $changed = $isNew || $oldRevision !== $newRevision
                || !hash_equals((string) ($existing['definition']['definition_hash'] ?? ''), (string) ($definition['definition_hash'] ?? ''));
            if (!$changed) {
                continue;
            }

            $records[$id] = [
                'definition' => $definition,
                'repository_id' => $repositoryId,
                'state' => 'monitor',
                'installed_at' => $isNew ? $now : (string) ($existing['installed_at'] ?? $now),
                'updated_at' => $now,
                'approved_by' => 0,
                'approved_at' => '',
                'source_sequence' => (int) ($bundle['sequence'] ?? 0),
            ];
            $summary[$isNew ? 'installed' : 'updated']++;
        }

        foreach ((array) ($bundle['revocations'] ?? []) as $revocation) {
            if (!is_array($revocation)) {
                continue;
            }
            $id = (string) ($revocation['id'] ?? '');
            if (!isset($records[$id]) || !is_array($records[$id])) {
                continue;
            }
            $records[$id]['state'] = 'revoked';
            $records[$id]['revoked_at'] = $now;
            $records[$id]['revocation_reason'] = substr((string) ($revocation['reason'] ?? ''), 0, 500);
            $summary['revoked']++;
        }

        if ((string) ($bundle['type'] ?? '') === 'full') {
            foreach ($records as $id => &$record) {
                if (isset($seen[$id]) || !is_array($record) || in_array((string) ($record['state'] ?? ''), ['revoked', 'superseded'], true)) {
                    continue;
                }
                $record['state'] = 'superseded';
                $record['updated_at'] = $now;
                $summary['superseded']++;
            }
            unset($record);
        }

        if (count($records) > self::MAX_HISTORY) {
            uasort($records, static function (array $left, array $right): int {
                return strcmp((string) ($right['updated_at'] ?? ''), (string) ($left['updated_at'] ?? ''));
            });
            $records = array_slice($records, 0, self::MAX_HISTORY, true);
        }

        return $summary;
    }

    /** @return array<string,array<string,mixed>> */
    public function all(): array
    {
        $records = $this->records();
        if ($this->refreshRecordStates($records)) {
            $this->saveRecords($records);
        }
        foreach ($this->repositoryIndex() as $repositoryId => $metadata) {
            unset($metadata);
            $repository = $this->repositoryData((string) $repositoryId);
            $repositoryRecords = isset($repository['records']) && is_array($repository['records']) ? $repository['records'] : [];
            if ($this->refreshRecordStates($repositoryRecords)) {
                $repository['records'] = $repositoryRecords;
                $this->saveRepository((string) $repositoryId, $repository);
            }
            foreach ($repositoryRecords as $id => $record) {
                if (is_array($record)) {
                    $records[(string) $id] = $record;
                }
            }
        }

        return $records;
    }

    /**
     * Return only records whose target component is currently installed.
     *
     * Platform records remain visible. Plugin and theme records are hidden as
     * soon as their component is no longer present, even before the next feed
     * synchronization removes the stale component repository.
     *
     * @return array<string,array<string,mixed>>
     */
    public function installedRecords(): array
    {
        $installed = [];
        foreach ($this->all() as $id => $record) {
            if (!is_array($record)) {
                continue;
            }
            $definition = isset($record['definition']) && is_array($record['definition']) ? $record['definition'] : [];
            if ($this->componentIsInstalled($definition)) {
                $installed[(string) $id] = $record;
            }
        }

        return $installed;
    }

    /** @return array<int,array<string,mixed>> */
    public function activeRules(): array
    {
        $cached = function_exists('get_transient') ? get_transient(self::COMPILED_TRANSIENT) : false;
        if (is_array($cached)) {
            return $cached;
        }
        $rules = [];
        foreach ($this->all() as $record) {
            if (!is_array($record) || !in_array((string) ($record['state'] ?? ''), ['monitor', 'challenge', 'block'], true)) {
                continue;
            }
            $definition = isset($record['definition']) && is_array($record['definition']) ? $record['definition'] : [];
            if (!$this->componentIsAffected($definition)) {
                continue;
            }
            $matcher = isset($definition['matcher']) && is_array($definition['matcher']) ? $definition['matcher'] : [];
            $state = (string) $record['state'];
            $rules[] = [
                'id' => (string) ($definition['id'] ?? ''),
                'name' => (string) ($definition['title'] ?? ($definition['id'] ?? 'Virtual patch')),
                'category' => 'virtual_patch',
                'severity' => (string) ($definition['severity'] ?? 'high'),
                'score' => $state === 'monitor' ? 0 : 100,
                'fields' => array_values((array) ($matcher['fields'] ?? [])),
                'patterns' => array_values((array) ($matcher['patterns'] ?? [])),
                'methods' => array_values((array) ($matcher['methods'] ?? [])),
                'path_prefixes' => array_values((array) ($matcher['path_prefixes'] ?? [])),
                'action' => $state === 'monitor' ? 'log' : $state,
                'cwe' => (string) ($definition['cwe'] ?? ''),
                'owasp' => (string) ($definition['owasp'] ?? ''),
                'virtual_patch' => true,
            ];
        }

        if (function_exists('set_transient')) {
            set_transient(self::COMPILED_TRANSIENT, $rules, 300);
        }

        return $rules;
    }

    public function setState(string $id, string $state, int $userId): bool
    {
        if (!in_array($state, ['monitor', 'challenge', 'block', 'disabled'], true)) {
            return false;
        }
        $records = $this->all();
        if (!isset($records[$id]) || !is_array($records[$id])) {
            return false;
        }
        $definition = isset($records[$id]['definition']) && is_array($records[$id]['definition'])
            ? $records[$id]['definition']
            : [];
        if (!$this->componentIsInstalled($definition)) {
            return false;
        }
        $current = (string) ($records[$id]['state'] ?? '');
        if (in_array($current, ['revoked', 'expired-monitor', 'fixed-monitor', 'superseded'], true)) {
            return false;
        }
        $repositoryId = (string) ($records[$id]['repository_id'] ?? 'legacy');
        if ($repositoryId !== '' && $repositoryId !== 'legacy') {
            $repository = $this->repositoryData($repositoryId);
            $target = isset($repository['records']) && is_array($repository['records']) ? $repository['records'] : [];
            if (!isset($target[$id]) || !is_array($target[$id])) {
                return false;
            }
            $this->updateRecordState($target[$id], $state, $userId);
            $repository['records'] = $target;
            $this->saveRepository($repositoryId, $repository);
        } else {
            $target = $this->records();
            if (!isset($target[$id]) || !is_array($target[$id])) {
                return false;
            }
            $this->updateRecordState($target[$id], $state, $userId);
            $this->saveRecords($target);
        }

        return true;
    }

    /** @param array<string,mixed> $state */
    public function recordFailure(array $state, string $repositoryId = ''): void
    {
        if ($repositoryId !== '') {
            $repository = $this->repositoryData($repositoryId);
            $current = isset($repository['state']) && is_array($repository['state']) ? $repository['state'] : [];
            $current['last_error'] = substr((string) ($state['message'] ?? 'Definition update failed.'), 0, 500);
            $current['last_error_code'] = substr((string) ($state['error_code'] ?? 'update_failed'), 0, 80);
            $current['last_attempt_at'] = gmdate('c');
            $repository['state'] = $current;
            $this->saveRepository($repositoryId, $repository);

            return;
        }
        $current = $this->feedState();
        $current['last_error'] = substr((string) ($state['message'] ?? 'Definition update failed.'), 0, 500);
        $current['last_error_code'] = substr((string) ($state['error_code'] ?? 'update_failed'), 0, 80);
        $current['last_attempt_at'] = gmdate('c');
        $this->saveFeedState($current);
    }

    /** @param array<string,mixed> $state */
    public function recordCatalogFailure(array $state): void
    {
        $current = $this->catalogState();
        $current['last_error'] = substr((string) ($state['message'] ?? 'Catalog update failed.'), 0, 500);
        $current['last_error_code'] = substr((string) ($state['error_code'] ?? 'catalog_failed'), 0, 80);
        $current['last_attempt_at'] = gmdate('c');
        $this->saveCatalogState($current);
    }

    /** @return array<string,array<string,mixed>> */
    private function records(): array
    {
        $value = function_exists('get_option') ? get_option(self::OPTION, []) : [];

        return is_array($value) ? $value : [];
    }

    /** @param array<string,array<string,mixed>> $records */
    private function saveRecords(array $records): void
    {
        if (function_exists('update_option')) {
            update_option(self::OPTION, $records, false);
        }
        if (function_exists('delete_transient')) {
            delete_transient(self::COMPILED_TRANSIENT);
        }
    }

    /** @param array<string,mixed> $state */
    private function saveFeedState(array $state): void
    {
        if (function_exists('update_option')) {
            update_option(self::FEED_OPTION, $state, false);
        }
    }

    /** @return array<string,array<string,mixed>> */
    public function repositoryStates(): array
    {
        $states = [];
        foreach ($this->repositoryIndex() as $repositoryId => $metadata) {
            unset($metadata);
            $states[(string) $repositoryId] = $this->feedState((string) $repositoryId);
        }

        return $states;
    }

    /** @return array<string,array<string,mixed>> */
    public function installedRepositoryStates(): array
    {
        $states = [];
        foreach ($this->repositoryIndex() as $repositoryId => $metadata) {
            $repositoryId = (string) $repositoryId;
            $component = is_array($metadata) && isset($metadata['component']) && is_array($metadata['component'])
                ? $metadata['component']
                : [];
            $type = (string) ($component['type'] ?? '');
            $slug = (string) ($component['slug'] ?? '');
            if (in_array($type, ['plugin', 'theme'], true) && $this->installedVersion($type, $slug) === '') {
                continue;
            }
            $states[$repositoryId] = $this->feedState($repositoryId);
        }

        return $states;
    }

    public function invalidateCompiledRules(): void
    {
        if (function_exists('delete_transient')) {
            delete_transient(self::COMPILED_TRANSIENT);
        }
    }

    /**
     * Remove component repositories that no longer belong to an installed
     * plugin or theme. Platform repositories are retained.
     *
     * @param array{plugin?:array<string,string>,theme?:array<string,string>} $inventory
     */
    public function reconcileInstalledComponents(array $inventory): int
    {
        if (!function_exists('delete_option') || !function_exists('update_option')) {
            return 0;
        }
        $index = $this->repositoryIndex();
        $removed = 0;
        foreach ($index as $repositoryId => $metadata) {
            $repositoryId = (string) $repositoryId;
            if ($repositoryId === 'platform') {
                continue;
            }
            $component = is_array($metadata) && isset($metadata['component']) && is_array($metadata['component'])
                ? $metadata['component']
                : [];
            if ($component === []) {
                $repository = $this->repositoryData($repositoryId);
                $component = isset($repository['component']) && is_array($repository['component']) ? $repository['component'] : [];
            }
            $type = (string) ($component['type'] ?? '');
            $slug = (string) ($component['slug'] ?? '');
            if (!in_array($type, ['plugin', 'theme'], true) || $slug === '') {
                continue;
            }
            $components = isset($inventory[$type]) && is_array($inventory[$type]) ? $inventory[$type] : [];
            if (isset($components[$slug]) && trim((string) $components[$slug]) !== '') {
                continue;
            }
            delete_option($this->repositoryOption($repositoryId));
            unset($index[$repositoryId]);
            ++$removed;
        }
        if ($removed > 0) {
            update_option(self::REPOSITORY_INDEX_OPTION, $index, false);
            $this->invalidateCompiledRules();
        }

        return $removed;
    }

    public function purge(): void
    {
        if (!function_exists('delete_option')) {
            return;
        }
        foreach (array_keys($this->repositoryIndex()) as $repositoryId) {
            delete_option($this->repositoryOption((string) $repositoryId));
        }
        delete_option(self::OPTION);
        delete_option(self::FEED_OPTION);
        delete_option(self::CATALOG_OPTION);
        delete_option(self::REPOSITORY_INDEX_OPTION);
        delete_option(self::MIGRATION_OPTION);
        if (function_exists('delete_transient')) {
            delete_transient(self::COMPILED_TRANSIENT);
        }
    }

    /** @return array<string,mixed> */
    private function repositoryData(string $repositoryId): array
    {
        if (!function_exists('get_option')) {
            return [];
        }
        $value = get_option($this->repositoryOption($repositoryId), []);

        return is_array($value) ? $value : [];
    }

    /** @param array<string,mixed> $repository */
    private function saveRepository(string $repositoryId, array $repository): void
    {
        if ($repositoryId === '' || !function_exists('update_option')) {
            return;
        }
        update_option($this->repositoryOption($repositoryId), $repository, false);
        $index = $this->repositoryIndex();
        $index[$repositoryId] = [
            'repository_id' => $repositoryId,
            'component' => isset($repository['component']) && is_array($repository['component']) ? $repository['component'] : [],
            'updated_at' => gmdate('c'),
        ];
        update_option(self::REPOSITORY_INDEX_OPTION, $index, false);
        if (function_exists('delete_transient')) {
            delete_transient(self::COMPILED_TRANSIENT);
        }
    }

    /** @return array<string,array<string,mixed>> */
    private function repositoryIndex(): array
    {
        $value = function_exists('get_option') ? get_option(self::REPOSITORY_INDEX_OPTION, []) : [];

        return is_array($value) ? $value : [];
    }

    private function repositoryOption(string $repositoryId): string
    {
        return self::REPOSITORY_OPTION_PREFIX . substr(hash('sha256', $repositoryId), 0, 32);
    }

    /** @param array<string,mixed> $state */
    private function saveCatalogState(array $state): void
    {
        if (function_exists('update_option')) {
            update_option(self::CATALOG_OPTION, $state, false);
        }
    }

    private function migrateLegacyRecords(): void
    {
        if (!function_exists('get_option') || !function_exists('update_option')) {
            return;
        }
        if ((int) get_option(self::MIGRATION_OPTION, 0) >= 1) {
            return;
        }
        $legacy = $this->records();
        $remaining = [];
        $groups = [];
        foreach ($legacy as $id => $record) {
            if (!is_array($record)) {
                continue;
            }
            $definition = isset($record['definition']) && is_array($record['definition']) ? $record['definition'] : [];
            $component = isset($definition['component']) && is_array($definition['component']) ? $definition['component'] : [];
            $repositoryId = $this->repositoryIdForComponent($component);
            if ($repositoryId === '') {
                $remaining[(string) $id] = $record;
                continue;
            }
            $record['repository_id'] = $repositoryId;
            $groups[$repositoryId]['component'] = $repositoryId === 'platform'
                ? ['type' => 'platform', 'slug' => 'platform']
                : ['type' => (string) ($component['type'] ?? ''), 'slug' => (string) ($component['slug'] ?? '')];
            $groups[$repositoryId]['records'][(string) $id] = $record;
        }
        foreach ($groups as $repositoryId => $group) {
            $repository = $this->repositoryData((string) $repositoryId);
            $existing = isset($repository['records']) && is_array($repository['records']) ? $repository['records'] : [];
            $repository['repository_id'] = (string) $repositoryId;
            $repository['component'] = $group['component'];
            $repository['records'] = array_merge((array) $group['records'], $existing);
            $this->saveRepository((string) $repositoryId, $repository);
        }
        $this->saveRecords($remaining);
        update_option(self::MIGRATION_OPTION, 1, false);
    }

    /** @param array<string,mixed> $component */
    private function repositoryIdForComponent(array $component): string
    {
        $type = (string) ($component['type'] ?? '');
        $slug = (string) ($component['slug'] ?? '');
        if (in_array($type, ['wordpress', 'software'], true)) {
            return 'platform';
        }
        if (in_array($type, ['plugin', 'theme'], true) && preg_match('/^[a-z0-9][a-z0-9._-]{0,190}$/', $slug) === 1) {
            return $type . ':' . $slug;
        }

        return '';
    }

    /** @param array<string,array<string,mixed>> $records */
    private function refreshRecordStates(array &$records): bool
    {
        $changed = false;
        foreach ($records as &$record) {
            if (!is_array($record)) {
                continue;
            }
            $state = (string) ($record['state'] ?? 'monitor');
            if (in_array($state, ['revoked', 'disabled', 'superseded'], true)) {
                continue;
            }
            $definition = isset($record['definition']) && is_array($record['definition']) ? $record['definition'] : [];
            $expires = strtotime((string) ($definition['expires_at'] ?? ''));
            if ($expires !== false && $expires <= time() && $state !== 'expired-monitor') {
                $record['state'] = 'expired-monitor';
                $changed = true;
                continue;
            }
            if ($this->componentIsFixed($definition) && $state !== 'fixed-monitor') {
                $record['state'] = 'fixed-monitor';
                $changed = true;
            }
        }
        unset($record);

        return $changed;
    }

    /** @param array<string,mixed> $record */
    private function updateRecordState(array &$record, string $state, int $userId): void
    {
        $record['state'] = $state;
        $record['approved_by'] = $state === 'monitor' || $state === 'disabled' ? 0 : max(0, $userId);
        $record['approved_at'] = $state === 'monitor' || $state === 'disabled' ? '' : gmdate('c');
        $record['updated_at'] = gmdate('c');
    }

    /** @param array<string,mixed> $definition */
    private function componentIsAffected(array $definition): bool
    {
        $component = isset($definition['component']) && is_array($definition['component']) ? $definition['component'] : [];
        $version = $this->installedVersion((string) ($component['type'] ?? ''), (string) ($component['slug'] ?? ''));
        if ($version === '') {
            return false;
        }

        return VirtualPatchVersionMatcher::matches($version, (string) ($component['affected_versions'] ?? ''));
    }

    /** @param array<string,mixed> $definition */
    private function componentIsInstalled(array $definition): bool
    {
        $component = isset($definition['component']) && is_array($definition['component']) ? $definition['component'] : [];
        $type = (string) ($component['type'] ?? '');
        if (!in_array($type, ['plugin', 'theme'], true)) {
            return true;
        }

        return $this->installedVersion($type, (string) ($component['slug'] ?? '')) !== '';
    }

    /** @param array<string,mixed> $definition */
    private function componentIsFixed(array $definition): bool
    {
        $component = isset($definition['component']) && is_array($definition['component']) ? $definition['component'] : [];
        $fixedIn = trim((string) ($component['fixed_in'] ?? ''));
        if ($fixedIn === '') {
            return false;
        }
        $version = $this->installedVersion((string) ($component['type'] ?? ''), (string) ($component['slug'] ?? ''));

        return $version !== '' && version_compare($version, $fixedIn, '>=');
    }

    private function installedVersion(string $type, string $slug): string
    {
        $filtered = function_exists('apply_filters')
            ? apply_filters('shieldx_virtual_patch_component_version', '', $type, $slug)
            : '';
        if (is_scalar($filtered) && trim((string) $filtered) !== '') {
            return trim((string) $filtered);
        }
        if ($type === 'wordpress') {
            return isset($GLOBALS['wp_version']) ? (string) $GLOBALS['wp_version'] : '';
        }
        if ($type === 'plugin' && !function_exists('get_plugins') && defined('ABSPATH')) {
            $pluginApi = ABSPATH . 'wp-admin/includes/plugin.php';
            if (is_file($pluginApi)) {
                require_once $pluginApi;
            }
        }
        if ($type === 'plugin' && function_exists('get_plugins')) {
            foreach ((array) get_plugins() as $file => $data) {
                if (dirname((string) $file) === $slug || basename((string) $file, '.php') === $slug) {
                    return is_array($data) ? (string) ($data['Version'] ?? '') : '';
                }
            }
        }
        if ($type === 'theme' && !function_exists('wp_get_theme') && defined('ABSPATH')) {
            $themeApi = ABSPATH . 'wp-admin/includes/theme.php';
            if (is_file($themeApi)) {
                require_once $themeApi;
            }
        }
        if ($type === 'theme' && function_exists('wp_get_theme')) {
            $theme = wp_get_theme($slug);
            if (is_object($theme) && method_exists($theme, 'exists') && $theme->exists()) {
                return (string) $theme->get('Version');
            }
        }

        return '';
    }

}
