<?php

/**
 * Redirects Module
 *
 * Handles URL redirection management with support for simple and regex redirects
 *
 * @package ProRank\SEO\Modules\Tools
 * @since   0.1.0
 */

declare(strict_types=1);

namespace ProRank\SEO\Modules\TechnicalSEO;

use ProRank\SEO\Modules\BaseModule;
use ProRank\SEO\Plugin;
use ProRank\SEO\Core\Cache\RedirectCache;
use ProRank\SEO\Core\Compatibility\CompetitorDetector;

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


/**
 * Redirects module class
 */


class RedirectsModule extends BaseModule {
    
    /**
     * Module slug
     *
     * @var string
     */
    protected string $slug = 'redirects';
    
    /**
     * Module name
     *
     * @var string
     */
    protected string $name = 'Redirect Manager';
    
    /**
     * Module description
     *
     * @var string
     */
    protected string $description = 'Manage URL redirects with support for 301/302 redirects and advanced regex patterns';
    
    /**
     * Module category
     *
     * @var string
     */
    protected string $category = 'tools';
    
    /**
     * Module tier
     *
     * @var string
     */
    protected string $feature_tier = 'free';
    
    /**
     * Parent module
     *
     * @var string|null
     */
    protected ?string $parent_slug = 'technical-seo';
    
    /**
     * Table name cache
     *
     * @var string|null
     */
    private ?string $table_name = null;
    
    /**
     * Initialize module hooks
     *
     * @return void
     */
    public function init_hooks(): void {
        // REST + admin surface must stay available even when frontend redirect
        // handling is suppressed by SEO conflict safe mode (so users can still
        // manage redirects and competing plugins can be retired safely).
        add_action('rest_api_init', [$this, 'register_rest_routes']);

        if (get_option('prorank_auto_redirects_enabled', true)) {
            add_action('post_updated', [$this, 'handle_post_url_change'], 10, 3);
            add_action('before_delete_post', [$this, 'handle_post_deletion']);
        }

        add_action('prorank_redirect_created', [$this, 'detect_redirect_chains']);
        add_action('prorank_redirect_updated', [$this, 'detect_redirect_chains']);

        // Cache invalidation must survive safe mode — frontend matching is
        // disabled there, but the cache backing it can still hold stale data
        // from a previous mode flip; invalidate on every CRUD event regardless.
        add_action('prorank_redirect_created', [$this, 'clear_redirect_cache']);
        add_action('prorank_redirect_updated', [$this, 'clear_redirect_cache']);
        add_action('prorank_redirect_deleted', [$this, 'clear_redirect_cache']);

        // Frontend-only hooks: skip these in safe mode to avoid conflicts with
        // competing SEO plugins (e.g. Rank Math). The REST/admin surface and
        // cache-invalidation hooks above stay registered so management still
        // works and the cache stays consistent.
        $detector = new CompetitorDetector();
        if ($detector->should_enable_seo_safe_mode()) {
            return;
        }

        add_action('template_redirect', [$this, 'handle_redirect'], 5);
        add_action('init', [RedirectCache::class, 'warm_cache'], 20);
    }
    
    /**
     * Handle redirect logic
     *
     * @return void
     */
    public function handle_redirect(): void {
        // Skip if in admin or doing AJAX
        if (is_admin() || wp_doing_ajax()) {
            return;
        }
        
        // Get current request path
        $request_path = $this->get_request_path();
        if (empty($request_path)) {
            return;
        }

        $exact_match_candidates = $this->build_request_exact_match_candidates($request_path);
        
        // Try to find a matching redirect
        $redirect = $this->find_redirect($request_path, $exact_match_candidates);
        if (!$redirect) {
            return;
        }
        
        // Regex redirects are not available in the free package.
        if ($redirect->is_regex) {
            return;
        }
        
        // Perform the redirect
        $this->perform_redirect($redirect);
    }
    
    /**
     * Get current request path
     *
     * @return string
     */
    private function get_request_path(): string {
        $request_uri = prorank_get_server_var( 'REQUEST_URI' );
        
        // Remove query string
        $path = wp_parse_url($request_uri, PHP_URL_PATH);
        if ($path === false || $path === null) {
            return '';
        }
        
        // Remove home path if in subdirectory
        $home_path = wp_parse_url(home_url(), PHP_URL_PATH);
        if ($home_path && strpos($path, $home_path) === 0) {
            $path = substr($path, strlen($home_path));
        }
        
        // Ensure leading slash
        return '/' . ltrim($path, '/');
    }

    /**
     * Build exact-match lookup candidates for the current request.
     *
     * @param string $request_path Request path without query string.
     * @return array<int, string>
     */
    private function build_request_exact_match_candidates(string $request_path): array {
        $candidates = [];
        $request_uri = prorank_get_server_var('REQUEST_URI');
        $query = wp_parse_url($request_uri, PHP_URL_QUERY);

        if (is_string($query) && $query !== '') {
            $this->add_exact_source_candidates($candidates, $request_path . '?' . $query);
        }

        $this->add_exact_source_candidates($candidates, $request_path);

        return $candidates;
    }

    /**
     * Add normalized exact-match source variants to a lookup list.
     *
     * @param array<int, string> $candidates Candidate list by reference.
     * @param string             $source_url Source URL candidate.
     * @return void
     */
    private function add_exact_source_candidates(array &$candidates, string $source_url): void {
        $source_url = trim($source_url);
        if ($source_url === '') {
            return;
        }

        $variants = [$source_url, $this->normalize_exact_source_url($source_url)];

        $parts = explode('?', $this->normalize_exact_source_url($source_url), 2);
        $path = $parts[0] ?? '';
        $query = $parts[1] ?? '';

        if ($path !== '' && $path !== '/') {
            $trailed = trailingslashit(untrailingslashit($path));
            $variants[] = $query !== '' ? $trailed . '?' . $query : $trailed;
        }

        foreach ($variants as $variant) {
            if ($variant === '' || in_array($variant, $candidates, true)) {
                continue;
            }

            $candidates[] = $variant;
        }
    }

    /**
     * Normalize an exact redirect source into a canonical path form.
     *
     * @param string $source_url Source URL or path.
     * @return string
     */
    private function normalize_exact_source_url(string $source_url): string {
        $source_url = trim(wp_unslash($source_url));
        if ($source_url === '') {
            return '';
        }

        if (preg_match('#^https?://#i', $source_url)) {
            $parts = wp_parse_url($source_url);
            if (is_array($parts)) {
                $path = isset($parts['path']) ? (string) $parts['path'] : '/';
                if (!empty($parts['query'])) {
                    $path .= '?' . $parts['query'];
                }
                $source_url = $path;
            }
        }

        $parts = explode('?', $source_url, 2);
        $path = $parts[0] ?? '';
        $query = $parts[1] ?? '';

        $path = '/' . ltrim($path, '/');
        if ($path !== '/') {
            $path = untrailingslashit($path);
            if ($path === '') {
                $path = '/';
            }
        }

        return $query !== '' ? $path . '?' . $query : $path;
    }

    /**
     * Normalize redirect sources before storage.
     *
     * @param string $source_url Source URL or pattern.
     * @param bool   $is_regex   Whether the source is a regex pattern.
     * @return string
     */
    private function normalize_source_url_for_storage(string $source_url, bool $is_regex): string {
        $source_url = trim(wp_unslash($source_url));
        if ($source_url === '') {
            return '';
        }

        if ($is_regex) {
            return sanitize_text_field($source_url);
        }

        return sanitize_text_field($this->normalize_exact_source_url($source_url));
    }

    /**
     * Find an existing exact redirect ID across canonical slash variants.
     *
     * @param string   $table      Redirect table name.
     * @param string   $source_url Source URL or path.
     * @param int|null $exclude_id Optional redirect ID to exclude.
     * @return int
     */
    private function find_existing_exact_redirect_id(string $table, string $source_url, ?int $exclude_id = null): int {
        global $wpdb;

        $candidates = [];
        $this->add_exact_source_candidates($candidates, $source_url);
        if (empty($candidates)) {
            return 0;
        }

        $placeholders = implode(', ', array_fill(0, count($candidates), '%s'));
        $args = $candidates;

        $sql = "SELECT id FROM {$table} WHERE source_url IN ({$placeholders})";
        if ($exclude_id !== null) {
            $sql .= ' AND id != %d';
            $args[] = $exclude_id;
        }

        $sql .= ' LIMIT 1';

        return (int) $wpdb->get_var($wpdb->prepare($sql, ...$args));
    }

    /**
     * Normalize a redirect target for same-site chain detection.
     *
     * @param string $target_url Target URL or path.
     * @return string
     */
    private function normalize_internal_target_for_lookup(string $target_url): string {
        if ($target_url === '') {
            return '';
        }

        if (strpos($target_url, 'http://') !== 0 && strpos($target_url, 'https://') !== 0) {
            return $this->normalize_exact_source_url($target_url);
        }

        $target_host = wp_parse_url($target_url, PHP_URL_HOST);
        $home_host = wp_parse_url(home_url('/'), PHP_URL_HOST);

        if (!$target_host || !$home_host || strtolower($target_host) !== strtolower($home_host)) {
            return $target_url;
        }

        $path = wp_parse_url($target_url, PHP_URL_PATH) ?: '/';
        $query = wp_parse_url($target_url, PHP_URL_QUERY);
        if (!empty($query)) {
            $path .= '?' . $query;
        }

        return $this->normalize_exact_source_url($path);
    }
    
