<?php

/**
 * Internal Linking REST API Controller
 *
 * Handles internal linking suggestions endpoints.
 *
 * @package ProRank\SEO\Core\RestApi
 * @since   1.0.0
 */

declare(strict_types=1);

namespace ProRank\SEO\Core\RestApi;

use ProRank\SEO\Modules\Content\InternalLinking;
use ProRank\SEO\Modules\Content\InternalLinking\BuilderContentSupport;
use ProRank\SEO\Modules\Content\InternalLinking\EnhancedLinkSuggestionService;
use ProRank\SEO\Modules\Content\InternalLinking\SettingsStore;
use WP_REST_Request;
use WP_REST_Response;
use WP_Error;

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


/**
 * LinkingController class
 */


class LinkingController extends BaseController {
    /**
     * Keep quick suggestions usable even if a very high global threshold is saved.
     */
    private const QUICK_SUGGESTIONS_MAX_MIN_WORD_COUNT = 120;

    /**
     * Native language packs currently shipped with internal linking.
     */
    private const SUPPORTED_LANGUAGE_CODES = [
        'en', 'es', 'fr', 'de', 'it', 'pt', 'nl', 'pl', 'sv', 'tr',
    ];

    /**
     * Free plugin keeps local keyword/category/tag sources only.
     */
    private const SUPPORTED_KEYWORD_SOURCES = [
        'focus', 'title', 'slug', 'categories', 'tags', 'custom',
    ];
    
    /**
     * Constructor
     */
    public function __construct() {
        $this->rest_base = 'linking';
    }
    
    /**
     * Register routes
     *
     * @return void
     */
    public function register_routes(): void {
        // GET /linking/basic-suggestions/{post_id}
        register_rest_route(
            $this->namespace,
            '/' . $this->rest_base . '/basic-suggestions/(?P<post_id>\d+)',
            [
                [
                    'methods'             => \WP_REST_Server::READABLE,
                    'callback'            => [$this, 'get_basic_suggestions'],
                    // Always allow the editor UI to call this endpoint (avoid 404/403 noise).
                    // The handler returns a locked/upgrade message when Premium/license is missing.
                    'permission_callback' => [$this, 'check_permission_edit_posts_basic'],
                    'args'                => [
                        'post_id' => [
                            'description' => __('Post ID to get suggestions for', 'prorank-seo'),
                            'type'        => 'integer',
                            'required'    => true,
                            'minimum'     => 1,
                        ],
                    ],
                ],
            ]
        );
        
        // GET/POST /settings/internal-linking
        register_rest_route(
            $this->namespace,
            '/settings/internal-linking',
            [
                [
                    'methods'             => \WP_REST_Server::READABLE,
                    'callback'            => [$this, 'get_settings'],
                    // Editors need read access for the post-sidebar quick linking UI.
                    // Use the basic read check so free installs do not hard-fail with 403.
                    'permission_callback' => [$this, 'check_permission_edit_posts_basic'],
                ],
                [
                    'methods'             => \WP_REST_Server::CREATABLE,
                    'callback'            => [$this, 'update_settings'],
                    'permission_callback' => [$this, 'check_permission_manage_options'],
                    'args'                => $this->get_settings_schema(),
                ],
            ]
        );
        
        // POST /linking/analyze - Real-time content analysis
        register_rest_route(
            $this->namespace,
            '/' . $this->rest_base . '/analyze',
            [
                [
                    'methods'             => \WP_REST_Server::CREATABLE,
                    'callback'            => [$this, 'analyze_content'],
                    'permission_callback' => [$this, 'check_permission_edit_posts'],
                ],
            ]
        );
        
        // GET /linking/orphan-check/{post_id}
        register_rest_route(
            $this->namespace,
            '/' . $this->rest_base . '/orphan-check/(?P<post_id>\d+)',
            [
                [
                    'methods'             => \WP_REST_Server::READABLE,
                    'callback'            => [$this, 'check_orphan_status'],
                    'permission_callback' => [$this, 'check_permission_edit_posts'],
                ],
            ]
        );
        
        // POST /linking/track-application
        register_rest_route(
            $this->namespace,
            '/' . $this->rest_base . '/track-application',
            [
                [
                    'methods'             => \WP_REST_Server::CREATABLE,
                    'callback'            => [$this, 'track_application'],
                    'permission_callback' => [$this, 'check_permission_edit_posts'],
                ],
            ]
        );
    }

