<?php

/**
 * Broken Link Checker Module
 *
 * Scans for broken internal and external links
 *
 * @package ProRank\SEO\Modules\Tools
 * @since   1.0.0
 */

declare(strict_types=1);

namespace ProRank\SEO\Modules\Tools;

use ProRank\SEO\Modules\BaseModule;
use ProRank\SEO\Modules\Content\InternalLinking\BuilderContentSupport;
use ProRank\SEO\Modules\Content\InternalLinking\TableRegistry;

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


/**
 * BrokenLinkChecker class
 */


class BrokenLinkCheckerModule extends BaseModule {
    
    /**
     * Module slug
     *
     * @var string
     */
    protected string $slug = 'broken_link_checker';
    
    /**
     * Module name
     *
     * @var string
     */
    protected string $name = 'Broken Link Checker';
    
    /**
     * Module description
     *
     * @var string
     */
    protected string $description = 'Find and fix broken internal and external links';
    
    /**
     * Required tier
     *
     * @var string
     */
    protected string $feature_tier = 'free';
    
    /**
     * Parent module
     *
     * @var string|null
     */
    protected ?string $parent_slug = 'site-audit';
    
    /**
     * Database table name
     *
     * @var string
     */
    private string $table_name;
    
    /**
     * Action Scheduler actions
     */
    private const ACTION_SCAN_LINKS = 'prorank_scan_broken_links';
    private const ACTION_CHECK_LINK = 'prorank_check_single_link';
    private const ACTION_RECHECK_LINK = 'prorank_recheck_broken_link';
    
    /**
     * Link status codes
     */
    private const STATUS_OK = 'ok';
    private const STATUS_BROKEN = 'broken';
    private const STATUS_REDIRECT = 'redirect';
    private const STATUS_TIMEOUT = 'timeout';
    private const STATUS_IGNORED = 'ignored';
    
    /**
     * Initialize module
     */
    public function __construct() {
        global $wpdb;
        $this->table_name = $wpdb->prefix . 'prorank_broken_links';
        // Parent constructor not needed
    }
    
    /**
     * Initialize module hooks
     *
     * @return void
     */
    public function init_hooks(): void {
        if (!$this->is_enabled()) {
            return;
        }
        
        // Create database table
        add_action('init', [$this, 'maybe_create_table']);
        
        // Register Action Scheduler actions
        add_action(self::ACTION_SCAN_LINKS, [$this, 'scan_links']);
        add_action(self::ACTION_CHECK_LINK, [$this, 'check_single_link']);
        add_action(self::ACTION_RECHECK_LINK, [$this, 'recheck_link']);
        
        // Register REST endpoints
        add_action('rest_api_init', [$this, 'register_rest_routes']);
        
        // Schedule periodic scanning if enabled
        add_action('init', [$this, 'maybe_schedule_scan']);
        
        // Hook into post save to check new content
        add_action('save_post', [$this, 'check_post_links'], 10, 2);
    }
    
    /**
     * Create database table if it doesn't exist
     *
     * @return void
     */
    public function maybe_create_table(): void {
        TableRegistry::ensure_tables(true);
    }
    
