<?php

declare(strict_types=1);

namespace ShieldX\Core;

defined('ABSPATH') || exit;

// phpcs:disable WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents,WordPress.WP.AlternativeFunctions.file_system_operations_scandir,WordPress.WP.AlternativeFunctions.unlink_unlink,WordPress.WP.AlternativeFunctions.file_system_operations_rmdir,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Uninstall cleanup removes plugin-owned files and plugin-owned option prefixes only.

use ShieldX\Database\SchemaInstaller;
use ShieldX\Licensing\LicenseSettings;
use ShieldX\Maintenance\BackupStorageSettings;
use ShieldX\Maintenance\GoogleDriveSettings;
use ShieldX\Maintenance\ScheduledBackupRunner;
use ShieldX\Maintenance\ScheduledBackupSettings;
use ShieldX\Maintenance\ScheduledRestorePointRunner;
use ShieldX\Maintenance\ScheduledRestorePointSettings;
use ShieldX\Scanner\ScheduledScanRunner;
use ShieldX\Scanner\ScheduledScanSettings;
use ShieldX\Scanner\ComponentProvenance;
use ShieldX\Maintenance\AdminChangeShadowScanner;
use ShieldX\Maintenance\RestorePointSettings;
use ShieldX\Security\HardeningSettings;
use ShieldX\Security\HumanChallengeSettings;
use ShieldX\Security\Lockdown;
use ShieldX\Security\LockdownSettings;
use ShieldX\Security\LoginSecurity;
use ShieldX\Security\LoginSecuritySettings;
use ShieldX\Security\RateLimitSettings;
use ShieldX\Security\ReputationFeed;
use ShieldX\Security\UserProfiler;
use ShieldX\Security\WafSettings;
use ShieldX\Storage\FileStorage;

final class Uninstaller
{
    public const PURGE_DATA_OPTION = 'shieldx_purge_data_on_uninstall';

    /** @var array<int,string> */
    private const PURGE_OPTION_NAMES = [
        self::PURGE_DATA_OPTION,
        'shieldx_schema_version',
        'shieldx_activated_at',
        'shieldx_activation_deferred_tasks',
        'shieldx_disabled_addons',
        'shieldx_audit_known_tables',
        'shieldx_maintenance_last_result',
        'shieldx_update_coordination',
        'shieldx_waf_rules_refreshed_at',
        'shieldx_waf_access_dismissed_events',
        'shieldx_hardening_htaccess_preflight',
        'shieldx_scheduler_settings',
        'shieldx_scheduler_setup_wizard_completed',
        'shieldx_scheduler_self_heal_bootstrap',
        'shieldx_updates_settings',
        'shieldx_updates_state',
        'shieldx_log_rotation_settings',
        'shieldx_log_rotation_last_result',
        'shieldx_log_rotation_last_file_scan',
        'shieldx_temp_cleanup_last_result',
        'shieldx_vulnerability_scanner_settings',
        // Virtual-patch definitions are private feed data. Keep every legacy
        // and repository-scoped index key here because the private Threat
        // Intelligence add-on may not be loaded during the core uninstall
        // request.
        'shieldx_virtual_patch_repository',
        'shieldx_virtual_patch_feed_state',
        'shieldx_virtual_patch_catalog_state',
        'shieldx_virtual_patch_repository_index',
        'shieldx_virtual_patch_repo_migration',
        'shieldx_db_admin_baseline',
        'shieldx_scanner_ignored_paths',
        'shieldx_scanner_ignored_path_hashes',
        'shieldx_scanner_ignored_rules',
        'shieldx_scheduled_scan_progress_file',
        'shieldx_scheduled_scan_progress_database',
        'shieldx_scheduled_scan_progress_vulnerability',
        'shieldx_scheduled_restore_point_progress',
        'shieldx_file_type_settings',
        // The core uninstall request does not load the private extensions
        // bootstrap, so keep the AI Gateway's encrypted connection metadata
        // in the core purge allow-list as a defense in depth cleanup.
        'shieldx_ai_settings',
        // Cloudflare is a private extension, but its encrypted connection
        // metadata must not survive an explicit core-data purge.
        'shieldx_cloudflare_settings',
        'shieldx_pulse_settings',
        'shieldx_recovery_settings',
        WafSettings::OPTION_NAME,
        HumanChallengeSettings::OPTION_NAME,
        HardeningSettings::OPTION_NAME,
        LockdownSettings::OPTION_NAME,
        RateLimitSettings::OPTION_NAME,
        ScheduledScanSettings::OPTION_NAME,
        ScheduledBackupSettings::OPTION_NAME,
        ScheduledRestorePointSettings::OPTION_NAME,
        BackupStorageSettings::OPTION_NAME,
        GoogleDriveSettings::OPTION_NAME,
        LoginSecuritySettings::OPTION_NAME,
        ComponentProvenance::OPTION_NAME,
        NotificationSettings::OPTION_NAME,
        NotificationManager::DIGEST_QUEUE_OPTION,
        LicenseSettings::OPTION_NAME,
        SetupModeManager::OPTION_NAME,
        RestorePointSettings::OPTION_NAME,
        UserProfiler::OPTION_NAME,
        AdminChangeShadowScanner::LAST_OPTION,
        AdminChangeShadowScanner::RECENT_OPTION,
        // Keep last: deleting other options can recreate this through the database-table cache invalidation hook.
        'shieldx_audit_database_tables_cache_version',
    ];

