<?php

/**
 * Auto-linking Rule Engine
 *
 * Processes auto-linking rules with pattern matching support
 *
 * @package ProRank\SEO\Modules\Content\InternalLinking\AutoLink
 * @since   1.0.0
 */

declare(strict_types=1);

namespace ProRank\SEO\Modules\Content\InternalLinking\AutoLink;

use ProRank\SEO\Modules\Content\InternalLinking\Parser\ContentParser;
use ProRank\SEO\Modules\Content\InternalLinking\Parser\ContextAnalyzer;

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


/**
 * RuleEngine class
 */


class RuleEngine {
    
    /**
     * Content parser
     *
     * @var ContentParser
     */
    private ContentParser $parser;
    
    /**
     * Context analyzer
     *
     * @var ContextAnalyzer
     */
    private ContextAnalyzer $context_analyzer;
    
    /**
     * Active rules cache
     *
     * @var array|null
     */
    private ?array $rules_cache = null;
    
    /**
     * Constructor
     */
    public function __construct() {
        $this->parser = new ContentParser();
        $this->context_analyzer = new ContextAnalyzer();
    }
    
    /**
     * Process content with auto-linking rules
     *
     * @param string $content    Content to process
     * @param int    $post_id    Post ID
     * @param array  $options    Processing options
     * @return string Processed content with links
     */
    public function process_content(string $content, int $post_id, array $options = []): string {
        // Get active rules
        $rules = $this->get_active_rules();
        if (empty($rules)) {
            return $content;
        }
        
        // Check exclusions
        if ($this->is_post_excluded($post_id)) {
            return $content;
        }
        
        // Get settings
        $max_links = $options['max_links'] ?? get_option('prorank_autolink_max_per_post', 5);
        $links_added = 0;
        $used_keywords = [];
        
        // Parse content
        $sentences = $this->parser->parse_sentences($content);
        if (empty($sentences)) {
            return $content;
        }
        
        // Process each rule
        foreach ($rules as $rule) {
            if ($links_added >= $max_links) {
                break;
            }
            
            // Check if rule already used
            if ($rule['limit_once'] && in_array($rule['id'], $used_keywords)) {
                continue;
            }
            
            // Find matches based on pattern type
            $matches = $this->find_pattern_matches($content, $rule, $sentences);
            
            foreach ($matches as $match) {
                if ($links_added >= $max_links) {
                    break;
                }
                
                // Analyze context
                $context_analysis = $this->context_analyzer->analyze_link_context(
                    $content,
                    $match['text'],
                    $rule['url']
                );
                
                if (!$context_analysis['suitable']) {
                    continue;
                }
                
                // Apply link
                $content = $this->apply_link($content, $match, $rule);
                $links_added++;
                $used_keywords[] = $rule['id'];
                
                if ($rule['limit_once']) {
                    break;
                }
            }
        }
        
        return $content;
    }
    
    /**
     * Get active auto-linking rules
     *
     * @return array Active rules
     */
    public function get_active_rules(): array {
        if ($this->rules_cache !== null) {
            return $this->rules_cache;
        }
        
        global $wpdb;
        $table = $wpdb->prefix . 'prorank_autolink_keywords';
        
        $rules = $wpdb->get_results(
            "SELECT * FROM $table 
            ORDER BY priority DESC, id ASC",
            ARRAY_A
        );
        
        if (empty($rules)) {
            $this->rules_cache = [];
            return [];
        }
        
        // Process rules
        $processed_rules = [];
        foreach ($rules as $rule) {
            $processed_rules[] = [
                'id' => (int) $rule['id'],
                'keyword' => $rule['keyword'],
                'url' => $rule['url'],
                'pattern_type' => $rule['pattern_type'] ?? 'exact',
                'variations' => !empty($rule['variations']) 
                    ? json_decode($rule['variations'], true) 
                    : [],
                'placement_preference' => $rule['placement_preference'] ?? 'any',
                'match_whole_word' => (bool) ($rule['match_whole_word'] ?? true),
                'context_words' => (int) ($rule['context_words'] ?? 5),
                'max_links' => (int) ($rule['max_links'] ?? 1),
                'case_sensitive' => (bool) ($rule['case_sensitive'] ?? false),
                'priority' => (int) ($rule['priority'] ?? 50),
                'exclude_posts' => !empty($rule['exclude_posts']) 
                    ? json_decode($rule['exclude_posts'], true) 
                    : [],
                'limit_once' => $rule['max_links'] == 1,
            ];
        }
        
        $this->rules_cache = $processed_rules;
        return $processed_rules;
    }
    