    /**
     * Find matching redirect for request path
     *
     * @param string $request_path
     * @return object|null
     */
    private function find_redirect(string $request_path, array $exact_match_candidates = []): ?object {
        if (empty($exact_match_candidates)) {
            $exact_match_candidates = [$request_path];
        }

        foreach ($exact_match_candidates as $candidate) {
            $cached = RedirectCache::get($candidate);
            if ($cached !== null) {
                return $cached;
            }
        }
        
        global $wpdb;
        
        $table = $this->get_table_name();
        
        // Check if table exists first
        if (!$this->table_exists()) {
            return null;
        }
        
        // Try each candidate in order. The candidate list is built by
        // build_request_exact_match_candidates() with most-specific entries
        // first (e.g. /page?utm=foo before /page), so a per-candidate lookup
        // preserves that priority deterministically. A previous IN(...) query
        // returned the candidates in unspecified order under MySQL's optimizer.
        foreach ($exact_match_candidates as $candidate) {
            $redirect = $wpdb->get_row(
                $wpdb->prepare(
                    "SELECT * FROM {$table}
                    WHERE source_url = %s
                    AND status = 'active'
                    AND is_regex = 0
                    LIMIT 1",
                    $candidate
                )
            );

            if ($redirect) {
                // Cache only under the matched candidate. Caching under
                // every candidate key would pin a path-only redirect under
                // a query-specific key, where clear_redirect_cache (which
                // keys off the stored source_url) wouldn't know to clear it.
                RedirectCache::set($candidate, $redirect);
                return $redirect;
            }
        }

        return null;
    }
    
    /**
     * Perform the redirect
     *
     * @param object $redirect
     * @return void
     */
    private function perform_redirect(object $redirect): void {
        // Perform safe redirect
        wp_safe_redirect($redirect->target_url, (int) $redirect->type);
        exit();
    }
    
    /**
     * Update redirect statistics
     *
     * @param int $redirect_id
     * @return void
     */
    private function update_redirect_stats(int $redirect_id): void {
        global $wpdb;
        
        $table = $this->get_table_name();
        
        $wpdb->query(
            $wpdb->prepare(
                "UPDATE {$table} 
                SET hits = hits + 1, 
                    last_hit_timestamp = %s 
                WHERE id = %d",
                current_time('mysql'),
                $redirect_id
            )
        );
    }
    
    /**
     * Clear redirect cache
     *
     * @param int $redirect_id Redirect ID (optional)
     * @return void
     */
    public function clear_redirect_cache(int $redirect_id = 0): void {
        if ($redirect_id > 0) {
            // Clear specific redirect from cache
            global $wpdb;
            $table = $this->get_table_name();
            $redirect = $wpdb->get_row(
                $wpdb->prepare(
                    "SELECT source_url, is_regex FROM {$table} WHERE id = %d",
                    $redirect_id
                )
            );

            if ($redirect) {
                RedirectCache::delete($redirect->source_url);

                // 2025 Enhancement: Clear regex list cache if this is a regex redirect
                if ($redirect->is_regex == 1) {
                    RedirectCache::clear_regex_redirects();
                }
            }
        } else {
            // Clear all redirect cache
            RedirectCache::flush();

            // 2025 Enhancement: Also clear regex redirects list cache
            RedirectCache::clear_regex_redirects();
        }
    }
    
    /**
     * Get table name
     *
     * @return string
     */
    private function get_table_name(): string {
        if ($this->table_name === null) {
            global $wpdb;
            $this->table_name = $wpdb->prefix . 'prorank_redirects';
        }
        
        return $this->table_name;
    }
    
    /**
     * Check if redirects table exists
     *
     * @return bool
     */
    private function table_exists(): bool {
        global $wpdb;
        $table = $this->get_table_name();

        // Check if table exists
        $result = $wpdb->get_var("SHOW TABLES LIKE '$table'");
        return $result === $table;
    }

    /**
     * Ensure redirects tables exist.
     *
     * @return bool
     */
    private function ensure_tables(): bool {
        if ($this->table_exists()) {
            return true;
        }

        if (class_exists('\\ProRank\\SEO\\Database\\RedirectsTableInstaller')) {
            \ProRank\SEO\Database\RedirectsTableInstaller::install();
        }

        return $this->table_exists();
    }

    /**
     * Validate a regex pattern and return detailed error info
     *
     * @param string $pattern The regex pattern to validate
     * @return array{valid: bool, error: string|null}
     */
    private function validate_regex_pattern(string $pattern): array {
        // Check for empty pattern
        if (empty($pattern)) {
            return [
                'valid' => false,
                'error' => __('Regex pattern cannot be empty', 'prorank-seo'),
            ];
        }

        // Check for valid delimiters
        $first_char = $pattern[0];
        $valid_delimiters = ['/', '#', '~', '!', '@', '%', '`'];
        if (!in_array($first_char, $valid_delimiters, true)) {
            return [
                'valid' => false,
                'error' => sprintf(
                    /* translators: %s: the delimiter character used */
                    __('Regex pattern should start with a delimiter (e.g., /pattern/). Found: %1$s', 'prorank-seo'),
                    $first_char
                ),
            ];
        }

        // Test the pattern with error handling
        // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_set_error_handler -- Needed to catch invalid regex warnings.
        set_error_handler(function($severity, $message) {
            // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- PHP error handler parameters passed to ErrorException
            throw new \ErrorException(esc_html($message), 0, absint($severity));
        });

        try {
            $result = preg_match($pattern, '');
            restore_error_handler();

            if ($result === false) {
                $error = preg_last_error_msg();
                return [
                    'valid' => false,
                    'error' => sprintf(
                        /* translators: %s: the preg error message */
                        __('Invalid regex pattern: %1$s', 'prorank-seo'),
                        $error
                    ),
                ];
            }

            return ['valid' => true, 'error' => null];
        } catch (\ErrorException $e) {
            restore_error_handler();
            return [
                'valid' => false,
                'error' => sprintf(
                    /* translators: %s: the error message */
                    __('Regex compilation error: %1$s', 'prorank-seo'),
                    $e->getMessage()
                ),
            ];
        }
    }

    /**
     * Register REST routes
     *
     * @return void
     */
    public function register_rest_routes(): void {
        register_rest_route('prorank-seo/v1', '/redirects', [
            [
                'methods' => 'GET',
                'callback' => [$this, 'get_redirects'],
                'permission_callback' => [$this, 'check_permissions'],
            ],
            [
                'methods' => 'POST',
                'callback' => [$this, 'create_redirect'],
                'permission_callback' => [$this, 'check_permissions'],
                'args' => $this->get_redirect_args(),
            ],
        ]);
        
        register_rest_route('prorank-seo/v1', '/redirects/(?P<id>\d+)', [
            [
                'methods' => 'GET',
                'callback' => [$this, 'get_redirect'],
                'permission_callback' => [$this, 'check_permissions'],
            ],
            [
                'methods' => 'PUT',
                'callback' => [$this, 'update_redirect'],
                'permission_callback' => [$this, 'check_permissions'],
                'args' => $this->get_redirect_args(),
            ],
            [
                'methods' => 'DELETE',
                'callback' => [$this, 'delete_redirect'],
                'permission_callback' => [$this, 'check_permissions'],
            ],
        ]);
        
        register_rest_route('prorank-seo/v1', '/redirects/bulk', [
            'methods' => 'POST',
            'callback' => [$this, 'bulk_action'],
            'permission_callback' => [$this, 'check_permissions'],
            'args' => [
                'action' => [
                    'required' => true,
                    'type' => 'string',
                    'enum' => ['activate', 'deactivate', 'delete'],
                ],
                'ids' => [
                    'required' => true,
                    'type' => 'array',
                    'items' => [
                        'type' => 'integer',
                    ],
                ],
            ],
        ]);
        
        register_rest_route('prorank-seo/v1', '/redirects/export', [
            'methods' => 'GET',
            'callback' => [$this, 'export_redirects'],
            'permission_callback' => [$this, 'check_permissions'],
        ]);
        
        register_rest_route('prorank-seo/v1', '/redirects/import', [
            'methods' => 'POST',
            'callback' => [$this, 'import_redirects'],
            'permission_callback' => [$this, 'check_permissions'],
            'args' => [
                'format' => [
                    'type' => 'string',
                    'default' => 'auto',
                    'enum' => ['auto', 'csv', 'json', 'htaccess', 'redirection', 'simple301', 'srm'],
                ],
                'dry_run' => [
                    'type' => 'boolean',
                    'default' => false,
                ],
            ],
        ]);

        register_rest_route('prorank-seo/v1', '/redirects/auto', [
            'methods' => 'GET',
            'callback' => [$this, 'get_auto_redirects'],
            'permission_callback' => [$this, 'check_permissions'],
        ]);

        register_rest_route('prorank-seo/v1', '/redirects/(?P<id>\d+)/undo-auto', [
            'methods' => 'POST',
            'callback' => [$this, 'undo_auto_redirect'],
            'permission_callback' => [$this, 'check_permissions'],
        ]);
        
        register_rest_route('prorank-seo/v1', '/redirects/check-chains', [
            'methods' => 'GET',
            'callback' => [$this, 'check_all_chains'],
            'permission_callback' => [$this, 'check_permissions'],
        ]);

        register_rest_route('prorank-seo/v1', '/redirects/flatten-chains', [
            'methods' => 'POST',
            'callback' => [$this, 'flatten_redirect_chains'],
            'permission_callback' => [$this, 'check_permissions'],
            'args' => [
                'redirect_ids' => [
                    'type' => 'array',
                    'default' => [],
                    'items' => [
                        'type' => 'integer',
                    ],
                ],
                'flatten_all' => [
                    'type' => 'boolean',
                    'default' => false,
                ],
            ],
        ]);

        register_rest_route('prorank-seo/v1', '/redirects/test-regex', [
            'methods' => 'POST',
            'callback' => [$this, 'test_regex_pattern'],
            'permission_callback' => [$this, 'check_permissions'],
            'args' => [
                'pattern' => [
                    'required' => true,
                    'type' => 'string',
                    'sanitize_callback' => 'sanitize_text_field',
                ],
                'test_urls' => [
                    'type' => 'array',
                    'default' => [],
                    'items' => [
                        'type' => 'string',
                    ],
                ],
                'target_url' => [
                    'type' => 'string',
                    'default' => '',
                ],
            ],
        ]);
    }
    