    /**
     * Get basic internal linking suggestions
     *
     * @param WP_REST_Request $request Request object.
     * @return WP_REST_Response|WP_Error
     */
    public function get_basic_suggestions(WP_REST_Request $request) {
        return $this->try_callback(function() use ($request) {
            $post_id = (int) $request->get_param('post_id');
            
            // Get post data
            $post = get_post($post_id);
            
            // Check if post exists
            if (!$post) {
                return $this->get_error_response(
                    __('Post not found.', 'prorank-seo'),
                    404,
                    'post_not_found'
                );
            }
            
            $settings = SettingsStore::get($this->get_default_settings());
            $target_posts = $this->get_target_posts($post_id, $settings);

            if (isset($settings['enabled']) && !$settings['enabled']) {
                return $this->get_success_response([
                    'suggestions' => [],
                    'message' => __('Internal linking is currently disabled in settings.', 'prorank-seo'),
                ]);
            }

            // Check post type is supported
            $supported_post_types = $settings['link_post_types'] ?? ($settings['post_types'] ?? ['post', 'page']);
            $supported_post_types = apply_filters(
                'prorank_seo_internal_linking_post_types',
                $supported_post_types
            );

            if (!is_array($supported_post_types)) {
                $supported_post_types = array_filter((array) $supported_post_types);
            }

            if (empty($supported_post_types)) {
                $supported_post_types = ['post', 'page'];
            }
            
            if (!in_array($post->post_type, $supported_post_types, true)) {
                return $this->get_error_response(
                    __('Post type not supported for internal linking.', 'prorank-seo'),
                    400,
                    'unsupported_post_type'
                );
            }

            if (!class_exists(InternalLinking::class)) {
                return $this->get_success_response([
                    'suggestions' => [],
                    'message' => __('Internal Linking engine is not available.', 'prorank-seo'),
                ]);
            }

            // Get post content and title
            $post_content = BuilderContentSupport::get_combined_content($post_id, $post);
            $post_title = $post->post_title;
            $content_word_count = str_word_count(wp_strip_all_tags($post_content));
            
            // Handle empty content
            if (empty($post_content)) {
                return $this->get_success_response([
                    'suggestions' => [],
                    'message' => __('No content to analyze for suggestions.', 'prorank-seo'),
                ]);
            }

            $min_word_count = (int) ($settings['min_word_count'] ?? 0);
            if ($min_word_count > self::QUICK_SUGGESTIONS_MAX_MIN_WORD_COUNT) {
                $min_word_count = self::QUICK_SUGGESTIONS_MAX_MIN_WORD_COUNT;
            }
            if ($min_word_count > 0) {
                if ($content_word_count < $min_word_count) {
                    return $this->get_success_response([
                        'suggestions' => [],
                        'message' => __('Content is too short for internal linking suggestions.', 'prorank-seo'),
                    ]);
                }
            }

            $max_suggestions = (int) ($settings['max_suggestions'] ?? 10);
            $algorithm = (string) ($settings['suggestion_algorithm'] ?? 'balanced');
            $min_anchor_words = max(1, (int) ($settings['anchor_text_length'] ?? 1));
            $randomize_suggestions = !empty($settings['randomize_suggestions']);
            $ignore_words_raw = (string) ($settings['ignore_words'] ?? '');
            $ignore_words = array_values(array_filter(array_map('trim', explode(',', $ignore_words_raw))));

            $suggestions = [];

            if ($algorithm !== 'keyword' && class_exists(EnhancedLinkSuggestionService::class)) {
                $service = new EnhancedLinkSuggestionService();
                $advanced = $service->get_suggestions($post_id, $post_content, [
                    'limit' => max(1, $max_suggestions),
                    'use_ai' => false,
                    'use_ai_anchor_generation' => false,
                    'use_gsc' => false,
                ]);

                foreach ($advanced as $item) {
                    $anchor_text = $item['anchor_text'] ?? $item['suggested_anchor'] ?? '';
                    if ($anchor_text === '') {
                        continue;
                    }
                    if (!empty($ignore_words)) {
                        $anchor_lower = strtolower($anchor_text);
                        $ignored = false;
                        foreach ($ignore_words as $ignore) {
                            if ($ignore === '') {
                                continue;
                            }
                            if (strpos($anchor_lower, strtolower($ignore)) !== false) {
                                $ignored = true;
                                break;
                            }
                        }
                        if ($ignored) {
                            continue;
                        }
                    }
                    if (!InternalLinking::is_anchor_valid_for_content($anchor_text, $post_content)) {
                        continue;
                    }
                    $suggestions[] = [
                        'title' => $item['title'] ?? '',
                        'url' => $item['url'] ?? '',
                        'suggested_anchor' => $anchor_text,
                        'relevance' => $item['final_score'] ?? $item['score'] ?? null,
                        'source' => $item['type'] ?? 'keyword',
                        'context' => $item['context'] ?? '',
                    ];
                }
            }

            if (empty($suggestions)) {
                $suggestions = InternalLinking::get_basic_suggestions(
                    $post_id,
                    $post_content,
                    $post_title
                );
            }

            $suggestions = array_values(array_filter($suggestions, static function(array $item) use ($min_anchor_words): bool {
                $anchor = (string) ($item['suggested_anchor'] ?? $item['anchor_text'] ?? '');
                if ($anchor === '') {
                    return false;
                }

                return str_word_count($anchor) >= $min_anchor_words;
            }));

            if ($randomize_suggestions && count($suggestions) > 1) {
                shuffle($suggestions);
            } elseif (count($suggestions) > 1) {
                usort($suggestions, static function($a, $b): int {
                    return (int) ($b['relevance'] ?? 0) <=> (int) ($a['relevance'] ?? 0);
                });
            }

            if ($max_suggestions > 0) {
                $suggestions = array_slice($suggestions, 0, $max_suggestions);
            }
            
            // Add additional data
            $response_data = [
                'suggestions' => $suggestions,
                'stats' => [
                    'total_suggestions' => count($suggestions),
                    'link_density' => InternalLinking::get_link_density($post_content),
                ],
                'diagnostics' => [
                    'target_posts_available' => count($target_posts),
                    'content_word_count' => $content_word_count,
                ],
            ];
            
            // Add message if no suggestions
            if (empty($suggestions)) {
                $response_data['message'] = $this->build_no_suggestions_message($post_content, $target_posts);
            }
            
            return $this->get_success_response($response_data);
        });
    }
    