    /**
     * Find pattern matches in content
     *
     * @param string $content   Content to search
     * @param array  $rule      Rule configuration
     * @param array  $sentences Parsed sentences
     * @return array Matches found
     */
    private function find_pattern_matches(string $content, array $rule, array $sentences): array {
        $matches = [];
        
        switch ($rule['pattern_type']) {
            case 'exact':
                $matches = $this->find_exact_matches($content, $rule, $sentences);
                break;
                
            case 'regex':
                $matches = $this->find_regex_matches($content, $rule, $sentences);
                break;
                
            case 'phrase':
                $matches = $this->find_phrase_matches($content, $rule, $sentences);
                break;
                
            case 'synonym':
                $matches = $this->find_synonym_matches($content, $rule, $sentences);
                break;
        }
        
        // Apply placement preference
        if ($rule['placement_preference'] !== 'any' && !empty($matches)) {
            $matches = $this->filter_by_placement($matches, $rule['placement_preference']);
        }
        
        return $matches;
    }
    
    /**
     * Find exact keyword matches
     *
     * @param string $content   Content to search
     * @param array  $rule      Rule configuration
     * @param array  $sentences Parsed sentences
     * @return array Matches
     */
    private function find_exact_matches(string $content, array $rule, array $sentences): array {
        $matches = [];
        $keyword = $rule['keyword'];
        
        // Build pattern
        $flags = $rule['case_sensitive'] ? '' : 'i';
        if ($rule['match_whole_word']) {
            $pattern = '/\b' . preg_quote($keyword, '/') . '\b/' . $flags;
        } else {
            $pattern = '/' . preg_quote($keyword, '/') . '/' . $flags;
        }
        
        // Search in content
        if (preg_match_all($pattern, $content, $found, PREG_OFFSET_CAPTURE)) {
            foreach ($found[0] as $match) {
                $matches[] = [
                    'text' => $match[0],
                    'position' => $match[1],
                    'type' => 'exact',
                ];
            }
        }
        
        return $matches;
    }
    
    /**
     * Find regex pattern matches
     *
     * @param string $content   Content to search
     * @param array  $rule      Rule configuration
     * @param array  $sentences Parsed sentences
     * @return array Matches
     */
    private function find_regex_matches(string $content, array $rule, array $sentences): array {
        $matches = [];
        $pattern = $rule['keyword'];
        
        // Validate regex
        if (@preg_match($pattern, '') === false) {
            prorank_log('ProRank SEO: Invalid regex pattern - ' . $pattern);
            return $matches;
        }
        
        // Add case insensitive flag if needed
        if (!$rule['case_sensitive'] && !preg_match('/\/[a-z]*i[a-z]*$/', $pattern)) {
            $pattern = rtrim($pattern, '/') . 'i/';
        }
        
        // Search in content
        if (preg_match_all($pattern, $content, $found, PREG_OFFSET_CAPTURE)) {
            foreach ($found[0] as $match) {
                $matches[] = [
                    'text' => $match[0],
                    'position' => $match[1],
                    'type' => 'regex',
                ];
            }
        }
        
        return $matches;
    }
    
    /**
     * Find phrase matches with variations
     *
     * @param string $content   Content to search
     * @param array  $rule      Rule configuration
     * @param array  $sentences Parsed sentences
     * @return array Matches
     */
    private function find_phrase_matches(string $content, array $rule, array $sentences): array {
        $matches = [];
        $base_phrase = $rule['keyword'];
        $variations = $rule['variations'];
        
        // Add base phrase to variations
        array_unshift($variations, $base_phrase);
        
        foreach ($variations as $phrase) {
            // Allow flexible word spacing and minor variations
            $words = preg_split('/\s+/', preg_quote($phrase, '/'));
            $pattern = '/\b' . implode('\s+', $words) . '\b/i';
            
            if (preg_match_all($pattern, $content, $found, PREG_OFFSET_CAPTURE)) {
                foreach ($found[0] as $match) {
                    $matches[] = [
                        'text' => $match[0],
                        'position' => $match[1],
                        'type' => 'phrase',
                        'variation' => $phrase,
                    ];
                }
            }
        }
        
        return $matches;
    }
    
