<?php
/**
 * Tam Rank — Auto-redirect on delete
 *
 * Prompts the admin to create a 301 redirect when a post/page is trashed or
 * permanently deleted. Supports:
 *  - Individual notices for small (≤ BULK_THRESHOLD) delete actions
 *  - Bulk-modal for large (> BULK_THRESHOLD) delete actions
 *  - Cumulative cap (> CUMULATIVE_CAP pending items → auto-promote to bulk)
 *  - Chain-repair (incoming redirects to trashed URL are re-targeted)
 *
 * Storage: user-scoped transient `tamrank_pending_redirects_{user_id}`, 24h TTL.
 * See Obsidian plan: Plugin/Features/Auto-redirect bij delete.md (PLAN v3).
 *
 * @package TamRank
 */

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

class TamRank_Auto_Redirect_On_Delete
{
    const TRANSIENT_PREFIX = 'tamrank_pending_redirects_';
    const BULK_THRESHOLD   = 10;
    const CUMULATIVE_CAP   = 25;
    const TRANSIENT_TTL    = HOUR_IN_SECONDS;
    const NONCE_ACTION     = 'tamrank_auto_redirect_on_delete';

    /** @var array In-request capture buffer, flushed on shutdown */
    private static $captures_this_request = array();

    public function __construct()
    {
        // Toggle-gate (decision #27): early-return prevents hook binding when
        // feature is off. Toggle changes require a page refresh to take effect.
        // Note: tamrank_can_use() lives in \TamRank\ namespace; from global scope
        // we use the global helper tam_rank_can_use_feature() from license-helpers.
        if (! function_exists('tam_rank_can_use_feature') || ! tam_rank_can_use_feature('auto_redirect_on_delete')) {
            return;
        }

        // User opt-out via Settings → Features toggle (default on).
        $general = tam_rank_cached_option('tam_rank_general_settings', array());
        $enabled = isset($general['enable_auto_redirect_on_delete']) ? (int) $general['enable_auto_redirect_on_delete'] : 1;
        if (! $enabled) {
            return;
        }

        add_action('wp_trash_post',      array($this, 'capture_on_trash'));
        add_action('before_delete_post', array($this, 'capture_on_delete'));
        add_action('untrashed_post',     array($this, 'cancel_on_untrash'));
        add_action('shutdown',           array($this, 'flush_captures'));
        add_action('admin_notices',      array($this, 'render_notice'));
        add_action('admin_enqueue_scripts', array($this, 'enqueue_assets'));

        add_action('wp_ajax_tamrank_create_delete_redirect',       array($this, 'ajax_create_redirect'));
        add_action('wp_ajax_tamrank_create_bulk_delete_redirects', array($this, 'ajax_create_bulk_redirects'));
        add_action('wp_ajax_tamrank_dismiss_delete_redirect',      array($this, 'ajax_dismiss'));
    }

    /* ---------------------------------------------------------------------
     * Capture phase (hooks → static buffer)
     * --------------------------------------------------------------------- */

    public function capture_on_trash($post_id)
    {
        $this->buffer((int) $post_id, 'trash');
    }

    public function capture_on_delete($post_id)
    {
        $this->buffer((int) $post_id, 'delete');
    }

    /**
     * Cancel a pending prompt when a post is restored from trash.
     * The URL is live again, so no redirect is needed.
     */
    public function cancel_on_untrash($post_id)
    {
        $post_id = (int) $post_id;
        if (! $post_id) {
            return;
        }

        // Also drop from in-request buffer if a trash + untrash race somehow lands here.
        if (isset(self::$captures_this_request[$post_id])) {
            unset(self::$captures_this_request[$post_id]);
        }

        $user_id = get_current_user_id();
        if (! $user_id) {
            return;
        }

        $key = self::TRANSIENT_PREFIX . $user_id;
        $pending = get_transient($key);
        if (! is_array($pending) || empty($pending['items'][$post_id])) {
            return;
        }

        unset($pending['items'][$post_id]);
        set_transient($key, $pending, self::TRANSIENT_TTL);
    }

