<?php

namespace WPSN\Core;

defined('ABSPATH') || exit;

/**
 * Optional MU-plugin installer for emergency recovery.
 *
 * The installed MU-plugin:
 * - Adds a "Disable WP SEO Next" button on WordPress fatal error pages when the fatal originated from wp-seo-next.
 * - Provides signed recovery URLs to disable the plugin and/or restore the latest config snapshot.
 *
 * This is NOT installed automatically. User must install via Diagnostics page.
 */
final class RecoveryHelper
{
    public const MU_PLUGIN_FILENAME = 'wpsn-recovery.php';
    public const MARKER = 'WPSN Recovery Helper (MU)';

    public static function muPluginsDir(): string
    {
        return WP_CONTENT_DIR . '/mu-plugins';
    }

    public static function muPluginFile(): string
    {
        return self::muPluginsDir() . '/' . self::MU_PLUGIN_FILENAME;
    }

    public static function isInstalled(): bool
    {
        $file = self::muPluginFile();
        if (!is_readable($file)) {
            return false;
        }
        $c = (string) file_get_contents($file);
        return $c !== '' && strpos($c, self::MARKER) !== false;
    }

    /**
     * @return array{ok:bool,message:string}
     */
    public static function install(): array
    {
        $dir = self::muPluginsDir();
        if (!is_dir($dir)) {
            if (function_exists('wp_mkdir_p')) {
                if (!wp_mkdir_p($dir)) {
                    return ['ok' => false, 'message' => sprintf(__('Failed to create mu-plugins directory: %s', 'wp-seo-next'), $dir)];
                }
            } else {
                if (!@mkdir($dir, 0755, true)) {
                    return ['ok' => false, 'message' => sprintf(__('Failed to create mu-plugins directory: %s', 'wp-seo-next'), $dir)];
                }
            }
        }

        if (!is_writable($dir)) {
            return ['ok' => false, 'message' => sprintf(__('mu-plugins directory is not writable: %s', 'wp-seo-next'), $dir)];
        }

        $file = self::muPluginFile();
        $code = self::muPluginSource();

        $ok = @file_put_contents($file, $code, LOCK_EX);
        if ($ok === false) {
            return ['ok' => false, 'message' => sprintf(__('Failed to write MU-plugin file: %s', 'wp-seo-next'), $file)];
        }

        return ['ok' => true, 'message' => __('Recovery helper installed (MU-plugin).', 'wp-seo-next')];
    }

    /**
     * @return array{ok:bool,message:string}
     */
    public static function uninstall(): array
    {
        $file = self::muPluginFile();
        if (!file_exists($file)) {
            return ['ok' => true, 'message' => __('Recovery helper is not installed.', 'wp-seo-next')];
        }
        if (!is_writable($file)) {
            return ['ok' => false, 'message' => sprintf(__('MU-plugin file is not writable: %s', 'wp-seo-next'), $file)];
        }
        $ok = @unlink($file);
        if (!$ok) {
            return ['ok' => false, 'message' => sprintf(__('Failed to remove MU-plugin file: %s', 'wp-seo-next'), $file)];
        }
        return ['ok' => true, 'message' => __('Recovery helper removed.', 'wp-seo-next')];
    }

    /**
     * Best-effort update of the installed MU-plugin if it matches our marker.
     *
     * This is intentionally silent and never throws.
     */
    public static function maybeUpdateMuPlugin(): void
    {
        try {
            if (!self::isInstalled()) {
                return;
            }

            $file = self::muPluginFile();
            if (!is_readable($file) || !is_writable($file)) {
                return;
            }

            $current = (string) file_get_contents($file);
            $desired = self::muPluginSource();

            if ($current !== $desired) {
                // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
                @file_put_contents($file, $desired, LOCK_EX);
            }
        } catch (\Throwable $e) {
            // Ignore.
        }
    }