    /**
     * Register REST routes
     *
     * @return void
     */
    public function register_rest_routes(): void {
        // Start scan
        register_rest_route(
            'prorank-seo/v1',
            '/broken-links/scan',
            [
                [
                    'methods' => \WP_REST_Server::CREATABLE,
                    'callback' => [$this, 'rest_start_scan'],
                    'permission_callback' => [$this, 'rest_permission_check'],
                ],
            ]
        );
        
        // Get broken links
        register_rest_route(
            'prorank-seo/v1',
            '/broken-links',
            [
                [
                    'methods' => \WP_REST_Server::READABLE,
                    'callback' => [$this, 'rest_get_links'],
                    'permission_callback' => [$this, 'rest_permission_check'],
                    'args' => [
                        'status' => [
                            'default' => 'broken',
                            'validate_callback' => function($param) {
                                return in_array($param, ['all', 'broken', 'redirect', 'timeout', 'ok']);
                            }
                        ],
                        'link_type' => [
                            'default' => 'all',
                            'validate_callback' => function($param) {
                                return in_array($param, ['all', 'internal', 'external']);
                            }
                        ],
                        'page' => [
                            'default' => 1,
                            'validate_callback' => function($param) {
                                return is_numeric($param) && $param > 0;
                            }
                        ],
                        'per_page' => [
                            'default' => 50,
                            'validate_callback' => function($param) {
                                return is_numeric($param) && $param > 0 && $param <= 100;
                            }
                        ],
                    ],
                ],
            ]
        );
        
        // Recheck link
        register_rest_route(
            'prorank-seo/v1',
            '/broken-links/(?P<id>\d+)/recheck',
            [
                [
                    'methods' => \WP_REST_Server::CREATABLE,
                    'callback' => [$this, 'rest_recheck_link'],
                    'permission_callback' => [$this, 'rest_permission_check'],
                    'args' => [
                        'id' => [
                            'validate_callback' => function($param) {
                                return is_numeric($param);
                            }
                        ],
                    ],
                ],
            ]
        );
        
        // Ignore/unignore link
        register_rest_route(
            'prorank-seo/v1',
            '/broken-links/(?P<id>\d+)/ignore',
            [
                [
                    'methods' => \WP_REST_Server::CREATABLE,
                    'callback' => [$this, 'rest_toggle_ignore'],
                    'permission_callback' => [$this, 'rest_permission_check'],
                    'args' => [
                        'id' => [
                            'validate_callback' => function($param) {
                                return is_numeric($param);
                            }
                        ],
                        'ignore' => [
                            'default' => true,
                            'validate_callback' => function($param) {
                                return is_bool($param);
                            }
                        ],
                    ],
                ],
            ]
        );
        
        // Get scan status
        register_rest_route(
            'prorank-seo/v1',
            '/broken-links/status',
            [
                [
                    'methods' => \WP_REST_Server::READABLE,
                    'callback' => [$this, 'rest_get_status'],
                    'permission_callback' => [$this, 'rest_permission_check'],
                ],
            ]
        );
    }
    
    /**
     * REST permission check
     *
     * @return bool
     */
    public function rest_permission_check(): bool {
        return current_user_can('edit_posts');
    }

    /**
     * Queue a background action without hard-failing if Action Scheduler is unavailable.
     *
     * @param string $hook Action hook.
     * @param array<string, mixed> $args Action arguments.
     * @return void
     */
    private function enqueue_background_action(string $hook, array $args = []): void {
        if (function_exists('as_enqueue_async_action')) {
            \as_enqueue_async_action($hook, $args);
            return;
        }

        if (!wp_next_scheduled($hook, $args)) {
            wp_schedule_single_event(time() + MINUTE_IN_SECONDS, $hook, $args);
        }
    }

    /**
     * Determine whether a scan action is already scheduled.
     *
     * @param string $hook Action hook.
     * @return bool
     */
    private function has_scheduled_background_action(string $hook): bool {
        if (function_exists('as_has_scheduled_action')) {
            return \as_has_scheduled_action($hook);
        }

        return wp_next_scheduled($hook) !== false;
    }

    /**
     * Schedule recurring scans, with a safe cron fallback when Action Scheduler is unavailable.
     *
     * @param string $hook Action hook.
     * @param int    $interval Interval in seconds.
     * @return void
     */
    private function schedule_scan_action(string $hook, int $interval): void {
        if (function_exists('as_schedule_recurring_action')) {
            \as_schedule_recurring_action(time(), $interval, $hook);
            return;
        }

        if (wp_next_scheduled($hook) === false) {
            wp_schedule_single_event(time() + $interval, $hook);
        }
    }
    
    /**
     * Schedule periodic scanning if enabled
     *
     * @return void
     */
    public function maybe_schedule_scan(): void {
        $settings = $this->plugin()->settings();
        
        if ($settings->get('broken_links_auto_scan', false)) {
            $frequency = $settings->get('broken_links_scan_frequency', 'weekly');
            
            if (!$this->has_scheduled_background_action(self::ACTION_SCAN_LINKS)) {
                $interval = $frequency === 'daily' ? DAY_IN_SECONDS : WEEK_IN_SECONDS;
                $this->schedule_scan_action(self::ACTION_SCAN_LINKS, $interval);
            }
        }
    }
    