    /**
     * Get internal linking settings
     *
     * @param WP_REST_Request $request Request object.
     * @return WP_REST_Response
     */
    public function get_settings(WP_REST_Request $request) {
        return $this->try_callback(function() {
            $settings = SettingsStore::migrate($this->get_default_settings());

            if (
                (empty($settings['post_types']) || !is_array($settings['post_types']))
                && !empty($settings['link_post_types'])
                && is_array($settings['link_post_types'])
            ) {
                $settings['post_types'] = $settings['link_post_types'];
            }

            if (
                (empty($settings['link_post_types']) || !is_array($settings['link_post_types']))
                && !empty($settings['post_types'])
                && is_array($settings['post_types'])
            ) {
                $settings['link_post_types'] = $settings['post_types'];
            }

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

            $click_tracking_enabled = isset($settings['enable_click_tracking'])
                ? (bool) $settings['enable_click_tracking']
                : (bool) ($settings['track_clicks'] ?? true);
            $settings['track_clicks'] = $click_tracking_enabled;
            $settings['enable_click_tracking'] = $click_tracking_enabled;
            $settings['open_internal_new_tab'] = isset($settings['open_internal_new_tab'])
                ? (bool) $settings['open_internal_new_tab']
                : (bool) ($settings['open_in_new_tab'] ?? false);
            $settings['open_in_new_tab'] = $settings['open_internal_new_tab'];
            
            return $this->get_success_response($settings);
        });
    }
    
    /**
     * Update internal linking settings
     *
     * @param WP_REST_Request $request Request object.
     * @return WP_REST_Response
     */
    public function update_settings(WP_REST_Request $request) {
        return $this->try_callback(function() use ($request) {
            $existing_settings = SettingsStore::get($this->get_default_settings());
            $sanitized_settings = $this->sanitize_settings($request->get_params());
            $settings = array_merge($existing_settings, $sanitized_settings);

            if (
                (empty($settings['post_types']) || !is_array($settings['post_types']))
                && !empty($settings['link_post_types'])
                && is_array($settings['link_post_types'])
            ) {
                $settings['post_types'] = $settings['link_post_types'];
            }

            if (
                (empty($settings['link_post_types']) || !is_array($settings['link_post_types']))
                && !empty($settings['post_types'])
                && is_array($settings['post_types'])
            ) {
                $settings['link_post_types'] = $settings['post_types'];
            }

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

            $click_tracking_enabled = isset($settings['enable_click_tracking'])
                ? (bool) $settings['enable_click_tracking']
                : (bool) ($settings['track_clicks'] ?? true);
            $settings['track_clicks'] = $click_tracking_enabled;
            $settings['enable_click_tracking'] = $click_tracking_enabled;
            $settings['open_internal_new_tab'] = isset($settings['open_internal_new_tab'])
                ? (bool) $settings['open_internal_new_tab']
                : (bool) ($settings['open_in_new_tab'] ?? false);
            $settings['open_in_new_tab'] = $settings['open_internal_new_tab'];
            
            SettingsStore::update($settings);
            update_option('prorank_seo_enable_click_tracking', $click_tracking_enabled ? 1 : 0);
            update_option('prorank_link_tracking_enabled', $click_tracking_enabled ? 1 : 0);

            if (empty($settings['generate_reports'])) {
                delete_option(LINKING_SCAN_HISTORY_OPTION);
                delete_option(LINKING_FULL_REPORT_OPTION);
            }
            
            return $this->get_success_response($settings);
        });
    }
    
    /**
     * Check permission to manage options
     *
     * @return bool
     */
    public function check_permission_manage_options() {
        $request_method = function_exists( 'prorank_get_server_var' )
            ? strtoupper( prorank_get_server_var( 'REQUEST_METHOD' ) )
            : '';

        if ($request_method === 'OPTIONS') {
            return true;
        }

        if (!current_user_can('manage_options')) {
            return false;
        }

        // Basic internal-linking settings are available in free.
        // Keep advanced analysis endpoints tier-gated via check_permission_edit_posts().
        return true;
    }

    /**
     * Check permission to edit posts.
     *
     * @return bool|WP_Error
     */
    public function check_permission_edit_posts() {
        $request_method = function_exists( 'prorank_get_server_var' )
            ? strtoupper( prorank_get_server_var( 'REQUEST_METHOD' ) )
            : '';

        if ($request_method === 'OPTIONS') {
            return true;
        }

        if (!current_user_can('edit_posts')) {
            return false;
        }

        return true;
    }