    private function buffer($post_id, $reason)
    {
        if (! $this->is_eligible($post_id)) {
            return;
        }

        self::$captures_this_request[$post_id] = array(
            'post_id'            => $post_id,
            'url'                => $this->get_post_url_before_delete($post_id),
            'relative'           => $this->get_relative_path($post_id),
            'post_title'         => get_the_title($post_id),
            'post_type'          => get_post_type($post_id),
            'suggestions'        => $this->get_target_suggestions($post_id),
            'incoming_redirects' => $this->find_redirects_pointing_to($post_id),
            'reason'             => $reason,
            'timestamp'          => time(),
        );
    }

    /* ---------------------------------------------------------------------
     * Eligibility
     * --------------------------------------------------------------------- */

    private function is_eligible($post_id)
    {
        if (! $post_id) {
            return false;
        }
        $post = get_post($post_id);
        if (! $post) {
            return false;
        }

        // Only publicly viewable post types (posts, pages, CPTs with public=true).
        if (! is_post_type_viewable($post->post_type)) {
            return false;
        }
        // Attachments use a separate URL system; 301 is risky.
        if ($post->post_type === 'attachment') {
            return false;
        }
        // Skip internal placeholder statuses that never had a live URL.
        $skip_statuses = array('auto-draft', 'inherit');
        if (in_array($post->post_status, $skip_statuses, true)) {
            return false;
        }

        // Skip homepage.
        $relative = $this->get_relative_path($post_id);
        if ($relative === '/' || $relative === '') {
            return false;
        }

        // Skip if redirect already exists with this URL as source.
        if ($this->source_redirect_exists($relative)) {
            return false;
        }

        // Capture unconditionally; the redirect itself is created and validated
        // server-side in ajax_create_redirect / ajax_create_bulk_redirects.
        return true;
    }

    /* ---------------------------------------------------------------------
     * URL helpers (4-variant matching — decision #22)
     * --------------------------------------------------------------------- */

    private function get_post_url_before_delete($post_id)
    {
        return get_permalink($post_id);
    }

    private function get_relative_path($post_id)
    {
        $absolute = get_permalink($post_id);
        if (! $absolute) {
            return '';
        }
        $parsed = wp_parse_url($absolute);
        $path   = isset($parsed['path']) ? $parsed['path'] : '/';
        $query  = ! empty($parsed['query']) ? ('?' . $parsed['query']) : '';
        return $path . $query;
    }

    /**
     * Returns up to 4 url variants for a post: [abs_trail, abs_no, rel_trail, rel_no].
     * De-duplicated via array_unique.
     */
    private function get_url_forms($post_id)
    {
        $absolute = get_permalink($post_id);
        if (! $absolute) {
            return array();
        }
        $parsed = wp_parse_url($absolute);
        $path   = isset($parsed['path']) ? $parsed['path'] : '/';
        $query  = ! empty($parsed['query']) ? ('?' . $parsed['query']) : '';

        $abs_base = isset($parsed['scheme']) ? ($parsed['scheme'] . '://' . $parsed['host']) : '';
        if (! empty($parsed['port'])) {
            $abs_base .= ':' . $parsed['port'];
        }

        $forms = array(
            user_trailingslashit($abs_base . $path) . $query,
            untrailingslashit($abs_base . $path) . $query,
            user_trailingslashit($path) . $query,
            untrailingslashit($path) . $query,
        );
        return array_values(array_unique(array_filter($forms)));
    }

    private function find_redirects_pointing_to($post_id)
    {
        global $wpdb;
        $forms = $this->get_url_forms($post_id);
        if (empty($forms)) {
            return array();
        }
        $table = $wpdb->prefix . 'tam_rank_redirects';
        $placeholders = implode(',', array_fill(0, count($forms), '%s'));
        // phpcs:disable WordPress.DB.PreparedSQL, WordPress.DB.PreparedSQLPlaceholders, WordPress.DB.DirectDatabaseQuery, PluginCheck.Security.DirectDB.UnescapedDBParameter -- only the table identifier and a runtime-built %s placeholder list are interpolated; user values bind via $wpdb->prepare(); direct read on custom plugin table tam_rank_redirects (no caching applicable).
        $sql = $wpdb->prepare(
            "SELECT id FROM {$table} WHERE target_url IN ({$placeholders})",
            $forms
        );
        return array_map('intval', (array) $wpdb->get_col($sql));
        // phpcs:enable WordPress.DB.PreparedSQL, WordPress.DB.PreparedSQLPlaceholders, WordPress.DB.DirectDatabaseQuery, PluginCheck.Security.DirectDB.UnescapedDBParameter
    }