    /**
     * Compute a signed token. Must match the algorithm embedded in the MU-plugin.
     */
    public static function token(string $action, int $daysAgo = 0): string
    {
        $action = preg_replace('/[^a-z0-9_]/i', '', strtolower($action));
        $ts = time() - ($daysAgo * 86400);
        $date = gmdate('Y-m-d', $ts);

        $secret = '';
        foreach (['AUTH_KEY', 'SECURE_AUTH_KEY', 'LOGGED_IN_KEY', 'NONCE_SALT', 'AUTH_SALT', 'SECURE_AUTH_SALT', 'LOGGED_IN_SALT'] as $k) {
            if (defined($k)) {
                $secret .= (string) constant($k);
            }
        }
        if ($secret === '') {
            $secret = __FILE__;
        }

        return hash_hmac('sha256', 'wpsn_recovery|' . $action . '|' . $date, $secret);
    }

    
    /**
     * Signed recovery URL (works without login — keep private).
     * The MU-plugin accepts today's token (+ yesterday).
     */
    public static function recoveryUrl(string $action, string $redirect = ''): string
    {
        $base = function_exists('home_url') ? (string) home_url('/') : '/';
        $redirect = $redirect !== '' ? $redirect : (function_exists('admin_url') ? (string) admin_url('/') : '/');

        // Today's token should work; MU-plugin also accepts yesterday (time drift).
        $token = self::token($action, 0);

        if (function_exists('add_query_arg')) {
            return (string) add_query_arg([
                'wpsn_recovery' => $action,
                'token' => $token,
                'redirect' => $redirect,
            ], $base);
        }

        // Fallback without add_query_arg.
        $sep = strpos($base, '?') === false ? '?' : '&';
        return $base . $sep . 'wpsn_recovery=' . rawurlencode($action) . '&token=' . rawurlencode($token) . '&redirect=' . rawurlencode($redirect);
    }

    /**
     * Admin-only recovery URL (no token). Works only if the user is logged-in and can activate plugins.
     */
    public static function recoveryUrlAdminOnly(string $action, string $redirect = ''): string
    {
        $base = function_exists('home_url') ? (string) home_url('/') : '/';
        $redirect = $redirect !== '' ? $redirect : (function_exists('admin_url') ? (string) admin_url('/') : '/');

        if (function_exists('add_query_arg')) {
            return (string) add_query_arg([
                'wpsn_recovery' => $action,
                'redirect' => $redirect,
            ], $base);
        }

        $sep = strpos($base, '?') === false ? '?' : '&';
        return $base . $sep . 'wpsn_recovery=' . rawurlencode($action) . '&redirect=' . rawurlencode($redirect);
    }

    public static function muPluginSource(): string
    {
        // NOTE: Keep this file self-contained. It must work even when the main plugin is broken.
        return <<<'PHP'
<?php
/**
 * Plugin Name: WPSN Recovery Helper (MU)
 * Description: Emergency recovery actions for WP SEO Next (disable plugin / restore config snapshot) and adds recovery buttons on fatal error pages.
 *
 * Marker: WPSN Recovery Helper (MU)
 */

if (!defined('ABSPATH')) {
    exit;
}

// --- Token helpers (signed by wp-config secrets; rotates daily). ---

function wpsn_recovery_secret(): string {
    $secret = '';
    foreach (['AUTH_KEY','SECURE_AUTH_KEY','LOGGED_IN_KEY','NONCE_SALT','AUTH_SALT','SECURE_AUTH_SALT','LOGGED_IN_SALT'] as $k) {
        if (defined($k)) {
            $secret .= (string) constant($k);
        }
    }
    if ($secret === '') {
        $secret = __FILE__;
    }
    return $secret;
}

function wpsn_recovery_token(string $action, int $daysAgo = 0): string {
    $action = preg_replace('/[^a-z0-9_]/i', '', strtolower($action));
    $ts = time() - ($daysAgo * 86400);
    $date = gmdate('Y-m-d', $ts);
    return hash_hmac('sha256', 'wpsn_recovery|' . $action . '|' . $date, wpsn_recovery_secret());
}

function wpsn_recovery_verify(string $action, string $token): bool {
    $action = preg_replace('/[^a-z0-9_]/i', '', strtolower($action));
    $token = (string) $token;
    if ($token === '') {
        return false;
    }
    // Accept today + yesterday to tolerate timezone / cache skew.
    return hash_equals(wpsn_recovery_token($action, 0), $token) || hash_equals(wpsn_recovery_token($action, 1), $token);
}

// --- Core actions ---

function wpsn_recovery_find_plugin_basename(): string {
    // Default expected basename.
    $default = 'wp-seo-next/wp-seo-next.php';

    if (!function_exists('get_option')) {
        return $default;
    }

    $act = get_option('active_plugins', []);
    if (is_array($act)) {
        foreach ($act as $p) {
            $p = (string) $p;
            if ($p === $default) {
                return $p;
            }
            if (substr($p, -strlen('/wp-seo-next.php')) === '/wp-seo-next.php') {
                return $p;
            }
        }
    }

    if (function_exists('get_site_option')) {
        $net = get_site_option('active_sitewide_plugins', []);
        if (is_array($net)) {
            foreach ($net as $p => $time) {
                $p = (string) $p;
                if ($p === $default) {
                    return $p;
                }
                if (substr($p, -strlen('/wp-seo-next.php')) === '/wp-seo-next.php') {
                    return $p;
                }
            }
        }
    }

    return $default;
}

function wpsn_recovery_deactivate(): void {
    if (!function_exists('get_option') || !function_exists('update_option')) {
        return;
    }
    $basename = wpsn_recovery_find_plugin_basename();

    $act = get_option('active_plugins', []);
    if (is_array($act)) {
        $act = array_values(array_filter($act, function($p) use ($basename) {
            return (string) $p !== $basename;
        }));
        update_option('active_plugins', $act, true);
    }

    if (function_exists('is_multisite') && is_multisite() && function_exists('get_site_option') && function_exists('update_site_option')) {
        $net = get_site_option('active_sitewide_plugins', []);
        if (is_array($net) && isset($net[$basename])) {
            unset($net[$basename]);
            update_site_option('active_sitewide_plugins', $net);
        }
    }

    // Marker for admins.
    if (function_exists('update_option')) {
        update_option('wpsn_recovery_last_action', ['action' => 'disable', 'at' => gmdate('c')], false);
    }
}

function wpsn_recovery_restore_latest_config_snapshot(): bool {
    global $wpdb;
    if (!$wpdb || !isset($wpdb->prefix)) {
        return false;
    }
    $table = $wpdb->prefix . 'wpsn_snapshots';

    // Check table exists (best-effort).
    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
    $exists = $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $table));
    if (!$exists) {
        return false;
    }

    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
    $row = $wpdb->get_row($wpdb->prepare("SELECT payload FROM {$table} WHERE type = %s ORDER BY id DESC LIMIT 1", 'config'), ARRAY_A);
    if (!is_array($row) || empty($row['payload'])) {
        return false;
    }

