<?php

namespace WPSN\Modules\Redirects\Admin;

use WPSN\Core\Logger;
use WPSN\Modules\Redirects\NotFoundLogRepository;
use WPSN\Modules\Redirects\RedirectRegex;
use WPSN\Modules\Redirects\RedirectRepository;
use WPSN\Modules\Redirects\RedirectsInstaller;
use WPSN\Modules\Redirects\RedirectsSettingsRepository;
use WPSN\Modules\Redirects\RedirectsUrl;
use WPSN\Modules\Redirects\RedirectSuggestionService;
use WPSN\Core\Capabilities;
use WPSN\Modules\Revisions\RevisionsInstaller;
use WPSN\Modules\Revisions\RevisionsRepository;

defined('ABSPATH') || exit;

final class RedirectsAdmin
{
    private const AJAX_NONCE_ACTION = 'wpsn_404_suggest_redirects';

    private RedirectRepository $redirects;
    private NotFoundLogRepository $notFound;
    private RedirectsSettingsRepository $settings;
    private RedirectSuggestionService $suggester;
    private Logger $logger;

    private string $redirectsHook = '';
    private string $monitorHook = '';

    public function __construct(
        RedirectRepository $redirects,
        NotFoundLogRepository $notFound,
        RedirectsSettingsRepository $settings,
        RedirectSuggestionService $suggester,
        Logger $logger
    ) {
        $this->redirects = $redirects;
        $this->notFound  = $notFound;
        $this->settings  = $settings;
        $this->suggester = $suggester;
        $this->logger    = $logger;
    }

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

        add_action('admin_post_wpsn_redirects_save_rule', [$this, 'handleSaveRule']);
        add_action('admin_post_wpsn_redirects_test_rule', [$this, 'handleTestRule']);
        add_action('admin_post_wpsn_redirects_delete_rule', [$this, 'handleDeleteRule']);
        add_action('admin_post_wpsn_redirects_toggle_rule', [$this, 'handleToggleRule']);
        add_action('admin_post_wpsn_redirects_import_csv', [$this, 'handleImportCsv']);

        add_action('admin_post_wpsn_redirects_save_settings', [$this, 'handleSaveRedirectsSettings']);
        add_action('admin_post_wpsn_404_save_settings', [$this, 'handleSave404Settings']);
        add_action('admin_post_wpsn_404_delete', [$this, 'handleDelete404']);
        add_action('admin_post_wpsn_404_clear', [$this, 'handleClear404']);
        add_action('admin_post_wpsn_404_bulk_create_redirects', [$this, 'handle404BulkCreateRedirects']);
        add_action('admin_post_wpsn_404_bulk_delete', [$this, 'handle404BulkDelete']);

        // Redirect suggestions from 404 log.
        add_action('wp_ajax_wpsn_404_suggest_redirects', [$this, 'ajaxSuggestRedirects']);
        add_action('admin_post_wpsn_404_create_redirect', [$this, 'handleCreateRedirectFrom404']);

        add_action('admin_enqueue_scripts', [$this, 'enqueueAssets']);

        add_action('admin_notices', [$this, 'adminNotices']);