    private function retarget_incoming_redirects($old_forms, $new_target)
    {
        global $wpdb;
        if (empty($old_forms)) {
            return 0;
        }
        $table = $wpdb->prefix . 'tam_rank_redirects';
        $placeholders = implode(',', array_fill(0, count($old_forms), '%s'));
        $args = array_merge(array($new_target), $old_forms);
        // phpcs:disable WordPress.DB.PreparedSQL, WordPress.DB.PreparedSQLPlaceholders, WordPress.DB.DirectDatabaseQuery, PluginCheck.Security.DirectDB.UnescapedDBParameter -- only the table identifier and a runtime-built %s placeholder list are interpolated; user values bind via $wpdb->prepare(); direct write on custom plugin table tam_rank_redirects (no caching applicable).
        $sql = $wpdb->prepare(
            "UPDATE {$table} SET target_url = %s WHERE target_url IN ({$placeholders})",
            $args
        );
        $result = $wpdb->query($sql);
        // phpcs:enable WordPress.DB.PreparedSQL, WordPress.DB.PreparedSQLPlaceholders, WordPress.DB.DirectDatabaseQuery, PluginCheck.Security.DirectDB.UnescapedDBParameter
        return $result === false ? 0 : (int) $result;
    }

    private function source_redirect_exists($relative)
    {
        global $wpdb;
        $table = $wpdb->prefix . 'tam_rank_redirects';
        // phpcs:disable WordPress.DB.PreparedSQL, WordPress.DB.PreparedSQLPlaceholders, WordPress.DB.DirectDatabaseQuery, PluginCheck.Security.DirectDB.UnescapedDBParameter -- only the table identifier is interpolated; user value binds via $wpdb->prepare(); direct read on custom plugin table tam_rank_redirects (no caching applicable).
        $count = $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM {$table} WHERE source_url = %s",
            $relative
        ));
        // phpcs:enable WordPress.DB.PreparedSQL, WordPress.DB.PreparedSQLPlaceholders, WordPress.DB.DirectDatabaseQuery, PluginCheck.Security.DirectDB.UnescapedDBParameter
        return (int) $count > 0;
    }

    /* ---------------------------------------------------------------------
     * Target suggestions
     * --------------------------------------------------------------------- */

    private function get_target_suggestions($post_id)
    {
        $post = get_post($post_id);
        $suggestions = array();

        // Parent page (hierarchical)
        if ($post && $post->post_parent && is_post_type_hierarchical($post->post_type)) {
            $parent = get_post($post->post_parent);
            if ($parent && $parent->post_status === 'publish') {
                $suggestions['parent'] = array(
                    'url'   => get_permalink($parent),
                    'label' => get_the_title($parent),
                );
            }
        }

        // Primary category archive (if the post type has the 'category' taxonomy attached)
        if ($post && in_array('category', get_object_taxonomies($post->post_type), true)) {
            $cats = get_the_category($post_id);
            if (! empty($cats)) {
                $cat = $cats[0];
                $suggestions['category'] = array(
                    'url'   => get_category_link($cat->term_id),
                    'label' => $cat->name,
                );
            }
        }

        // Homepage — always available
        $suggestions['home'] = array(
            'url'   => home_url('/'),
            'label' => home_url('/'),
        );

        return $suggestions;
    }

    /* ---------------------------------------------------------------------
     * Flush (shutdown → transient write)
     * --------------------------------------------------------------------- */

    public function flush_captures()
    {
        if (empty(self::$captures_this_request)) {
            return;
        }
        $user_id = get_current_user_id();
        if (! $user_id) {
            // CLI / cron / unauthenticated → no one to notice
            return;
        }

        $count = count(self::$captures_this_request);
        $key   = self::TRANSIENT_PREFIX . $user_id;
        $pending = get_transient($key);
        if (! is_array($pending)) {
            $pending = array('items' => array(), 'bulk_flag' => false, 'bulk_count' => 0, 'bulk_urls' => array());
        }

        $existing_individual = is_array($pending['items']) ? count($pending['items']) : 0;
        $existing_bulk       = ! empty($pending['bulk_flag']) ? (int) $pending['bulk_count'] : 0;
        $existing_total      = $existing_individual + $existing_bulk;

        // Filter for cumulative cap
        $cap = (int) apply_filters('tamrank_auto_redirect_cumulative_cap', self::CUMULATIVE_CAP);

        $goes_bulk = ($count > self::BULK_THRESHOLD)
                  || (($existing_total + $count) > $cap);

        if ($goes_bulk) {
            $cumulative = $existing_total + $count;
            $merged_urls = array_merge(
                $this->strip_urls_from_items($pending['items']),
                ! empty($pending['bulk_urls']) ? (array) $pending['bulk_urls'] : array(),
                $this->strip_urls_from_captures(self::$captures_this_request)
            );

            $pending['bulk_flag']   = true;
            $pending['bulk_count']  = $cumulative;
            $pending['bulk_reason'] = $this->dominant_reason();
            $pending['bulk_urls']   = $merged_urls;
            $pending['items']       = array(); // clear to avoid duplicate UI
        } else {
            // Use union operator to preserve integer post_id keys (array_merge reindexes).
            $pending['items'] = self::$captures_this_request + (is_array($pending['items']) ? $pending['items'] : array());
        }

        set_transient($key, $pending, self::TRANSIENT_TTL);

        // Reset static buffer (shutdown runs once, but be defensive)
        self::$captures_this_request = array();
    }

    private function dominant_reason()
    {
        $counts = array('trash' => 0, 'delete' => 0);
        foreach (self::$captures_this_request as $entry) {
            if (isset($entry['reason']) && isset($counts[$entry['reason']])) {
                $counts[$entry['reason']]++;
            }
        }
        return $counts['delete'] > $counts['trash'] ? 'delete' : 'trash';
    }

    private function strip_urls_from_items($items)
    {
        $out = array();
        if (! is_array($items)) {
            return $out;
        }
        foreach ($items as $item) {
            $out[] = array(
                'id'       => isset($item['post_id']) ? (int) $item['post_id'] : 0,
                'url'      => isset($item['url']) ? (string) $item['url'] : '',
                'relative' => isset($item['relative']) ? (string) $item['relative'] : '',
            );
        }
        return $out;
    }

    private function strip_urls_from_captures($captures)
    {
        return $this->strip_urls_from_items($captures);
    }

    /* ---------------------------------------------------------------------
     * Rendering (admin_notices → HTML branches)
     * --------------------------------------------------------------------- */

    public function render_notice()
    {
        $user_id = get_current_user_id();
        if (! $user_id) {
            return;
        }
        $key = self::TRANSIENT_PREFIX . $user_id;
        $pending = get_transient($key);
        if (! is_array($pending) || (empty($pending['items']) && empty($pending['bulk_flag']))) {
            return;
        }

        if (! empty($pending['bulk_flag'])) {
            $this->render_bulk_notice($pending);
        } elseif (! empty($pending['items'])) {
            $cleaned = false;
            foreach ($pending['items'] as $post_id => $item) {
                $pid = (int) $post_id;
                // Skip / clean up stale entries with invalid post_id (pre-fix legacy).
                if ($pid <= 0) {
                    unset($pending['items'][$post_id]);
                    $cleaned = true;
                    continue;
                }
                $this->render_individual_notice($pid, $item);
            }
            if ($cleaned) {
                set_transient(self::TRANSIENT_PREFIX . $user_id, $pending, self::TRANSIENT_TTL);
            }
        }
    }

    private function render_individual_notice($post_id, $item)
    {
        $nonce = wp_create_nonce(self::NONCE_ACTION);
        $title = isset($item['post_title']) ? $item['post_title'] : '';
        $suggestions = isset($item['suggestions']) ? (array) $item['suggestions'] : array();
        $incoming_count = isset($item['incoming_redirects']) ? count((array) $item['incoming_redirects']) : 0;

        ?>
        <div class="notice notice-info is-dismissible tr-redirect-prompt" data-post-id="<?php echo (int) $post_id; ?>" data-incoming-count="<?php echo (int) $incoming_count; ?>">
            <p><strong><?php echo esc_html(sprintf(tamrank__('auto_redirect_on_delete_prompt_title', 'Post "%s" moved to trash.'), $title)); ?></strong></p>
            <p>
                <?php
                $body = tamrank__('auto_redirect_on_delete_prompt_body', 'Create a 301 redirect for %s to:');
                $parts = explode('%s', $body, 2);
                echo esc_html($parts[0]);
                ?>
                <code><?php echo esc_html($item['relative']); ?></code>
                <?php if (isset($parts[1])) echo esc_html($parts[1]); ?>
            </p>
            <div class="tr-redirect-prompt-form">
                <select class="tr-redirect-target-select">
                    <?php if (isset($suggestions['parent'])) : ?>
                        <option value="parent" data-url="<?php echo esc_attr($suggestions['parent']['url']); ?>" selected>
                            <?php echo esc_html(sprintf(tamrank__('auto_redirect_target_parent', 'Parent page — %s'), $suggestions['parent']['label'])); ?>
                        </option>
                    <?php endif; ?>
                    <?php if (isset($suggestions['category'])) : ?>
                        <option value="category" data-url="<?php echo esc_attr($suggestions['category']['url']); ?>"<?php if (! isset($suggestions['parent'])) echo ' selected'; ?>>
                            <?php echo esc_html(sprintf(tamrank__('auto_redirect_target_category', 'Category archive — %s'), $suggestions['category']['label'])); ?>
                        </option>
                    <?php endif; ?>
                    <option value="home" data-url="<?php echo esc_attr($suggestions['home']['url']); ?>"<?php if (! isset($suggestions['parent']) && ! isset($suggestions['category'])) echo ' selected'; ?>>
                        <?php echo esc_html(tamrank__('auto_redirect_target_home', 'Homepage')); ?>
                    </option>
                    <option value="custom"><?php echo esc_html(tamrank__('auto_redirect_target_custom', 'Custom URL...')); ?></option>
                </select>
                <input type="url" class="tr-redirect-custom-url" placeholder="https://..." style="display:none" />
                <button class="tr-btn tr-btn--primary tr-redirect-create-btn" data-post-id="<?php echo (int) $post_id; ?>" data-nonce="<?php echo esc_attr($nonce); ?>">
                    <?php echo esc_html(tamrank__('auto_redirect_create_btn', 'Create redirect')); ?>
                </button>
                <button class="tr-redirect-dismiss-btn" data-post-id="<?php echo (int) $post_id; ?>" data-nonce="<?php echo esc_attr($nonce); ?>">
                    <?php echo esc_html(tamrank__('auto_redirect_dismiss_btn', 'Dismiss')); ?>
                </button>
            </div>
        </div>
        <?php
    }

    private function render_bulk_notice($pending)
    {
        $nonce = wp_create_nonce(self::NONCE_ACTION);
        $bulk_count = (int) $pending['bulk_count'];
        $effective  = $bulk_count;

        ?>
        <div class="notice notice-info is-dismissible tr-redirect-prompt tr-redirect-prompt--bulk" data-bulk-count="<?php echo (int) $bulk_count; ?>">
            <p><strong><?php echo esc_html(sprintf(tamrank__('auto_redirect_bulk_title', '%d posts moved to trash.'), $bulk_count)); ?></strong></p>
            <p><?php echo esc_html(sprintf(tamrank__('auto_redirect_bulk_body', 'Create a 301 redirect for all %d URLs to:'), $bulk_count)); ?></p>
            <div class="tr-redirect-prompt-form">
                <select class="tr-redirect-bulk-target-select">
                    <option value="home" data-url="<?php echo esc_attr(home_url('/')); ?>" selected><?php echo esc_html(tamrank__('auto_redirect_target_home', 'Homepage')); ?></option>
                    <option value="custom"><?php echo esc_html(tamrank__('auto_redirect_target_custom', 'Custom URL...')); ?></option>
                </select>
                <input type="url" class="tr-redirect-custom-url" placeholder="https://..." style="display:none" />
                <button class="tr-btn tr-btn--primary tr-redirect-bulk-create-btn" data-nonce="<?php echo esc_attr($nonce); ?>">
                    <?php echo esc_html(sprintf(tamrank__('auto_redirect_bulk_create_btn', 'Create %d redirects'), $effective)); ?>
                </button>
                <button class="tr-redirect-dismiss-btn" data-bulk="1" data-nonce="<?php echo esc_attr($nonce); ?>">
                    <?php echo esc_html(tamrank__('auto_redirect_dismiss_btn', 'Dismiss')); ?>
                </button>
            </div>
        </div>
        <?php
    }

    /* ---------------------------------------------------------------------
     * Assets
     * --------------------------------------------------------------------- */

    public function enqueue_assets($hook)
    {
        // The redirect prompt renders via admin_notices on whatever screen the
        // user lands on after a delete, including core screens (Dashboard, Posts
        // list) that do not load the TamRank global stylesheet. Only load the
        // notice assets when a prompt is actually pending for this user (the same
        // transient render_notice() checks), so every other admin page stays
        // asset-free and the prompt is styled wherever it appears.
        $user_id = get_current_user_id();
        if (! $user_id) {
            return;
        }
        $pending = get_transient(self::TRANSIENT_PREFIX . $user_id);
        if (! is_array($pending) || (empty($pending['items']) && empty($pending['bulk_flag']))) {
            return;
        }

        // Depend on the registered common/ handles instead of @import-ing them
        // from the stylesheet: an @import fetches the sub-file without the
        // ?ver= query string, so a plugin update never busts its cache and the
        // user keeps the old tokens. These handles are registered on
        // admin_enqueue_scripts prio 1 (TamRank::register_common_styles), so
        // they exist on every admin screen the prompt can render on, and each
        // carries TAM_RANK_VERSION.
        wp_enqueue_style(
            'tamrank-auto-redirect-on-delete',
            plugins_url('assets/css/auto-redirect-on-delete.css', dirname(__FILE__)),
            array('tamrank-variables', 'tamrank-buttons', 'tamrank-redirect-prompt'),
            defined('TAM_RANK_VERSION') ? TAM_RANK_VERSION : '1.3.0'
        );

        wp_enqueue_script(
            'tamrank-auto-redirect-on-delete',
            plugins_url('assets/js/auto-redirect-on-delete.js', dirname(__FILE__)),
            array('jquery'),
            defined('TAM_RANK_VERSION') ? TAM_RANK_VERSION : '1.3.0',
            true
        );
        wp_localize_script('tamrank-auto-redirect-on-delete', 'tamrankAutoRedirect', array(
            'ajaxUrl' => admin_url('admin-ajax.php'),
            'i18n'    => array(
                'success'             => tamrank__('auto_redirect_success', 'Redirect created.'),
                'successWithChain'    => tamrank__('auto_redirect_success_with_chain', 'Redirect created. %d incoming redirects updated.'),
                'error'               => tamrank__('auto_redirect_error', 'Could not create redirect: %s'),
                'bulkSuccess'         => tamrank__('auto_redirect_bulk_success', '%d redirects created.'),
                'bulkPartialSuccess'  => tamrank__('auto_redirect_bulk_partial_success', '%d created, %d skipped.'),
                'bulkErrors'          => tamrank__('auto_redirect_bulk_errors', '%d redirects created, %d failed. See error log.'),
                'customUrlRequired'   => tamrank__('auto_redirect_custom_url_required', 'Please enter a URL.'),
            ),
        ));
    }

    /* ---------------------------------------------------------------------
     * AJAX endpoints
     * --------------------------------------------------------------------- */

    public function ajax_create_redirect()
    {
        check_ajax_referer(self::NONCE_ACTION, 'nonce');
        // Creating a 301 mutates site-wide redirect state, so require the same
        // capability as the redirect manager (manage_options), not edit_posts.
        if (! current_user_can('manage_options')) {
            wp_send_json_error(array('message' => 'insufficient_permissions'), 403);
        }

        $post_id = isset($_POST['post_id']) ? (int) $_POST['post_id'] : 0;
        $target  = isset($_POST['target_url']) ? esc_url_raw(wp_unslash($_POST['target_url'])) : '';

        if (! $post_id || ! $target) {
            wp_send_json_error(array('message' => 'missing_parameters'), 400);
        }

        $user_id = get_current_user_id();
        $key = self::TRANSIENT_PREFIX . $user_id;
        $pending = get_transient($key);
        if (! is_array($pending) || empty($pending['items'][$post_id])) {
            wp_send_json_error(array('message' => 'pending_not_found'), 404);
        }

        $item = $pending['items'][$post_id];
        $relative = isset($item['relative']) ? $item['relative'] : '';

        global $tam_rank_redirect_manager;
        if (! isset($tam_rank_redirect_manager) || ! method_exists($tam_rank_redirect_manager, 'create_redirect')) {
            wp_send_json_error(array('message' => 'redirect_manager_unavailable'), 500);
        }

        $result = $tam_rank_redirect_manager->create_redirect($relative, $target, '301', true);

        if (is_wp_error($result)) {
            wp_send_json_error(array('message' => $result->get_error_code(), 'detail' => $result->get_error_message()), 400);
        }

        // Chain-repair: re-target any redirects that pointed to the deleted URL
        $incoming_count = 0;
        if (! empty($item['incoming_redirects'])) {
            $forms = array();
            // Build forms from stored url + relative (post may no longer exist on permanent delete)
            $abs = isset($item['url']) ? $item['url'] : '';
            $rel = $relative;
            if ($abs) {
                $forms[] = user_trailingslashit($abs);
                $forms[] = untrailingslashit($abs);
            }
            if ($rel) {
                $forms[] = user_trailingslashit($rel);
                $forms[] = untrailingslashit($rel);
            }
            $forms = array_values(array_unique(array_filter($forms)));
            $incoming_count = $this->retarget_incoming_redirects($forms, $target);
        }

        // Remove from pending
        unset($pending['items'][$post_id]);
        set_transient($key, $pending, self::TRANSIENT_TTL);

        wp_send_json_success(array(
            'created'        => 1,
            'incoming_count' => (int) $incoming_count,
        ));
    }

    public function ajax_create_bulk_redirects()
    {
        check_ajax_referer(self::NONCE_ACTION, 'nonce');
        // Bulk redirect creation mutates site-wide state — require manage_options.
        if (! current_user_can('manage_options')) {
            wp_send_json_error(array('message' => 'insufficient_permissions'), 403);
        }

        $target = isset($_POST['target_url']) ? esc_url_raw(wp_unslash($_POST['target_url'])) : '';
        if (! $target) {
            wp_send_json_error(array('message' => 'missing_target'), 400);
        }

        $user_id = get_current_user_id();
        $key = self::TRANSIENT_PREFIX . $user_id;
        $pending = get_transient($key);
        if (! is_array($pending) || empty($pending['bulk_flag']) || empty($pending['bulk_urls'])) {
            wp_send_json_error(array('message' => 'bulk_pending_not_found'), 404);
        }

        global $tam_rank_redirect_manager;
        if (! isset($tam_rank_redirect_manager) || ! method_exists($tam_rank_redirect_manager, 'create_redirect')) {
            wp_send_json_error(array('message' => 'redirect_manager_unavailable'), 500);
        }

        $urls = (array) $pending['bulk_urls'];
        $total = count($urls);
        $to_process = $urls;

        $created   = 0;
        $skipped   = 0;
        $failed    = 0;

        foreach ($to_process as $entry) {
            $rel = isset($entry['relative']) ? $entry['relative'] : '';
            if (! $rel) {
                $skipped++;
                continue;
            }
            $result = $tam_rank_redirect_manager->create_redirect($rel, $target, '301', true);
            if (is_wp_error($result)) {
                if ($result->get_error_code() === 'redirect_exists') {
                    $skipped++;
                } else {
                    $failed++;
                }
            } else {
                $created++;
            }
        }

        $overflow_skipped = max(0, $total - count($to_process));

        // Clear bulk state
        $pending['bulk_flag']   = false;
        $pending['bulk_count']  = 0;
        $pending['bulk_urls']   = array();
        $pending['bulk_reason'] = '';
        set_transient($key, $pending, self::TRANSIENT_TTL);

        wp_send_json_success(array(
            'created'          => $created,
            'skipped'          => $skipped,
            'failed'           => $failed,
            'overflow_skipped' => $overflow_skipped,
        ));
    }

    public function ajax_dismiss()
    {
        check_ajax_referer(self::NONCE_ACTION, 'nonce');
        if (! current_user_can('edit_posts')) {
            wp_send_json_error(array('message' => 'insufficient_permissions'), 403);
        }

        $user_id = get_current_user_id();
        $key = self::TRANSIENT_PREFIX . $user_id;
        $pending = get_transient($key);
        if (! is_array($pending)) {
            wp_send_json_success(array('dismissed' => false));
        }

        $is_bulk = ! empty($_POST['bulk']);
        if ($is_bulk) {
            $pending['bulk_flag']   = false;
            $pending['bulk_count']  = 0;
            $pending['bulk_urls']   = array();
            $pending['bulk_reason'] = '';
        } else {
            $post_id = isset($_POST['post_id']) ? (int) $_POST['post_id'] : 0;
            if ($post_id && isset($pending['items'][$post_id])) {
                unset($pending['items'][$post_id]);
            }
        }
        set_transient($key, $pending, self::TRANSIENT_TTL);

        wp_send_json_success(array('dismissed' => true));
    }
}