    /**
     * Check REST permissions
     *
     * @return bool
     */
    public function check_permissions(): bool {
        return current_user_can('manage_options');
    }
    
    /**
     * Get redirect validation args
     *
     * @return array
     */
    private function get_redirect_args(): array {
        return [
            'source_url' => [
                'required' => true,
                'type' => 'string',
                'sanitize_callback' => 'sanitize_text_field',
                'validate_callback' => function($value) {
                    return !empty($value) && strlen($value) <= 2048;
                },
            ],
            'target_url' => [
                'required' => true,
                'type' => 'string',
                'sanitize_callback' => 'esc_url_raw',
                'validate_callback' => function($value) {
                    return !empty($value) && strlen($value) <= 2048;
                },
            ],
            'type' => [
                'type' => 'integer',
                'default' => 301,
                'enum' => [301, 302, 303, 307, 308],
            ],
            'is_regex' => [
                'type' => 'boolean',
                'default' => false,
            ],
            'status' => [
                'type' => 'string',
                'default' => 'active',
                'enum' => ['active', 'inactive'],
            ],
        ];
    }
    
    /**
     * Get redirects
     *
     * @param \WP_REST_Request $request
     * @return \WP_REST_Response|\WP_Error
     */
    public function get_redirects(\WP_REST_Request $request) {
        global $wpdb;
        
        if (!$this->ensure_tables()) {
            $response = new \WP_REST_Response([]);
            $response->header('X-WP-Total', '0');
            $response->header('X-WP-TotalPages', '0');
            return $response;
        }

        $table = $this->get_table_name();
        
        // Get parameters
        $page = max(1, (int) $request->get_param('page'));
        $per_page = max(1, min(100, (int) $request->get_param('per_page') ?: 20));
        $search = $request->get_param('search');
        $status = $request->get_param('status');
        $type = $request->get_param('type');
        $orderby = $request->get_param('orderby') ?: 'created_at';
        $order = strtoupper($request->get_param('order') ?: 'DESC');
        
        // Validate order
        if (!in_array($order, ['ASC', 'DESC'], true)) {
            $order = 'DESC';
        }
        
        // Validate orderby
        $allowed_orderby = ['id', 'source_url', 'target_url', 'type', 'status', 'hits', 'created_at', 'last_hit_timestamp'];
        if (!in_array($orderby, $allowed_orderby, true)) {
            $orderby = 'created_at';
        }
        
        // Build query
        $where = '1=1';
        $params = [];
        
        if ($search) {
            $where .= ' AND (source_url LIKE %s OR target_url LIKE %s)';
            $search_like = '%' . $wpdb->esc_like($search) . '%';
            $params[] = $search_like;
            $params[] = $search_like;
        }
        
        // WP REST schema may provide "all" as a default value. Treat it as no filter.
        if ($status && $status !== 'all') {
            $where .= ' AND status = %s';
            $params[] = $status;
        }

        // Apply redirect type filter when requested.
        if ($type && $type !== 'all') {
            $type_int = (int) $type;
            if (in_array($type_int, [301, 302, 303, 307, 308], true)) {
                $where .= ' AND type = %d';
                $params[] = $type_int;
            }
        }
        
        // Get total count
        $count_query = "SELECT COUNT(*) FROM {$table} WHERE {$where}";
        if (!empty($params)) {
            $count_query = $wpdb->prepare($count_query, ...$params); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
        }
        $total = (int) $wpdb->get_var($count_query); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
        
        // Get redirects
        $offset = ($page - 1) * $per_page;
        $query = "SELECT * FROM {$table} WHERE {$where} ORDER BY {$orderby} {$order} LIMIT %d OFFSET %d";
        $query_args = array_merge($params, [$per_page, $offset]);
        $prepared_query = $wpdb->prepare($query, ...$query_args); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
        
        $redirects = $wpdb->get_results($prepared_query); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
        
        // Format redirects
        $formatted_redirects = array_map([$this, 'format_redirect'], $redirects);
        
        $response = new \WP_REST_Response($formatted_redirects);
        $response->header('X-WP-Total', (string) $total);
        $response->header('X-WP-TotalPages', (string) ceil($total / $per_page));
        
        return $response;
    }
    
    /**
     * Get single redirect
     *
     * @param \WP_REST_Request $request
     * @return \WP_REST_Response|\WP_Error
     */
    public function get_redirect(\WP_REST_Request $request) {
        global $wpdb;
        
        if (!$this->ensure_tables()) {
            return new \WP_Error(
                'redirects_table_missing',
                __('Redirects storage is not available', 'prorank-seo'),
                ['status' => 500]
            );
        }

        $id = (int) $request->get_param('id');
        $table = $this->get_table_name();
        
        $redirect = $wpdb->get_row(
            $wpdb->prepare(
                "SELECT * FROM {$table} WHERE id = %d",
                $id
            )
        );
        
        if (!$redirect) {
            return new \WP_Error(
                'redirect_not_found',
                __('Redirect not found', 'prorank-seo'),
                ['status' => 404]
            );
        }
        
        return new \WP_REST_Response($this->format_redirect($redirect));
    }
    
    /**
     * Create redirect
     *
     * @param \WP_REST_Request $request
     * @return \WP_REST_Response|\WP_Error
     */
    public function create_redirect(\WP_REST_Request $request) {
        global $wpdb;
        
        if (!$this->ensure_tables()) {
            return new \WP_Error(
                'redirects_table_missing',
                __('Redirects storage is not available', 'prorank-seo'),
                ['status' => 500]
            );
        }

        $table = $this->get_table_name();
        
        // Get parameters
        $is_regex = (bool) $request->get_param('is_regex');
        $source_url = $this->normalize_source_url_for_storage((string) $request->get_param('source_url'), $is_regex);
        $target_url = $request->get_param('target_url');
        $type = (int) $request->get_param('type');
        $status = $request->get_param('status');

        if ($source_url === '') {
            return new \WP_Error(
                'invalid_source_url',
                __('Please provide a valid source URL', 'prorank-seo'),
                ['status' => 400]
            );
        }
        
        // Validate regex if enabled
        if ($is_regex) {
            return new \WP_Error(
                'regex_not_available_in_free',
                __('Regex redirects are not available in the free plugin.', 'prorank-seo'),
                ['status' => 403]
            );

            // Validate regex pattern
            $validation = $this->validate_regex_pattern($source_url);
            if (!$validation['valid']) {
                return new \WP_Error(
                    'invalid_regex',
                    $validation['error'],
                    ['status' => 400]
                );
            }
        }
        
        // Check for duplicate
        $existing = $is_regex
            ? (int) $wpdb->get_var(
                $wpdb->prepare(
                    "SELECT id FROM {$table} WHERE source_url = %s",
                    $source_url
                )
            )
            : $this->find_existing_exact_redirect_id($table, $source_url);
        
        if ($existing) {
            return new \WP_Error(
                'duplicate_redirect',
                __('A redirect with this source URL already exists', 'prorank-seo'),
                ['status' => 409]
            );
        }
        
        // Insert redirect
        $result = $wpdb->insert(
            $table,
            [
                'source_url' => $source_url,
                'target_url' => $target_url,
                'type' => $type,
                'is_regex' => $is_regex ? 1 : 0,
                'status' => $status,
            ],
            ['%s', '%s', '%d', '%d', '%s']
        );
        
        if ($result === false) {
            return new \WP_Error(
                'db_error',
                __('Failed to create redirect', 'prorank-seo'),
                ['status' => 500]
            );
        }

        do_action('prorank_redirect_created', (int) $wpdb->insert_id);
        
        $redirect = $wpdb->get_row(
            $wpdb->prepare(
                "SELECT * FROM {$table} WHERE id = %d",
                $wpdb->insert_id
            )
        );
        
        return new \WP_REST_Response(
            $this->format_redirect($redirect),
            201
        );
    }
    
    /**
     * Update redirect
     *
     * @param \WP_REST_Request $request
     * @return \WP_REST_Response|\WP_Error
     */
    public function update_redirect(\WP_REST_Request $request) {
        global $wpdb;
        
        if (!$this->ensure_tables()) {
            return new \WP_Error(
                'redirects_table_missing',
                __('Redirects storage is not available', 'prorank-seo'),
                ['status' => 500]
            );
        }

        $id = (int) $request->get_param('id');
        $table = $this->get_table_name();
        
        // Check if exists
        $existing = $wpdb->get_row(
            $wpdb->prepare(
                "SELECT * FROM {$table} WHERE id = %d",
                $id
            )
        );
        
        if (!$existing) {
            return new \WP_Error(
                'redirect_not_found',
                __('Redirect not found', 'prorank-seo'),
                ['status' => 404]
            );
        }
        
        // Prepare update data
        $update_data = [];
        $update_format = [];
        
        if ($request->has_param('source_url')) {
            $is_regex = isset($update_data['is_regex'])
                ? (bool) $update_data['is_regex']
                : (bool) $existing->is_regex;
            $new_source_url = $this->normalize_source_url_for_storage(
                (string) $request->get_param('source_url'),
                $is_regex
            );

            $duplicate = $is_regex
                ? (int) $wpdb->get_var(
                    $wpdb->prepare(
                        "SELECT id FROM {$table} WHERE source_url = %s AND id != %d",
                        $new_source_url,
                        $id
                    )
                )
                : $this->find_existing_exact_redirect_id($table, $new_source_url, $id);

            if ($duplicate) {
                return new \WP_Error(
                    'duplicate_redirect',
                    __('A redirect with this source URL already exists', 'prorank-seo'),
                    ['status' => 409]
                );
            }

            $update_data['source_url'] = $new_source_url;
            $update_format[] = '%s';
        }
        
        if ($request->has_param('target_url')) {
            $update_data['target_url'] = $request->get_param('target_url');
            $update_format[] = '%s';
        }
        
        if ($request->has_param('type')) {
            $update_data['type'] = (int) $request->get_param('type');
            $update_format[] = '%d';
        }
        
        if ($request->has_param('is_regex')) {
            $is_regex = (bool) $request->get_param('is_regex');
            
            if ($is_regex) {
                return new \WP_Error(
                    'regex_not_available_in_free',
                    __('Regex redirects are not available in the free plugin.', 'prorank-seo'),
                    ['status' => 403]
                );
            }
            
            // Validate regex pattern if enabling
            if ($is_regex && isset($update_data['source_url'])) {
                $validation = $this->validate_regex_pattern($update_data['source_url']);
                if (!$validation['valid']) {
                    return new \WP_Error(
                        'invalid_regex',
                        $validation['error'],
                        ['status' => 400]
                    );
                }
            }
            
            $update_data['is_regex'] = $is_regex ? 1 : 0;
            $update_format[] = '%d';
        }
        
        if ($request->has_param('status')) {
            $update_data['status'] = $request->get_param('status');
            $update_format[] = '%s';
        }
        
        // Update redirect
        $result = $wpdb->update(
            $table,
            $update_data,
            ['id' => $id],
            $update_format,
            ['%d']
        );
        
        if ($result === false) {
            return new \WP_Error(
                'db_error',
                __('Failed to update redirect', 'prorank-seo'),
                ['status' => 500]
            );
        }

        do_action('prorank_redirect_updated', $id);
        
        $redirect = $wpdb->get_row(
            $wpdb->prepare(
                "SELECT * FROM {$table} WHERE id = %d",
                $id
            )
        );
        
        return new \WP_REST_Response($this->format_redirect($redirect));
    }
    