    /**
     * Check permission for endpoints that should not hard-fail for free installs.
     *
     * This is used by the editor sidebar so it can show a locked message instead of
     * triggering noisy 404/403 errors in the console.
     *
     * @return bool
     */
    public function check_permission_edit_posts_basic(): bool {
        $request_method = function_exists( 'prorank_get_server_var' )
            ? strtoupper( prorank_get_server_var( 'REQUEST_METHOD' ) )
            : '';

        if ($request_method === 'OPTIONS') {
            return true;
        }

        return current_user_can('edit_posts');
    }
    
    /**
     * Get default settings
     *
     * @return array
     */
    private function get_default_settings() {
        return [
            'enabled' => true,
            'auto_linking' => false,
            'enable_link_analysis' => true,
            'track_ctr' => true,
            'generate_reports' => true,
            'detect_broken_links' => true,
            'check_frequency' => 'weekly',
            'email_broken_links' => false,
            'enable_link_map' => true,
            'show_orphan_pages' => true,
            'show_link_strength' => true,
            'max_suggestions' => 10,
            'max_links_per_post' => 5,
            'min_word_count' => 80,
            'link_post_types' => ['post', 'page'],
            'exclude_categories' => [],
            'ignore_words' => '',
            'open_internal_new_tab' => false,
            'open_external_new_tab' => true,
            'open_in_new_tab' => false, // Keep for backward compatibility
            'add_nofollow' => false,
            'add_title_attribute' => false,
            'track_clicks' => false,
            'enable_click_tracking' => false,
            'suggestion_algorithm' => 'balanced',
            'anchor_text_length' => 4,
            'skip_sentences_with_links' => true,
            'randomize_suggestions' => false,
            'post_types' => ['post', 'page'],
            'post_statuses' => 'publish',
            'skip_sentences' => 3,
            'max_post_age' => 'no_limit',
            'max_outbound_links' => 0,
            'max_inbound_links' => 0,
            'keyword_sources' => ['title', 'slug', 'focus', 'categories', 'tags', 'custom'],
            'ignore_numbers' => true,
            'language' => 'en',
            'ignored_words' => $this->get_default_ignored_words(),
        ];
    }
    
    /**
     * Get default ignored words based on language
     */
    private function get_default_ignored_words() {
        return "a\nabout\nabove\nafter\nagain\nall\nam\nan\nand\nany\nare\nas\nat\nbe\nbeen\nbefore\nbeing\nbelow\nbetween\nboth\nbut\nby\ncan\ncould\ndid\ndo\ndoes\ndoing\ndown\nduring\neach\nfew\nfor\nfrom\nfurther\nhad\nhas\nhave\nhaving\nhe\nher\nhere\nhers\nherself\nhim\nhimself\nhis\nhow\nif\nin\ninto\nis\nit\nits\nitself\njust\nme\nmore\nmost\nmy\nmyself\nno\nnor\nnot\nof\noff\non\nonce\nonly\nor\nother\nour\nours\nourselves\nout\nover\nown\nsame\nshe\nshould\nso\nsome\nsuch\nthan\nthat\nthe\ntheir\nthem\nthemselves\nthen\nthere\nthese\nthey\nthis\nthose\nthrough\nto\ntoo\nunder\nuntil\nup\nvery\nwas\nwe\nwere\nwhat\nwhen\nwhere\nwhich\nwhile\nwho\nwhom\nwhy\nwill\nwith\nwould\nyou\nyour\nyours\nyourself\nyourselves";
    }
    
