<?php

namespace WPSN\Modules\Revisions\Admin;

use WPSN\Core\Capabilities;
use WPSN\Core\Container;
use WPSN\Core\Logger;
use WPSN\Admin\SettingsRepository;
use WPSN\Modules\Redirects\RedirectRepository;
use WPSN\Modules\Redirects\RedirectsInstaller;
use WPSN\Modules\Redirects\RedirectsUrl;
use WPSN\Modules\Revisions\RevisionsInstaller;
use WPSN\Modules\Revisions\RevisionsRepository;

defined('ABSPATH') || exit;

final class RevisionsAdmin
{
    private Container $container;
    private Logger $logger;

    private ?string $hookSuffix = null;

    public function __construct(Container $container)
    {
        $this->container = $container;
        $this->logger = $container->get('logger');
    }

    public function register(): void
    {
        add_action('admin_menu', [$this, 'addMenu']);
        add_action('admin_post_wpsn_revisions_rollback', [$this, 'handleRollback']);
    }

    public function addMenu(): void
    {
        $this->hookSuffix = add_management_page(
            __('Revisions', 'wp-seo-next'),
            __('Revisions', 'wp-seo-next'),
            Capabilities::CAP_TOOLS,
            'wpsn-revisions',
            [$this, 'render']
        );
    }

    private function pageUrl(array $args = []): string
    {
        $base = admin_url('tools.php');
        $args = array_merge(['page' => 'wpsn-revisions'], $args);
        return (string) add_query_arg($args, $base);
    }

    private function redirectWithNotice(string $msg, string $type = 'success'): void
    {
        $url = $this->pageUrl([
            'wpsn_notice' => rawurlencode($msg),
            'wpsn_notice_type' => $type,
        ]);
        wp_safe_redirect($url);
        exit;
    }