    /**
     * Delete redirect
     *
     * @param \WP_REST_Request $request
     * @return \WP_REST_Response|\WP_Error
     */
    public function delete_redirect(\WP_REST_Request $request) {
        global $wpdb;
        
        if (!$this->ensure_tables()) {
            return new \WP_Error(
                'redirects_table_missing',
                __('Redirects storage is not available', 'prorank-seo'),
                ['status' => 500]
            );
        }

        $id = (int) $request->get_param('id');
        $table = $this->get_table_name();
        
        $result = $wpdb->delete(
            $table,
            ['id' => $id],
            ['%d']
        );
        
        if ($result === false) {
            return new \WP_Error(
                'db_error',
                __('Failed to delete redirect', 'prorank-seo'),
                ['status' => 500]
            );
        }
        
        if ($result === 0) {
            return new \WP_Error(
                'redirect_not_found',
                __('Redirect not found', 'prorank-seo'),
                ['status' => 404]
            );
        }

        do_action('prorank_redirect_deleted', $id);
        
        return new \WP_REST_Response(null, 204);
    }
    
    /**
     * Bulk action handler
     *
     * @param \WP_REST_Request $request
     * @return \WP_REST_Response|\WP_Error
     */
    public function bulk_action(\WP_REST_Request $request) {
        global $wpdb;
        
        if (!$this->ensure_tables()) {
            return new \WP_Error(
                'redirects_table_missing',
                __('Redirects storage is not available', 'prorank-seo'),
                ['status' => 500]
            );
        }

        $action = $request->get_param('action');
        $ids = $request->get_param('ids');
        $table = $this->get_table_name();
        
        if (empty($ids)) {
            return new \WP_Error(
                'no_ids',
                __('No redirect IDs provided', 'prorank-seo'),
                ['status' => 400]
            );
        }
        
        // Sanitize IDs
        $ids = array_map('intval', $ids);
        $placeholders = implode(',', array_fill(0, count($ids), '%d'));
        
        switch ($action) {
            case 'activate':
                $result = $wpdb->query(
                    $wpdb->prepare(
                        "UPDATE {$table} SET status = 'active' WHERE id IN ({$placeholders})",
                        ...$ids
                    )
                );
                break;
                
            case 'deactivate':
                $result = $wpdb->query(
                    $wpdb->prepare(
                        "UPDATE {$table} SET status = 'inactive' WHERE id IN ({$placeholders})",
                        ...$ids
                    )
                );
                break;
                
            case 'delete':
                $result = $wpdb->query(
                    $wpdb->prepare(
                        "DELETE FROM {$table} WHERE id IN ({$placeholders})",
                        ...$ids
                    )
                );
                break;
                
            default:
                return new \WP_Error(
                    'invalid_action',
                    __('Invalid bulk action', 'prorank-seo'),
                    ['status' => 400]
                );
        }
        
        if ($result === false) {
            return new \WP_Error(
                'db_error',
                __('Failed to perform bulk action', 'prorank-seo'),
                ['status' => 500]
            );
        }

        if ($action === 'activate' || $action === 'deactivate') {
            foreach ($ids as $redirect_id) {
                do_action('prorank_redirect_updated', (int) $redirect_id);
            }
        } elseif ($action === 'delete') {
            foreach ($ids as $redirect_id) {
                do_action('prorank_redirect_deleted', (int) $redirect_id);
            }
        }
        
        return new \WP_REST_Response([
            'affected' => $result,
        ]);
    }

    /**
     * Test a regex pattern against test URLs
     *
     * @param \WP_REST_Request $request
     * @return \WP_REST_Response|\WP_Error
     */
    public function test_regex_pattern(\WP_REST_Request $request) {
        return new \WP_Error(
            'regex_not_available_in_free',
            __('Regex redirects are not available in the free plugin.', 'prorank-seo'),
            ['status' => 403]
        );
    }

    /**
     * Format redirect for response
     *
     * @param object $redirect
     * @return array
     */
    private function format_redirect(object $redirect): array {
        $auto_reason = $this->parse_auto_reason($redirect->notes ?? null);

        return [
            'id' => (int) $redirect->id,
            'source_url' => $redirect->source_url,
            'target_url' => $redirect->target_url,
            'type' => (int) $redirect->type,
            'is_regex' => (bool) $redirect->is_regex,
            'status' => $redirect->status,
            'hits' => (int) $redirect->hits,
            'last_hit_timestamp' => $redirect->last_hit_timestamp,
            'created_at' => $redirect->created_at,
            'updated_at' => $redirect->updated_at,
            'is_auto' => $auto_reason !== null,
            'auto_reason' => $auto_reason,
        ];
    }
    
    /**
     * Handle post URL changes
     *
     * @param int $post_id Post ID
     * @param \WP_Post $post_after Post object after update
     * @param \WP_Post $post_before Post object before update
     * @return void
     */
    public function handle_post_url_change(int $post_id, \WP_Post $post_after, \WP_Post $post_before): void {
        // Skip auto-saves, revisions, and drafts
        if (wp_is_post_autosave($post_id) || wp_is_post_revision($post_id)) {
            return;
        }
        
        if ($post_after->post_status !== 'publish' || $post_before->post_status !== 'publish') {
            return;
        }
        
        // Check if URL has changed
        $old_url = get_permalink($post_before);
        $new_url = get_permalink($post_after);
        
        // Also check for slug changes
        if ($post_before->post_name !== $post_after->post_name) {
            $old_path = wp_parse_url($old_url, PHP_URL_PATH);
            $new_path = wp_parse_url($new_url, PHP_URL_PATH);
            
            if ($old_path && $new_path && $old_path !== $new_path) {
                $this->create_auto_redirect($old_path, $new_path, $post_id);
            }
        }
    }
    
    /**
     * Handle post deletion
     *
     * @param int $post_id Post ID
     * @return void
     */
    public function handle_post_deletion(int $post_id): void {
        $post = get_post($post_id);
        
        if (!$post || $post->post_status !== 'publish') {
            return;
        }
        
        // Skip if redirect to trash is disabled
        if (!get_option('prorank_redirect_deleted_posts', get_option('prorank_redirect_deleted_to_home', false))) {
            return;
        }
        
        $old_url = get_permalink($post);
        $old_path = wp_parse_url($old_url, PHP_URL_PATH);
        
        if ($old_path) {
            // Redirect to home page or a configured page
            $target = get_option('prorank_deleted_redirect_target', home_url('/'));
            $this->create_auto_redirect($old_path, $target, $post_id, 'deleted');
        }
    }
    
    /**
     * Create automatic redirect
     *
     * @param string $source_url Source URL
     * @param string $target_url Target URL
     * @param int $post_id Related post ID
     * @param string $reason Reason for redirect
     * @return void
     */
    private function create_auto_redirect(string $source_url, string $target_url, int $post_id, string $reason = 'url_changed'): void {
        global $wpdb;
        
        $table = $this->get_table_name();
        $source_url = $this->normalize_source_url_for_storage($source_url, false);
        if ($source_url === '') {
            return;
        }
        
        // Check if redirect already exists
        $existing = $this->find_existing_exact_redirect_id($table, $source_url);
        
        if ($existing) {
            // Update existing redirect
            $wpdb->update(
                $table,
                [
                    'target_url' => $target_url,
                    'updated_at' => current_time('mysql'),
                    'notes' => $this->build_auto_notes($reason, $post_id, true),
                ],
                ['id' => $existing],
                ['%s', '%s', '%s'],
                ['%d']
            );
        } else {
            // Create new redirect
            $redirect_type = (int) get_option('prorank_default_redirect_type', 301);
            if (!in_array($redirect_type, [301, 302, 303, 307, 308], true)) {
                $redirect_type = 301;
            }

            $wpdb->insert(
                $table,
                [
                    'source_url' => $source_url,
                    'target_url' => $target_url,
                    'type' => $redirect_type,
                    'status' => 'active',
                    'notes' => $this->build_auto_notes($reason, $post_id, false),
                    'created_by' => get_current_user_id(),
                ],
                ['%s', '%s', '%d', '%s', '%s', '%d']
            );

            do_action('prorank_redirect_created', $wpdb->insert_id);
        }
    }
    