    /**
     * Check links when post is saved
     *
     * @param int $post_id Post ID
     * @param \WP_Post $post Post object
     * @return void
     */
    public function check_post_links(int $post_id, \WP_Post $post): void {
        if ( \ProRank\SEO\Plugin::is_elementor_builder_save_request() ) {
            return;
        }

        // Skip autosaves and revisions
        if (wp_is_post_autosave($post_id) || wp_is_post_revision($post_id)) {
            return;
        }
        
        // Skip if not published
        if ($post->post_status !== 'publish') {
            return;
        }
        
        // Extract and queue links for checking
        $this->extract_and_queue_post_links($post_id);
    }
    
    /**
     * Scan all links on the site
     *
     * @return void
     */
    public function scan_links(): void {
        // Update scan status
        update_option('prorank_broken_links_scan', [
            'status' => 'running',
            'started_at' => time(),
            'progress' => 0,
        ]);
        
        // Get posts to scan
        $args = [
            'post_type' => 'any',
            'post_status' => 'publish',
            'posts_per_page' => -1,
            'fields' => 'ids',
        ];
        
        $posts = get_posts($args);
        $total_posts = count($posts);
        
        $links_found = 0;
        
        foreach ($posts as $index => $post_id) {
            // Extract links from post
            $links_found += $this->extract_and_queue_post_links($post_id);
            
            // Update progress
            $progress = round((($index + 1) / $total_posts) * 100);
            update_option('prorank_broken_links_scan', [
                'status' => 'running',
                'started_at' => get_option('prorank_broken_links_scan')['started_at'],
                'progress' => $progress,
                'links_found' => $links_found,
            ]);
        }
        
        // Mark scan as completed
        update_option('prorank_broken_links_scan', [
            'status' => 'completed',
            'started_at' => get_option('prorank_broken_links_scan')['started_at'],
            'completed_at' => time(),
            'progress' => 100,
            'links_found' => $links_found,
        ]);
    }
    
    /**
     * Extract links from post and queue for checking
     *
     * @param int $post_id Post ID
     * @return int Number of links found
     */
    private function extract_and_queue_post_links(int $post_id): int {
        global $wpdb;
        
        $post = get_post($post_id);
        if (!$post) {
            return 0;
        }

        $links = $this->collect_post_links($post_id, $post);
        
        $site_url = get_site_url();
        $links_found = 0;
        
        // First, remove links from this post that are no longer in content
        $existing_urls = array_values(array_filter(array_map(
            static fn(array $link): string => (string) ($link['url'] ?? ''),
            $links
        )));
        
        if (!empty($existing_urls)) {
            $placeholders = implode(',', array_fill(0, count($existing_urls), '%s'));
            $sql = "DELETE FROM {$this->table_name} 
                WHERE post_id = %d 
                AND url NOT IN ({$placeholders})";
            $args = array_merge([$post_id], $existing_urls);
            $wpdb->query($wpdb->prepare($sql, ...$args)); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
        } else {
            // No links in content, remove all for this post
            $wpdb->delete($this->table_name, ['post_id' => $post_id], ['%d']);
        }
        
        foreach ($links as $link) {
            $url = (string) ($link['url'] ?? '');
            $anchor_text = (string) ($link['anchor_text'] ?? '');
            
            // Skip empty URLs and anchors
            if (empty($url) || strpos($url, '#') === 0) {
                continue;
            }
            
            // Skip mailto, tel, and javascript links
            if (preg_match('/^(mailto:|tel:|javascript:)/i', $url)) {
                continue;
            }
            
            // Make URL absolute
            $absolute_url = $this->make_absolute_url($url, $site_url);
            
            // Determine if internal or external
            $link_type = (strpos($absolute_url, $site_url) === 0) ? 'internal' : 'external';
            
            // Insert or update link
            $wpdb->replace(
                $this->table_name,
                [
                    'post_id' => $post_id,
                    'url' => $absolute_url,
                    'anchor_text' => $anchor_text,
                    'link_type' => $link_type,
                    'status' => 'pending',
                ],
                ['%d', '%s', '%s', '%s', '%s']
            );
            
            // Get the inserted/updated link ID
            $link_id = $wpdb->insert_id ?: $wpdb->get_var($wpdb->prepare(
                "SELECT id FROM {$this->table_name} WHERE post_id = %d AND url = %s",
                $post_id,
                $absolute_url
            ));
            
            // Schedule link check
            $this->enqueue_background_action(
                self::ACTION_CHECK_LINK,
                ['link_id' => $link_id]
            );
            
            $links_found++;
        }
        
        return $links_found;
    }