    /**
     * Sanitize settings
     *
     * @param array $settings Raw settings.
     * @return array Sanitized settings.
     */
    private function sanitize_settings($settings) {
        $defaults = $this->get_default_settings();
        $sanitized = [];
        
        // Boolean fields
        $bool_fields = [
            'enabled', 'auto_linking', 'open_in_new_tab', 'open_internal_new_tab', 'open_external_new_tab',
            'add_nofollow', 'add_title_attribute', 'track_clicks', 'skip_sentences_with_links', 'randomize_suggestions',
            'ignore_numbers', 'enable_link_map', 'show_orphan_pages', 'show_link_strength',
            'enable_link_analysis', 'track_ctr', 'generate_reports', 'detect_broken_links',
            'email_broken_links', 'enable_click_tracking'
        ];
        
        foreach ($bool_fields as $field) {
            $sanitized[$field] = isset($settings[$field]) ? (bool) $settings[$field] : $defaults[$field];
        }

        // Integer fields
        $int_fields = [
            'max_suggestions', 'max_links_per_post', 
            'min_word_count', 'anchor_text_length',
            'skip_sentences', 'max_outbound_links', 'max_inbound_links'
        ];
        
        foreach ($int_fields as $field) {
            $sanitized[$field] = isset($settings[$field]) ? absint($settings[$field]) : $defaults[$field];
        }
        
        // String fields
        $sanitized['ignore_words'] = isset($settings['ignore_words']) ? 
            sanitize_text_field($settings['ignore_words']) : $defaults['ignore_words'];
        
        $sanitized['ignored_words'] = isset($settings['ignored_words']) ? 
            sanitize_textarea_field($settings['ignored_words']) : $defaults['ignored_words'];
            
        $sanitized['post_statuses'] = isset($settings['post_statuses']) ? 
            sanitize_text_field($settings['post_statuses']) : $defaults['post_statuses'];
            
        $sanitized['max_post_age'] = isset($settings['max_post_age']) ? 
            sanitize_text_field($settings['max_post_age']) : $defaults['max_post_age'];

        $allowed_check_frequencies = ['daily', 'weekly', 'monthly'];
        $sanitized['check_frequency'] = isset($settings['check_frequency']) && in_array($settings['check_frequency'], $allowed_check_frequencies, true)
            ? $settings['check_frequency']
            : $defaults['check_frequency'];
            
        $sanitized['language'] = isset($settings['language']) && 
            in_array($settings['language'], self::SUPPORTED_LANGUAGE_CODES, true) ?
            $settings['language'] : $defaults['language'];
        
        $sanitized['suggestion_algorithm'] = isset($settings['suggestion_algorithm']) && 
            in_array($settings['suggestion_algorithm'], ['keyword', 'semantic', 'balanced']) ?
            $settings['suggestion_algorithm'] : $defaults['suggestion_algorithm'];
        
        // Array fields
        $sanitized['link_post_types'] = isset($settings['link_post_types']) && is_array($settings['link_post_types']) ?
            array_map('sanitize_text_field', $settings['link_post_types']) : $defaults['link_post_types'];
            
        $sanitized['post_types'] = isset($settings['post_types']) && is_array($settings['post_types']) ?
            array_map('sanitize_text_field', $settings['post_types']) : $defaults['post_types'];
            
        $sanitized['keyword_sources'] = isset($settings['keyword_sources']) && is_array($settings['keyword_sources']) ?
            array_map('sanitize_text_field', $settings['keyword_sources']) : $defaults['keyword_sources'];
        
        $sanitized['exclude_categories'] = isset($settings['exclude_categories']) && is_array($settings['exclude_categories']) ?
            array_map('sanitize_text_field', $settings['exclude_categories']) : $defaults['exclude_categories'];

        $normalized_post_types = [];
        if (!empty($sanitized['post_types']) && is_array($sanitized['post_types'])) {
            $normalized_post_types = $sanitized['post_types'];
        } elseif (!empty($sanitized['link_post_types']) && is_array($sanitized['link_post_types'])) {
            $normalized_post_types = $sanitized['link_post_types'];
        }

        $normalized_post_types = array_values(array_unique(array_filter(array_map('sanitize_text_field', $normalized_post_types))));
        if (empty($normalized_post_types)) {
            $normalized_post_types = ['post', 'page'];
        }

        $sanitized['post_types'] = $normalized_post_types;
        $sanitized['link_post_types'] = $normalized_post_types;

        // Keep click-tracking aliases synchronized for compatibility with tracker/runtime.
        $click_tracking_enabled = isset($settings['enable_click_tracking'])
            ? (bool) $settings['enable_click_tracking']
            : (isset($settings['track_clicks']) ? (bool) $settings['track_clicks'] : (bool) $defaults['track_clicks']);
        $sanitized['track_clicks'] = $click_tracking_enabled;
        $sanitized['enable_click_tracking'] = $click_tracking_enabled;

        if (empty($sanitized['enable_link_analysis'])) {
            $sanitized['track_ctr'] = false;
            $sanitized['generate_reports'] = false;
        }

        if (empty($sanitized['detect_broken_links'])) {
            $sanitized['email_broken_links'] = false;
        }

        return $this->normalize_free_only_settings($sanitized);
    }
    
    /**
     * Get settings schema for validation
     *
     * @return array
     */
    private function get_settings_schema() {
        return [
            'enabled' => [
                'type' => 'boolean',
                'default' => true,
            ],
            'auto_linking' => [
                'type' => 'boolean',
                'default' => false,
            ],
            'enable_link_map' => [
                'type' => 'boolean',
                'default' => true,
            ],
            'show_orphan_pages' => [
                'type' => 'boolean',
                'default' => true,
            ],
            'show_link_strength' => [
                'type' => 'boolean',
                'default' => true,
            ],
            'max_suggestions' => [
                'type' => 'integer',
                'minimum' => 1,
                'maximum' => 50,
                'default' => 10,
            ],
            'max_links_per_post' => [
                'type' => 'integer',
                'minimum' => 1,
                'maximum' => 20,
                'default' => 5,
            ],
            'min_word_count' => [
                'type' => 'integer',
                'minimum' => 30,
                'maximum' => 500,
                'default' => 80,
            ],
            'anchor_text_length' => [
                'type' => 'integer',
                'minimum' => 1,
                'maximum' => 10,
                'default' => 4,
            ],
            'suggestion_algorithm' => [
                'type' => 'string',
                'enum' => ['keyword', 'semantic', 'balanced'],
                'default' => 'balanced',
            ],
            'ignore_words' => [
                'type' => 'string',
                'default' => '',
            ],
        ];
    }