    /**
     * Detect redirect chains
     *
     * @param int $redirect_id Redirect ID
     * @return array|null Chain information if detected
     */
    public function detect_redirect_chains(int $redirect_id): ?array {
        global $wpdb;
        
        $table = $this->get_table_name();
        $max_depth = (int) get_option('prorank_max_redirect_chain_depth', 3);
        
        // Get the redirect
        $redirect = $wpdb->get_row(
            $wpdb->prepare(
                "SELECT * FROM {$table} WHERE id = %d",
                $redirect_id
            )
        );
        
        if (!$redirect) {
            return null;
        }
        
        $chain = [$redirect->source_url];
        $current_target = $redirect->target_url;
        $depth = 1;
        
        // Follow the chain
        while ($depth < 10) { // Safety limit
            // Check if target is another redirect source
            $lookup_target = $this->normalize_internal_target_for_lookup($current_target);
            $next_redirect = $wpdb->get_row(
                $wpdb->prepare(
                    "SELECT * FROM {$table} 
                    WHERE source_url = %s 
                    AND status = 'active'
                    AND id != %d",
                    $lookup_target,
                    $redirect_id
                )
            );
            
            if (!$next_redirect) {
                break;
            }
            
            $chain[] = $next_redirect->source_url;
            $current_target = $next_redirect->target_url;
            $depth++;
            
            // Check for loops
            if (in_array($current_target, $chain)) {
                // Redirect loop detected!
                $this->handle_redirect_loop($redirect_id, $chain);
                return [
                    'type' => 'loop',
                    'chain' => $chain,
                    'depth' => $depth,
                ];
            }
        }
        
        if ($depth > $max_depth) {
            // Chain too long
            $this->handle_long_chain($redirect_id, $chain, $depth);
            return [
                'type' => 'long_chain',
                'chain' => $chain,
                'depth' => $depth,
                'final_target' => $current_target,
            ];
        }
        
        return null;
    }
    
    /**
     * Handle redirect loop detection
     *
     * @param int $redirect_id Redirect ID
     * @param array $chain Redirect chain
     * @return void
     */
    private function handle_redirect_loop(int $redirect_id, array $chain): void {
        global $wpdb;
        
        $table = $this->get_table_name();
        
        // Deactivate the redirect to prevent loops
        $wpdb->update(
            $table,
            [
                'status' => 'inactive',
                'notes' => __('Deactivated: Redirect loop detected', 'prorank-seo'),
            ],
            ['id' => $redirect_id],
            ['%s', '%s'],
            ['%d']
        );
        
        // Log the issue
        prorank_log(sprintf(
            'ProRank SEO: Redirect loop detected for redirect ID %d. Chain: %s',
            $redirect_id,
            implode(' -> ', $chain)
        ));
        
        // Add admin notice
        set_transient(
            'prorank_redirect_loop_notice',
/* translators: %1$: value */
            sprintf(__('Redirect loop detected and deactivated for redirect ID: %1$d', 'prorank-seo'), $redirect_id),
            HOUR_IN_SECONDS
        );
    }
    
    /**
     * Handle long redirect chain
     *
     * @param int $redirect_id Redirect ID
     * @param array $chain Redirect chain
     * @param int $depth Chain depth
     * @return void
     */
    private function handle_long_chain(int $redirect_id, array $chain, int $depth): void {
        // Add admin notice about long chain
        set_transient(
            'prorank_long_chain_notice_' . $redirect_id,
            sprintf(
/* translators: %1$: value, %2$: value */
                __('Long redirect chain detected (depth: %1$d) for redirect ID: %2$d. Consider optimizing.', 'prorank-seo'),
                $depth,
                $redirect_id
            ),
            DAY_IN_SECONDS
        );
    }
    
    /**
     * Export redirects REST endpoint
     *
     * @param \WP_REST_Request $request
     * @return \WP_REST_Response
     */
    public function export_redirects(\WP_REST_Request $request): \WP_REST_Response {
        $format = strtolower((string) ($request->get_param('format') ?: 'csv'));
        $date = wp_date('Y-m-d');

        if (!$this->ensure_tables()) {
            // Return an empty payload of the requested shape so the UI does not break.
            switch ($format) {
                case 'json':
                    return new \WP_REST_Response([
                        'format' => 'json',
                        'redirects' => [],
                        'count' => 0,
                        'exported_at' => current_time('mysql'),
                    ]);
                case 'htaccess':
                case 'apache':
                    return new \WP_REST_Response([
                        'format' => 'htaccess',
                        'content' => "# ProRank SEO redirects ({$date}) — table unavailable\n",
                        'filename' => "prorank-redirects-{$date}.htaccess",
                    ]);
                case 'nginx':
                    return new \WP_REST_Response([
                        'format' => 'nginx',
                        'content' => "# ProRank SEO redirects ({$date}) — table unavailable\n",
                        'filename' => "prorank-redirects-{$date}.nginx.conf",
                    ]);
                default:
                    return new \WP_REST_Response([
                        'format' => 'csv',
                        'csv' => "Source URL,Target URL,Type,Status,Regex,Hits,Created,Updated\n",
                        'filename' => "redirects-{$date}.csv",
                    ]);
            }
        }

        global $wpdb;
        $table = $this->get_table_name();

        switch ($format) {
            case 'json':
                $redirects = $wpdb->get_results("SELECT * FROM {$table} ORDER BY id");
                return new \WP_REST_Response([
                    'format' => 'json',
                    'redirects' => $redirects,
                    'count' => is_array($redirects) ? count($redirects) : 0,
                    'exported_at' => current_time('mysql'),
                ]);
            case 'htaccess':
            case 'apache':
                $content = $this->export_redirects_htaccess();
                return new \WP_REST_Response([
                    'format' => 'htaccess',
                    'content' => $content,
                    'filename' => "prorank-redirects-{$date}.htaccess",
                ]);
            case 'nginx':
                $content = $this->export_redirects_nginx();
                return new \WP_REST_Response([
                    'format' => 'nginx',
                    'content' => $content,
                    'filename' => "prorank-redirects-{$date}.nginx.conf",
                ]);
            case 'csv':
            default:
                $csv = $this->export_redirects_csv();
                return new \WP_REST_Response([
                    'format' => 'csv',
                    'csv' => $csv,
                    'filename' => "redirects-{$date}.csv",
                ]);
        }
    }
    
    /**
     * Import redirects REST endpoint
     *
     * @param \WP_REST_Request $request
     * @return \WP_REST_Response|\WP_Error
     */
    public function import_redirects(\WP_REST_Request $request) {
        if (!$this->ensure_tables()) {
            return new \WP_Error(
                'redirects_table_missing',
                __('Redirects storage is not available', 'prorank-seo'),
                ['status' => 500]
            );
        }

        // Accept the body under either parameter name for back-compat.
        $content = (string) ($request->get_param('content') ?? '');
        if ($content === '') {
            $content = (string) ($request->get_param('csv') ?? '');
        }

        $file = $request->get_file_params();
        if (empty($content) && !empty($file['file']['tmp_name']) && is_readable($file['file']['tmp_name'])) {
            $content = (string) file_get_contents($file['file']['tmp_name']);
        }

        if ($content === '') {
            return new \WP_Error(
                'no_data',
                __('No import data provided', 'prorank-seo'),
                ['status' => 400]
            );
        }

        $format = strtolower((string) ($request->get_param('format') ?: 'auto'));
        $dry_run = filter_var($request->get_param('dry_run'), FILTER_VALIDATE_BOOLEAN);

        if ($format === 'auto' || $format === '') {
            $format = $this->detect_import_format($content);
        }

        switch ($format) {
            case 'json':
                $results = $this->import_redirects_json($content, $dry_run);
                break;
            case 'htaccess':
            case 'apache':
                $results = $this->import_redirects_htaccess($content, $dry_run);
                $format = 'htaccess';
                break;
            case 'redirection':
                $results = $this->import_redirects_redirection_plugin($content, $dry_run);
                break;
            case 'simple301':
                $results = $this->import_redirects_simple301($content, $dry_run);
                break;
            case 'srm':
                $results = $this->import_redirects_srm($content, $dry_run);
                break;
            case 'csv':
            default:
                $results = $this->import_redirects_csv($content, $dry_run);
                $format = 'csv';
                break;
        }

        $results['format'] = $format;
        $results['dry_run'] = (bool) $dry_run;

        return new \WP_REST_Response($results);
    }
    
    /**
     * Check all redirects for chains
     *
     * @return \WP_REST_Response
     */
    public function check_all_chains(): \WP_REST_Response {
        global $wpdb;

        if (!$this->ensure_tables()) {
            return new \WP_REST_Response([
                'total_redirects' => 0,
                'chains_detected' => 0,
                'loops_detected' => 0,
                'chains' => [],
                'loops' => [],
            ]);
        }
        
        $table = $this->get_table_name();
        $redirects = $wpdb->get_results("SELECT id FROM {$table} WHERE status = 'active'");
        
        $chains = [];
        $loops = [];
        
        foreach ($redirects as $redirect) {
            $chain_info = $this->detect_redirect_chains((int) $redirect->id);
            if ($chain_info) {
                if ($chain_info['type'] === 'loop') {
                    $loops[] = $chain_info;
                } else {
                    $chains[] = $chain_info;
                }
            }
        }
        
        return new \WP_REST_Response([
            'total_redirects' => count($redirects),
            'chains_detected' => count($chains),
            'loops_detected' => count($loops),
            'chains' => $chains,
            'loops' => $loops,
        ]);
    }

