<?php
/**
 * Uninstall handler for Brainwerk SEO Suite.
 *
 * Runs only when the plugin is deleted from the Plugins screen. All data is
 * preserved UNLESS the user opted in via the "Delete all data on uninstall"
 * setting (Privacy / GDPR tab). Multisite-aware: the analytics tables live on
 * the network prefix and are dropped once; per-site options and transients are
 * cleared for every site.
 *
 * @package Brainwerk SEO Suite
 */

// Only ever run in WordPress's uninstall context.
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
    exit;
}

/**
 * Read the merged delete-on-uninstall preference (network + main site).
 */
function seoforge_uninstall_merged_settings() {
    $opt = array();
    if ( is_multisite() ) {
        $net = get_site_option( 'seoforge_settings', array() );
        if ( is_array( $net ) ) {
            $opt = $net;
        }
    }
    $site = get_option( 'seoforge_settings', array() );
    if ( is_array( $site ) ) {
        $opt = array_replace( $opt, $site );
    }
    return $opt;
}

function seoforge_uninstall_should_purge() {
    $opt = seoforge_uninstall_merged_settings();
    // Undecided (onboarding never finished, so the key was never stored) purges
    // by default for data minimisation. An explicit user choice — including a
    // stored `false` from an existing install — is always honoured.
    $network_wants_purge = ! array_key_exists( 'delete_on_uninstall', $opt )
        ? true
        : ! empty( $opt['delete_on_uninstall'] );

    if ( ! is_multisite() ) {
        return $network_wants_purge;
    }

    // Multisite: the analytics tables are shared (network-prefixed), so this is
    // a network-level decision — dropping them destroys every site's data at
    // once. Err toward KEEPING data: only purge if the network-level setting
    // says so AND no individual site has explicitly opted out. A site's own
    // `delete_on_uninstall === false` always wins over the network default,
    // even if that site never re-confirmed the choice after this plugin
    // introduced the setting.
    if ( ! $network_wants_purge ) {
        return false;
    }
    $site_ids = get_sites( array( 'number' => 0, 'fields' => 'ids' ) );
    foreach ( $site_ids as $blog_id ) {
        $site_opt = get_blog_option( (int) $blog_id, 'seoforge_settings', array() );
        if ( is_array( $site_opt )
            && array_key_exists( 'delete_on_uninstall', $site_opt )
            && empty( $site_opt['delete_on_uninstall'] )
        ) {
            return false; // At least one site explicitly opted to keep its data.
        }
    }
    return true;
}

/**
 * Whether to keep a config backup so a future reinstall restores the settings.
 * Defaults to true (the key is absent only on very old installs).
 */
function seoforge_uninstall_should_persist_settings() {
    $opt = seoforge_uninstall_merged_settings();
    return ! array_key_exists( 'persist_settings', $opt ) || ! empty( $opt['persist_settings'] );
}

/**
 * Per-site cleanup: options, transients, cron events, uploaded geo data.
 */
function seoforge_uninstall_site( $options, $crons ) {
    global $wpdb;

    foreach ( $options as $o ) {
        delete_option( $o );
    }
    foreach ( $crons as $hook ) {
        wp_clear_scheduled_hook( $hook );
    }

    // Transients (value + timeout rows).
    $wpdb->query( "DELETE FROM `{$wpdb->options}` WHERE option_name LIKE '\_transient\_seoforge%' OR option_name LIKE '\_transient\_timeout\_seoforge%'" ); // phpcs:ignore WordPress.DB

    // Inflated geo database + any downloaded MaxMind DB.
    $upload = wp_get_upload_dir();
    $dir    = trailingslashit( $upload['basedir'] ) . 'seoforge';
    if ( is_dir( $dir ) ) {
        foreach ( (array) glob( $dir . '/*' ) as $f ) {
            if ( is_file( $f ) ) {
                @unlink( $f ); // phpcs:ignore WordPress.PHP.NoSilencedErrors, WordPress.WP.AlternativeFunctions
            }
        }
        @rmdir( $dir ); // phpcs:ignore WordPress.PHP.NoSilencedErrors, WordPress.WP.AlternativeFunctions
    }
}