    /**
     * Force free-only settings so old premium options cannot leak into runtime.
     */
    private function normalize_free_only_settings(array $settings): array {
        $settings['ai_suggestions'] = false;
        $settings['ai_anchor_generation'] = false;
        $settings['gsc_integration'] = false;

        $keyword_sources = $settings['keyword_sources'] ?? self::SUPPORTED_KEYWORD_SOURCES;
        if (!is_array($keyword_sources)) {
            $keyword_sources = (array) $keyword_sources;
        }

        $keyword_sources = array_values(array_intersect(
            array_map('sanitize_text_field', $keyword_sources),
            self::SUPPORTED_KEYWORD_SOURCES
        ));

        if (empty($keyword_sources)) {
            $keyword_sources = ['title', 'slug', 'focus'];
        }

        $settings['keyword_sources'] = $keyword_sources;

        return $settings;
    }
    
    /**
     * Analyze content for internal linking opportunities
     *
     * @param WP_REST_Request $request Request object.
     * @return WP_REST_Response
     */
    public function analyze_content(WP_REST_Request $request) {
        return $this->try_callback(function() use ($request) {
            $post_id = $request->get_param('post_id');
            $content = $request->get_param('content');
            $title = $request->get_param('title');
            $settings = SettingsStore::get($this->get_default_settings());
            $target_posts = $this->get_target_posts((int) $post_id, $settings);
            $content_word_count = str_word_count(wp_strip_all_tags((string) $content));
            
            if (empty($content)) {
                return $this->get_success_response([
                    'suggestions' => [],
                    'stats' => [
                        'internal' => 0,
                        'external' => 0,
                        'density' => 0,
                    ],
                    'message' => __('No content to analyze for suggestions.', 'prorank-seo'),
                    'diagnostics' => [
                        'target_posts_available' => count($target_posts),
                        'content_word_count' => 0,
                    ],
                ]);
            }
            
            // Get suggestions using enhanced algorithm
            $suggestions = $this->get_smart_suggestions($post_id, $content, $title, $settings, $target_posts);
            
            // Calculate link statistics
            $stats = $this->calculate_link_stats($content);

            $response_data = [
                'suggestions' => $suggestions,
                'stats' => $stats,
                'diagnostics' => [
                    'target_posts_available' => count($target_posts),
                    'content_word_count' => $content_word_count,
                ],
            ];

            if (empty($suggestions)) {
                $response_data['message'] = $this->build_no_suggestions_message((string) $content, $target_posts);
            }
            
            return $this->get_success_response($response_data);
        });
    }
    
    /**
     * Check if a post is orphaned (no internal links pointing to it)
     *
     * @param WP_REST_Request $request Request object.
     * @return WP_REST_Response
     */
    public function check_orphan_status(WP_REST_Request $request) {
        return $this->try_callback(function() use ($request) {
            $post_id = (int) $request->get_param('post_id');
            if ($post_id <= 0 || !get_post($post_id)) {
                return $this->get_error_response(
                    __('Post not found.', 'prorank-seo'),
                    404,
                    'post_not_found'
                );
            }

            $inbound_count = $this->get_live_inbound_count($post_id);
            
            return $this->get_success_response([
                'is_orphan' => $inbound_count === 0,
                'inbound_count' => $inbound_count,
                'post_id' => $post_id,
            ]);
        });
    }

    /**
     * Count inbound internal links using the same builder-aware URL extraction as the main workflow.
     */
    private function get_live_inbound_count(int $post_id): int {
        $runtime_settings = function_exists(__NAMESPACE__ . '\get_internal_linking_runtime_settings')
            ? get_internal_linking_runtime_settings()
            : SettingsStore::get($this->get_default_settings());

        $post_types = function_exists(__NAMESPACE__ . '\get_internal_linking_scoped_post_types')
            ? get_internal_linking_scoped_post_types($runtime_settings)
            : ($runtime_settings['link_post_types'] ?? $runtime_settings['post_types'] ?? ['post', 'page']);

        $post_statuses = function_exists(__NAMESPACE__ . '\get_internal_linking_scoped_post_statuses')
            ? get_internal_linking_scoped_post_statuses($runtime_settings)
            : ['publish'];

        $max_age_days = function_exists(__NAMESPACE__ . '\get_internal_linking_max_age_days')
            ? get_internal_linking_max_age_days($runtime_settings)
            : null;

        $query_args = [
            'post_type'              => is_array($post_types) ? array_values($post_types) : ['post', 'page'],
            'post_status'            => is_array($post_statuses) ? array_values($post_statuses) : ['publish'],
            'post__not_in'           => [$post_id],
            'fields'                 => 'ids',
            'numberposts'            => -1,
            'no_found_rows'          => true,
            'update_post_meta_cache' => false,
            'update_post_term_cache' => false,
            'suppress_filters'       => false,
        ];

        if ($max_age_days !== null) {
            $query_args['date_query'] = [
                [
                    'after'     => sprintf('%d days ago', max(1, $max_age_days)),
                    'inclusive' => true,
                ],
            ];
        }

        $source_ids = get_posts($query_args);
        if (empty($source_ids)) {
            return 0;
        }

        $inbound_source_ids = [];

        foreach ($source_ids as $source_id) {
            $source_urls = function_exists(__NAMESPACE__ . '\get_post_linking_urls')
                ? get_post_linking_urls((int) $source_id, (string) get_post_field('post_content', (int) $source_id))
                : [];

            foreach ($source_urls as $source_url) {
                $resolved_target_id = function_exists(__NAMESPACE__ . '\resolve_internal_post_id')
                    ? resolve_internal_post_id((string) $source_url)
                    : 0;

                if ((int) $resolved_target_id === $post_id) {
                    $inbound_source_ids[(int) $source_id] = true;
                    break;
                }
            }
        }

        return count($inbound_source_ids);
    }
    