    /**
     * Flatten redirect chains by updating intermediate redirects to point directly to final target
     *
     * @param \WP_REST_Request $request
     * @return \WP_REST_Response|\WP_Error
     */
    public function flatten_redirect_chains(\WP_REST_Request $request) {
        global $wpdb;

        if (!$this->ensure_tables()) {
            return new \WP_Error(
                'redirects_table_missing',
                __('Redirects storage is not available', 'prorank-seo'),
                ['status' => 500]
            );
        }

        $redirect_ids = $request->get_param('redirect_ids') ?: [];
        $flatten_all = $request->get_param('flatten_all');
        $table = $this->get_table_name();

        // If flatten_all is true, get all redirects that are part of chains
        if ($flatten_all) {
            $redirects = $wpdb->get_results("SELECT id FROM {$table} WHERE status = 'active'");
            $redirect_ids = [];

            foreach ($redirects as $redirect) {
                $chain_info = $this->detect_redirect_chains((int) $redirect->id);
                if ($chain_info && $chain_info['type'] === 'long_chain') {
                    $redirect_ids[] = (int) $redirect->id;
                }
            }
        }

        if (empty($redirect_ids)) {
            return new \WP_REST_Response([
                'flattened' => 0,
                'message' => __('No redirect chains found to flatten', 'prorank-seo'),
            ]);
        }

        $flattened = 0;
        $errors = [];

        foreach ($redirect_ids as $redirect_id) {
            // Get the redirect
            $redirect = $wpdb->get_row(
                $wpdb->prepare(
                    "SELECT * FROM {$table} WHERE id = %d",
                    $redirect_id
                )
            );

            if (!$redirect) {
/* translators: %1$: value */
                $errors[] = sprintf(__('Redirect ID %1$d not found', 'prorank-seo'), $redirect_id);
                continue;
            }

            // Skip regex redirects
            if ($redirect->is_regex) {
/* translators: %1$: value */
                $errors[] = sprintf(__('Redirect ID %1$d is regex-based and cannot be flattened', 'prorank-seo'), $redirect_id);
                continue;
            }

            // Follow the chain to find the final target
            $current_target = $redirect->target_url;
            $visited = [$redirect->source_url];
            $max_depth = 10; // Safety limit
            $depth = 0;

            while ($depth < $max_depth) {
                // Check if target matches another redirect's source
                $lookup_target = $this->normalize_internal_target_for_lookup($current_target);
                $next_redirect = $wpdb->get_row(
                    $wpdb->prepare(
                        "SELECT * FROM {$table} WHERE source_url = %s AND status = 'active' AND is_regex = 0",
                        $lookup_target
                    )
                );

                if (!$next_redirect) {
                    break; // Reached the end of the chain
                }

                // Check for loops
                if (in_array($next_redirect->source_url, $visited)) {
/* translators: %1$: value */
                    $errors[] = sprintf(__('Redirect ID %1$d has a loop and cannot be flattened', 'prorank-seo'), $redirect_id);
                    break;
                }

                $visited[] = $next_redirect->source_url;
                $current_target = $next_redirect->target_url;
                $depth++;
            }

            // Only update if target changed and we didn't hit a loop
            if ($current_target !== $redirect->target_url && !in_array($current_target, $visited)) {
                $result = $wpdb->update(
                    $table,
                    ['target_url' => $current_target, 'updated_at' => current_time('mysql')],
                    ['id' => $redirect_id],
                    ['%s', '%s'],
                    ['%d']
                );

                if ($result !== false) {
                    $flattened++;
                    // Clear cache for this redirect
                    $this->clear_redirect_cache($redirect_id);
                }
            }
        }

        return new \WP_REST_Response([
            'flattened' => $flattened,
            'errors' => $errors,
            'message' => sprintf(
                /* translators: %d: number of flattened redirects */
                __('%1$d redirect chain(s) flattened successfully', 'prorank-seo'),
                $flattened
            ),
        ]);
    }

    /**
     * Export redirects to CSV
     *
     * @return string CSV content
     */
    private function export_redirects_csv(): string {
        global $wpdb;
        
        $table = $this->get_table_name();
        $redirects = $wpdb->get_results("SELECT * FROM {$table} ORDER BY id");
        
        $csv = "Source URL,Target URL,Type,Status,Regex,Hits,Created,Updated\n";
        
        foreach ($redirects as $redirect) {
            $csv .= sprintf(
                '"%s","%s","%d","%s","%s","%d","%s","%s"' . "\n",
                $redirect->source_url,
                $redirect->target_url,
                $redirect->type,
                $redirect->status,
                $redirect->is_regex ? 'Yes' : 'No',
                $redirect->hits,
                $redirect->created_at,
                $redirect->updated_at
            );
        }
        
        return $csv;
    }
    
    /**
     * Import redirects from CSV
     *
     * @param string $csv_content CSV content
     * @return array Import results
     */
    public function import_redirects_csv(string $csv_content, bool $dry_run = false): array {
        $lines = preg_split('/\r\n|\r|\n/', $csv_content);
        if (!is_array($lines) || empty($lines)) {
            return $this->empty_import_result();
        }

        $first_line = (string) array_shift($lines);
        $header = is_string($first_line) && $first_line !== '' ? str_getcsv($first_line) : [];

        $rows = [];
        foreach ($lines as $line) {
            if (trim((string) $line) === '') {
                continue;
            }
            $data = str_getcsv($line);
            if (count($data) < 2) {
                $rows[] = ['error' => sprintf(/* translators: %s: raw CSV line */ __('Invalid line: %s', 'prorank-seo'), $line)];
                continue;
            }

            $rows[] = [
                'source_url' => (string) ($data[0] ?? ''),
                'target_url' => (string) ($data[1] ?? ''),
                'type' => isset($data[2]) ? (int) $data[2] : 301,
                'status' => isset($data[3]) ? (string) $data[3] : 'active',
                'is_regex' => isset($data[4]) ? in_array(strtolower(trim((string) $data[4])), ['1', 'yes', 'true'], true) : false,
            ];
        }

        return $this->apply_import_rows($rows, $dry_run, 'csv');
    }

    // ---------------------------------------------------------------------
    // Task 2 — additional import / export formats + auto-redirect helpers.
    // Helpers added on top of the existing engine. find_redirect() itself
    // was also adjusted in this task to preserve candidate priority (see
    // its docblock) and to scope the result cache to the matched candidate.
    // ---------------------------------------------------------------------

    /**
     * Build the locale-safe notes string for an auto-created redirect.
     * Format: "[prorank-auto:<reason>] Auto-{created|updated}: <reason> (Post ID: <id>)".
     */
    private function build_auto_notes(string $reason, int $post_id, bool $updating = false): string {
        $reason_key = preg_replace('/[^a-z0-9_\-]/', '', strtolower($reason));
        if ($reason_key === null || $reason_key === '') {
            $reason_key = 'unknown';
        }
        $prefix = '[prorank-auto:' . $reason_key . '] ';
        if ($updating) {
            /* translators: 1: reason key, 2: post ID */
            return $prefix . sprintf(__('Auto-updated: %1$s (Post ID: %2$d)', 'prorank-seo'), $reason_key, $post_id);
        }
        /* translators: 1: reason key, 2: post ID */
        return $prefix . sprintf(__('Auto-created: %1$s (Post ID: %2$d)', 'prorank-seo'), $reason_key, $post_id);
    }

    /**
     * Extract the auto-redirect reason from a notes string, or null if not auto.
     */
    private function parse_auto_reason(?string $notes): ?string {
        if (!is_string($notes) || $notes === '') {
            return null;
        }
        if (preg_match('/\[prorank-auto:([a-z0-9_\-]+)\]/i', $notes, $m)) {
            return strtolower($m[1]);
        }
        return null;
    }

    /**
     * Detect the import format by sniffing the first non-blank line of $content.
     */
    private function detect_import_format(string $content): string {
        $trim = ltrim($content);
        if ($trim === '') {
            return 'csv';
        }
        if ($trim[0] === '{' || $trim[0] === '[') {
            return 'json';
        }
        // Apache directives: Redirect / RedirectMatch / RedirectPermanent.
        if (preg_match('/^\s*Redirect(?:Match|Permanent)?\s+/m', $content)) {
            return 'htaccess';
        }

        // Inspect CSV headers when present.
        $first_line = (string) strtok($content, "\r\n");
        $lower = strtolower($first_line);
        if (strpos($lower, 'redirect_from') !== false || strpos($lower, 'redirect_to') !== false) {
            return 'srm';
        }
        if (strpos($lower, 'enable_regex') !== false || strpos($lower, 'redirect_status_code') !== false) {
            return 'srm';
        }
        if (preg_match('/^source\s*,/', $lower) && strpos($lower, 'regex') !== false) {
            return 'redirection';
        }
        if (preg_match('/^"?request"?\s*,/i', $first_line) || preg_match('/^"?old\s*url"?\s*,/i', $first_line)) {
            return 'simple301';
        }
        return 'csv';
    }

    /**
     * Shared empty import-result shape.
     */
    private function empty_import_result(): array {
        return [
            'imported' => 0,
            'duplicates' => 0,
            'invalid' => 0,
            'errors' => [],
            'preview' => [],
        ];
    }