    /** @var array<int,string> */
    private const PURGE_USER_META_KEYS = [
        'shieldx_overview_disabled_cards',
        'shieldx_overview_disabled_cards_initialized',
        'shieldx_admin_review_flag',
    ];

    public static function uninstall(): void
    {
        if (self::isNetworkUninstall() && self::canIterateNetworkSites()) {
            self::forEachNetworkSite([self::class, 'uninstallCurrentSite']);
            return;
        }

        self::uninstallCurrentSite();
    }

    public static function purgeDataOnUninstall(): bool
    {
        if (!function_exists('get_option')) {
            return false;
        }

        $stored = get_option(self::PURGE_DATA_OPTION, false);

        return $stored === true || $stored === '1' || $stored === 1 || $stored === 'yes';
    }

    public static function savePurgeDataOnUninstall(bool $purge): void
    {
        if (!function_exists('update_option')) {
            return;
        }

        update_option(self::PURGE_DATA_OPTION, $purge ? '1' : '0', false);
        if (function_exists('wp_set_option_autoload')) {
            wp_set_option_autoload(self::PURGE_DATA_OPTION, false);
        }
    }

    private static function uninstallCurrentSite(): void
    {
        AddonRegistry::fromWordPress()->dispatchLifecycle('shieldx_addon_uninstalled');
        self::teardownRecoveryLayers();
        wp_clear_scheduled_hook('shieldx_hourly_maintenance');
        wp_clear_scheduled_hook('shieldx_temp_cleanup');
        wp_clear_scheduled_hook(SchedulerBridge::SELF_HEAL_ACTION);
        wp_clear_scheduled_hook(NotificationManager::DIGEST_CRON_HOOK);
        wp_clear_scheduled_hook(UserProfiler::CLEANUP_CRON_HOOK);
        ReputationFeed::clearScheduled();
        ScheduledScanRunner::clearScheduled();
        ScheduledBackupRunner::clearScheduled();
        ScheduledRestorePointRunner::clearScheduled();

        self::removeAdministratorCapability();

        $storage = FileStorage::fromWordPress();
        self::removeAiGatewayData($storage);
        Lockdown::fromWordPress()->revert(false, 'uninstall');
        self::removeSafetyMarkers($storage);

        if (!self::shouldPurgeData()) {
            return;
        }

        try {
            SchemaInstaller::fromWordPress()->uninstall();
        } catch (\Throwable $error) {
            // A failed DROP TABLE must not prevent the remaining uninstall cleanup.
            // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- This is a non-sensitive, last-resort diagnostic for a failed purge.
            \ShieldX\Support\ErrorLog::write('[SHIELD-X] Schema cleanup failed during uninstall; continuing with remaining cleanup.');
        }
        self::deleteKnownOptions();
        self::deleteKnownOptionPrefixes([
            'shieldx_scheduled_scan_lease_',
            'shieldx_manual_cli_restore_',
            'shieldx_permission_fix_job_',
            'shieldx_durable_job_',
            'shieldx_virtual_patch_repo_',
        ]);
        self::deleteKnownUserMeta();
        self::resetLoginSecurityState();
        self::deleteKnownTransients();

        $storage->remove();
    }