    /**
     * Find synonym matches
     *
     * @param string $content   Content to search
     * @param array  $rule      Rule configuration
     * @param array  $sentences Parsed sentences
     * @return array Matches
     */
    private function find_synonym_matches(string $content, array $rule, array $sentences): array {
        $matches = [];
        $synonyms = array_merge([$rule['keyword']], $rule['variations']);
        
        foreach ($synonyms as $synonym) {
            $flags = $rule['case_sensitive'] ? '' : 'i';
            $pattern = '/\b' . preg_quote($synonym, '/') . '\b/' . $flags;
            
            if (preg_match_all($pattern, $content, $found, PREG_OFFSET_CAPTURE)) {
                foreach ($found[0] as $match) {
                    $matches[] = [
                        'text' => $match[0],
                        'position' => $match[1],
                        'type' => 'synonym',
                        'synonym' => $synonym,
                    ];
                }
            }
        }
        
        return $matches;
    }
    
    /**
     * Filter matches by placement preference
     *
     * @param array  $matches   All matches
     * @param string $placement Placement preference
     * @return array Filtered matches
     */
    private function filter_by_placement(array $matches, string $placement): array {
        if (empty($matches)) {
            return $matches;
        }
        
        // Sort by position
        usort($matches, function($a, $b) {
            return $a['position'] <=> $b['position'];
        });
        
        switch ($placement) {
            case 'beginning':
                // Return first 25% of matches
                $count = max(1, (int) (count($matches) * 0.25));
                return array_slice($matches, 0, $count);
                
            case 'middle':
                // Return middle 50% of matches
                $start = (int) (count($matches) * 0.25);
                $count = max(1, (int) (count($matches) * 0.5));
                return array_slice($matches, $start, $count);
                
            case 'end':
                // Return last 25% of matches
                $count = max(1, (int) (count($matches) * 0.25));
                return array_slice($matches, -$count);
                
            default:
                return $matches;
        }
    }
    
    /**
     * Apply link to content
     *
     * @param string $content Content to modify
     * @param array  $match   Match data
     * @param array  $rule    Rule configuration
     * @return string Modified content
     */
    private function apply_link(string $content, array $match, array $rule): string {
        $anchor_text = $match['text'];
        $position = $match['position'];
        
        // Check if already linked
        if ($this->is_already_linked($content, $position, strlen($anchor_text))) {
            return $content;
        }
        
        // Build link HTML
        $link = sprintf(
            '<a href="%s" data-autolinked="true">%s</a>',
            esc_url($rule['url']),
            esc_html($anchor_text)
        );
        
        // Replace in content
        $before = substr($content, 0, $position);
        $after = substr($content, $position + strlen($anchor_text));
        
        return $before . $link . $after;
    }
    
    /**
     * Check if text at position is already linked
     *
     * @param string $content  Content to check
     * @param int    $position Position in content
     * @param int    $length   Length of text
     * @return bool
     */
    private function is_already_linked(string $content, int $position, int $length): bool {
        // Look for opening <a> tag before position
        $before = substr($content, 0, $position);
        $last_a_open = strrpos($before, '<a');
        $last_a_close = strrpos($before, '</a>');
        
        // If last opening tag is after last closing tag, we're inside a link
        if ($last_a_open !== false && $last_a_open > $last_a_close) {
            return true;
        }
        
        // Check if position is within an HTML tag
        $last_tag_open = strrpos($before, '<');
        $last_tag_close = strrpos($before, '>');
        
        if ($last_tag_open !== false && $last_tag_open > $last_tag_close) {
            return true;
        }
        
        return false;
    }
    
    /**
     * Check if post is excluded
     *
     * @param int $post_id Post ID
     * @return bool
     */
    private function is_post_excluded(int $post_id): bool {
        // Check global exclusions
        $global_exclusions = get_option('prorank_autolink_exclude_posts', []);
        if (in_array($post_id, $global_exclusions)) {
            return true;
        }
        
        // Check rule-specific exclusions
        $rules = $this->get_active_rules();
        foreach ($rules as $rule) {
            if (in_array($post_id, $rule['exclude_posts'])) {
                return true;
            }
        }
        
        return false;
    }
    
    /**
     * Clear rules cache
     *
     * @return void
     */
    public function clear_cache(): void {
        $this->rules_cache = null;
    }
}