        // Ensure tables exist in admin as well.
        add_action('admin_init', [RedirectsInstaller::class, 'maybeInstall'], 1);
    }

    public function addMenu(): void
    {
        $this->redirectsHook = (string) add_management_page(
            __('SEO Redirects', 'wp-seo-next'),
            __('SEO Redirects', 'wp-seo-next'),
            Capabilities::CAP_REDIRECTS,
            'wpsn-redirects',
            [$this, 'renderRedirectsPage']
        );

        $this->monitorHook = (string) add_management_page(
            __('SEO 404 Monitor', 'wp-seo-next'),
            __('SEO 404 Monitor', 'wp-seo-next'),
            Capabilities::CAP_REDIRECTS,
            'wpsn-404-monitor',
            [$this, 'render404Page']
        );
    }

    public function enqueueAssets(string $hook): void
    {
        if ($this->monitorHook !== '' && $hook !== $this->monitorHook) {
            return;
        }

        // Fallback: match by page slug.
        if ($this->monitorHook === '' && (!isset($_GET['page']) || (string) wp_unslash($_GET['page']) !== 'wpsn-404-monitor')) {
            return;
        }

        wp_enqueue_script(
            'wpsn-404-suggest',
            defined('WPSN_PLUGIN_URL') ? (WPSN_PLUGIN_URL . 'assets/admin/redirect-404-suggest.js') : plugins_url('assets/admin/redirect-404-suggest.js', defined('WPSN_PLUGIN_FILE') ? WPSN_PLUGIN_FILE : __FILE__),
            ['jquery'],
            defined('WPSN_VERSION') ? WPSN_VERSION : '0.0.0',
            true
        );

        wp_localize_script('wpsn-404-suggest', 'WPSN404Suggest', [
            'ajaxUrl' => admin_url('admin-ajax.php'),
            'adminPostUrl' => admin_url('admin-post.php'),
            'pageUrl' => admin_url('tools.php?page=wpsn-404-monitor'),
            'redirectsUrl' => admin_url('tools.php?page=wpsn-redirects'),
            'nonce' => wp_create_nonce(self::AJAX_NONCE_ACTION),
            'createNonce' => wp_create_nonce('wpsn_404_create_redirect'),
            'i18n' => [
                'loading' => __('Loading suggestions…', 'wp-seo-next'),
                'no_suggestions' => __('No suggestions found.', 'wp-seo-next'),
                'error' => __('Failed to load suggestions.', 'wp-seo-next'),
                'busy_503' => __('Server is temporarily busy (503). Use the direct view instead.', 'wp-seo-next'),
                'open_fallback' => __('Open suggestions (no AJAX)', 'wp-seo-next'),
                'create_301' => __('Create 301', 'wp-seo-next'),
                'prefill_form' => __('Prefill form', 'wp-seo-next'),
                'close' => __('Close', 'wp-seo-next'),
                'confidence' => __('Confidence', 'wp-seo-next'),
            ],
        ]);
    }

    public function adminNotices(): void
    {
        // Avoid showing Redirects notices on unrelated admin pages (some modules use wpsn_notice as raw text).
        $page = isset($_GET['page']) ? sanitize_key((string) wp_unslash($_GET['page'])) : '';
        if (!in_array($page, ['wpsn-redirects', 'wpsn-404-monitor'], true)) {
            return;
        }

        if (empty($_GET['wpsn_notice'])) {
            return;
        }
        $notice = sanitize_key((string) wp_unslash($_GET['wpsn_notice']));
        $type = isset($_GET['wpsn_notice_type']) ? sanitize_key((string) wp_unslash($_GET['wpsn_notice_type'])) : 'success';

        // Dynamic message for bulk actions.
        if ($notice === 'bulk_redirects_created') {
            $created = isset($_GET['created']) ? (int) $_GET['created'] : 0;
            $exists = isset($_GET['skipped_exists']) ? (int) $_GET['skipped_exists'] : 0;
            $noSug = isset($_GET['skipped_no_suggestion']) ? (int) $_GET['skipped_no_suggestion'] : 0;
            $low = isset($_GET['skipped_low_conf']) ? (int) $_GET['skipped_low_conf'] : 0;
            $errors = isset($_GET['errors']) ? (int) $_GET['errors'] : 0;

            $msg = sprintf(
                /* translators: 1: created, 2: exists, 3: no suggestion, 4: low confidence, 5: errors */
                __('Bulk redirect creation: created %1$d, skipped existing %2$d, skipped (no suggestion) %3$d, skipped (low confidence) %4$d, errors %5$d.', 'wp-seo-next'),
                max(0, $created),
                max(0, $exists),
                max(0, $noSug),
                max(0, $low),
                max(0, $errors)
            );

            $class = 'notice notice-success';
            if ($errors > 0 && $created === 0) {
                $class = 'notice notice-error';
            } elseif ($errors > 0 || $low > 0) {
                $class = 'notice notice-warning';
            }

            echo '<div class="' . esc_attr($class) . ' is-dismissible"><p>' . esc_html($msg) . '</p></div>';
            return;
        }

        if ($notice === 'bulk_log_deleted') {
            $deleted = isset($_GET['deleted']) ? (int) $_GET['deleted'] : 0;
            $errors = isset($_GET['errors']) ? (int) $_GET['errors'] : 0;

            $msg = sprintf(
                /* translators: 1: deleted, 2: errors */
                __('Bulk 404 log delete: deleted %1$d, errors %2$d.', 'wp-seo-next'),
                max(0, $deleted),
                max(0, $errors)
            );

            $class = 'notice notice-success';
            if ($errors > 0 && $deleted === 0) {
                $class = 'notice notice-error';
            } elseif ($errors > 0) {
                $class = 'notice notice-warning';
            }

            echo '<div class="' . esc_attr($class) . ' is-dismissible"><p>' . esc_html($msg) . '</p></div>';
            return;
        }

        if ($notice === 'import_done') {
            $imported = isset($_GET['imported']) ? (int) $_GET['imported'] : 0;
            $skipped = isset($_GET['skipped']) ? (int) $_GET['skipped'] : 0;

            $msg = sprintf(
                /* translators: 1: imported, 2: skipped */
                __('CSV import completed. Imported %1$d rule(s), skipped %2$d.', 'wp-seo-next'),
                max(0, $imported),
                max(0, $skipped)
            );

            $class = 'notice notice-success';
            if ($imported === 0 && $skipped > 0) {
                $class = 'notice notice-warning';
            }

            echo '<div class="' . esc_attr($class) . ' is-dismissible"><p>' . esc_html($msg) . '</p></div>';
            return;
        }

        if (!empty($_GET['wpsn_msg'])) {
            $class = 'notice notice-success';
            if ($type === 'error') {
                $class = 'notice notice-error';
            } elseif ($type === 'warning') {
                $class = 'notice notice-warning';
            } elseif ($type === 'info') {
                $class = 'notice notice-info';
            }

            $msg = sanitize_text_field((string) wp_unslash($_GET['wpsn_msg']));
            if ($msg !== '') {
                echo '<div class="' . esc_attr($class) . ' is-dismissible"><p>' . esc_html($msg) . '</p></div>';
                return;
            }
        }

        $map = [
            'rule_saved' => __('Redirect saved.', 'wp-seo-next'),
            'rule_deleted' => __('Redirect deleted.', 'wp-seo-next'),
            'rule_toggled' => __('Redirect updated.', 'wp-seo-next'),
            'redirect_settings_saved' => __('Settings saved.', 'wp-seo-next'),
            '404_settings_saved' => __('Settings saved.', 'wp-seo-next'),
            'log_cleared' => __('404 log cleared.', 'wp-seo-next'),
            'log_deleted' => __('404 entry deleted.', 'wp-seo-next'),
            'import_failed' => __('CSV import failed. Please check the file format.', 'wp-seo-next'),
            'suggest_redirect_created' => __('Redirect created from suggestion.', 'wp-seo-next'),
            'suggest_redirect_exists' => __('Redirect already exists for this path.', 'wp-seo-next'),
            'suggest_redirect_failed' => __('Failed to create redirect.', 'wp-seo-next'),
        ];
        if (!isset($map[$notice])) {
            return;
        }

        $class = 'notice notice-success';
        if ($type === 'error') {
            $class = 'notice notice-error';
        } elseif ($type === 'warning') {
            $class = 'notice notice-warning';
        } elseif ($type === 'info') {
            $class = 'notice notice-info';
        }

        echo '<div class="' . esc_attr($class) . ' is-dismissible"><p>' . esc_html($map[$notice]) . '</p></div>';
    }

    private function sanitizeBackUrl(string $raw): string
    {
        $raw = trim($raw);
        if ($raw === '') {
            return '';
        }

        // Allow relative admin paths as a convenience.
        if (strpos($raw, 'http://') !== 0 && strpos($raw, 'https://') !== 0 && strpos($raw, '//') !== 0) {
            if (strpos($raw, '/') === 0) {
                $raw = home_url($raw);
            } else {
                $raw = admin_url($raw);
            }
        }

        $raw = esc_url_raw($raw);
        if ($raw === '') {
            return '';
        }

        $validated = wp_validate_redirect($raw, '');
        if ($validated === '') {
            return '';
        }

        $p = wp_parse_url($validated);
        $a = wp_parse_url(admin_url());
        if (!is_array($p) || !is_array($a)) {
            return '';
        }

        $host = isset($p['host']) ? (string) $p['host'] : '';
        $adminHost = isset($a['host']) ? (string) $a['host'] : '';
        if ($host === '' || $adminHost === '' || strcasecmp($host, $adminHost) !== 0) {
            return '';
        }

        $path = isset($p['path']) ? (string) $p['path'] : '';
        $adminPath = isset($a['path']) ? (string) $a['path'] : '';
        if ($adminPath === '') {
            $adminPath = '/wp-admin/';
        }
        if ($path === '' || strpos($path, $adminPath) !== 0) {
            return '';
        }

        return $validated;
    }

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

        $settings = $this->settings->getAll();

        $search = isset($_GET['s']) ? sanitize_text_field((string) wp_unslash($_GET['s'])) : '';
        $paged  = isset($_GET['paged']) ? max(1, (int) $_GET['paged']) : 1;
        $perPage = 20;

        $data = $this->redirects->list($paged, $perPage, $search);
        $items = $data['items'];
        $total = (int) $data['total'];
        $totalPages = $perPage > 0 ? (int) ceil($total / $perPage) : 1;

        $editId = isset($_GET['edit']) ? (int) $_GET['edit'] : 0;
        $editRule = $editId > 0 ? $this->redirects->getById($editId) : null;

        $prefillMatch = isset($_GET['match_type']) ? sanitize_key((string) wp_unslash($_GET['match_type'])) : '';
        if (!in_array($prefillMatch, ['exact', 'prefix', 'regex'], true)) {
            $prefillMatch = '';
        }

        $prefillFrom = isset($_GET['from']) ? (string) wp_unslash($_GET['from']) : '';
        if ($prefillFrom !== '') {
            if ($prefillMatch === 'regex') {
                $prefillFrom = RedirectRegex::normalizeStoredPattern($prefillFrom);
            } else {
                $prefillFrom = RedirectsUrl::normalizeFrom($prefillFrom);
            }
        }

        $prefillTo = isset($_GET['to']) ? (string) wp_unslash($_GET['to']) : '';
        if ($prefillTo !== '') {
            $prefillTo = RedirectsUrl::normalizeTo($prefillTo);
        }

        $prefillCode = isset($_GET['status_code']) ? (int) $_GET['status_code'] : 0;
        $prefillEnabled = isset($_GET['enabled']) ? ((int) $_GET['enabled'] === 1) : null;
        $testRequestPath = isset($_GET['test_request_path']) ? (string) wp_unslash($_GET['test_request_path']) : '';
        if ($testRequestPath !== '') {
            $testRequestPath = RedirectsUrl::normalizeFrom($testRequestPath);
        }

        $formFrom = $prefillFrom !== '' ? $prefillFrom : (string) ($editRule['from_url'] ?? '');
        $formTo = $prefillTo !== '' ? $prefillTo : (string) ($editRule['to_url'] ?? '');
        $formCode = $prefillCode > 0 ? $prefillCode : (int) ($editRule['status_code'] ?? 301);
        $formMatch = $prefillMatch !== '' ? $prefillMatch : (string) ($editRule['match_type'] ?? 'exact');
        $formEnabled = is_bool($prefillEnabled) ? $prefillEnabled : (isset($editRule['enabled']) ? (bool) $editRule['enabled'] : true);

        $regexEnabled = !empty($settings['enable_regex']);
        $showRegexOption = $regexEnabled || $formMatch === 'regex';
        $actionUrl = admin_url('admin-post.php');
        $pageUrl = admin_url('tools.php?page=wpsn-redirects');

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

            <p class="description">
                <?php echo esc_html__('Add 301/302/410/451 redirects. By default, redirects are applied only on 404 pages for safety.', 'wp-seo-next'); ?>
            </p>

            <p style="margin: 10px 0 18px;">
                <a class="button" href="<?php echo esc_url(admin_url('tools.php?page=wpsn-redirect-migration')); ?>">
                    <?php echo esc_html__('Migrate redirects from other plugins', 'wp-seo-next'); ?>
                </a>
            </p>

            <h2><?php echo esc_html($editRule ? __('Edit redirect', 'wp-seo-next') : __('Add new redirect', 'wp-seo-next')); ?></h2>
            <form method="post" action="<?php echo esc_url($actionUrl); ?>">
                <?php wp_nonce_field('wpsn_redirects_save_rule', 'wpsn_nonce'); ?>

                <?php if ($editRule && !empty($editRule['id'])) : ?>
                    <input type="hidden" name="id" value="<?php echo esc_attr((string) $editRule['id']); ?>" />
                <?php endif; ?>

                <table class="form-table" role="presentation" style="max-width: 900px;">
                    <tr>
                        <th scope="row"><label for="wpsn_from"><?php echo esc_html__('From (path or regex)', 'wp-seo-next'); ?></label></th>
                        <td>
                            <input id="wpsn_from" type="text" class="large-text" name="from_url" placeholder="/old-page/" value="<?php echo esc_attr((string) $formFrom); ?>" required />
                            <p class="description"><?php echo esc_html__('Use a relative path (e.g. /old/). You can paste a full URL — we will extract the path. For regex rules, enter the regex pattern directly (example: ^/old/(.+)$).', 'wp-seo-next'); ?></p>
                        </td>
                    </tr>
                    <tr>
                        <th scope="row"><label for="wpsn_to"><?php echo esc_html__('To (URL or path)', 'wp-seo-next'); ?></label></th>
                        <td>
                            <input id="wpsn_to" type="text" class="large-text" name="to_url" placeholder="/new-page/" value="<?php echo esc_attr((string) $formTo); ?>" />
                            <p class="description"><?php echo esc_html__('Leave empty only for 410 (Gone) or 451 (Unavailable for Legal Reasons).', 'wp-seo-next'); ?></p>
                        </td>
                    </tr>
                    <tr>
                        <th scope="row"><?php echo esc_html__('Match type', 'wp-seo-next'); ?></th>
                        <td>
                            <select name="match_type">
                                <option value="exact" <?php selected($formMatch, 'exact'); ?>><?php echo esc_html__('Exact', 'wp-seo-next'); ?></option>
                                <option value="prefix" <?php selected($formMatch, 'prefix'); ?>><?php echo esc_html__('Prefix', 'wp-seo-next'); ?></option>
                                <?php if ($showRegexOption) : ?>
                                    <option value="regex" <?php selected($formMatch, 'regex'); ?>><?php echo esc_html__('Regex (advanced)', 'wp-seo-next'); ?></option>
                                <?php endif; ?>
                            </select>
                            <p class="description"><?php echo esc_html__('Prefix example: /old/ → /new/ will redirect /old/a to /new/a', 'wp-seo-next'); ?></p>
                            <?php if ($showRegexOption) : ?>
                                <p class="description"><?php echo esc_html__('Regex example: ^/old/(.+)$ → /new/$1 . Regex rules are path-only, must start with ^/, and are ignored on the frontend until the advanced regex setting is enabled.', 'wp-seo-next'); ?></p>
                            <?php else : ?>
                                <p class="description"><?php echo esc_html__('Advanced regex redirects are currently disabled in module settings.', 'wp-seo-next'); ?></p>
                            <?php endif; ?>
                        </td>
                    </tr>
                    <tr>
                        <th scope="row"><?php echo esc_html__('Status code', 'wp-seo-next'); ?></th>
                        <td>
                            <select name="status_code">
                                <?php foreach ([301, 302, 307, 308, 410, 451] as $code) : ?>
                                    <option value="<?php echo esc_attr((string) $code); ?>" <?php selected($formCode, $code); ?>><?php echo esc_html((string) $code); ?></option>
                                <?php endforeach; ?>
                            </select>
                        </td>
                    </tr>
                    <tr>
                        <th scope="row"><?php echo esc_html__('Enabled', 'wp-seo-next'); ?></th>
                        <td>
                            <label>
                                <input type="checkbox" name="enabled" value="1" <?php checked($formEnabled); ?> />
                                <?php echo esc_html__('Active', 'wp-seo-next'); ?>
                            </label>
                        </td>
                    </tr>
                    <tr>
                        <th scope="row"><label for="wpsn_test_request_path"><?php echo esc_html__('Test request path', 'wp-seo-next'); ?></label></th>
                        <td>
                            <input id="wpsn_test_request_path" type="text" class="regular-text" name="test_request_path" placeholder="/old/example/" value="<?php echo esc_attr((string) $testRequestPath); ?>" />
                            <p class="description"><?php echo esc_html__('Optional. Use this before saving to simulate the destination for exact, prefix, or regex rules.', 'wp-seo-next'); ?></p>
                        </td>
                    </tr>
                </table>

                <p class="submit">
                    <button type="submit" class="button button-primary" name="action" value="wpsn_redirects_save_rule"><?php echo esc_html($editRule ? __('Update redirect', 'wp-seo-next') : __('Add redirect', 'wp-seo-next')); ?></button>
                    <button type="submit" class="button button-secondary" name="action" value="wpsn_redirects_test_rule" formnovalidate><?php echo esc_html__('Test rule', 'wp-seo-next'); ?></button>
                </p>
            </form>

            <?php $this->renderRedirectSimulationBox(); ?>

            <hr />

            <h2><?php echo esc_html__('Redirects list', 'wp-seo-next'); ?></h2>

            <form method="get" action="<?php echo esc_url(admin_url('tools.php')); ?>" style="margin: 12px 0;">
                <input type="hidden" name="page" value="wpsn-redirects" />
                <p class="search-box">
                    <label class="screen-reader-text" for="redirect-search-input"><?php echo esc_html__('Search redirects', 'wp-seo-next'); ?></label>
                    <input type="search" id="redirect-search-input" name="s" value="<?php echo esc_attr($search); ?>" />
                    <?php submit_button(__('Search', 'wp-seo-next'), 'secondary', false, false, ['id' => 'search-submit']); ?>
                </p>
            </form>

            <table class="widefat striped">
                <thead>
                    <tr>
                        <th><?php echo esc_html__('From', 'wp-seo-next'); ?></th>
                        <th><?php echo esc_html__('To', 'wp-seo-next'); ?></th>
                        <th><?php echo esc_html__('Match', 'wp-seo-next'); ?></th>
                        <th><?php echo esc_html__('Code', 'wp-seo-next'); ?></th>
                        <th><?php echo esc_html__('Hits', 'wp-seo-next'); ?></th>
                        <th><?php echo esc_html__('Last hit', 'wp-seo-next'); ?></th>
                        <th><?php echo esc_html__('Enabled', 'wp-seo-next'); ?></th>
                        <th><?php echo esc_html__('Actions', 'wp-seo-next'); ?></th>
                    </tr>
                </thead>
                <tbody>
                    <?php if (empty($items)) : ?>
                        <tr><td colspan="8"><?php echo esc_html__('No redirects found.', 'wp-seo-next'); ?></td></tr>
                    <?php else : ?>
                        <?php foreach ($items as $row) :
                            $id = (int) ($row['id'] ?? 0);
                            $enabled = !empty($row['enabled']);
                            $toggleUrl = wp_nonce_url(
                                admin_url('admin-post.php?action=wpsn_redirects_toggle_rule&id=' . $id . '&enabled=' . ($enabled ? '0' : '1')),
                                'wpsn_redirects_toggle_rule'
                            );
                            $deleteUrl = wp_nonce_url(
                                admin_url('admin-post.php?action=wpsn_redirects_delete_rule&id=' . $id),
                                'wpsn_redirects_delete_rule'
                            );
                            $editUrl = add_query_arg(['page' => 'wpsn-redirects', 'edit' => $id], admin_url('tools.php'));
                        ?>
                            <tr>
                                <td><code><?php echo esc_html((string) ($row['from_url'] ?? '')); ?></code></td>
                                <td><?php echo in_array((int) ($row['status_code'] ?? 0), [410, 451], true) ? '<em>—</em>' : '<code>' . esc_html((string) ($row['to_url'] ?? '')) . '</code>'; ?></td>
                                <td><?php echo esc_html((string) ($row['match_type'] ?? '')); ?></td>
                                <td><?php echo esc_html((string) ($row['status_code'] ?? '')); ?></td>
                                <td><?php echo esc_html((string) ($row['hits'] ?? '0')); ?></td>
                                <td><?php echo esc_html((string) ($row['last_hit'] ?? '')); ?></td>
                                <td><?php echo $enabled ? esc_html__('Yes', 'wp-seo-next') : esc_html__('No', 'wp-seo-next'); ?></td>
                                <td>
                                    <a href="<?php echo esc_url($editUrl); ?>"><?php echo esc_html__('Edit', 'wp-seo-next'); ?></a>
                                    | <a href="<?php echo esc_url($toggleUrl); ?>"><?php echo $enabled ? esc_html__('Disable', 'wp-seo-next') : esc_html__('Enable', 'wp-seo-next'); ?></a>
                                    | <a href="<?php echo esc_url($deleteUrl); ?>" onclick="return confirm('<?php echo esc_js(__('Delete this redirect?', 'wp-seo-next')); ?>');"><?php echo esc_html__('Delete', 'wp-seo-next'); ?></a>
                                </td>
                            </tr>
                        <?php endforeach; ?>
                    <?php endif; ?>
                </tbody>
            </table>

            <?php $this->renderPagination($pageUrl, $paged, $totalPages, $search); ?>

            <hr />

            <h2><?php echo esc_html__('Module settings', 'wp-seo-next'); ?></h2>
            <form method="post" action="<?php echo esc_url($actionUrl); ?>">
                <input type="hidden" name="action" value="wpsn_redirects_save_settings" />
                <?php wp_nonce_field('wpsn_redirects_save_settings', 'wpsn_nonce'); ?>

                <table class="form-table" role="presentation" style="max-width: 900px;">
                    <tr>
                        <th scope="row"><?php echo esc_html__('Apply redirects only on 404', 'wp-seo-next'); ?></th>
                        <td>
                            <label>
                                <input type="checkbox" name="settings[apply_only_on_404]" value="1" <?php checked(!empty($settings['apply_only_on_404'])); ?> />
                                <?php echo esc_html__('Recommended for safety', 'wp-seo-next'); ?>
                            </label>
                        </td>
                    </tr>
                    <tr>
                        <th scope="row"><?php echo esc_html__('Preserve query string', 'wp-seo-next'); ?></th>
                        <td>
                            <label>
                                <input type="checkbox" name="settings[preserve_query]" value="1" <?php checked(!empty($settings['preserve_query'])); ?> />
                                <?php echo esc_html__('Append original ?query=... to the destination URL (if destination has no query).', 'wp-seo-next'); ?>
                            </label>
                        </td>
                    </tr>
                    <tr>
                        <th scope="row"><?php echo esc_html__('Enable regex redirects', 'wp-seo-next'); ?></th>
                        <td>
                            <label>
                                <input type="checkbox" name="settings[enable_regex]" value="1" <?php checked(!empty($settings['enable_regex'])); ?> />
                                <?php echo esc_html__('Allow advanced regex-based redirects.', 'wp-seo-next'); ?>
                            </label>
                            <p class="description"><?php echo esc_html__('Recommended only for advanced users. Regex rules must start with ^/, are validated before save, and can be tested with the simulator above.', 'wp-seo-next'); ?></p>
                        </td>
                    </tr>
                    <tr>
                        <th scope="row"><?php echo esc_html__('Auto-create redirects on URL change', 'wp-seo-next'); ?></th>
                        <td>
                            <label>
                                <input type="checkbox" name="settings[auto_redirect_post_url_changes]" value="1" <?php checked(!empty($settings['auto_redirect_post_url_changes'])); ?> />
                                <?php echo esc_html__('Create an exact 301 redirect when a published post or page URL changes.', 'wp-seo-next'); ?>
                            </label>
                            <p class="description"><?php echo esc_html__('Uses the last known public permalink, so slug, parent, category/permalink, and similar URL changes can be captured without overwriting existing redirect rules.', 'wp-seo-next'); ?></p>
                        </td>
                    </tr>
                </table>

                <?php submit_button(__('Save module settings', 'wp-seo-next')); ?>
            </form>

            <hr />

            <h2><?php echo esc_html__('Import redirects (CSV)', 'wp-seo-next'); ?></h2>
            <p class="description">
                <?php echo esc_html__('CSV columns: from_url,to_url,status_code,match_type,enabled. Header row is optional. match_type supports exact, prefix, and regex (when advanced regex is enabled).', 'wp-seo-next'); ?>
            </p>
            <form method="post" action="<?php echo esc_url($actionUrl); ?>" enctype="multipart/form-data">
                <input type="hidden" name="action" value="wpsn_redirects_import_csv" />
                <?php wp_nonce_field('wpsn_redirects_import_csv', 'wpsn_nonce'); ?>
                <input type="file" name="csv" accept="text/csv,.csv" required />
                <?php submit_button(__('Import CSV', 'wp-seo-next'), 'secondary'); ?>
            </form>
        </div>
        <?php
    }

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

        $settings = $this->settings->getAll();
        $search = isset($_GET['s']) ? sanitize_text_field((string) wp_unslash($_GET['s'])) : '';
        $paged  = isset($_GET['paged']) ? max(1, (int) $_GET['paged']) : 1;
        $perPage = 20;

        $data = $this->notFound->list($paged, $perPage, $search);
        $items = $data['items'];
        $total = (int) $data['total'];
        $totalPages = $perPage > 0 ? (int) ceil($total / $perPage) : 1;

        $actionUrl = admin_url('admin-post.php');
        $pageUrl = admin_url('tools.php?page=wpsn-404-monitor');

        // Optional (non-AJAX) suggest view.
        // Progressive enhancement: the "Suggest" action is a normal link.
        // When JS + admin-ajax works, it intercepts the click and loads inline.
        // When admin-ajax is blocked/503, the link still works as a full-page view.
        $suggestId = isset($_GET['suggest']) ? (int) $_GET['suggest'] : 0;
        $suggestUrl = isset($_GET['suggest_url']) ? esc_url_raw((string) wp_unslash($_GET['suggest_url'])) : '';

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

            <p class="description"><?php echo esc_html__('This log is limited and automatically pruned. Use it to quickly create redirects for broken URLs.', 'wp-seo-next'); ?></p>

            <h2><?php echo esc_html__('404 Monitor settings', 'wp-seo-next'); ?></h2>
            <form method="post" action="<?php echo esc_url($actionUrl); ?>">
                <input type="hidden" name="action" value="wpsn_404_save_settings" />
                <?php wp_nonce_field('wpsn_redirects_save_settings', 'wpsn_nonce'); ?>

                <table class="form-table" role="presentation" style="max-width: 900px;">
                    <tr>
                        <th scope="row"><?php echo esc_html__('Enable 404 logging', 'wp-seo-next'); ?></th>
                        <td>
                            <label>
                                <input type="checkbox" name="settings[log_404]" value="1" <?php checked(!empty($settings['log_404'])); ?> />
                                <?php echo esc_html__('Log 404 requests', 'wp-seo-next'); ?>
                            </label>
                        </td>
                    </tr>
                    <tr>
                        <th scope="row"><?php echo esc_html__('Max log entries', 'wp-seo-next'); ?></th>
                        <td>
                            <input type="number" min="0" max="50000" name="settings[log_max_rows]" value="<?php echo esc_attr((string) ($settings['log_max_rows'] ?? 500)); ?>" />
                            <p class="description"><?php echo esc_html__('0 = unlimited (not recommended).', 'wp-seo-next'); ?></p>
                        </td>
                    </tr>
                    <tr>
                        <th scope="row"><?php echo esc_html__('Ignore logged-in users', 'wp-seo-next'); ?></th>
                        <td>
                            <label>
                                <input type="checkbox" name="settings[log_ignore_logged_in]" value="1" <?php checked(!empty($settings['log_ignore_logged_in'])); ?> />
                                <?php echo esc_html__('Do not log 404s generated by admins/editors while browsing.', 'wp-seo-next'); ?>
                            </label>
                        </td>
                    </tr>
                </table>
                <?php submit_button(__('Save settings', 'wp-seo-next')); ?>
            </form>

            <hr />

            <h2><?php echo esc_html__('404 Log', 'wp-seo-next'); ?></h2>

            <?php
            if ($suggestId > 0) {
                $this->render404SuggestBox($suggestId, $search, $paged);
            } elseif ($suggestUrl !== '') {
                $this->render404SuggestBoxForUrl($suggestUrl, $search, $paged);
            }
            ?>

            <form method="get" action="<?php echo esc_url(admin_url('tools.php')); ?>" style="margin: 12px 0;">
                <input type="hidden" name="page" value="wpsn-404-monitor" />
                <p class="search-box">
                    <label class="screen-reader-text" for="log-search-input"><?php echo esc_html__('Search 404 log', 'wp-seo-next'); ?></label>
                    <input type="search" id="log-search-input" name="s" value="<?php echo esc_attr($search); ?>" />
                    <?php submit_button(__('Search', 'wp-seo-next'), 'secondary', false, false, ['id' => 'search-submit']); ?>
                </p>
            </form>

            <p>
                <a class="button button-secondary" href="<?php echo esc_url(wp_nonce_url(admin_url('admin-post.php?action=wpsn_404_clear'), 'wpsn_404_clear')); ?>" onclick="return confirm('<?php echo esc_js(__('Clear the entire 404 log?', 'wp-seo-next')); ?>');">
                    <?php echo esc_html__('Clear log', 'wp-seo-next'); ?>
                </a>
            </p>

            <?php
                $currentUrlArgs = ['page' => 'wpsn-404-monitor'];
                if ($search !== '') {
                    $currentUrlArgs['s'] = $search;
                }
                if ($paged > 1) {
                    $currentUrlArgs['paged'] = $paged;
                }
                $currentUrl = add_query_arg($currentUrlArgs, admin_url('tools.php'));
            ?>

            <form method="post" action="<?php echo esc_url($actionUrl); ?>" style="margin: 0 0 12px 0;">
                <?php wp_nonce_field('wpsn_404_bulk', 'wpsn_nonce'); ?>
                <input type="hidden" name="back" value="<?php echo esc_attr($currentUrl); ?>" />

                <div class="tablenav top" style="margin: 0 0 10px 0;">
                    <div class="alignleft actions">
                        <button type="submit" class="button action" formaction="<?php echo esc_url(admin_url('admin-post.php?action=wpsn_404_bulk_create_redirects')); ?>">
                            <?php echo esc_html__('Create redirects (best suggestion) for selected', 'wp-seo-next'); ?>
                        </button>
                        <button type="submit" class="button action" formaction="<?php echo esc_url(admin_url('admin-post.php?action=wpsn_404_bulk_delete')); ?>" onclick="return confirm('<?php echo esc_js(__('Delete selected 404 log entries?', 'wp-seo-next')); ?>');">
                            <?php echo esc_html__('Delete selected log entries', 'wp-seo-next'); ?>
                        </button>
                        <span class="description" style="margin-left: 10px;"><?php echo esc_html__('Skips low-confidence matches and existing redirects.', 'wp-seo-next'); ?></span>
                    </div>
                    <br class="clear" />
                </div>

                <table class="widefat striped">
                <thead>
                    <tr>
                        <th scope="col" class="manage-column column-cb check-column"><input id="cb-select-all-1" type="checkbox" /></th>
                        <th><?php echo esc_html__('URL', 'wp-seo-next'); ?></th>
                        <th><?php echo esc_html__('Hits', 'wp-seo-next'); ?></th>
                        <th><?php echo esc_html__('Last seen', 'wp-seo-next'); ?></th>
                        <th><?php echo esc_html__('Referrer', 'wp-seo-next'); ?></th>
                        <th><?php echo esc_html__('Actions', 'wp-seo-next'); ?></th>
                    </tr>
                </thead>
                <tbody>
                    <?php if (empty($items)) : ?>
                        <tr><td colspan="6"><?php echo esc_html__('No 404 entries logged yet.', 'wp-seo-next'); ?></td></tr>
                    <?php else : ?>
                        <?php foreach ($items as $row) :
                            $id = (int) ($row['id'] ?? 0);
                            $url = (string) ($row['url'] ?? '');
                            $addUrl = add_query_arg(
                                ['page' => 'wpsn-redirects', 'from' => $url],
                                admin_url('tools.php')
                            );
                            $deleteUrl = wp_nonce_url(
                                admin_url('admin-post.php?action=wpsn_404_delete&id=' . $id),
                                'wpsn_404_delete'
                            );

                            $from = RedirectsUrl::normalizeFrom($url);
                            $existing = $from !== '' ? $this->redirects->findByFromAndType($from, 'exact') : null;
                            $existingId = is_array($existing) ? (int) ($existing['id'] ?? 0) : 0;
                            $existingEditUrl = $existingId > 0 ? add_query_arg(['page' => 'wpsn-redirects', 'edit' => $existingId], admin_url('tools.php')) : '';

                            // Fallback (no AJAX) suggest URL.
                            $suggestUrlArgs = [
                                'page' => 'wpsn-404-monitor',
                                'suggest' => $id,
                            ];
                            if ($search !== '') {
                                $suggestUrlArgs['s'] = $search;
                            }
                            if ($paged > 1) {
                                $suggestUrlArgs['paged'] = $paged;
                            }
                            $suggestUrl = add_query_arg($suggestUrlArgs, admin_url('tools.php'));
                        ?>
                            <tr>
                                <th scope="row" class="check-column"><input type="checkbox" name="ids[]" value="<?php echo esc_attr((string) $id); ?>" /></th>
                                <td><code><?php echo esc_html($url); ?></code></td>
                                <td><?php echo esc_html((string) ($row['hits'] ?? '1')); ?></td>
                                <td><?php echo esc_html((string) ($row['last_seen'] ?? '')); ?></td>
                                <td style="max-width: 360px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
                                    <?php
                                        $ref = (string) ($row['last_referrer'] ?? '');
                                        echo $ref !== '' ? '<code>' . esc_html($ref) . '</code>' : '<em>—</em>';
                                    ?>
                                </td>
                                <td>
                                    <?php if ($existingId > 0) : ?>
                                        <a href="<?php echo esc_url($existingEditUrl); ?>"><?php echo esc_html__('View redirect', 'wp-seo-next'); ?></a>
                                        | <a href="<?php echo esc_url($deleteUrl); ?>" onclick="return confirm('<?php echo esc_js(__('Delete this 404 entry?', 'wp-seo-next')); ?>');"><?php echo esc_html__('Delete log entry', 'wp-seo-next'); ?></a>
                                    <?php else : ?>
                                        <a href="<?php echo esc_url($addUrl); ?>"><?php echo esc_html__('Create redirect', 'wp-seo-next'); ?></a>
                                        | <a href="<?php echo esc_url($suggestUrl); ?>" class="wpsn-404-suggest" data-id="<?php echo esc_attr((string) $id); ?>" data-url="<?php echo esc_attr($url); ?>"><?php echo esc_html__('Suggest', 'wp-seo-next'); ?></a>
                                        | <a href="<?php echo esc_url($deleteUrl); ?>" onclick="return confirm('<?php echo esc_js(__('Delete this 404 entry?', 'wp-seo-next')); ?>');"><?php echo esc_html__('Delete', 'wp-seo-next'); ?></a>
                                    <?php endif; ?>
                                </td>
                            </tr>
                        <?php endforeach; ?>
                    <?php endif; ?>
                </tbody>
            </table>

            </form>

            <?php $this->renderPagination($pageUrl, $paged, $totalPages, $search); ?>
        </div>
        <?php
    }

    private function render404SuggestBox(int $id, string $search, int $paged): void
    {
        if ($id <= 0) {
            return;
        }

        $row = $this->notFound->getById($id);
        $url = is_array($row) && !empty($row['url']) ? (string) $row['url'] : '';
        $url = sanitize_text_field($url);

        $backRaw = isset($_POST['back']) ? (string) wp_unslash($_POST['back']) : '';
        $backUrl = $this->sanitizeBackUrl($backRaw);
        if ($backUrl === '') {
            $ref = wp_get_referer();
            if (is_string($ref) && $ref !== '') {
                $backUrl = $this->sanitizeBackUrl($ref);
            }
        }

        $backOverrideRaw = isset($_GET['back']) ? (string) wp_unslash($_GET['back']) : '';
        $backOverride = $this->sanitizeBackUrl($backOverrideRaw);

        $closeArgs = ['page' => 'wpsn-404-monitor'];
        if ($search !== '') {
            $closeArgs['s'] = $search;
        }
        if ($paged > 1) {
            $closeArgs['paged'] = $paged;
        }
        $closeUrl = add_query_arg($closeArgs, admin_url('tools.php'));

        $selfArgs = $closeArgs;
        $selfArgs['suggest'] = $id;
        $selfUrl = add_query_arg($selfArgs, admin_url('tools.php'));

        $backUrl = $backOverride !== '' ? $backOverride : $selfUrl;

        echo '<div id="wpsn-404-suggest-box" style="margin: 14px 0; padding: 12px 14px; border: 1px solid #dcdcde; background: #fff; border-radius: 6px; max-width: 1100px;">';

        echo '<div style="display:flex; align-items:center; justify-content:space-between; gap: 12px;">';
        echo '<div>';
        echo '<h3 style="margin:0;">' . esc_html__('Redirect suggestions', 'wp-seo-next') . '</h3>';
        if ($url !== '') {
            echo '<div class="description" style="margin-top:4px;">' . esc_html__('For:', 'wp-seo-next') . ' <code>' . esc_html($url) . '</code></div>';
        }
        echo '</div>';
        echo '<div style="display:flex; gap:6px; align-items:center;">';
        if ($backOverride !== '') {
            echo '<a class="button button-small" href="' . esc_url($backOverride) . '">' . esc_html__('Back', 'wp-seo-next') . '</a>';
        }
        echo '<a class="button button-small" href="' . esc_url($closeUrl) . '">' . esc_html__('Close', 'wp-seo-next') . '</a>';
        echo '</div>';
        echo '</div>';

        if ($url === '') {
            echo '<p style="margin:10px 0 0;"><em>' . esc_html__('404 entry not found.', 'wp-seo-next') . '</em></p>';
            echo '</div>';
            return;
        }

        $suggestions = $this->suggester->suggestForLoggedUrl($url, 7);
        if (empty($suggestions)) {
            echo '<p style="margin:10px 0 0;"><em>' . esc_html__('No suggestions found.', 'wp-seo-next') . '</em></p>';
            echo '</div>';
            return;
        }

        $from = RedirectsUrl::normalizeFrom($url);

        echo '<table class="widefat striped" style="margin-top: 10px;">';
        echo '<thead><tr>';
        echo '<th>' . esc_html__('To', 'wp-seo-next') . '</th>';
        echo '<th style="width: 110px;">' . esc_html__('Confidence', 'wp-seo-next') . '</th>';
        echo '<th>' . esc_html__('Reason', 'wp-seo-next') . '</th>';
        echo '<th style="width: 240px;">' . esc_html__('Actions', 'wp-seo-next') . '</th>';
        echo '</tr></thead><tbody>';

        foreach ($suggestions as $s) {
            $to = isset($s['to']) ? (string) $s['to'] : '';
            $to = trim($to);
            if ($to === '') {
                continue;
            }

            $label = isset($s['label']) ? (string) $s['label'] : '';
            $reason = isset($s['reason']) ? (string) $s['reason'] : '';
            $score = isset($s['score']) ? (float) $s['score'] : 0.0;

            $prefillUrl = add_query_arg(
                [
                    'page' => 'wpsn-redirects',
                    'from' => $from,
                    'to' => $to,
                ],
                admin_url('tools.php')
            );

            $createUrl = wp_nonce_url(
                add_query_arg(
                    [
                        'action' => 'wpsn_404_create_redirect',
                        'from' => $from,
                        'to' => $to,
                        'back' => $backUrl,
                    ],
                    admin_url('admin-post.php')
                ),
                'wpsn_404_create_redirect'
            );

            $pct = (int) round(max(0.0, min(1.0, $score)) * 100);

            echo '<tr>';
            echo '<td><code>' . esc_html($to) . '</code>';
            if ($label !== '') {
                echo '<div class="description" style="margin-top:4px;">' . esc_html($label) . '</div>';
            }
            echo '</td>';
            echo '<td>' . esc_html((string) $pct) . '%</td>';
            echo '<td>' . esc_html($reason) . '</td>';
            echo '<td>';
            echo '<a class="button button-small" href="' . esc_url($createUrl) . '">' . esc_html__('Create 301', 'wp-seo-next') . '</a> ';
            echo '<a class="button button-small" href="' . esc_url($prefillUrl) . '">' . esc_html__('Prefill form', 'wp-seo-next') . '</a>';
            echo '</td>';
            echo '</tr>';
        }

        echo '</tbody></table>';
        echo '</div>';
    }

    private function render404SuggestBoxForUrl(string $url, string $search, int $paged): void
    {
        $url = sanitize_text_field($url);

        $backOverrideRaw = isset($_GET['back']) ? (string) wp_unslash($_GET['back']) : '';
        $backOverride = $this->sanitizeBackUrl($backOverrideRaw);

        $closeArgs = ['page' => 'wpsn-404-monitor'];
        if ($search !== '') {
            $closeArgs['s'] = $search;
        }
        if ($paged > 1) {
            $closeArgs['paged'] = $paged;
        }
        $closeUrl = add_query_arg($closeArgs, admin_url('tools.php'));

        $selfArgs = $closeArgs;
        $selfArgs['suggest_url'] = $url;
        $selfUrl = add_query_arg($selfArgs, admin_url('tools.php'));

        $backUrl = $backOverride !== '' ? $backOverride : $selfUrl;

        echo '<div id="wpsn-404-suggest-box" style="margin: 14px 0; padding: 12px 14px; border: 1px solid #dcdcde; background: #fff; border-radius: 6px; max-width: 1100px;">';

        echo '<div style="display:flex; align-items:center; justify-content:space-between; gap: 12px;">';
        echo '<div>';
        echo '<h3 style="margin:0;">' . esc_html__('Redirect suggestions', 'wp-seo-next') . '</h3>';
        if ($url !== '') {
            echo '<div class="description" style="margin-top:4px;">' . esc_html__('For:', 'wp-seo-next') . ' <code>' . esc_html($url) . '</code></div>';
        }
        echo '</div>';
        echo '<div style="display:flex; gap:6px; align-items:center;">';
        if ($backOverride !== '') {
            echo '<a class="button button-small" href="' . esc_url($backOverride) . '">' . esc_html__('Back', 'wp-seo-next') . '</a>';
        }
        echo '<a class="button button-small" href="' . esc_url($closeUrl) . '">' . esc_html__('Close', 'wp-seo-next') . '</a>';
        echo '</div>';
        echo '</div>';

        if ($url === '') {
            echo '<p style="margin:10px 0 0;"><em>' . esc_html__('Missing URL.', 'wp-seo-next') . '</em></p>';
            echo '</div>';
            return;
        }

        $suggestions = $this->suggester->suggestForLoggedUrl($url, 7);
        if (empty($suggestions)) {
            echo '<p style="margin:10px 0 0;"><em>' . esc_html__('No suggestions found.', 'wp-seo-next') . '</em></p>';
            echo '</div>';
            return;
        }

        $from = RedirectsUrl::normalizeFrom($url);

        echo '<table class="widefat striped" style="margin-top: 10px;">';
        echo '<thead><tr>';
        echo '<th>' . esc_html__('To', 'wp-seo-next') . '</th>';
        echo '<th style="width: 110px;">' . esc_html__('Confidence', 'wp-seo-next') . '</th>';
        echo '<th>' . esc_html__('Reason', 'wp-seo-next') . '</th>';
        echo '<th style="width: 240px;">' . esc_html__('Actions', 'wp-seo-next') . '</th>';
        echo '</tr></thead><tbody>';

        foreach ($suggestions as $s) {
            $to = isset($s['to']) ? (string) $s['to'] : '';
            $to = trim($to);
            if ($to === '') {
                continue;
            }

            $label = isset($s['label']) ? (string) $s['label'] : '';
            $reason = isset($s['reason']) ? (string) $s['reason'] : '';
            $score = isset($s['score']) ? (float) $s['score'] : 0.0;

            $prefillUrl = add_query_arg(
                [
                    'page' => 'wpsn-redirects',
                    'from' => $from,
                    'to' => $to,
                ],
                admin_url('tools.php')
            );

            $createUrl = wp_nonce_url(
                add_query_arg(
                    [
                        'action' => 'wpsn_404_create_redirect',
                        'from' => $from,
                        'to' => $to,
                        'back' => $backUrl,
                    ],
                    admin_url('admin-post.php')
                ),
                'wpsn_404_create_redirect'
            );

            $pct = (int) round(max(0.0, min(1.0, $score)) * 100);

            echo '<tr>';
            echo '<td><code>' . esc_html($to) . '</code>';
            if ($label !== '') {
                echo '<div class="description" style="margin-top:4px;">' . esc_html($label) . '</div>';
            }
            echo '</td>';
            echo '<td>' . esc_html((string) $pct) . '%</td>';
            echo '<td>' . esc_html($reason) . '</td>';
            echo '<td>';
            echo '<a class="button button-small" href="' . esc_url($createUrl) . '">' . esc_html__('Create 301', 'wp-seo-next') . '</a> ';
            echo '<a class="button button-small" href="' . esc_url($prefillUrl) . '">' . esc_html__('Prefill form', 'wp-seo-next') . '</a>';
            echo '</td>';
            echo '</tr>';
        }

        echo '</tbody></table>';
        echo '</div>';
    }

    private function renderRedirectSimulationBox(): void
    {
        if (empty($_GET['wpsn_sim'])) {
            return;
        }

        $matched = !empty($_GET['sim_match']);
        $destination = isset($_GET['sim_dest']) ? (string) wp_unslash($_GET['sim_dest']) : '';
        $message = isset($_GET['sim_msg']) ? sanitize_text_field((string) wp_unslash($_GET['sim_msg'])) : '';
        $requestPath = isset($_GET['test_request_path']) ? RedirectsUrl::normalizeFrom((string) wp_unslash($_GET['test_request_path'])) : '';
        $statusCode = isset($_GET['status_code']) ? (int) $_GET['status_code'] : 0;
        $warningsRaw = isset($_GET['sim_warnings']) ? (string) wp_unslash($_GET['sim_warnings']) : '';
        $warnings = array_values(array_filter(array_map('trim', explode("\n", str_replace(["\r\n", "\r"], "\n", $warningsRaw)))));

        $class = 'notice notice-info inline';
        if ($matched) {
            $class = 'notice notice-success inline';
            if (!empty($warnings)) {
                $class = 'notice notice-warning inline';
            }
        }

        echo '<div class="' . esc_attr($class) . '">';
        echo '<p><strong>' . esc_html__('Rule simulation', 'wp-seo-next') . '</strong></p>';
        echo '<p>';
        if ($requestPath !== '') {
            echo esc_html__('Request path:', 'wp-seo-next') . ' <code>' . esc_html($requestPath) . '</code><br />';
        }
        echo esc_html__('Matched:', 'wp-seo-next') . ' ' . ($matched ? esc_html__('Yes', 'wp-seo-next') : esc_html__('No', 'wp-seo-next'));
        echo '</p>';

        if ($matched) {
            if (in_array($statusCode, [410, 451], true)) {
                echo '<p>' . esc_html(sprintf(__('Would return HTTP %d.', 'wp-seo-next'), $statusCode)) . '</p>';
            } elseif ($destination !== '') {
                echo '<p>' . esc_html__('Destination:', 'wp-seo-next') . ' <code>' . esc_html($destination) . '</code></p>';
            }
        }

        if ($message !== '') {
            echo '<p>' . esc_html($message) . '</p>';
        }

        if (!empty($warnings)) {
            echo '<ul style="list-style: disc; margin-left: 20px;">';
            foreach ($warnings as $warning) {
                echo '<li>' . esc_html((string) $warning) . '</li>';
            }
            echo '</ul>';
        }

        echo '</div>';
    }

    /**
     * @return array<string, mixed>
     */
    private function readRedirectFormInput(): array
    {
        $matchType = isset($_POST['match_type']) ? sanitize_key((string) wp_unslash($_POST['match_type'])) : 'exact';
        if (!in_array($matchType, ['exact', 'prefix', 'regex'], true)) {
            $matchType = 'exact';
        }

        return [
            'from_url' => isset($_POST['from_url']) ? trim((string) wp_unslash($_POST['from_url'])) : '',
            'to_url' => isset($_POST['to_url']) ? trim((string) wp_unslash($_POST['to_url'])) : '',
            'match_type' => $matchType,
            'status_code' => isset($_POST['status_code']) ? (int) $_POST['status_code'] : 301,
            'enabled' => !empty($_POST['enabled']),
            'test_request_path' => isset($_POST['test_request_path']) ? trim((string) wp_unslash($_POST['test_request_path'])) : '',
        ];
    }

    /**
     * @param array<string,mixed> $data
     */
    private function buildRedirectFormUrl(array $data, int $id = 0, array $extra = []): string
    {
        $args = [
            'page' => 'wpsn-redirects',
            'from' => (string) ($data['from_url'] ?? ''),
            'to' => (string) ($data['to_url'] ?? ''),
            'match_type' => (string) ($data['match_type'] ?? 'exact'),
            'status_code' => (int) ($data['status_code'] ?? 301),
            'enabled' => !empty($data['enabled']) ? 1 : 0,
        ];

        if ($id > 0) {
            $args['edit'] = $id;
        }

        $testPath = isset($data['test_request_path']) ? (string) $data['test_request_path'] : '';
        if ($testPath !== '') {
            $args['test_request_path'] = $testPath;
        }

        foreach ($extra as $key => $value) {
            if ($value === null || $value === '') {
                continue;
            }
            $args[$key] = $value;
        }

        return add_query_arg($args, admin_url('tools.php'));
    }

    /**
     * @param array<string,mixed> $data
     */
    private function validateRedirectForm(array $data): string
    {
        $matchType = (string) ($data['match_type'] ?? 'exact');
        $from = (string) ($data['from_url'] ?? '');
        $to = (string) ($data['to_url'] ?? '');
        $statusCode = (int) ($data['status_code'] ?? 301);

        if ($matchType === 'regex') {
            $settings = $this->settings->getAll();
            if (empty($settings['enable_regex'])) {
                return __('Regex redirects are disabled. Enable them in module settings first.', 'wp-seo-next');
            }

            $validation = RedirectRegex::validateRule($from, $to, $statusCode);
            if (!$validation['ok']) {
                return (string) $validation['message'];
            }

            return '';
        }

        $from = RedirectsUrl::normalizeFrom($from);
        if ($from === '') {
            return __('Source path is required.', 'wp-seo-next');
        }

        if (!in_array($statusCode, [410, 451], true) && RedirectsUrl::normalizeTo($to) === '') {
            return __('Destination URL is required for redirect status codes.', 'wp-seo-next');
        }

        return '';
    }

    /**
     * @param array<string,mixed> $data
     * @return array{matched: bool, destination: string, message: string, warnings: array<int,string>}
     */
    private function simulateRedirectRule(array $data): array
    {
        $requestPath = RedirectsUrl::normalizeFrom((string) ($data['test_request_path'] ?? ''));
        if ($requestPath === '') {
            return [
                'matched' => false,
                'destination' => '',
                'message' => __('Enter a test request path first.', 'wp-seo-next'),
                'warnings' => [],
            ];
        }

        $matchType = (string) ($data['match_type'] ?? 'exact');
        $statusCode = (int) ($data['status_code'] ?? 301);
        $warnings = [];

        $rule = [
            'from_url' => $data['from_url'] ?? '',
            'to_url' => $data['to_url'] ?? '',
            'match_type' => $matchType,
            'status_code' => $statusCode,
        ];

        $matched = false;
        if ($matchType === 'exact') {
            $from = RedirectsUrl::normalizeFrom((string) ($data['from_url'] ?? ''));
            $rule['from_url'] = $from;
            $matched = ($from !== '' && $from === $requestPath);
        } elseif ($matchType === 'prefix') {
            $from = RedirectsUrl::normalizeFrom((string) ($data['from_url'] ?? ''));
            $rule['from_url'] = $from;
            $matched = ($from !== '' && strpos($requestPath, $from) === 0);
        } else {
            $validation = RedirectRegex::validateRule((string) ($data['from_url'] ?? ''), (string) ($data['to_url'] ?? ''), $statusCode);
            if (!$validation['ok']) {
                return [
                    'matched' => false,
                    'destination' => '',
                    'message' => (string) $validation['message'],
                    'warnings' => [],
                ];
            }

            $warnings = array_values(array_map('strval', $validation['warnings']));
            $rule['from_url'] = RedirectRegex::normalizeStoredPattern((string) ($data['from_url'] ?? ''));
            $matched = RedirectRegex::matches((string) $rule['from_url'], $requestPath);
        }

        if (!$matched) {
            return [
                'matched' => false,
                'destination' => '',
                'message' => __('This rule does not match the supplied request path.', 'wp-seo-next'),
                'warnings' => $warnings,
            ];
        }

        if (in_array($statusCode, [410, 451], true)) {
            return [
                'matched' => true,
                'destination' => '',
                'message' => sprintf(__('This rule would return HTTP %d.', 'wp-seo-next'), $statusCode),
                'warnings' => $warnings,
            ];
        }

        $rule['to_url'] = RedirectsUrl::normalizeTo((string) ($data['to_url'] ?? ''));
        $destination = RedirectsUrl::buildDestination($rule, $requestPath, false);
        if ($destination === '') {
            return [
                'matched' => true,
                'destination' => '',
                'message' => __('The destination could not be resolved.', 'wp-seo-next'),
                'warnings' => $warnings,
            ];
        }

        $currentAbsolute = home_url($requestPath);
        if (RedirectsUrl::urlsEqual($currentAbsolute, $destination)) {
            $warnings[] = __('Destination resolves to the same URL as the request path. This would be ignored as a loop.', 'wp-seo-next');
        }

        return [
            'matched' => true,
            'destination' => $destination,
            'message' => __('Simulation completed.', 'wp-seo-next'),
            'warnings' => $warnings,
        ];
    }

    public function handleSaveRule(): void
    {
        if (!current_user_can(Capabilities::CAP_REDIRECTS)) {
            wp_die(esc_html__('You do not have permission to perform this action.', 'wp-seo-next'));
        }
        check_admin_referer('wpsn_redirects_save_rule', 'wpsn_nonce');

        $id = isset($_POST['id']) ? (int) $_POST['id'] : 0;
        $data = $this->readRedirectFormInput();
        $back = $this->buildRedirectFormUrl($data, $id);

        $validationError = $this->validateRedirectForm($data);
        if ($validationError !== '') {
            wp_safe_redirect(add_query_arg([
                'wpsn_notice' => 'rule_save_failed',
                'wpsn_notice_type' => 'error',
                'wpsn_msg' => $validationError,
            ], $back));
            exit;
        }

        $saved = false;

        if ($id > 0) {
            $beforeRow = $this->redirects->getById($id);
            $saved = $this->redirects->update($id, $data);
            if ($saved) {
                $afterRow = $this->redirects->getById($id);
                $this->maybeLogRedirectRevision('update', $id, $beforeRow, $afterRow);

                /**
                 * Fires when a redirect rule is updated.
                 *
                 * @since 0.13.0
                 */
                do_action('seo/redirect/updated', $id, $data);
            }
        } else {
            $newId = $this->redirects->insert($data);
            if ($newId > 0) {
                $saved = true;
                $afterRow = $this->redirects->getById($newId);
                $this->maybeLogRedirectRevision('create', $newId, null, $afterRow);

                /**
                 * Fires when a redirect rule is created.
                 *
                 * @since 0.13.0
                 */
                do_action('seo/redirect/created', $newId, $data);
            }
        }

        if (!$saved) {
            wp_safe_redirect(add_query_arg([
                'wpsn_notice' => 'rule_save_failed',
                'wpsn_notice_type' => 'error',
                'wpsn_msg' => __('Failed to save redirect. Please review the rule and try again.', 'wp-seo-next'),
            ], $back));
            exit;
        }

        wp_safe_redirect(add_query_arg('wpsn_notice', 'rule_saved', admin_url('tools.php?page=wpsn-redirects')));
        exit;
    }

    public function handleTestRule(): void
    {
        if (!current_user_can(Capabilities::CAP_REDIRECTS)) {
            wp_die(esc_html__('You do not have permission to perform this action.', 'wp-seo-next'));
        }
        check_admin_referer('wpsn_redirects_save_rule', 'wpsn_nonce');

        $id = isset($_POST['id']) ? (int) $_POST['id'] : 0;
        $data = $this->readRedirectFormInput();
        $result = $this->simulateRedirectRule($data);

        $back = $this->buildRedirectFormUrl($data, $id, [
            'wpsn_sim' => 1,
            'sim_match' => !empty($result['matched']) ? 1 : 0,
            'sim_dest' => (string) ($result['destination'] ?? ''),
            'sim_msg' => (string) ($result['message'] ?? ''),
            'sim_warnings' => implode("\n", array_map('strval', (array) ($result['warnings'] ?? []))),
        ]);

        wp_safe_redirect($back);
        exit;
    }

    public function handleDeleteRule(): void
    {
        if (!current_user_can(Capabilities::CAP_REDIRECTS)) {
            wp_die(esc_html__('You do not have permission to perform this action.', 'wp-seo-next'));
        }
        check_admin_referer('wpsn_redirects_delete_rule');

        $id = isset($_GET['id']) ? (int) $_GET['id'] : 0;
        if ($id > 0) {
            $beforeRow = $this->redirects->getById($id);
            $ok = $this->redirects->delete($id);
            if ($ok) {
                $this->maybeLogRedirectRevision('delete', $id, $beforeRow, null);
            }
        }

        wp_safe_redirect(add_query_arg('wpsn_notice', 'rule_deleted', admin_url('tools.php?page=wpsn-redirects')));
        exit;
    }

    public function handleToggleRule(): void
    {
        if (!current_user_can(Capabilities::CAP_REDIRECTS)) {
            wp_die(esc_html__('You do not have permission to perform this action.', 'wp-seo-next'));
        }
        check_admin_referer('wpsn_redirects_toggle_rule');

        $id = isset($_GET['id']) ? (int) $_GET['id'] : 0;
        $enabled = isset($_GET['enabled']) ? (bool) (int) $_GET['enabled'] : true;
        if ($id > 0) {
            $beforeRow = $this->redirects->getById($id);
            $this->redirects->toggle($id, $enabled);

            $afterRow = $this->redirects->getById($id);
            $this->maybeLogRedirectRevision('toggle', $id, $beforeRow, $afterRow, ['enabled' => $enabled ? 1 : 0]);
        }

        wp_safe_redirect(add_query_arg('wpsn_notice', 'rule_toggled', admin_url('tools.php?page=wpsn-redirects')));
        exit;
    }

    public function handleSaveRedirectsSettings(): void
    {
        if (!current_user_can(Capabilities::CAP_REDIRECTS)) {
            wp_die(esc_html__('You do not have permission to perform this action.', 'wp-seo-next'));
        }
        check_admin_referer('wpsn_redirects_save_settings', 'wpsn_nonce');

        $incoming = $_POST['settings'] ?? [];
        if (!is_array($incoming)) {
            $incoming = [];
        }

        $current = $this->settings->getAll();
        $merged = array_merge($current, [
            'apply_only_on_404' => !empty($incoming['apply_only_on_404']),
            'preserve_query'    => !empty($incoming['preserve_query']),
            'enable_regex'      => !empty($incoming['enable_regex']),
            'auto_redirect_post_url_changes' => !empty($incoming['auto_redirect_post_url_changes']),
        ]);

        $this->settings->update($merged);

        $back = wp_get_referer();
        if (!$back) {
            $back = admin_url('tools.php?page=wpsn-redirects');
        }
        wp_safe_redirect(add_query_arg('wpsn_notice', 'redirect_settings_saved', $back));
        exit;
    }

    public function handleSave404Settings(): void
    {
        if (!current_user_can(Capabilities::CAP_REDIRECTS)) {
            wp_die(esc_html__('You do not have permission to perform this action.', 'wp-seo-next'));
        }
        check_admin_referer('wpsn_redirects_save_settings', 'wpsn_nonce');

        $incoming = $_POST['settings'] ?? [];
        if (!is_array($incoming)) {
            $incoming = [];
        }

        $current = $this->settings->getAll();

        $max = isset($incoming['log_max_rows']) ? (int) $incoming['log_max_rows'] : (int) ($current['log_max_rows'] ?? 500);

        $merged = array_merge($current, [
            'log_404' => !empty($incoming['log_404']),
            'log_ignore_logged_in' => !empty($incoming['log_ignore_logged_in']),
            'log_max_rows' => $max,
        ]);

        $this->settings->update($merged);

        // Prune immediately if needed.
        $s = $this->settings->getAll();
        if (!empty($s['log_max_rows'])) {
            $this->notFound->prune((int) $s['log_max_rows']);
        }

        $back = wp_get_referer();
        if (!$back) {
            $back = admin_url('tools.php?page=wpsn-404-monitor');
        }
        wp_safe_redirect(add_query_arg('wpsn_notice', '404_settings_saved', $back));
        exit;
    }

    public function handleDelete404(): void
    {
        if (!current_user_can(Capabilities::CAP_REDIRECTS)) {
            wp_die(esc_html__('You do not have permission to perform this action.', 'wp-seo-next'));
        }
        check_admin_referer('wpsn_404_delete');

        $id = isset($_GET['id']) ? (int) $_GET['id'] : 0;
        if ($id > 0) {
            $this->notFound->delete($id);
        }

        wp_safe_redirect(add_query_arg('wpsn_notice', 'log_deleted', admin_url('tools.php?page=wpsn-404-monitor')));
        exit;
    }

    public function handleClear404(): void
    {
        if (!current_user_can(Capabilities::CAP_REDIRECTS)) {
            wp_die(esc_html__('You do not have permission to perform this action.', 'wp-seo-next'));
        }
        check_admin_referer('wpsn_404_clear');

        $this->notFound->clear();
        wp_safe_redirect(add_query_arg('wpsn_notice', 'log_cleared', admin_url('tools.php?page=wpsn-404-monitor')));
        exit;
    }

    public function handle404BulkCreateRedirects(): void
    {
        if (!current_user_can(Capabilities::CAP_REDIRECTS)) {
            wp_die(esc_html__('You do not have permission to perform this action.', 'wp-seo-next'));
        }

        check_admin_referer('wpsn_404_bulk', 'wpsn_nonce');

        $idsRaw = $_POST['ids'] ?? [];
        if (!is_array($idsRaw)) {
            $idsRaw = [];
        }

        $ids = array_values(array_unique(array_filter(array_map('intval', $idsRaw), static function ($v) {
            return is_int($v) && $v > 0;
        })));

        $maxItems = (int) apply_filters('wpsn/redirects/404_bulk/max_items', 200);
        if ($maxItems < 1) {
            $maxItems = 200;
        }
        if (count($ids) > $maxItems) {
            $ids = array_slice($ids, 0, $maxItems);
        }

        $minScore = (float) apply_filters('wpsn/redirects/404_bulk/min_score', 0.72);
        $minScore = max(0.0, min(1.0, $minScore));

        $created = 0;
        $skippedExists = 0;
        $skippedNoSuggestion = 0;
        $skippedLowConf = 0;
        $errors = 0;

        foreach ($ids as $id) {
            $row = $this->notFound->getById((int) $id);
            $url = is_array($row) && !empty($row['url']) ? (string) $row['url'] : '';
            $url = sanitize_text_field($url);
            if ($url === '') {
                $errors++;
                continue;
            }

            $from = RedirectsUrl::normalizeFrom($url);
            if ($from === '' || $from === '/') {
                $errors++;
                continue;
            }

            $existing = $this->redirects->findByFromAndType($from, 'exact');
            if (is_array($existing) && !empty($existing['id'])) {
                $skippedExists++;
                continue;
            }

            $suggestions = $this->suggester->suggestForLoggedUrl($url, 7);
            if (empty($suggestions)) {
                $skippedNoSuggestion++;
                continue;
            }

            // Pick the highest score suggestion.
            $best = null;
            foreach ($suggestions as $s) {
                if (!is_array($s)) {
                    continue;
                }
                $to = isset($s['to']) ? (string) $s['to'] : '';
                $score = isset($s['score']) ? (float) $s['score'] : 0.0;
                if ($to === '') {
                    continue;
                }
                if ($best === null || $score > (float) ($best['score'] ?? 0.0)) {
                    $best = ['to' => $to, 'score' => $score];
                }
            }

            if ($best === null) {
                $skippedNoSuggestion++;
                continue;
            }

            $to = RedirectsUrl::normalizeTo((string) ($best['to'] ?? ''));
            $score = (float) ($best['score'] ?? 0.0);

            if ($to === '' || $score < $minScore) {
                $skippedLowConf++;
                continue;
            }

            // Safety: do not bulk-create external redirects.
            $toHost = wp_parse_url($to, PHP_URL_HOST);
            if (is_string($toHost) && $toHost !== '') {
                $homeHost = wp_parse_url(home_url('/'), PHP_URL_HOST);
                if (is_string($homeHost) && $homeHost !== '' && strtolower($toHost) !== strtolower($homeHost)) {
                    $skippedLowConf++;
                    continue;
                }
            }

            $data = [
                'from_url' => $from,
                'to_url' => $to,
                'match_type' => 'exact',
                'status_code' => 301,
                'enabled' => true,
            ];

            $newId = $this->redirects->insert($data);
            if ($newId > 0) {
                $afterRow = $this->redirects->getById($newId);
                $this->maybeLogRedirectRevision('create', $newId, null, $afterRow);

                /**
                 * Fires when a redirect rule is created.
                 */
                do_action('seo/redirect/created', $newId, $data);

                $created++;
                continue;
            }

            $errors++;
        }

        $back = admin_url('tools.php?page=wpsn-404-monitor');
        $backRaw = isset($_POST['back']) ? (string) wp_unslash($_POST['back']) : '';
        $backSanitized = $this->sanitizeBackUrl($backRaw);
        if ($backSanitized !== '') {
            $back = $backSanitized;
        }

        wp_safe_redirect(add_query_arg([
            'wpsn_notice' => 'bulk_redirects_created',
            'created' => (string) $created,
            'skipped_exists' => (string) $skippedExists,
            'skipped_no_suggestion' => (string) $skippedNoSuggestion,
            'skipped_low_conf' => (string) $skippedLowConf,
            'errors' => (string) $errors,
        ], $back));
        exit;
    }

    public function handle404BulkDelete(): void
    {
        if (!current_user_can(Capabilities::CAP_REDIRECTS)) {
            wp_die(esc_html__('You do not have permission to perform this action.', 'wp-seo-next'));
        }

        check_admin_referer('wpsn_404_bulk', 'wpsn_nonce');

        $idsRaw = $_POST['ids'] ?? [];
        if (!is_array($idsRaw)) {
            $idsRaw = [];
        }

        $ids = array_values(array_unique(array_filter(array_map('intval', $idsRaw), static function ($v) {
            return is_int($v) && $v > 0;
        })));

        $maxItems = (int) apply_filters('wpsn/redirects/404_bulk_delete/max_items', 1000);
        if ($maxItems < 1) {
            $maxItems = 1000;
        }
        if (count($ids) > $maxItems) {
            $ids = array_slice($ids, 0, $maxItems);
        }

        $deleted = 0;
        $errors = 0;

        foreach ($ids as $id) {
            $ok = $this->notFound->delete((int) $id);
            if ($ok) {
                $deleted++;
            } else {
                $errors++;
            }
        }

        $back = admin_url('tools.php?page=wpsn-404-monitor');
        $backRaw = isset($_POST['back']) ? (string) wp_unslash($_POST['back']) : '';
        $backSanitized = $this->sanitizeBackUrl($backRaw);
        if ($backSanitized !== '') {
            $back = $backSanitized;
        }

        wp_safe_redirect(add_query_arg([
            'wpsn_notice' => 'bulk_log_deleted',
            'deleted' => (string) $deleted,
            'errors' => (string) $errors,
        ], $back));
        exit;
    }

    public function ajaxSuggestRedirects(): void
    {
        if (!current_user_can(Capabilities::CAP_REDIRECTS)) {
            wp_send_json_error(['message' => __('Forbidden', 'wp-seo-next')], 403);
        }

        check_ajax_referer(self::AJAX_NONCE_ACTION, 'nonce');

        $id = isset($_POST['id']) ? (int) $_POST['id'] : 0;

        $url = '';
        if ($id > 0) {
            $row = $this->notFound->getById($id);
            if (is_array($row) && !empty($row['url'])) {
                $url = (string) $row['url'];
            }
        }

        if ($url === '') {
            $url = isset($_POST['url']) ? (string) wp_unslash($_POST['url']) : '';
        }
        $url = sanitize_text_field($url);

        if ($url === '') {
            wp_send_json_success(['suggestions' => []]);
        }

        $suggestions = $this->suggester->suggestForLoggedUrl($url, 7);

        $from = RedirectsUrl::normalizeFrom($url);
        $out = [];
        foreach ($suggestions as $s) {
            $to = isset($s['to']) ? (string) $s['to'] : '';
            $to = trim($to);
            if ($to === '') {
                continue;
            }

            $prefillUrl = add_query_arg(
                [
                    'page' => 'wpsn-redirects',
                    'from' => $from,
                    'to' => $to,
                ],
                admin_url('tools.php')
            );

            $createUrl = wp_nonce_url(
                add_query_arg(
                    [
                        'action' => 'wpsn_404_create_redirect',
                        'from' => $from,
                        'to' => $to,
                        'back' => $backUrl,
                    ],
                    admin_url('admin-post.php')
                ),
                'wpsn_404_create_redirect'
            );

            $out[] = [
                'to' => $to,
                'label' => isset($s['label']) ? (string) $s['label'] : '',
                'reason' => isset($s['reason']) ? (string) $s['reason'] : '',
                'score' => isset($s['score']) ? (float) $s['score'] : 0.0,
                'prefill_url' => $prefillUrl,
                'create_url' => $createUrl,
            ];

            if (count($out) >= 7) {
                break;
            }
        }

        wp_send_json_success([
            'requested' => $from,
            'suggestions' => $out,
        ]);
    }

    public function handleCreateRedirectFrom404(): void
    {
        if (!current_user_can(Capabilities::CAP_REDIRECTS)) {
            wp_die(esc_html__('You do not have permission to perform this action.', 'wp-seo-next'));
        }

        check_admin_referer('wpsn_404_create_redirect');

        $fromRaw = isset($_GET['from']) ? (string) wp_unslash($_GET['from']) : '';
        $toRaw = isset($_GET['to']) ? (string) wp_unslash($_GET['to']) : '';

        $from = RedirectsUrl::normalizeFrom(sanitize_text_field($fromRaw));
        $to = RedirectsUrl::normalizeTo(sanitize_text_field($toRaw));

        $back = admin_url('tools.php?page=wpsn-404-monitor');
        $backRaw = isset($_GET['back']) ? (string) wp_unslash($_GET['back']) : '';
        $backSanitized = $this->sanitizeBackUrl($backRaw);
        if ($backSanitized !== '') {
            $back = $backSanitized;
        }

        if ($from === '' || $from === '/' || $to === '') {
            wp_safe_redirect(add_query_arg(['wpsn_notice' => 'suggest_redirect_failed', 'wpsn_notice_type' => 'error'], $back));
            exit;
        }

        $existing = $this->redirects->findByFromAndType($from, 'exact');
        if (is_array($existing) && !empty($existing['id'])) {
            wp_safe_redirect(add_query_arg(['wpsn_notice' => 'suggest_redirect_exists', 'wpsn_notice_type' => 'warning'], $back));
            exit;
        }

        $newId = $this->redirects->insert([
            'from_url' => $from,
            'to_url' => $to,
            'match_type' => 'exact',
            'status_code' => 301,
            'enabled' => true,
        ]);

        if ($newId > 0) {
            $afterRow = $this->redirects->getById($newId);
            $this->maybeLogRedirectRevision('create', $newId, null, $afterRow);

            /**
             * Fires when a redirect rule is created.
             */
            do_action('seo/redirect/created', $newId, [
                'from_url' => $from,
                'to_url' => $to,
                'match_type' => 'exact',
                'status_code' => 301,
                'enabled' => true,
                'source' => '404_suggestion',
            ]);

            wp_safe_redirect(add_query_arg(['wpsn_notice' => 'suggest_redirect_created', 'wpsn_notice_type' => 'success'], $back));
            exit;
        }

        wp_safe_redirect(add_query_arg(['wpsn_notice' => 'suggest_redirect_failed', 'wpsn_notice_type' => 'error'], $back));
        exit;
    }

    public function handleImportCsv(): void
    {
        if (!current_user_can(Capabilities::CAP_REDIRECTS)) {
            wp_die(esc_html__('You do not have permission to perform this action.', 'wp-seo-next'));
        }
        check_admin_referer('wpsn_redirects_import_csv', 'wpsn_nonce');

        if (empty($_FILES['csv']) || !is_array($_FILES['csv'])) {
            wp_safe_redirect(add_query_arg('wpsn_notice', 'import_failed', admin_url('tools.php?page=wpsn-redirects')));
            exit;
        }

        $file = $_FILES['csv'];
        if (!empty($file['error'])) {
            wp_safe_redirect(add_query_arg('wpsn_notice', 'import_failed', admin_url('tools.php?page=wpsn-redirects')));
            exit;
        }

        $path = isset($file['tmp_name']) ? (string) $file['tmp_name'] : '';
        if ($path === '' || !is_readable($path)) {
            wp_safe_redirect(add_query_arg('wpsn_notice', 'import_failed', admin_url('tools.php?page=wpsn-redirects')));
            exit;
        }

        $handle = fopen($path, 'r');
        if (!$handle) {
            wp_safe_redirect(add_query_arg('wpsn_notice', 'import_failed', admin_url('tools.php?page=wpsn-redirects')));
            exit;
        }

        $settings = $this->settings->getAll();
        $allowRegex = !empty($settings['enable_regex']);

        $rowNum = 0;
        $imported = 0;
        $skipped = 0;

        while (($row = fgetcsv($handle)) !== false) {
            $rowNum++;
            if (!is_array($row) || count($row) < 2) {
                $skipped++;
                continue;
            }

            // Optional header detection.
            if ($rowNum === 1 && isset($row[0]) && is_string($row[0]) && strtolower(trim($row[0])) === 'from_url') {
                continue;
            }

            $matchType = isset($row[3]) ? sanitize_key((string) $row[3]) : 'exact';
            if (!in_array($matchType, ['exact', 'prefix', 'regex'], true)) {
                $matchType = 'exact';
            }

            $data = [
                'from_url' => $row[0] ?? '',
                'to_url' => $row[1] ?? '',
                'status_code' => isset($row[2]) ? (int) $row[2] : 301,
                'match_type' => $matchType,
                'enabled' => isset($row[4]) ? (bool) (int) $row[4] : true,
            ];

            if ($matchType === 'regex') {
                if (!$allowRegex) {
                    $skipped++;
                    continue;
                }

                $validation = RedirectRegex::validateRule(
                    (string) ($data['from_url'] ?? ''),
                    (string) ($data['to_url'] ?? ''),
                    (int) ($data['status_code'] ?? 301)
                );
                if (!$validation['ok']) {
                    $skipped++;
                    continue;
                }
            }

            $id = $this->redirects->insert($data);
            if ($id > 0) {
                $imported++;
            } else {
                $skipped++;
            }

            if (($imported + $skipped) > 5000) {
                break; // safety
            }
        }

        fclose($handle);

        wp_safe_redirect(add_query_arg([
            'wpsn_notice' => 'import_done',
            'imported' => (string) $imported,
            'skipped' => (string) $skipped,
        ], admin_url('tools.php?page=wpsn-redirects')));
        exit;
    }


    private function renderPagination(string $baseUrl, int $current, int $totalPages, string $search = ''): void
    {
        if ($totalPages <= 1) {
            return;
        }

        echo '<div class="tablenav"><div class="tablenav-pages">';

        $args = [];
        if ($search !== '') {
            $args['s'] = $search;
        }

        $prev = max(1, $current - 1);
        $next = min($totalPages, $current + 1);

        $prevUrl = add_query_arg(array_merge($args, ['paged' => $prev]), $baseUrl);
        $nextUrl = add_query_arg(array_merge($args, ['paged' => $next]), $baseUrl);

        printf(
            '<span class="pagination-links"><a class="prev-page button" href="%s">%s</a> <span class="paging-input">%d / <span class="total-pages">%d</span></span> <a class="next-page button" href="%s">%s</a></span>',
            esc_url($prevUrl),
            esc_html__('‹', 'wp-seo-next'),
            (int) $current,
            (int) $totalPages,
            esc_url($nextUrl),
            esc_html__('›', 'wp-seo-next')
        );

        echo '</div></div>';
    }

    /**
     * @param array<string,mixed>|null $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'] : '',
        ];
    }

    /**
     * Persist unified revision entry for redirects changes.
     *
     * @param array<string,mixed>|null $beforeRow
     * @param array<string,mixed>|null $afterRow
     * @param array<string,mixed> $meta
     */
    private function maybeLogRedirectRevision(string $action, int $redirectId, ?array $beforeRow, ?array $afterRow, array $meta = []): void
    {
        if ($redirectId <= 0) {
            return;
        }

        try {
            // Never hard-fail the admin action if revisions logging is unavailable.
            RevisionsInstaller::maybeInstall();
            $revRepo = new RevisionsRepository($this->logger);

            $before = ['redirect' => $this->sanitizeRedirectRow($beforeRow)];
            $after  = ['redirect' => $this->sanitizeRedirectRow($afterRow)];

            // Avoid noise: skip no-op updates.
            if ($before['redirect'] === $after['redirect'] && !in_array($action, ['create', 'delete'], true)) {
                return;
            }

            $sub = '';
            if (is_array($after['redirect']) && !empty($after['redirect']['match_type'])) {
                $sub = (string) $after['redirect']['match_type'];
            } elseif (is_array($before['redirect']) && !empty($before['redirect']['match_type'])) {
                $sub = (string) $before['redirect']['match_type'];
            }

            $revRepo->insert([
                'object_type' => 'redirect',
                'object_id' => $redirectId,
                'object_subtype' => $sub,
                'action' => $action,
                'source' => 'redirects',
                'actor_user_id' => (int) get_current_user_id(),
                'before' => $before,
                'after' => $after,
                'meta' => $meta,
            ]);
        } catch (\Throwable $e) {
            // Silent by design.
        }
    }
}