    $payload = json_decode((string) $row['payload'], true);
    if (!is_array($payload)) {
        return false;
    }

    $settings = isset($payload['settings']) && is_array($payload['settings']) ? $payload['settings'] : $payload;
    if (!is_array($settings)) {
        $settings = [];
    }

    if (function_exists('update_option')) {
        update_option('wpsn_settings', $settings, false);
        update_option('wpsn_recovery_last_action', ['action' => 'restore_config', 'at' => gmdate('c')], false);
        return true;
    }

    return false;
}

function wpsn_recovery_set_compat_flag(string $flag): bool {
    if (!function_exists('get_option') || !function_exists('update_option')) {
        return false;
    }

    $flag = preg_replace('/[^a-z0-9_]/i', '', strtolower((string) $flag));
    if ($flag !== 'safe_mode' && $flag !== 'disable_output') {
        return false;
    }

    $settings = get_option('wpsn_settings', []);
    if (!is_array($settings)) {
        $settings = [];
    }
    if (!isset($settings['compatibility']) || !is_array($settings['compatibility'])) {
        $settings['compatibility'] = [];
    }

    if ($flag === 'safe_mode') {
        $settings['compatibility']['safe_mode'] = true;
    }
    if ($flag === 'disable_output') {
        $settings['compatibility']['disable_output'] = true;
    }

    update_option('wpsn_settings', $settings, false);
    update_option('wpsn_recovery_last_action', ['action' => $flag, 'at' => gmdate('c')], false);
    return true;
}

// --- Request handler (runs before normal plugins load). ---