    /**
     * Track when a suggestion is applied
     *
     * @param WP_REST_Request $request Request object.
     * @return WP_REST_Response
     */
    public function track_application(WP_REST_Request $request) {
        return $this->try_callback(function() use ($request) {
            $suggestion_id = $request->get_param('suggestion_id');
            $post_id = $request->get_param('post_id');
            
            // Track in database for analytics
            global $wpdb;
            $table = $wpdb->prefix . 'prorank_link_suggestions';
            
            // Check if table exists first
            if ($wpdb->get_var("SHOW TABLES LIKE '$table'") === $table) {
                $wpdb->update(
                    $table,
                    ['status' => 'applied', 'applied_date' => current_time('mysql')],
                    ['id' => $suggestion_id]
                );
            }
            
            // Update post meta for tracking
            $applied_count = get_post_meta($post_id, '_prorank_links_applied', true);
            update_post_meta($post_id, '_prorank_links_applied', intval($applied_count) + 1);
            
            return $this->get_success_response([
                'tracked' => true,
            ]);
        });
    }
    
    /**
     * Get smart suggestions using enhanced algorithm
     *
     * @param int $post_id Post ID.
     * @param string $content Post content.
     * @param string $title Post title.
     * @return array
     */
    private function get_smart_suggestions($post_id, $content, $title, ?array $settings = null, ?array $target_posts = null) {
        $suggestions = [];
        $settings = $settings ?: SettingsStore::get($this->get_default_settings());

        if (isset($settings['enabled']) && !$settings['enabled']) {
            return [];
        }

        $max_suggestions = max(1, (int) ($settings['max_suggestions'] ?? 10));
        // Quick editor scans currently extract 1-2 word candidate anchors.
        // Cap the minimum here so the default site-wide setting cannot suppress all results.
        $min_anchor_words = min(2, max(1, (int) ($settings['anchor_text_length'] ?? 1)));
        $randomize_suggestions = !empty($settings['randomize_suggestions']);
        
        // Clean content
        $clean_content = wp_strip_all_tags($content);
        $sentences = preg_split('/[.!?]+/', $clean_content);
        
        // Get potential target posts
        $target_posts = is_array($target_posts) ? $target_posts : $this->get_target_posts((int) $post_id, $settings);
        
        foreach ($target_posts as $target_post) {
            // Look for matching phrases
            $target_title_words = $this->extract_keywords($target_post->post_title);
            
            foreach ($sentences as $sentence) {
                $sentence = trim($sentence);
                if (strlen($sentence) < 20) continue;
                
                foreach ($target_title_words as $keyword) {
                    if (strlen($keyword) < 3) continue;
                    
                    if (stripos($sentence, $keyword) !== false) {
                        // Calculate relevance score
                        $relevance = $this->calculate_relevance($keyword, $sentence, $target_post);
                        
                        if ($relevance > 30) {
                            $anchor_text = trim((string) $keyword);
                            if (!InternalLinking::is_anchor_valid_for_content($anchor_text, (string) $content)) {
                                continue;
                            }
                            if (str_word_count($anchor_text) < $min_anchor_words) {
                                continue;
                            }

                            $suggestion_id = 'sug_' . $target_post->ID . '_' . md5($anchor_text);
                            $candidate = [
                                'id' => $suggestion_id,
                                'target_id' => $target_post->ID,
                                'target_title' => $target_post->post_title,
                                'target_url' => get_permalink($target_post->ID),
                                'anchor_text' => $anchor_text,
                                'context' => $this->get_context($sentence, $anchor_text),
                                'relevance' => $relevance,
                            ];

                            if (
                                !isset($suggestions[$suggestion_id]) ||
                                (int) ($candidate['relevance'] ?? 0) > (int) ($suggestions[$suggestion_id]['relevance'] ?? 0)
                            ) {
                                $suggestions[$suggestion_id] = $candidate;
                            }
                        }

                        // Limit suggestions
                        if (count($suggestions) >= $max_suggestions) {
                            break 3;
                        }
                    }
                }
            }
        }
        
        $suggestions = array_values($suggestions);

        if ($randomize_suggestions && count($suggestions) > 1) {
            shuffle($suggestions);
        } else {
            // Sort by relevance
            usort($suggestions, function($a, $b) {
                return $b['relevance'] - $a['relevance'];
            });
        }
        
        return array_slice($suggestions, 0, $max_suggestions);
    }