    /**
     * Sanitize and validate a parsed import row, returning the normalized values
     * or an error string. Source URLs without a leading slash are accepted but
     * normalized via the existing storage helper.
     *
     * @return array{source:string,target:string,type:int,status:string,is_regex:bool}|string
     */
    private function sanitize_import_row(array $row, bool $allow_regex = false) {
        $source_raw = (string) ($row['source_url'] ?? '');
        $target_raw = (string) ($row['target_url'] ?? '');
        $wants_regex = !empty($row['is_regex']);

        // A regex row in a package without regex support cannot be safely
        // demoted to an exact source (the pattern would never match and the
        // table would carry dead rows like "/^/blog/(.*)$"). Reject it.
        if ($wants_regex && !$allow_regex) {
            return sprintf(
                /* translators: %s: the regex source pattern */
                __('Regex redirects are not supported in this package; skipped: %s', 'prorank-seo'),
                $source_raw
            );
        }

        $is_regex = $wants_regex;

        $source = $this->normalize_source_url_for_storage($source_raw, $is_regex);
        if ($source === '') {
            return sprintf(/* translators: %s: raw URL */ __('Invalid source URL: %s', 'prorank-seo'), $source_raw);
        }

        $target = trim($target_raw);
        if ($target === '') {
            return sprintf(/* translators: %s: source URL */ __('Missing target URL for: %s', 'prorank-seo'), $source);
        }
        // External targets get full URL validation; relative paths pass through.
        if (preg_match('#^https?://#i', $target)) {
            $validated = esc_url_raw($target);
            if ($validated === '') {
                return sprintf(/* translators: %s: target URL */ __('Invalid target URL: %s', 'prorank-seo'), $target);
            }
            $target = $validated;
        } else {
            // Relative target: strip control characters, keep leading slash.
            $target = preg_replace('/[\x00-\x1F\x7F]/', '', $target) ?? '';
            if ($target === '') {
                return __('Invalid relative target URL', 'prorank-seo');
            }
        }

        $type = (int) ($row['type'] ?? 301);
        if (!in_array($type, [301, 302, 303, 307, 308], true)) {
            $type = 301;
        }

        $status = strtolower((string) ($row['status'] ?? 'active'));
        if (!in_array($status, ['active', 'inactive'], true)) {
            $status = 'active';
        }

        return [
            'source' => $source,
            'target' => $target,
            'type' => $type,
            'status' => $status,
            'is_regex' => $is_regex,
        ];
    }

    /**
     * Apply parsed rows to the DB (or dry-run them) and return result counts.
     */
    private function apply_import_rows(array $rows, bool $dry_run, string $source_label): array {
        global $wpdb;

        $table = $this->get_table_name();
        $allow_regex = $this->import_allows_regex();

        $result = $this->empty_import_result();
        $preview = [];

        foreach ($rows as $row) {
            if (isset($row['error']) && is_string($row['error'])) {
                $result['invalid']++;
                $result['errors'][] = $row['error'];
                $preview[] = ['status' => 'invalid', 'message' => $row['error']];
                continue;
            }

            $sanitized = $this->sanitize_import_row($row, $allow_regex);
            if (is_string($sanitized)) {
                $result['invalid']++;
                $result['errors'][] = $sanitized;
                $preview[] = ['status' => 'invalid', 'message' => $sanitized];
                continue;
            }

            // Duplicate check differs for regex vs exact: the exact helper
            // normalizes the source as a URL path, which would never match a
            // regex pattern's literal source_url storage form.
            if ($sanitized['is_regex']) {
                $existing = (int) $wpdb->get_var(
                    $wpdb->prepare(
                        "SELECT id FROM {$table} WHERE source_url = %s AND is_regex = 1 LIMIT 1",
                        $sanitized['source']
                    )
                );
            } else {
                $existing = $this->find_existing_exact_redirect_id($table, $sanitized['source']);
            }
            if ($existing) {
                $result['duplicates']++;
                $preview[] = [
                    'status' => 'duplicate',
                    'source_url' => $sanitized['source'],
                    'target_url' => $sanitized['target'],
                ];
                continue;
            }

            if ($dry_run) {
                $result['imported']++;
                $preview[] = [
                    'status' => 'will_create',
                    'source_url' => $sanitized['source'],
                    'target_url' => $sanitized['target'],
                    'type' => $sanitized['type'],
                ];
                continue;
            }

            $note_label = sprintf(/* translators: %s: source format */ __('Imported from %s', 'prorank-seo'), $source_label);

            $insert_data = [
                'source_url' => $sanitized['source'],
                'target_url' => $sanitized['target'],
                'type' => $sanitized['type'],
                'status' => $sanitized['status'],
                'is_regex' => $sanitized['is_regex'] ? 1 : 0,
                'notes' => $note_label,
            ];
            $insert_formats = ['%s', '%s', '%d', '%s', '%d', '%s'];

            $insert_result = $wpdb->insert($table, $insert_data, $insert_formats);
            if ($insert_result) {
                $result['imported']++;
                $preview[] = [
                    'status' => 'created',
                    'id' => (int) $wpdb->insert_id,
                    'source_url' => $sanitized['source'],
                    'target_url' => $sanitized['target'],
                ];
                do_action('prorank_redirect_created', $wpdb->insert_id);
            } else {
                $msg = sprintf(/* translators: %s: source URL */ __('Failed to import: %s', 'prorank-seo'), $sanitized['source']);
                $result['errors'][] = $msg;
                $result['invalid']++;
                $preview[] = ['status' => 'invalid', 'message' => $msg];
            }
        }

        // Cap preview payload to keep responses bounded (UI just needs a sample).
        $result['preview'] = array_slice($preview, 0, 200);

        return $result;
    }

    /**
     * Allow regex import rows only when the runtime accepts them. The free
     * package leaves is_regex=0 even if the import file declares a regex row.
     */
    private function import_allows_regex(): bool {
        return defined('PRORANK_PLUGIN_PACKAGE') && PRORANK_PLUGIN_PACKAGE === 'premium';
    }

    /**
     * Import from a ProRank JSON export (the same shape /redirects/export returns).
     */
    public function import_redirects_json(string $json_content, bool $dry_run = false): array {
        $decoded = json_decode($json_content, true);
        if (!is_array($decoded)) {
            $result = $this->empty_import_result();
            $result['invalid']++;
            $result['errors'][] = __('Invalid JSON payload', 'prorank-seo');
            return $result;
        }

        if (isset($decoded['redirects']) && is_array($decoded['redirects'])) {
            $records = $decoded['redirects'];
        } elseif (isset($decoded[0]) && is_array($decoded[0])) {
            $records = $decoded;
        } else {
            $records = [];
        }

        $rows = [];
        foreach ($records as $record) {
            if (!is_array($record)) {
                $rows[] = ['error' => __('Skipped non-object JSON entry', 'prorank-seo')];
                continue;
            }
            $rows[] = [
                'source_url' => (string) ($record['source_url'] ?? ''),
                'target_url' => (string) ($record['target_url'] ?? ''),
                'type' => isset($record['type']) ? (int) $record['type'] : 301,
                'status' => (string) ($record['status'] ?? 'active'),
                'is_regex' => !empty($record['is_regex']),
            ];
        }

        return $this->apply_import_rows($rows, $dry_run, 'JSON');
    }

    /**
     * Import Apache .htaccess Redirect / RedirectMatch / RedirectPermanent lines.
     *
     * Recognized forms:
     *   Redirect 301 /old /new
     *   Redirect /old https://example.com/new
     *   RedirectPermanent /old /new
     *   RedirectMatch 301 ^/old/(.*)$ /new/$1
     */
    public function import_redirects_htaccess(string $htaccess_content, bool $dry_run = false): array {
        $lines = preg_split('/\r\n|\r|\n/', $htaccess_content);
        $rows = [];

        foreach ((array) $lines as $raw_line) {
            $line = trim((string) $raw_line);
            if ($line === '' || $line[0] === '#') {
                continue;
            }

            // Match: Redirect | RedirectPermanent | RedirectMatch [code] source target
            if (!preg_match('/^(Redirect(?:Match|Permanent)?)\s+(.+)$/i', $line, $m)) {
                continue;
            }
            $directive = strtolower($m[1]);
            $tail = trim($m[2]);

            // Tokenize respecting quoted strings.
            $tokens = $this->shell_tokenize($tail);
            if (count($tokens) < 2) {
                $rows[] = ['error' => sprintf(/* translators: %s: directive line */ __('Skipped htaccess line: %s', 'prorank-seo'), $line)];
                continue;
            }

            // Optional leading status code.
            $type = 301;
            if (preg_match('/^[0-9]{3}$/', $tokens[0])) {
                $type = (int) array_shift($tokens);
            } elseif ($directive === 'redirectpermanent') {
                $type = 301;
            }

            if (count($tokens) < 2) {
                $rows[] = ['error' => sprintf(/* translators: %s: directive line */ __('Skipped htaccess line: %s', 'prorank-seo'), $line)];
                continue;
            }

            $source = $tokens[0];
            $target = $tokens[1];
            $is_regex = ($directive === 'redirectmatch');

            $rows[] = [
                'source_url' => $source,
                'target_url' => $target,
                'type' => $type,
                'status' => 'active',
                'is_regex' => $is_regex,
            ];
        }

        return $this->apply_import_rows($rows, $dry_run, 'htaccess');
    }

    /**
     * Tokenize a directive body honoring double-quoted strings.
     */
    private function shell_tokenize(string $input): array {
        $tokens = [];
        if (preg_match_all('/"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"|(\S+)/', $input, $matches, PREG_SET_ORDER)) {
            foreach ($matches as $match) {
                $tokens[] = $match[1] !== '' ? $match[1] : $match[2];
            }
        }
        return $tokens;
    }

    /**
     * Import a CSV exported from the Redirection plugin.
     * Common headers: source,target,regex,type,code,group / "source url","target url","regex",...
     */
    public function import_redirects_redirection_plugin(string $csv_content, bool $dry_run = false): array {
        $lines = preg_split('/\r\n|\r|\n/', $csv_content);
        if (empty($lines)) {
            return $this->empty_import_result();
        }
        $header_row = (string) array_shift($lines);
        $header_cells = $header_row !== '' ? array_map(
            static function ($h) {
                return strtolower(trim((string) str_replace(['"', "'"], '', (string) $h)));
            },
            str_getcsv($header_row)
        ) : [];
        $cols = array_flip($header_cells);

        $src_idx = $cols['source'] ?? ($cols['source url'] ?? ($cols['url'] ?? 0));
        $tgt_idx = $cols['target'] ?? ($cols['target url'] ?? ($cols['action_data'] ?? 1));
        $regex_idx = $cols['regex'] ?? ($cols['match_regex'] ?? null);
        $type_idx = $cols['code'] ?? ($cols['action_code'] ?? ($cols['type'] ?? null));

        $rows = [];
        foreach ($lines as $line) {
            if (trim((string) $line) === '') {
                continue;
            }
            $data = str_getcsv($line);
            $source = (string) ($data[$src_idx] ?? '');
            $target = (string) ($data[$tgt_idx] ?? '');
            $is_regex = $regex_idx !== null
                ? in_array(strtolower(trim((string) ($data[$regex_idx] ?? ''))), ['1', 'yes', 'true'], true)
                : false;
            $type = $type_idx !== null ? (int) ($data[$type_idx] ?? 301) : 301;

            $rows[] = [
                'source_url' => $source,
                'target_url' => $target,
                'type' => $type,
                'status' => 'active',
                'is_regex' => $is_regex,
            ];
        }

        return $this->apply_import_rows($rows, $dry_run, 'Redirection plugin');
    }