    /**
     * Collect links from raw post content and supported builder data.
     *
     * Avoid rendering `the_content` during save_post because page builders can
     * execute fragile widget logic inside admin AJAX saves.
     *
     * @param int      $post_id Post ID.
     * @param \WP_Post $post    Post object.
     * @return array<int, array{url:string,anchor_text:string}>
     */
    private function collect_post_links(int $post_id, \WP_Post $post): array {
        $links = [];

        foreach ($this->extract_links_from_html((string) $post->post_content) as $link) {
            $url = trim((string) ($link['url'] ?? ''));
            if ($url === '') {
                continue;
            }

            $links[$url] = [
                'url' => $url,
                'anchor_text' => trim((string) ($link['anchor_text'] ?? '')),
            ];
        }

        if (class_exists(BuilderContentSupport::class)) {
            $builder_links = BuilderContentSupport::extract_all_links($post_id, $post);
            foreach ($builder_links as $url) {
                $url = trim((string) $url);
                if ($url === '' || isset($links[$url])) {
                    continue;
                }

                $links[$url] = [
                    'url' => $url,
                    'anchor_text' => '',
                ];
            }
        }

        return array_values($links);
    }

    /**
     * Extract anchors from raw HTML without invoking frontend rendering.
     *
     * @param string $html HTML content.
     * @return array<int, array{url:string,anchor_text:string}>
     */
    private function extract_links_from_html(string $html): array {
        if ($html === '' || !class_exists(\DOMDocument::class)) {
            return [];
        }

        $use_internal_errors = libxml_use_internal_errors(true);
        $links = [];
        $dom   = new \DOMDocument();

        if (@$dom->loadHTML('<!DOCTYPE html><html><head><meta charset="utf-8"></head><body>' . $html . '</body></html>')) {
            $xpath = new \DOMXPath($dom);
            $nodes = $xpath->query('//a[@href]');

            if ($nodes !== false) {
                foreach ($nodes as $node) {
                    if (!($node instanceof \DOMElement)) {
                        continue;
                    }

                    $url = trim($node->getAttribute('href'));
                    if ($url === '') {
                        continue;
                    }

                    $links[] = [
                        'url' => $url,
                        'anchor_text' => trim($node->textContent),
                    ];
                }
            }
        }

        libxml_clear_errors();
        libxml_use_internal_errors($use_internal_errors);

        return $links;
    }
    
    /**
     * Make URL absolute
     *
     * @param string $url URL to convert
     * @param string $base Base URL
     * @return string Absolute URL
     */
    private function make_absolute_url(string $url, string $base): string {
        // Already absolute
        if (filter_var($url, FILTER_VALIDATE_URL)) {
            return $url;
        }
        
        // Protocol-relative
        if (strpos($url, '//') === 0) {
            return 'https:' . $url;
        }
        
        // Root-relative
        if (strpos($url, '/') === 0) {
            $parsed = wp_parse_url($base);
            $scheme = (string) ($parsed['scheme'] ?? wp_parse_url(home_url('/'), PHP_URL_SCHEME) ?? (is_ssl() ? 'https' : 'http'));
            $host = (string) ($parsed['host'] ?? wp_parse_url(home_url('/'), PHP_URL_HOST) ?? '');
            if ($host === '') {
                return home_url($url);
            }
            return $scheme . '://' . $host . $url;
        }
        
        // Relative
        return rtrim($base, '/') . '/' . ltrim($url, '/');
    }
    
    /**
     * Check single link
     *
     * @param int $link_id Link ID
     * @return void
     */
    public function check_single_link(int $link_id): void {
        global $wpdb;
        
        // Get link data
        $link = $wpdb->get_row($wpdb->prepare(
            "SELECT * FROM {$this->table_name} WHERE id = %d",
            $link_id
        ));
        
        if (!$link) {
            return;
        }
        
        // Skip if ignored
        if ($link->status === self::STATUS_IGNORED) {
            return;
        }
        
        // Check the link
        $result = $this->check_url($link->url);
        
        // Update link status
        $wpdb->update(
            $this->table_name,
            [
                'status' => $result['status'],
                'status_code' => $result['status_code'],
                'status_message' => $result['message'],
                'redirect_url' => $result['redirect_url'],
                'last_checked' => current_time('mysql'),
                'check_count' => $link->check_count + 1,
            ],
            ['id' => $link_id],
            ['%s', '%d', '%s', '%s', '%s', '%d'],
            ['%d']
        );
    }
    