/**
 * Top-level orchestration, extracted into a function so tests can invoke it
 * repeatedly against different global state without redeclaring the helper
 * functions above. Behaviour is unchanged from the previous flat script —
 * this is called exactly once below, exactly as the flat script ran once
 * when required.
 */
function seoforge_uninstall_run() {
    global $wpdb;

    // Preserve the configuration for a future reinstall (unless the user opted
    // out). This runs even on a full "delete all data" purge, because the
    // settings are configuration — not visitor/personal data. The backup lives
    // in a DB option (never the web-accessible uploads dir) that the purge
    // below deliberately keeps.
    $seoforge_persist = seoforge_uninstall_should_persist_settings();
    if ( $seoforge_persist ) {
        $seoforge_cfg = seoforge_uninstall_merged_settings();
        if ( ! empty( $seoforge_cfg ) ) {
            $seoforge_backup = array(
                'settings' => $seoforge_cfg,
                'ai'       => is_multisite() ? get_site_option( 'seoforge_ai', array() ) : get_option( 'seoforge_ai', array() ),
                'version'  => '',
                'at'       => gmdate( 'Y-m-d\TH:i:s\Z' ),
            );
            if ( is_multisite() ) {
                update_site_option( 'seoforge_settings_backup', $seoforge_backup );
            } else {
                update_option( 'seoforge_settings_backup', $seoforge_backup );
            }
        }
    } else {
        // User opted out of persistence — make sure no stale backup lingers.
        if ( is_multisite() ) {
            delete_site_option( 'seoforge_settings_backup' );
        } else {
            delete_option( 'seoforge_settings_backup' );
        }
    }

    if ( ! seoforge_uninstall_should_purge() ) {
        return; // Keep all data — user did not opt in to a purge.
    }

    $tables = array( 'visits', 'sessions', 'redirects', '404', 'meta', 'daily', 'audit', 'history' );
    foreach ( $tables as $t ) {
        $table = $wpdb->base_prefix . 'seoforge_' . $t;
        // %i (WP 6.2+) safely quotes the table identifier.
        $wpdb->query( $wpdb->prepare( 'DROP TABLE IF EXISTS %i', $table ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
    }

    $options = array(
        'seoforge_settings',
        'seoforge_db_version',
        'seoforge_onboarding_pending',
        'seoforge_ai',
        'seoforge_review_notice',
        'seoforge_intel_feed',
        'seoforge_intel_feed_seen_version',
        'seoforge_intel_feed_last_error',
        'seoforge_intel_feed_backoff',
        'seoforge_verify_notice',
    );

    // uninstall.php runs standalone (WP never loads the rest of the plugin
    // here), so pull in only the dependency-free canonical hook list, not the
    // full bootstrap.
    require_once __DIR__ . '/includes/class-seoforge-cron-hooks.php';
    $crons = Seoforge_Cron_Hooks::all();

    if ( is_multisite() ) {
        // Network-level option + site transients.
        delete_site_option( 'seoforge_settings' );
        delete_site_option( 'seoforge_db_version' );
        delete_site_option( 'seoforge_onboarding_pending' );
        delete_site_option( 'seoforge_ai' );
        delete_site_option( 'seoforge_review_notice' );
        delete_site_option( 'seoforge_intel_feed' );
        delete_site_option( 'seoforge_intel_feed_seen_version' );
        delete_site_option( 'seoforge_intel_feed_last_error' );
        delete_site_option( 'seoforge_intel_feed_backoff' );
        $wpdb->query( "DELETE FROM `{$wpdb->sitemeta}` WHERE meta_key LIKE '\_site\_transient\_seoforge%' OR meta_key LIKE '\_site\_transient\_timeout\_seoforge%'" ); // phpcs:ignore WordPress.DB

        $site_ids = get_sites( array( 'fields' => 'ids', 'number' => 0 ) );
        foreach ( $site_ids as $blog_id ) {
            switch_to_blog( (int) $blog_id );
            seoforge_uninstall_site( $options, $crons );
            restore_current_blog();
        }
    } else {
        seoforge_uninstall_site( $options, $crons );
    }
}

seoforge_uninstall_run();