    /**
     * Get candidate target posts that can be suggested for internal linking.
     */
    private function get_target_posts(int $post_id, array $settings): array {
        $post_types = $settings['link_post_types'] ?? ($settings['post_types'] ?? ['post', 'page']);

        return get_posts([
            'post_type' => $post_types,
            'post_status' => 'publish',
            'numberposts' => 50,
            'exclude' => [$post_id],
            'orderby' => 'modified',
            'order' => 'DESC',
        ]);
    }

    /**
     * Build a clearer message when no quick suggestions were found.
     */
    private function build_no_suggestions_message(string $content, array $target_posts): string {
        if (empty($target_posts)) {
            return __('Publish at least one other post or page to generate internal link suggestions.', 'prorank-seo');
        }

        $plain_content = trim(preg_replace('/\s+/', ' ', wp_strip_all_tags($content)));
        if (str_word_count($plain_content) < 20) {
            return __('Add a bit more descriptive content about the topic you want to link, then scan again.', 'prorank-seo');
        }

        $example_title = get_the_title($target_posts[0]);
        if (is_string($example_title) && $example_title !== '') {
            return sprintf(
                /* translators: %s: example internal target title. */
                __('No matching internal link opportunities were found in this draft yet. Mention one of your published topics, such as %s, and scan again.', 'prorank-seo'),
                '"' . $example_title . '"'
            );
        }

        return __('No matching internal link opportunities were found in this draft yet. Try mentioning one of your other published topics and scan again.', 'prorank-seo');
    }
    
    /**
     * Extract keywords from text
     *
     * @param string $text Text to extract from.
     * @return array
     */
    private function extract_keywords($text) {
        $stop_words = ['the', 'is', 'at', 'which', 'on', 'a', 'an', 'as', 'are', 'was', 'were', 'been', 'be'];
        $words = preg_split('/\s+/', strtolower($text));
        $keywords = [];
        
        foreach ($words as $word) {
            $word = trim($word, '.,!?;:"\'-');
            if (strlen($word) >= 3 && !in_array($word, $stop_words)) {
                $keywords[] = $word;
            }
        }
        
        // Also add multi-word phrases (2-3 words)
        for ($i = 0; $i < count($words) - 1; $i++) {
            $phrase = $words[$i] . ' ' . $words[$i + 1];
            if (!in_array($words[$i], $stop_words) && !in_array($words[$i + 1], $stop_words)) {
                $keywords[] = $phrase;
            }
        }
        
        return array_unique($keywords);
    }
    
    /**
     * Calculate relevance score
     *
     * @param string $keyword Keyword.
     * @param string $sentence Sentence.
     * @param WP_Post $target_post Target post.
     * @return int
     */
    private function calculate_relevance($keyword, $sentence, $target_post) {
        $score = 50; // Base score
        
        // Exact match bonus
        if (stripos($sentence, $keyword) !== false) {
            $score += 20;
        }
        
        // Position in sentence bonus (earlier is better)
        $position = stripos($sentence, $keyword);
        if ($position !== false && $position < 50) {
            $score += 10;
        }
        
        // Recent post bonus
        $days_old = (time() - strtotime($target_post->post_modified)) / (60 * 60 * 24);
        if ($days_old < 30) {
            $score += 15;
        } elseif ($days_old < 90) {
            $score += 10;
        }
        
        // Popular post bonus (based on comment count as proxy)
        if ($target_post->comment_count > 10) {
            $score += 10;
        }
        
        return min(100, $score);
    }
    
    /**
     * Get context around keyword
     *
     * @param string $sentence Sentence.
     * @param string $keyword Keyword.
     * @return string
     */
    private function get_context($sentence, $keyword) {
        $position = stripos($sentence, $keyword);
        if ($position === false) {
            return substr($sentence, 0, 100);
        }
        
        $start = max(0, $position - 30);
        $length = 100;
        
        return substr($sentence, $start, $length);
    }
    
    /**
     * Calculate link statistics
     *
     * @param string $content Content to analyze.
     * @return array
     */
    private function calculate_link_stats($content) {
        $internal_count = 0;
        $external_count = 0;
        
        // Count existing links
        if (preg_match_all('/<a[^>]+href=["\']([^"\']+)["\'][^>]*>/i', $content, $matches)) {
            $site_url = get_site_url();
            
            foreach ($matches[1] as $url) {
                if (strpos($url, $site_url) !== false || strpos($url, '/') === 0) {
                    $internal_count++;
                } elseif (strpos($url, 'http') === 0) {
                    $external_count++;
                }
            }
        }
        
        // Calculate density
        $word_count = str_word_count(wp_strip_all_tags($content));
        $total_links = $internal_count + $external_count;
        $density = $word_count > 0 ? round(($total_links / $word_count) * 100, 2) : 0;
        
        return [
            'internal' => $internal_count,
            'external' => $external_count,
            'density' => $density,
            'total' => $total_links,
            'word_count' => $word_count,
        ];
    }
}