    /**
     * Check URL status
     *
     * @param string $url URL to check
     * @return array Check result
     */
    private function check_url(string $url): array {
        // SECURITY: Validate URL to prevent SSRF attacks
        if (!$this->is_url_safe($url)) {
            return [
                'status' => self::STATUS_BROKEN,
                'status_code' => null,
                'message' => __('URL validation failed: Internal/private URLs are not allowed', 'prorank-seo'),
                'redirect_url' => null,
            ];
        }

        // Set up request args
        $args = [
            'timeout' => 15,
            'redirection' => 5,
            'user-agent' => 'ProRank SEO Broken Link Checker',
            'sslverify' => false,
            'headers' => [
                'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
            ],
        ];

        // Make HEAD request first (faster)
        $response = wp_remote_head($url, $args);
        
        // If HEAD fails, try GET
        if (is_wp_error($response)) {
            $response = wp_remote_get($url, $args);
        }
        
        // Handle errors
        if (is_wp_error($response)) {
            $error_message = $response->get_error_message();
            
            // Check for timeout
            if (strpos($error_message, 'cURL error 28') !== false || 
                strpos($error_message, 'Operation timed out') !== false) {
                return [
                    'status' => self::STATUS_TIMEOUT,
                    'status_code' => null,
                    'message' => __('Connection timeout', 'prorank-seo'),
                    'redirect_url' => null,
                ];
            }
            
            return [
                'status' => self::STATUS_BROKEN,
                'status_code' => null,
                'message' => $error_message,
                'redirect_url' => null,
            ];
        }
        
        // Get response code
        $status_code = wp_remote_retrieve_response_code($response);
        
        // Check for redirects
        $redirect_url = null;
        if ($status_code >= 300 && $status_code < 400) {
            $redirect_url = wp_remote_retrieve_header($response, 'location');
            
            return [
                'status' => self::STATUS_REDIRECT,
                'status_code' => $status_code,
/* translators: %1$: value */
                'message' => sprintf(__('Redirects to %1$s', 'prorank-seo'), $redirect_url),
                'redirect_url' => $redirect_url,
            ];
        }
        
        // Check status code
        if ($status_code >= 200 && $status_code < 300) {
            return [
                'status' => self::STATUS_OK,
                'status_code' => $status_code,
                'message' => __('Link is working', 'prorank-seo'),
                'redirect_url' => null,
            ];
        }
        
        // Handle specific error codes
        $error_messages = [
            400 => __('Bad Request', 'prorank-seo'),
            401 => __('Unauthorized', 'prorank-seo'),
            403 => __('Forbidden', 'prorank-seo'),
            404 => __('Not Found', 'prorank-seo'),
            500 => __('Internal Server Error', 'prorank-seo'),
            502 => __('Bad Gateway', 'prorank-seo'),
            503 => __('Service Unavailable', 'prorank-seo'),
        ];
        
/* translators: %1$: value */
        $message = $error_messages[$status_code] ?? sprintf(__('HTTP %1$d Error', 'prorank-seo'), $status_code);
        
        return [
            'status' => self::STATUS_BROKEN,
            'status_code' => $status_code,
            'message' => $message,
            'redirect_url' => null,
        ];
    }
    
    /**
     * Recheck a link
     *
     * @param int $link_id Link ID
     * @return void
     */
    public function recheck_link(int $link_id): void {
        $this->check_single_link($link_id);
    }
    
    /**
     * Get scan limit.
     *
     * @return int URL limit (-1 for unlimited)
     */
    private function get_tier_limit(): int {
        return -1;
    }
    