    /**
     * Import a Simple 301 Redirects-style CSV (two columns: source,target).
     * Skips a header row if present.
     */
    public function import_redirects_simple301(string $csv_content, bool $dry_run = false): array {
        $lines = preg_split('/\r\n|\r|\n/', $csv_content);
        if (empty($lines)) {
            return $this->empty_import_result();
        }

        $first = (string) ($lines[0] ?? '');
        $first_lower = strtolower($first);
        $has_header = (strpos($first_lower, 'request') !== false)
            || (strpos($first_lower, 'old url') !== false)
            || (strpos($first_lower, 'source') !== false);
        if ($has_header) {
            array_shift($lines);
        }

        $rows = [];
        foreach ($lines as $line) {
            if (trim((string) $line) === '') {
                continue;
            }
            $data = str_getcsv($line);
            if (count($data) < 2) {
                $rows[] = ['error' => sprintf(/* translators: %s: raw line */ __('Invalid Simple 301 line: %s', 'prorank-seo'), $line)];
                continue;
            }
            $rows[] = [
                'source_url' => (string) ($data[0] ?? ''),
                'target_url' => (string) ($data[1] ?? ''),
                'type' => 301,
                'status' => 'active',
                'is_regex' => false,
            ];
        }

        return $this->apply_import_rows($rows, $dry_run, 'Simple 301');
    }

    /**
     * Import a Safe Redirect Manager CSV export.
     * Standard header: redirect_from,redirect_to,redirect_status_code,enable_regex
     */
    public function import_redirects_srm(string $csv_content, bool $dry_run = false): array {
        $lines = preg_split('/\r\n|\r|\n/', $csv_content);
        if (empty($lines)) {
            return $this->empty_import_result();
        }
        $header = (string) array_shift($lines);
        $header_cells = $header !== '' ? array_map(
            static function ($h) {
                return strtolower(trim((string) $h));
            },
            str_getcsv($header)
        ) : [];
        $cols = array_flip($header_cells);

        $src_idx = $cols['redirect_from'] ?? 0;
        $tgt_idx = $cols['redirect_to'] ?? 1;
        $type_idx = $cols['redirect_status_code'] ?? null;
        $regex_idx = $cols['enable_regex'] ?? null;

        $rows = [];
        foreach ($lines as $line) {
            if (trim((string) $line) === '') {
                continue;
            }
            $data = str_getcsv($line);

            $rows[] = [
                'source_url' => (string) ($data[$src_idx] ?? ''),
                'target_url' => (string) ($data[$tgt_idx] ?? ''),
                'type' => $type_idx !== null ? (int) ($data[$type_idx] ?? 301) : 301,
                'status' => 'active',
                'is_regex' => $regex_idx !== null
                    ? in_array(strtolower(trim((string) ($data[$regex_idx] ?? ''))), ['1', 'yes', 'true'], true)
                    : false,
            ];
        }

        return $this->apply_import_rows($rows, $dry_run, 'Safe Redirect Manager');
    }

    /**
     * Export active redirects as Apache .htaccess directives.
     * Uses RedirectMatch for regex rows, Redirect for the rest.
     */
    public function export_redirects_htaccess(): string {
        global $wpdb;
        $table = $this->get_table_name();
        $redirects = $wpdb->get_results("SELECT * FROM {$table} WHERE status = 'active' ORDER BY id");

        $date = wp_date('Y-m-d H:i:s');
        $out = "# ProRank SEO redirects export ({$date})\n";
        $out .= "# Add inside <IfModule mod_alias.c> if not already present.\n\n";

        foreach ((array) $redirects as $r) {
            $code = (int) ($r->type ?? 301);
            if (!in_array($code, [301, 302, 303, 307, 308], true)) {
                $code = 301;
            }
            $source = (string) ($r->source_url ?? '');
            $target = (string) ($r->target_url ?? '');
            if ($source === '' || $target === '') {
                continue;
            }

            $source_q = $this->htaccess_quote($source);
            $target_q = $this->htaccess_quote($target);

            if (!empty($r->is_regex)) {
                $out .= "RedirectMatch {$code} {$source_q} {$target_q}\n";
            } else {
                $out .= "Redirect {$code} {$source_q} {$target_q}\n";
            }
        }

        return $out;
    }

    /**
     * Export active redirects as Nginx rewrite/return directives.
     */
    public function export_redirects_nginx(): string {
        global $wpdb;
        $table = $this->get_table_name();
        $redirects = $wpdb->get_results("SELECT * FROM {$table} WHERE status = 'active' ORDER BY id");

        $date = wp_date('Y-m-d H:i:s');
        $out = "# ProRank SEO redirects export ({$date})\n";
        $out .= "# Place inside an appropriate server{} or location{} block.\n\n";

        foreach ((array) $redirects as $r) {
            $code = (int) ($r->type ?? 301);
            if (!in_array($code, [301, 302, 303, 307, 308], true)) {
                $code = 301;
            }
            $source = (string) ($r->source_url ?? '');
            $target = (string) ($r->target_url ?? '');
            if ($source === '' || $target === '') {
                continue;
            }

            if (!empty($r->is_regex)) {
                $pattern = $this->nginx_quote($source);
                $replacement = $this->nginx_quote($target);
                // The Nginx `rewrite` flag set only encodes 301 (permanent)
                // and 302 (redirect). For 303/307/308 fall back to an
                // explicit `return CODE …` inside an `if` block so the
                // original status code is preserved instead of being
                // silently rewritten to 301. Match against $uri (path only)
                // — ProRank's regex matching is path-only, so $request_uri
                // (which includes the query string) would not match parity.
                if ($code === 301) {
                    $out .= "rewrite {$pattern} {$replacement} permanent;\n";
                } elseif ($code === 302) {
                    $out .= "rewrite {$pattern} {$replacement} redirect;\n";
                } else {
                    $out .= "if (\$uri ~ {$pattern}) { return {$code} {$replacement}; }\n";
                }
            } else {
                $tgt = $this->nginx_quote($target);
                if (strpos($source, '?') !== false) {
                    // `location = /path?x=1` is not valid Nginx (the query
                    // string is not part of the location URI). Emit a
                    // $request_uri equality test instead so the query is
                    // included in the match.
                    $req = $this->nginx_quote($source);
                    $out .= "# Source contains a query string; uses request_uri equality.\n";
                    $out .= "if (\$request_uri = {$req}) { return {$code} {$tgt}; }\n";
                } else {
                    $loc = $this->nginx_quote($source);
                    $out .= "location = {$loc} { return {$code} {$tgt}; }\n";
                }
            }
        }

        return $out;
    }

    /**
     * Quote a value for safe embedding in an .htaccess directive.
     */
    private function htaccess_quote(string $value): string {
        if (preg_match('/[\s"]/', $value)) {
            return '"' . str_replace('"', '\"', $value) . '"';
        }
        return $value;
    }

    /**
     * Quote a value for safe embedding in an Nginx directive.
     */
    private function nginx_quote(string $value): string {
        if (preg_match('/[\s;{}"]/', $value)) {
            return '"' . str_replace('"', '\"', $value) . '"';
        }
        return $value;
    }

    /**
     * List redirects that were automatically created (detected via notes sentinel).
     */
    public function get_auto_redirects(\WP_REST_Request $request) {
        global $wpdb;

        if (!$this->ensure_tables()) {
            return new \WP_REST_Response([]);
        }

        $table = $this->get_table_name();
        $rows = $wpdb->get_results("SELECT * FROM {$table} WHERE notes LIKE '[prorank-auto:%' ORDER BY id DESC LIMIT 500");

        $out = [];
        foreach ((array) $rows as $row) {
            $out[] = $this->format_redirect($row);
        }

        return new \WP_REST_Response($out);
    }

    /**
     * Undo (delete) an automatically created redirect. Refuses to delete
     * manually created redirects so the action cannot be misused.
     */
    public function undo_auto_redirect(\WP_REST_Request $request) {
        global $wpdb;

        if (!$this->ensure_tables()) {
            return new \WP_Error(
                'redirects_table_missing',
                __('Redirects storage is not available', 'prorank-seo'),
                ['status' => 500]
            );
        }

        $id = (int) $request->get_param('id');
        $table = $this->get_table_name();
        $row = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$table} WHERE id = %d", $id));
        if (!$row) {
            return new \WP_Error(
                'redirect_not_found',
                __('Redirect not found', 'prorank-seo'),
                ['status' => 404]
            );
        }

        if ($this->parse_auto_reason($row->notes ?? null) === null) {
            return new \WP_Error(
                'not_auto_redirect',
                __('This redirect was created manually; use the standard delete action instead.', 'prorank-seo'),
                ['status' => 422]
            );
        }

        $deleted = $wpdb->delete($table, ['id' => $id], ['%d']);
        if ($deleted === false) {
            return new \WP_Error(
                'db_error',
                __('Failed to delete auto-redirect', 'prorank-seo'),
                ['status' => 500]
            );
        }

        do_action('prorank_redirect_deleted', $id);

        return new \WP_REST_Response([
            'undone' => true,
            'id' => $id,
        ], 200);
    }
}