if (isset($_GET['wpsn_recovery'])) {
    $action = (string) $_GET['wpsn_recovery'];
    $action = preg_replace('/[^a-z0-9_]/i', '', strtolower($action));
    $token = isset($_GET['token']) ? (string) $_GET['token'] : '';

    $allowed = false;

    if ($token !== '' && wpsn_recovery_verify($action, $token)) {
        $allowed = true;
    }

    // Admin-only mode (no token): allow logged-in users who can activate plugins.
    // NOTE: MU-plugins load BEFORE wp-includes/pluggable.php, so we must load it on-demand.
    if (!$allowed && $token === '') {
        if (!function_exists('is_user_logged_in')) {
            $pluggable = ABSPATH . WPINC . '/pluggable.php';
            if (is_readable($pluggable)) {
                require_once $pluggable;
            }
        }
        if (function_exists('wp_get_current_user')) {
            // Initialize current user (cookie-based).
            wp_get_current_user();
        }
        if (function_exists('is_user_logged_in') && function_exists('current_user_can')) {
            if (is_user_logged_in() && current_user_can('activate_plugins')) {
                $allowed = true;
            }
        }
    }

    if (!$allowed) {
        header('Content-Type: text/plain; charset=utf-8');
        echo "Unauthorized recovery request.\n";
        exit;
    }

    if ($action === 'disable') {
        wpsn_recovery_deactivate();
    } elseif ($action === 'restore_config') {
        wpsn_recovery_restore_latest_config_snapshot();
    } elseif ($action === 'safe_mode') {
        wpsn_recovery_set_compat_flag('safe_mode');
    } elseif ($action === 'disable_output') {
        wpsn_recovery_set_compat_flag('disable_output');
    }

    $redirect = isset($_GET['redirect']) ? (string) $_GET['redirect'] : '';
    if ($redirect === '') {
        $redirect = function_exists('admin_url') ? (string) admin_url('/') : '/';
    }

    // Basic safe redirect (no wp_safe_redirect needed).
    header('Location: ' . $redirect, true, 302);
    exit;
}

// --- Fatal error page enhancement (show buttons only when wp-seo-next is implicated). ---

if (function_exists('add_filter')) {
    add_filter('wp_php_error_message', function($message, $error) {
        if (!is_array($error)) {
            return $message;
        }
        $file = isset($error['file']) ? (string) $error['file'] : '';
        // Only show for fatals that appear to originate from WP SEO Next.
        if ($file === '' || strpos($file, 'wp-seo-next') === false) {
            return $message;
        }

        // Only show recovery buttons to logged-in admins.
        if (!function_exists('is_user_logged_in') || !function_exists('current_user_can')) {
            return $message;
        }
        if (!is_user_logged_in() || !current_user_can('activate_plugins')) {
            return $message;
        }

        // Build recovery URLs.
        $base = function_exists('home_url') ? (string) home_url('/') : (function_exists('site_url') ? (string) site_url('/') : '/');
        $redirect = function_exists('admin_url') ? (string) admin_url('/') : '/';

        // Prefer signed URLs so recovery works even before pluggable.php is loaded.
        $disable = function_exists('add_query_arg')
            ? (string) add_query_arg(['wpsn_recovery' => 'disable', 'token' => wpsn_recovery_token('disable', 0), 'redirect' => $redirect], $base)
            : $base;

        $safe = function_exists('add_query_arg')
            ? (string) add_query_arg(['wpsn_recovery' => 'safe_mode', 'token' => wpsn_recovery_token('safe_mode', 0), 'redirect' => $redirect], $base)
            : $base;

        $out = function_exists('add_query_arg')
            ? (string) add_query_arg(['wpsn_recovery' => 'disable_output', 'token' => wpsn_recovery_token('disable_output', 0), 'redirect' => $redirect], $base)
            : $base;

        $restore = function_exists('add_query_arg')
            ? (string) add_query_arg(['wpsn_recovery' => 'restore_config', 'token' => wpsn_recovery_token('restore_config', 0), 'redirect' => $redirect], $base)
            : $base;

        $esc = function($s) {
            return htmlspecialchars((string) $s, ENT_QUOTES, 'UTF-8');
        };

        $html  = "<hr style=\"margin:18px 0;\" />";
        $html .= "<p><strong>WP SEO Next recovery</strong></p>";
        $html .= "<p>";
        $html .= "<a class=\"button button-primary\" href=\"" . $esc($disable) . "\">Disable WP SEO Next</a> ";
        $html .= "<a class=\"button\" href=\"" . $esc($safe) . "\">Enable Safe Mode</a> ";
        $html .= "<a class=\"button\" href=\"" . $esc($out) . "\">Disable Output</a> ";
        $html .= "<a class=\"button\" href=\"" . $esc($restore) . "\">Restore last config snapshot</a>";
        $html .= "</p>";
        $html .= "<p class=\"description\" style=\"margin-top:6px;\">Actions are restricted to administrators. Links are signed and work even if the main plugin is broken.</p>";

        return $message . $html;
    }, 10, 2);
}

PHP;
    }
}