    private static function isNetworkUninstall(): bool
    {
        if (!function_exists('is_multisite') || !is_multisite()) {
            return false;
        }

        return self::pluginActiveForNetwork();
    }

    private static function pluginActiveForNetwork(): bool
    {
        if (!function_exists('is_plugin_active_for_network') && defined('ABSPATH')) {
            $pluginApi = rtrim((string) ABSPATH, '/\\') . '/wp-admin/includes/plugin.php';
            if (is_readable($pluginApi)) {
                require_once $pluginApi;
            }
        }

        if (!function_exists('is_plugin_active_for_network')) {
            return false;
        }

        $basename = defined('WP_UNINSTALL_PLUGIN') ? (string) constant('WP_UNINSTALL_PLUGIN') : '';
        if ($basename === '' && defined('SHIELDX_PLUGIN_BASENAME')) {
            $basename = (string) constant('SHIELDX_PLUGIN_BASENAME');
        }
        if ($basename === '') {
            $basename = 'xerotact-shield-x-security/shield-x.php';
        }

        return is_plugin_active_for_network($basename);
    }

    private static function canIterateNetworkSites(): bool
    {
        return function_exists('get_sites')
            && function_exists('switch_to_blog')
            && function_exists('restore_current_blog');
    }

    /**
     * @param callable():void $callback
     */
    private static function forEachNetworkSite(callable $callback): void
    {
        $siteIds = get_sites([
            'fields' => 'ids',
            'number' => 0,
        ]);
        if (!is_array($siteIds)) {
            $callback();
            return;
        }

        foreach ($siteIds as $siteId) {
            $siteId = (int) $siteId;
            if ($siteId <= 0) {
                continue;
            }

            switch_to_blog($siteId);
            try {
                RequestCache::clear();
                $callback();
            } finally {
                restore_current_blog();
                RequestCache::clear();
            }
        }
    }

    private static function teardownRecoveryLayers(): void
    {
        // Addon lifecycle discovery normally loads Recovery before this point.
        // Keep a guarded direct teardown as a last-resort safety net: a missing
        // addon class must never make core uninstall fatal, but a loaded one
        // must not leave its MU bridge or auto_prepend_file configuration live.
        if (!class_exists('\\ShieldXRecovery\\RecoveryInstaller')) {
            return;
        }

        try {
            \ShieldXRecovery\RecoveryInstaller::fromWordPress()->uninstall(true, true);
        } catch (\Throwable $error) {
            unset($error);
            // The lifecycle path has already been attempted; continue core
            // uninstall cleanup rather than turning one Recovery filesystem
            // failure into an incomplete WordPress uninstall.
        }
    }

    private static function removeAdministratorCapability(): void
    {
        if (!defined('SHIELDX_MANAGE_CAPABILITY') || !function_exists('get_role')) {
            return;
        }

        $role = get_role('administrator');
        if (is_object($role) && method_exists($role, 'remove_cap')) {
            $role->remove_cap((string) SHIELDX_MANAGE_CAPABILITY);
        }
    }

    private static function removeSafetyMarkers(FileStorage $storage): void
    {
        if (!defined('SHIELDX_CONTENT_DIR')) {
            return;
        }

        $signedLockdownKillswitch = $storage->baseDir() . '/lockdown/disable.json';
        if (is_file($signedLockdownKillswitch)) {
            self::deleteFile($signedLockdownKillswitch);
        }

        $contentDir = rtrim((string) SHIELDX_CONTENT_DIR, '/\\');
        $lockdownKillswitch = $contentDir . '/.shieldx-lockdown-disable';
        if (is_file($lockdownKillswitch)) {
            self::deleteFile($lockdownKillswitch);
        }
    }

    /**
     * Remove the AI Gateway's confidential data even when the administrator
     * keeps ordinary SHIELD-X settings. WordPress loads only the core plugin
     * during its uninstall request, so the private extension cannot reliably
     * dispatch its own cleanup hook here.
     */
    private static function removeAiGatewayData(FileStorage $storage): void
    {
        delete_option('shieldx_ai_settings');
        if (function_exists('delete_transient')) {
            delete_transient('shieldx_ai_openrouter_models');
            delete_transient('shieldx_ai_openrouter_usage');
        }

        self::removePath(rtrim($storage->baseDir(), '/\\') . '/addons/shieldx-ai');
    }