    /**
     * Get broken links
     *
     * @param array $filters Filters
     * @return array Links data
     */
    public function get_links(array $filters = []): array {
        global $wpdb;
        
        $where_clauses = ['1=1'];
        $where_values = [];
        
        // Status filter
        if (!empty($filters['status']) && $filters['status'] !== 'all') {
            $where_clauses[] = 'status = %s';
            $where_values[] = $filters['status'];
        }
        
        // Link type filter
        if (!empty($filters['link_type']) && $filters['link_type'] !== 'all') {
            $where_clauses[] = 'link_type = %s';
            $where_values[] = $filters['link_type'];
        }
        
        $where_sql = implode(' AND ', $where_clauses);
        
        // Get total count
        $total = $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM {$this->table_name} WHERE {$where_sql}",
            ...$where_values
        ));
        
        // Get links with pagination
        $page = $filters['page'] ?? 1;
        $per_page = $filters['per_page'] ?? 50;
        $offset = ($page - 1) * $per_page;
        
        $links = $wpdb->get_results($wpdb->prepare(
            "SELECT l.*, p.post_title, p.post_type
            FROM {$this->table_name} l
            LEFT JOIN {$wpdb->posts} p ON l.post_id = p.ID
            WHERE {$where_sql}
            ORDER BY l.last_checked DESC, l.id DESC
            LIMIT %d OFFSET %d",
            array_merge($where_values, [$per_page, $offset])
        ));
        
        // Format links
        $formatted_links = array_map(function($link) {
            return [
                'id' => $link->id,
                'post_id' => $link->post_id,
                'post_title' => $link->post_title,
                'post_type' => $link->post_type,
                'url' => $link->url,
                'anchor_text' => $link->anchor_text,
                'link_type' => $link->link_type,
                'status' => $link->status,
                'status_code' => $link->status_code,
                'status_message' => $link->status_message,
                'redirect_url' => $link->redirect_url,
                'last_checked' => $link->last_checked,
                'check_count' => $link->check_count,
            ];
        }, $links);
        
        return [
            'links' => $formatted_links,
            'total' => (int) $total,
            'pages' => ceil($total / $per_page),
        ];
    }
    
    /**
     * Get scan status
     *
     * @return array Status data
     */
    public function get_scan_status(): array {
        global $wpdb;
        
        $scan_info = get_option('prorank_broken_links_scan', []);
        
        // Get link counts
        $counts = $wpdb->get_results(
            "SELECT status, COUNT(*) as count 
            FROM {$this->table_name} 
            GROUP BY status"
        );
        
        $status_counts = [];
        foreach ($counts as $row) {
            $status_counts[$row->status] = (int) $row->count;
        }
        
        $tier_limit = $this->get_tier_limit();
        $total_links = array_sum($status_counts);
        
        return [
            'scan_status' => $scan_info['status'] ?? 'idle',
            'last_scan' => $scan_info['completed_at'] ?? null,
            'progress' => $scan_info['progress'] ?? 0,
            'status_counts' => $status_counts,
            'total_links' => $total_links,
            'tier' => 'free',
            'tier_limit' => $tier_limit,
            'limit_reached' => $tier_limit > 0 && $total_links >= $tier_limit,
        ];
    }
    
    /**
     * REST: Start scan
     *
     * @return \WP_REST_Response
     */
    public function rest_start_scan(): \WP_REST_Response {
        $this->enqueue_background_action(self::ACTION_SCAN_LINKS);
        
        return rest_ensure_response([
            'success' => true,
            'message' => __('Broken link scan started', 'prorank-seo'),
        ]);
    }
    
    /**
     * REST: Get links
     *
     * @param \WP_REST_Request $request Request object
     * @return \WP_REST_Response
     */
    public function rest_get_links(\WP_REST_Request $request): \WP_REST_Response {
        $filters = [
            'status' => $request->get_param('status'),
            'link_type' => $request->get_param('link_type'),
            'page' => (int) $request->get_param('page'),
            'per_page' => (int) $request->get_param('per_page'),
        ];
        
        $result = $this->get_links($filters);
        
        return rest_ensure_response([
            'success' => true,
            'data' => $result,
        ]);
    }
    
    /**
     * REST: Recheck link
     *
     * @param \WP_REST_Request $request Request object
     * @return \WP_REST_Response
     */
    public function rest_recheck_link(\WP_REST_Request $request): \WP_REST_Response {
        $link_id = (int) $request->get_param('id');
        
        $this->enqueue_background_action(
            self::ACTION_RECHECK_LINK,
            ['link_id' => $link_id]
        );
        
        return rest_ensure_response([
            'success' => true,
            'message' => __('Link recheck scheduled', 'prorank-seo'),
        ]);
    }
    
    /**
     * REST: Toggle ignore status
     *
     * @param \WP_REST_Request $request Request object
     * @return \WP_REST_Response
     */
    public function rest_toggle_ignore(\WP_REST_Request $request): \WP_REST_Response {
        global $wpdb;
        
        $link_id = (int) $request->get_param('id');
        $ignore = (bool) $request->get_param('ignore');
        
        $wpdb->update(
            $this->table_name,
            ['status' => $ignore ? self::STATUS_IGNORED : 'pending'],
            ['id' => $link_id],
            ['%s'],
            ['%d']
        );
        
        // Recheck if unignoring
        if (!$ignore) {
            $this->enqueue_background_action(
                self::ACTION_RECHECK_LINK,
                ['link_id' => $link_id]
            );
        }
        
        return rest_ensure_response([
            'success' => true,
            'message' => $ignore 
                ? __('Link ignored', 'prorank-seo') 
                : __('Link unignored and scheduled for recheck', 'prorank-seo'),
        ]);
    }
    
    /**
     * REST: Get status
     *
     * @return \WP_REST_Response
     */
    public function rest_get_status(): \WP_REST_Response {
        $status = $this->get_scan_status();

        return rest_ensure_response([
            'success' => true,
            'data' => $status,
        ]);
    }

    /**
     * Validate URL to prevent SSRF attacks
     *
     * Blocks access to:
     * - Private IP ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)
     * - Loopback addresses (127.0.0.0/8, ::1)
     * - Link-local addresses (169.254.0.0/16, fe80::/10)
     * - AWS metadata service (169.254.169.254)
     * - Internal/localhost hostnames
     * - File:// and other non-HTTP(S) protocols
     *
     * @param string $url URL to validate
     * @return bool True if URL is safe, false otherwise
     */
    private function is_url_safe(string $url): bool {
        // Parse URL
        $parsed = wp_parse_url($url);

        // Validate URL structure
        if (!$parsed || !isset($parsed['scheme']) || !isset($parsed['host'])) {
            return false;
        }

        // Only allow HTTP and HTTPS protocols
        if (!in_array(strtolower($parsed['scheme']), ['http', 'https'], true)) {
            return false;
        }

        // Normalize hostname
        $host = strtolower(trim($parsed['host']));

        // Block localhost variations
        $localhost_patterns = [
            'localhost',
            '127.0.0.1',
            '::1',
            '0.0.0.0',
            '[::]',
        ];

        if (in_array($host, $localhost_patterns, true)) {
            return false;
        }

        // Block *.local domains
        if (substr($host, -6) === '.local') {
            return false;
        }

        // Resolve hostname to IP
        $ip = gethostbyname($host);

        // If gethostbyname returns the same string, resolution failed
        // This could be a domain that doesn't exist - allow it for now
        // (will fail naturally when wp_remote_get tries to connect)
        if ($ip === $host) {
            // But still block obvious internal patterns
            if (preg_match('/^(internal|localhost|admin|test|dev|staging)\./i', $host)) {
                return false;
            }
            return true;
        }

        // Validate resolved IP is not private/reserved
        if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
            return false;
        }

        // Additional check for AWS metadata service (169.254.169.254)
        if ($ip === '169.254.169.254') {
            return false;
        }

        // Additional check for common internal IP patterns
        $ip_parts = explode('.', $ip);
        if (count($ip_parts) === 4) {
            $first_octet = (int)$ip_parts[0];
            $second_octet = (int)$ip_parts[1];

            // Block 10.0.0.0/8
            if ($first_octet === 10) {
                return false;
            }

            // Block 172.16.0.0/12
            if ($first_octet === 172 && $second_octet >= 16 && $second_octet <= 31) {
                return false;
            }

            // Block 192.168.0.0/16
            if ($first_octet === 192 && $second_octet === 168) {
                return false;
            }

            // Block 127.0.0.0/8 (loopback)
            if ($first_octet === 127) {
                return false;
            }

            // Block 169.254.0.0/16 (link-local)
            if ($first_octet === 169 && $second_octet === 254) {
                return false;
            }
        }

        // URL is safe
        return true;
    }
}