    public function render(): void
    {
        if (!Capabilities::canManageTools()) {
            wp_die(esc_html__('You do not have permission to access this page.', 'wp-seo-next'));
        }

        RevisionsInstaller::maybeInstall();
        $repo = new RevisionsRepository($this->logger);

        $notice = isset($_GET['wpsn_notice']) ? sanitize_text_field((string) wp_unslash($_GET['wpsn_notice'])) : '';
        $noticeType = isset($_GET['wpsn_notice_type']) ? sanitize_key((string) wp_unslash($_GET['wpsn_notice_type'])) : 'success';
        if (!in_array($noticeType, ['success', 'error', 'warning', 'info'], true)) {
            $noticeType = 'success';
        }

        $source = isset($_GET['source']) ? sanitize_key((string) wp_unslash($_GET['source'])) : '';
        $objectType = isset($_GET['object_type']) ? sanitize_key((string) wp_unslash($_GET['object_type'])) : '';
        $objectId = isset($_GET['object_id']) ? (int) $_GET['object_id'] : 0;
        $paged = isset($_GET['paged']) ? (int) $_GET['paged'] : 1;
        $paged = max(1, $paged);
        $perPage = isset($_GET['per_page']) ? (int) $_GET['per_page'] : 50;
        $perPage = max(10, min(200, $perPage));
        $offset = ($paged - 1) * $perPage;

        $revisionId = isset($_GET['revision_id']) ? (int) $_GET['revision_id'] : 0;
        $selected = $revisionId > 0 ? $repo->get($revisionId) : null;

        $list = $repo->list([
            'source' => $source,
            'object_type' => $objectType,
            'object_id' => $objectId,
            'limit' => $perPage,
            'offset' => $offset,
        ]);

        $rows = $list['rows'] ?? [];
        $total = (int) ($list['total'] ?? 0);
        $totalPages = (int) ceil($total / max(1, $perPage));

        $sources = [
            '' => __('All', 'wp-seo-next'),
            'meta_tags' => 'Meta tags',
            'admin_lists' => 'Admin lists',
            'bulk' => 'Bulk',
            'media' => 'Media',
            'internal_links' => 'Internal links',
            'snapshots' => 'Snapshots',
            'redirects' => 'Redirects',
            'config_sync' => 'Config Sync',
            'schema' => 'Schema',
            'revisions' => 'Revisions',
        ];
        $objectTypes = [
            '' => __('All', 'wp-seo-next'),
            'post' => 'Post',
            'term' => 'Term',
            'option' => 'Option',
            'redirect' => 'Redirect',
            'user' => 'User',
        ];

        ?>
        <div class="wrap">
            <h1><?php echo esc_html__('Revisions', 'wp-seo-next'); ?></h1>

            <?php if ($notice !== '') : ?>
                <div class="notice notice-<?php echo esc_attr($noticeType); ?> is-dismissible"><p><?php echo esc_html($notice); ?></p></div>
            <?php endif; ?>

            <p class="description">
                <?php echo esc_html__('Unified change history for actions performed by WP SEO Next. You can inspect changes and roll back safely.', 'wp-seo-next'); ?>
            </p>

            <form method="get" action="<?php echo esc_url(admin_url('tools.php')); ?>" style="margin: 12px 0;">
                <input type="hidden" name="page" value="wpsn-revisions" />

                <label style="margin-right:10px;">
                    <?php echo esc_html__('Source', 'wp-seo-next'); ?>
                    <select name="source">
                        <?php foreach ($sources as $k => $label) : ?>
                            <option value="<?php echo esc_attr($k); ?>" <?php selected($source, $k); ?>><?php echo esc_html($label); ?></option>
                        <?php endforeach; ?>
                    </select>
                </label>

                <label style="margin-right:10px;">
                    <?php echo esc_html__('Object type', 'wp-seo-next'); ?>
                    <select name="object_type">
                        <?php foreach ($objectTypes as $k => $label) : ?>
                            <option value="<?php echo esc_attr($k); ?>" <?php selected($objectType, $k); ?>><?php echo esc_html($label); ?></option>
                        <?php endforeach; ?>
                    </select>
                </label>

                <label style="margin-right:10px;">
                    <?php echo esc_html__('Object ID', 'wp-seo-next'); ?>
                    <input type="number" name="object_id" value="<?php echo (int) $objectId; ?>" style="width:120px;" />
                </label>

                <label style="margin-right:10px;">
                    <?php echo esc_html__('Per page', 'wp-seo-next'); ?>
                    <input type="number" min="10" max="200" name="per_page" value="<?php echo (int) $perPage; ?>" style="width:90px;" />
                </label>

                <button class="button button-secondary"><?php echo esc_html__('Filter', 'wp-seo-next'); ?></button>
            </form>

            <table class="widefat striped">
                <thead>
                    <tr>
                        <th><?php echo esc_html__('ID', 'wp-seo-next'); ?></th>
                        <th><?php echo esc_html__('Date', 'wp-seo-next'); ?></th>
                        <th><?php echo esc_html__('Source', 'wp-seo-next'); ?></th>
                        <th><?php echo esc_html__('Object', 'wp-seo-next'); ?></th>
                        <th><?php echo esc_html__('Actor', 'wp-seo-next'); ?></th>
                        <th><?php echo esc_html__('Summary', 'wp-seo-next'); ?></th>
                        <th><?php echo esc_html__('Actions', 'wp-seo-next'); ?></th>
                    </tr>
                </thead>
                <tbody>
                    <?php if (empty($rows)) : ?>
                        <tr><td colspan="7"><?php echo esc_html__('No revisions yet.', 'wp-seo-next'); ?></td></tr>
                    <?php else : ?>
                        <?php foreach ($rows as $r) : ?>
                            <?php
                            $id = (int) ($r['id'] ?? 0);
                            $objType = (string) ($r['object_type'] ?? '');
                            $objId = (int) ($r['object_id'] ?? 0);
                            $src = (string) ($r['source'] ?? '');
                            $actorId = (int) ($r['actor_user_id'] ?? 0);
                            $actor = $actorId > 0 ? get_userdata($actorId) : null;
                            $actorName = $actor ? $actor->display_name : '';
                            $createdAt = (string) ($r['created_at'] ?? '');

                            $objLabel = $this->formatObjectLabel($r);
                            $summary = $this->summarizeRevision($r);

                            $viewUrl = $this->pageUrl([
                                'source' => $source,
                                'object_type' => $objectType,
                                'object_id' => $objectId,
                                'per_page' => $perPage,
                                'paged' => $paged,
                                'revision_id' => $id,
                            ]);

                            $rolledBackAt = isset($r['rolled_back_at']) ? (string) $r['rolled_back_at'] : '';
                            $rolled = ($rolledBackAt !== '' && $rolledBackAt !== '0000-00-00 00:00:00');

                            $canRollback = $this->canRollbackRow($r);

                            $rollbackUrl = wp_nonce_url(
                                admin_url('admin-post.php?action=wpsn_revisions_rollback&revision_id=' . $id),
                                'wpsn_revisions_rollback_' . $id,
                                'wpsn_nonce'
                            );
                            ?>
                            <tr>
                                <td><?php echo (int) $id; ?></td>
                                <td><?php echo esc_html($createdAt); ?></td>
                                <td><?php echo esc_html($src !== '' ? $src : '-'); ?></td>
                                <td><?php echo $objLabel; ?></td>
                                <td><?php echo esc_html($actorName !== '' ? $actorName : '-'); ?></td>
                                <td><?php echo esc_html($summary); ?></td>
                                <td>
                                    <a class="button button-small" href="<?php echo esc_url($viewUrl); ?>"><?php echo esc_html__('View', 'wp-seo-next'); ?></a>
                                    <?php if (!$rolled && $canRollback) : ?>
                                        <a class="button button-small" href="<?php echo esc_url($rollbackUrl); ?>" onclick="return confirm('<?php echo esc_js(__('Rollback this change? This will restore the previous values.', 'wp-seo-next')); ?>');"><?php echo esc_html__('Rollback', 'wp-seo-next'); ?></a>
                                    <?php elseif ($rolled) : ?>
                                        <span style="opacity:.7;">(<?php echo esc_html__('rolled back', 'wp-seo-next'); ?>)</span>
                                    <?php else : ?>
                                        <span style="opacity:.7;">(<?php echo esc_html__('not supported', 'wp-seo-next'); ?>)</span>
                                    <?php endif; ?>
                                </td>
                            </tr>
                        <?php endforeach; ?>
                    <?php endif; ?>
                </tbody>
            </table>

            <?php if ($totalPages > 1) : ?>
                <div class="tablenav" style="margin-top: 10px;">
                    <div class="tablenav-pages">
                        <?php
                        $baseArgs = [
                            'source' => $source,
                            'object_type' => $objectType,
                            'object_id' => $objectId,
                            'per_page' => $perPage,
                        ];
                        $prev = max(1, $paged - 1);
                        $next = min($totalPages, $paged + 1);
                        ?>
                        <a class="button" href="<?php echo esc_url($this->pageUrl(array_merge($baseArgs, ['paged' => $prev]))); ?>" <?php echo $paged <= 1 ? 'disabled' : ''; ?>><?php echo esc_html__('Prev', 'wp-seo-next'); ?></a>
                        <span style="margin: 0 8px;">
                            <?php echo esc_html(sprintf(__('Page %d of %d', 'wp-seo-next'), $paged, $totalPages)); ?>
                        </span>
                        <a class="button" href="<?php echo esc_url($this->pageUrl(array_merge($baseArgs, ['paged' => $next]))); ?>" <?php echo $paged >= $totalPages ? 'disabled' : ''; ?>><?php echo esc_html__('Next', 'wp-seo-next'); ?></a>
                    </div>
                </div>
            <?php endif; ?>

            <?php if (is_array($selected)) : ?>
                <hr style="margin: 18px 0;" />
                <h2><?php echo esc_html(sprintf(__('Revision #%d', 'wp-seo-next'), (int) ($selected['id'] ?? 0))); ?></h2>
                <p>
                    <strong><?php echo esc_html__('Source', 'wp-seo-next'); ?>:</strong> <?php echo esc_html((string) ($selected['source'] ?? '')); ?> &nbsp;|
                    <strong><?php echo esc_html__('Action', 'wp-seo-next'); ?>:</strong> <?php echo esc_html((string) ($selected['action'] ?? '')); ?> &nbsp;|
                    <strong><?php echo esc_html__('Date', 'wp-seo-next'); ?>:</strong> <?php echo esc_html((string) ($selected['created_at'] ?? '')); ?>
                </p>
                <div style="display:flex; gap:16px;">
                    <div style="flex:1;">
                        <h3><?php echo esc_html__('Before', 'wp-seo-next'); ?></h3>
                        <pre style="max-height:320px; overflow:auto; background:#f6f7f7; padding:10px; border:1px solid #e5e5e5;"><?php echo esc_html(wp_json_encode($selected['before_decoded'] ?? null, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); ?></pre>
                    </div>
                    <div style="flex:1;">
                        <h3><?php echo esc_html__('After', 'wp-seo-next'); ?></h3>
                        <pre style="max-height:320px; overflow:auto; background:#f6f7f7; padding:10px; border:1px solid #e5e5e5;"><?php echo esc_html(wp_json_encode($selected['after_decoded'] ?? null, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); ?></pre>
                    </div>
                </div>
            <?php endif; ?>
        </div>
        <?php
    }

    /**
     * @param array<string,mixed> $row
     */
    private function formatObjectLabel(array $row): string
    {
        $type = (string) ($row['object_type'] ?? '');
        $id   = (int) ($row['object_id'] ?? 0);
        $sub  = (string) ($row['object_subtype'] ?? '');

        if ($type === 'post') {
            $title = $id > 0 ? (string) get_the_title($id) : '';
            $title = $title !== '' ? $title : sprintf(__('Post #%d', 'wp-seo-next'), $id);
            $editUrl = $id > 0 ? get_edit_post_link($id, '') : '';
            if (is_string($editUrl) && $editUrl !== '') {
                return '<a href="' . esc_url($editUrl) . '">' . esc_html($title) . '</a>';
            }
            return esc_html($title);
        }

        if ($type === 'term') {
            $term = null;
            if ($id > 0) {
                $term = $sub !== '' ? get_term($id, $sub) : get_term($id);
                if (is_wp_error($term)) {
                    $term = null;
                }
            }

            $name = $term ? (string) ($term->name ?? '') : '';
            $tax = $term ? (string) ($term->taxonomy ?? '') : ($sub !== '' ? $sub : '');

            $label = $name !== '' ? $name : sprintf(__('Term #%d', 'wp-seo-next'), $id);
            if ($tax !== '') {
                $label .= ' (' . $tax . ')';
            }

            $url = ($id > 0 && $tax !== '' && function_exists('get_edit_term_link')) ? get_edit_term_link($id, $tax) : '';
            if (is_string($url) && $url !== '') {
                return '<a href="' . esc_url($url) . '">' . esc_html($label) . '</a>';
            }
            return esc_html($label);
        }

        if ($type === 'option') {
            $label = $sub !== '' ? $sub : __('Option', 'wp-seo-next');
            if ($sub === SettingsRepository::OPTION_NAME) {
                $label = __('WP SEO Next settings', 'wp-seo-next');
            }
            return esc_html($label);
        }

        if ($type === 'redirect') {
            $label = $id > 0 ? sprintf(__('Redirect #%d', 'wp-seo-next'), $id) : __('Redirect', 'wp-seo-next');

            $redir = null;
            $after = $row['after_decoded'] ?? null;
            $before = $row['before_decoded'] ?? null;
            if (is_array($after) && isset($after['redirect']) && is_array($after['redirect'])) {
                $redir = $after['redirect'];
            } elseif (is_array($before) && isset($before['redirect']) && is_array($before['redirect'])) {
                $redir = $before['redirect'];
            }

            if (is_array($redir)) {
                $from = isset($redir['from_url']) ? (string) $redir['from_url'] : '';
                $to = array_key_exists('to_url', $redir) ? $redir['to_url'] : null;
                $toStr = is_string($to) ? (string) $to : '';
                if ($from !== '') {
                    $label = $from . ' → ' . ($toStr !== '' ? $toStr : '∅');
                }
            }

            $editUrl = $id > 0 ? add_query_arg(['page' => 'wpsn-redirects', 'edit' => $id], admin_url('tools.php')) : '';
            if (is_string($editUrl) && $editUrl !== '') {
                return '<a href="' . esc_url($editUrl) . '">' . esc_html($label) . '</a>';
            }
            return esc_html($label);
        }

        if ($type === 'user') {
            $u = $id > 0 ? get_userdata($id) : null;
            $name = ($u && !empty($u->display_name)) ? (string) $u->display_name : ($id > 0 ? ('User #' . $id) : __('User', 'wp-seo-next'));
            $url = $id > 0 ? get_edit_user_link($id) : '';
            if (is_string($url) && $url !== '') {
                return '<a href="' . esc_url($url) . '">' . esc_html($name) . '</a>';
            }
            return esc_html($name);
        }

        return esc_html($type . ($id > 0 ? '#' . $id : ''));
    }

    /**
     * @param array<string,mixed> $row
     */
    private function summarizeRevision(array $row): string
    {
        $before = $row['before_decoded'] ?? null;
        $after  = $row['after_decoded'] ?? null;

        $parts = [];

        $type = (string) ($row['object_type'] ?? '');

        if ($type === 'redirect') {
            $b = (is_array($before) && array_key_exists('redirect', $before)) ? $before['redirect'] : null;
            $a = (is_array($after) && array_key_exists('redirect', $after)) ? $after['redirect'] : null;

            $bRow = is_array($b) ? $b : null;
            $aRow = is_array($a) ? $a : null;

            if ($bRow && $aRow) {
                $changed = [];
                foreach (['from_url', 'to_url', 'match_type', 'status_code', 'enabled'] as $k) {
                    $bv = $bRow[$k] ?? null;
                    $av = $aRow[$k] ?? null;
                    if ($bv !== $av) {
                        $changed[] = $k;
                    }
                }
                if (!empty($changed)) {
                    $parts[] = 'fields: ' . implode(', ', array_slice($changed, 0, 5));
                }
            } elseif ($bRow && !$aRow) {
                $parts[] = __('deleted', 'wp-seo-next');
            } elseif (!$bRow && $aRow) {
                $parts[] = __('created', 'wp-seo-next');
            }
        }

        if ($type === 'user') {
            if (is_array($before) && isset($before['user_meta']) && is_array($before['user_meta'])) {
                $keys = array_keys($before['user_meta']);
                $keys = array_values(array_filter(array_map('strval', $keys)));
                if (!empty($keys)) {
                    $parts[] = 'user_meta: ' . implode(', ', array_slice($keys, 0, 4)) . (count($keys) > 4 ? '…' : '');
                }
            }
        }

        if ($type === 'option') {
            $optName = '';
            if (is_array($before) && isset($before['option']) && is_array($before['option'])) {
                $optName = isset($before['option']['name']) ? (string) $before['option']['name'] : '';
            }
            if ($optName === '' && is_array($after) && isset($after['option']) && is_array($after['option'])) {
                $optName = isset($after['option']['name']) ? (string) $after['option']['name'] : '';
            }

            if ($optName !== '') {
                if ($optName === SettingsRepository::OPTION_NAME) {
                    $optName = 'settings';
                }

                $bv = null;
                $av = null;
                if (is_array($before) && isset($before['option']) && is_array($before['option']) && array_key_exists('value', $before['option'])) {
                    $bv = $before['option']['value'];
                }
                if (is_array($after) && isset($after['option']) && is_array($after['option']) && array_key_exists('value', $after['option'])) {
                    $av = $after['option']['value'];
                }

                $changed = [];
                if (is_array($bv) && is_array($av)) {
                    $keys = array_unique(array_merge(array_keys($bv), array_keys($av)));
                    foreach ($keys as $k) {
                        if (!is_string($k)) {
                            continue;
                        }
                        $bvv = $bv[$k] ?? null;
                        $avv = $av[$k] ?? null;
                        if ($bvv !== $avv) {
                            $changed[] = $k;
                        }
                    }
                    if (!empty($changed)) {
                        $parts[] = $optName . ': ' . implode(', ', array_slice($changed, 0, 4)) . (count($changed) > 4 ? '…' : '');
                    } else {
                        $parts[] = $optName;
                    }
                } else {
                    $parts[] = $optName;
                }
            }
        }

        if (is_array($before) && isset($before['meta']) && is_array($before['meta'])) {
            $keys = array_keys($before['meta']);
            $keys = array_values(array_filter(array_map('strval', $keys)));
            if (!empty($keys)) {
                $parts[] = 'meta: ' . implode(', ', array_slice($keys, 0, 4)) . (count($keys) > 4 ? '…' : '');
            }
        }

        if (is_array($before) && isset($before['post']) && is_array($before['post'])) {
            if (array_key_exists('post_content', $before['post'])) {
                $parts[] = 'post_content';
            }
        }

        if (is_array($before) && isset($before['elementor']) && is_array($before['elementor'])) {
            if (array_key_exists('_elementor_data', $before['elementor'])) {
                $parts[] = 'elementor';
            }
        }

        if (empty($parts) && is_array($after)) {
            $parts[] = __('Change', 'wp-seo-next');
        }

        return implode(' | ', $parts);
    }

    /**
     * @param array<string,mixed> $row
     */
    private function canRollbackRow(array $row): bool
    {
        $type = (string) ($row['object_type'] ?? '');
        return in_array($type, ['post', 'term', 'option', 'redirect', 'user'], true);
    }

    /**
     * @param mixed $row
     * @return array<string,mixed>|null
     */
    private function sanitizeRedirectRow($row): ?array
    {
        if (!is_array($row)) {
            return null;
        }

        $to = null;
        if (array_key_exists('to_url', $row)) {
            $to = $row['to_url'];
            if (!is_string($to) && $to !== null) {
                $to = null;
            }
        }

        $lastHit = null;
        if (array_key_exists('last_hit', $row)) {
            $lastHit = $row['last_hit'];
            if (!is_string($lastHit) && $lastHit !== null) {
                $lastHit = null;
            }
        }

        return [
            'id' => (int) ($row['id'] ?? 0),
            'from_url' => isset($row['from_url']) ? (string) $row['from_url'] : '',
            'to_url' => $to,
            'match_type' => isset($row['match_type']) ? (string) $row['match_type'] : 'exact',
            'status_code' => isset($row['status_code']) ? (int) $row['status_code'] : 301,
            'enabled' => !empty($row['enabled']),
            'hits' => isset($row['hits']) ? (int) $row['hits'] : 0,
            'last_hit' => $lastHit,
            'created_at' => isset($row['created_at']) ? (string) $row['created_at'] : '',
            'updated_at' => isset($row['updated_at']) ? (string) $row['updated_at'] : '',
        ];
    }

    public function handleRollback(): void
    {
        if (!Capabilities::canManageTools()) {
            wp_die(esc_html__('Permission denied.', 'wp-seo-next'));
        }

        $id = isset($_GET['revision_id']) ? (int) $_GET['revision_id'] : 0;
        check_admin_referer('wpsn_revisions_rollback_' . $id, 'wpsn_nonce');

        if ($id <= 0) {
            $this->redirectWithNotice(__('Invalid revision ID.', 'wp-seo-next'), 'error');
        }

        RevisionsInstaller::maybeInstall();
        $repo = new RevisionsRepository($this->logger);
        $row = $repo->get($id);
        if (!is_array($row)) {
            $this->redirectWithNotice(__('Revision not found.', 'wp-seo-next'), 'error');
        }

        $rolledBackAt = isset($row['rolled_back_at']) ? (string) $row['rolled_back_at'] : '';
        if ($rolledBackAt !== '' && $rolledBackAt !== '0000-00-00 00:00:00') {
            $this->redirectWithNotice(__('This revision has already been rolled back.', 'wp-seo-next'), 'warning');
        }

        $objectType = (string) ($row['object_type'] ?? '');
        $objectId   = (int) ($row['object_id'] ?? 0);
        $objectSubtype = (string) ($row['object_subtype'] ?? '');

        $before = $row['before_decoded'] ?? null;
        if (!is_array($before)) {
            $this->redirectWithNotice(__('Nothing to roll back (missing before state).', 'wp-seo-next'), 'error');
        }

        $actor = (int) get_current_user_id();

        // Build current state snapshot (same shape as $before) before applying rollback.
        $current = [];

        if ($objectType === 'post') {
            if ($objectId <= 0 || !current_user_can('edit_post', $objectId)) {
                $this->redirectWithNotice(__('You do not have permission to roll back this post.', 'wp-seo-next'), 'error');
            }

            $post = get_post($objectId);
            if (!$post) {
                $this->redirectWithNotice(__('Post not found.', 'wp-seo-next'), 'error');
            }

            $didMeta = false;

            // Meta restore.
            if (isset($before['meta']) && is_array($before['meta'])) {
                $current['meta'] = [];
                foreach ($before['meta'] as $k => $v) {
                    $key = is_string($k) ? $k : '';
                    if ($key === '') {
                        continue;
                    }
                    $exists = function_exists('metadata_exists') ? metadata_exists('post', $objectId, $key) : true;
                    $current['meta'][$key] = $exists ? (string) get_post_meta($objectId, $key, true) : null;
                }

                foreach ($before['meta'] as $k => $v) {
                    $key = is_string($k) ? $k : '';
                    if ($key === '') {
                        continue;
                    }

                    if ($v === null) {
                        delete_post_meta($objectId, $key);
                        $didMeta = true;
                        continue;
                    }

                    $val = is_scalar($v) ? (string) $v : '';
                    if ($val === '') {
                        delete_post_meta($objectId, $key);
                    } else {
                        update_post_meta($objectId, $key, $val);
                    }
                    $didMeta = true;
                }
            }

            // Post content restore.
            if (isset($before['post']) && is_array($before['post']) && array_key_exists('post_content', $before['post'])) {
                $current['post'] = [
                    'post_content' => (string) $post->post_content,
                ];

                $newContent = is_scalar($before['post']['post_content']) ? (string) $before['post']['post_content'] : '';

                // Use wp_update_post to keep hooks consistent.
                wp_update_post([
                    'ID' => $objectId,
                    'post_content' => $newContent,
                ]);
            }

            // Elementor restore.
            if (isset($before['elementor']) && is_array($before['elementor']) && array_key_exists('_elementor_data', $before['elementor'])) {
                $current['elementor'] = [
                    '_elementor_data' => (string) get_post_meta($objectId, '_elementor_data', true),
                ];
                $newData = is_scalar($before['elementor']['_elementor_data']) ? (string) $before['elementor']['_elementor_data'] : '';
                update_post_meta($objectId, '_elementor_data', $newData);
            }

            // Notify dependent modules (analysis, sitemap cache, etc.).
            if ($didMeta) {
                do_action('seo/meta/updated', $objectId, 'post');
            }
        } elseif ($objectType === 'term') {
            if ($objectId <= 0) {
                $this->redirectWithNotice(__('Invalid term ID.', 'wp-seo-next'), 'error');
            }

            $term = $objectSubtype !== '' ? get_term($objectId, $objectSubtype) : get_term($objectId);
            if (!$term || is_wp_error($term)) {
                $this->redirectWithNotice(__('Term not found.', 'wp-seo-next'), 'error');
            }

            $taxonomy = isset($term->taxonomy) ? (string) $term->taxonomy : (string) $objectSubtype;
            if ($taxonomy === '') {
                $this->redirectWithNotice(__('Invalid taxonomy for this term revision.', 'wp-seo-next'), 'error');
            }

            $taxObj = function_exists('get_taxonomy') ? get_taxonomy($taxonomy) : null;
            $capEdit = ($taxObj && isset($taxObj->cap) && isset($taxObj->cap->edit_terms)) ? (string) $taxObj->cap->edit_terms : '';
            if ($capEdit !== '') {
                if (!current_user_can($capEdit)) {
                    $this->redirectWithNotice(__('You do not have permission to roll back this term.', 'wp-seo-next'), 'error');
                }
            } else {
                // Fallback capability.
                if (!current_user_can('manage_categories')) {
                    $this->redirectWithNotice(__('You do not have permission to roll back this term.', 'wp-seo-next'), 'error');
                }
            }

            if (!isset($before['meta']) || !is_array($before['meta'])) {
                $this->redirectWithNotice(__('Nothing to roll back (missing term meta state).', 'wp-seo-next'), 'error');
            }

            $current['meta'] = [];
            foreach ($before['meta'] as $k => $v) {
                $key = is_string($k) ? $k : '';
                if ($key === '') {
                    continue;
                }
                $exists = function_exists('metadata_exists') ? metadata_exists('term', $objectId, $key) : true;
                $current['meta'][$key] = $exists ? (string) get_term_meta($objectId, $key, true) : null;
            }

            foreach ($before['meta'] as $k => $v) {
                $key = is_string($k) ? $k : '';
                if ($key === '') {
                    continue;
                }

                if ($v === null) {
                    delete_term_meta($objectId, $key);
                    continue;
                }

                $val = is_scalar($v) ? (string) $v : '';
                if ($val === '') {
                    delete_term_meta($objectId, $key);
                } else {
                    update_term_meta($objectId, $key, $val);
                }
            }

            // Notify dependent modules (analysis, sitemap cache, etc.).
            do_action('seo/meta/updated', $objectId, 'term');
        } elseif ($objectType === 'option') {
            if (!Capabilities::canManageSettings()) {
                $this->redirectWithNotice(__('You do not have permission to roll back settings/options.', 'wp-seo-next'), 'error');
            }

            $optName = $objectSubtype !== '' ? $objectSubtype : '';
            if ($optName === '') {
                $this->redirectWithNotice(__('Invalid option name for this revision.', 'wp-seo-next'), 'error');
            }

            $current = [
                'option' => [
                    'name' => $optName,
                    'value' => get_option($optName),
                ],
            ];

            // For options we store state as whatever was in "before".
            $value = $before;
            if (isset($before['option']) && is_array($before['option']) && array_key_exists('value', $before['option'])) {
                $value = $before['option']['value'];
            }

            update_option($optName, $value, false);

            if ($optName === SettingsRepository::OPTION_NAME) {
                // Flush runtime cache.
                $this->container->get('settings')->flush();
            }
        } elseif ($objectType === 'redirect') {
            if (!Capabilities::canManageRedirects()) {
                $this->redirectWithNotice(__('You do not have permission to roll back redirects.', 'wp-seo-next'), 'error');
            }

            if ($objectId <= 0) {
                $this->redirectWithNotice(__('Invalid redirect ID.', 'wp-seo-next'), 'error');
            }

            $desired = null;
            if (array_key_exists('redirect', $before)) {
                $desired = $before['redirect'];
            } else {
                // Back-compat if a very old revision stored raw row.
                $desired = $before;
            }

            if ($desired !== null && !is_array($desired)) {
                $desired = null;
            }

            // Snapshot current state.
            RedirectsInstaller::maybeInstall();
            $redirRepo = new RedirectRepository($this->container->get('cache'), $this->logger);
            $existingRow = $redirRepo->getById($objectId);
            $current = ['redirect' => $this->sanitizeRedirectRow($existingRow)];

            // Apply desired state.
            if ($desired === null) {
                if (is_array($existingRow)) {
                    $redirRepo->delete($objectId);
                }
            } else {
                $from = isset($desired['from_url']) ? (string) $desired['from_url'] : '';
                $toRaw = array_key_exists('to_url', $desired) ? $desired['to_url'] : null;
                $to = is_string($toRaw) ? (string) $toRaw : '';

                $match = isset($desired['match_type']) ? sanitize_key((string) $desired['match_type']) : 'exact';
                if (!in_array($match, ['exact', 'prefix'], true)) {
                    $match = 'exact';
                }

                $code = isset($desired['status_code']) ? (int) $desired['status_code'] : 301;
                if (!in_array($code, [301, 302, 307, 308, 410, 451], true)) {
                    $code = 301;
                }

                $enabled = array_key_exists('enabled', $desired) ? (bool) $desired['enabled'] : true;

                $data = [
                    'from_url' => RedirectsUrl::normalizeFrom($from),
                    'to_url' => $to !== '' ? RedirectsUrl::normalizeTo($to) : '',
                    'match_type' => $match,
                    'status_code' => $code,
                    'enabled' => $enabled,
                ];

                if ($data['from_url'] === '') {
                    $this->redirectWithNotice(__('Cannot restore redirect: missing from_url.', 'wp-seo-next'), 'error');
                }

                if (is_array($existingRow)) {
                    $redirRepo->update($objectId, $data);
                } else {
                    // Restore deleted redirect with the same ID.
                    global $wpdb;
                    $table = $wpdb->prefix . 'wpsn_redirects';
                    $now = current_time('mysql', true);

                    $created = isset($desired['created_at']) ? (string) $desired['created_at'] : $now;
                    $updated = isset($desired['updated_at']) ? (string) $desired['updated_at'] : $now;
                    $hits = isset($desired['hits']) ? (int) $desired['hits'] : 0;

                    $lastHit = array_key_exists('last_hit', $desired) ? $desired['last_hit'] : null;
                    if (!is_string($lastHit) && $lastHit !== null) {
                        $lastHit = null;
                    }

                    $res = $wpdb->insert(
                        $table,
                        [
                            'id' => $objectId,
                            'from_url' => $data['from_url'],
                            'to_url' => $data['to_url'] !== '' ? $data['to_url'] : null,
                            'match_type' => $data['match_type'],
                            'status_code' => $data['status_code'],
                            'enabled' => $data['enabled'] ? 1 : 0,
                            'hits' => max(0, $hits),
                            'last_hit' => $lastHit,
                            'created_at' => $created !== '' ? $created : $now,
                            'updated_at' => $updated !== '' ? $updated : $now,
                        ],
                        [
                            '%d',
                            '%s',
                            '%s',
                            '%s',
                            '%d',
                            '%d',
                            '%d',
                            '%s',
                            '%s',
                            '%s',
                        ]
                    );

                    if (!$res) {
                        $this->logger->error('Failed to restore redirect via rollback: ' . (string) $wpdb->last_error);
                        $this->redirectWithNotice(__('Failed to restore redirect.', 'wp-seo-next'), 'error');
                    }
                }
            }
        } elseif ($objectType === 'user') {
            if ($objectId <= 0 || !current_user_can('edit_user', $objectId)) {
                $this->redirectWithNotice(__('You do not have permission to roll back this user.', 'wp-seo-next'), 'error');
            }

            if (!isset($before['user_meta']) || !is_array($before['user_meta'])) {
                $this->redirectWithNotice(__('Nothing to roll back (missing user meta state).', 'wp-seo-next'), 'error');
            }

            $current['user_meta'] = [];
            foreach ($before['user_meta'] as $k => $v) {
                $key = is_string($k) ? $k : '';
                if ($key === '') {
                    continue;
                }
                $current['user_meta'][$key] = (string) get_user_meta($objectId, $key, true);
            }

            foreach ($before['user_meta'] as $k => $v) {
                $key = is_string($k) ? $k : '';
                if ($key === '') {
                    continue;
                }

                if ($v === null) {
                    delete_user_meta($objectId, $key);
                    continue;
                }

                $val = is_scalar($v) ? (string) $v : '';
                if ($val === '') {
                    delete_user_meta($objectId, $key);
                } else {
                    update_user_meta($objectId, $key, $val);
                }
            }
        } else {
            $this->redirectWithNotice(__('Rollback not supported for this object type.', 'wp-seo-next'), 'error');
        }

        // Record rollback as a new revision.
        $rollbackId = $repo->insert([
            'object_type' => $objectType,
            'object_id' => $objectId,
            'object_subtype' => $objectSubtype,
            'action' => 'rollback',
            'source' => 'revisions',
            'actor_user_id' => $actor,
            'before' => $current,
            'after' => $before,
            'meta' => [
                'rollback_of' => $id,
            ],
        ]);

        if ($rollbackId > 0) {
            $repo->markRolledBack($id, $rollbackId);
        }

        $this->redirectWithNotice(__('Rolled back successfully.', 'wp-seo-next'), 'success');
    }
}