    private static function shouldPurgeData(): bool
    {
        $purge = defined('SHIELDX_PURGE_ON_UNINSTALL') && (bool) constant('SHIELDX_PURGE_ON_UNINSTALL');

        if (!$purge) {
            $purge = self::purgeDataOnUninstall();
        }

        if (function_exists('apply_filters')) {
            $purge = (bool) apply_filters('shieldx_purge_data_on_uninstall', $purge);
        }

        return $purge;
    }

    private static function resetLoginSecurityState(): void
    {
        // Uninstall must leave no SHIELD-X login gate behind, so a reinstall or
        // removal cannot strand administrators behind old 2FA/recovery state.
        delete_option(LoginSecuritySettings::OPTION_NAME);

        foreach (LoginSecurity::userMetaKeys() as $metaKey) {
            delete_metadata('user', 0, $metaKey, '', true);
        }

        self::deleteLoginSecurityTransients();
    }

    private static function deleteLoginSecurityTransients(): void
    {
        global $wpdb;

        if (!is_object($wpdb) || !isset($wpdb->options)) {
            return;
        }

        $prefixes = [
            'shieldx_2fa_codes_',
            'shieldx_2fa_reset_',
            'shieldx_recovery_reveal_',
        ];

        foreach ($prefixes as $prefix) {
            $transientLike = method_exists($wpdb, 'esc_like')
                ? $wpdb->esc_like('_transient_' . $prefix) . '%'
                : '_transient_' . $prefix . '%';
            $timeoutLike = method_exists($wpdb, 'esc_like')
                ? $wpdb->esc_like('_transient_timeout_' . $prefix) . '%'
                : '_transient_timeout_' . $prefix . '%';

            // phpcs:ignore -- Uninstall intentionally removes only SHIELD-X-owned login/recovery transients by escaped option_name prefix.
            $wpdb->query($wpdb->prepare(
                "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s",
                $transientLike,
                $timeoutLike
            ));
        }
    }

    private static function deleteKnownOptions(): void
    {
        foreach (self::PURGE_OPTION_NAMES as $optionName) {
            delete_option($optionName);
        }
    }

    /**
     * @param array<int,string> $prefixes
     */
    private static function deleteKnownOptionPrefixes(array $prefixes): void
    {
        global $wpdb;

        if (!is_object($wpdb) || !isset($wpdb->options)) {
            return;
        }

        foreach ($prefixes as $prefix) {
            $like = method_exists($wpdb, 'esc_like')
                ? $wpdb->esc_like($prefix) . '%'
                : $prefix . '%';

            // phpcs:ignore -- Purge-data uninstall removes only SHIELD-X-owned option names by explicit escaped prefix.
            $wpdb->query($wpdb->prepare("DELETE FROM {$wpdb->options} WHERE option_name LIKE %s", $like));
        }
    }

    private static function deleteKnownUserMeta(): void
    {
        foreach (self::PURGE_USER_META_KEYS as $metaKey) {
            delete_metadata('user', 0, $metaKey, '', true);
        }
    }

    private static function deleteKnownTransients(): void
    {
        global $wpdb;

        if (!is_object($wpdb) || !isset($wpdb->options)) {
            return;
        }

        $prefixes = [
            '_transient_shieldx_',
            '_transient_timeout_shieldx_',
        ];

        foreach ($prefixes as $prefix) {
            $like = method_exists($wpdb, 'esc_like')
                ? $wpdb->esc_like($prefix) . '%'
                : $prefix . '%';

            // phpcs:ignore -- Purge-data uninstall removes only SHIELD-X-owned transient option names by escaped prefix.
            $wpdb->query($wpdb->prepare("DELETE FROM {$wpdb->options} WHERE option_name LIKE %s", $like));
        }
    }

    private static function removePath(string $path): void
    {
        if (!file_exists($path) && !is_link($path)) {
            return;
        }

        if (is_file($path) || is_link($path)) {
            self::deleteFile($path);
            return;
        }

        $items = scandir($path);
        if (!is_array($items)) {
            return;
        }

        foreach ($items as $item) {
            if ($item === '.' || $item === '..') {
                continue;
            }

            self::removePath($path . DIRECTORY_SEPARATOR . $item);
        }

        rmdir($path);
    }

    private static function deleteFile(string $path): bool
    {
        if (!is_file($path) && !is_link($path)) {
            return true;
        }

        return unlink($path);
    }
}
