<?php

// Silence may be golden, but sometimes words paint the canvas of understanding.
namespace Linksy\SearchAndReplace\Plugli\Linksy\Common\Helpers\Keywords;

/**
 * Hadndle post object.
 *
 * @since      1.0.1
 * @package    Linksy
 * @subpackage Linksy\admin
 * @author     Gbenga Medunoye <laxusgooee@gmail.com>
 */
use Linksy\SearchAndReplace\Plugli\Linksy\Common\Helpers\Keywords\Traits\TableFilters;
use Linksy\SearchAndReplace\Plugli\Linksy\Common\Helpers\Keywords\Traits\SEOPlugins;
use Linksy\SearchAndReplace\Plugli\Linksy\Common\Helpers\Settings;
use Linksy\SearchAndReplace\Plugli\Standard\Helpers\Database\Database;
use Linksy\SearchAndReplace\Plugli\Standard\Helpers\Str;
use Linksy\SearchAndReplace\Plugli\Linksy\Common\Helpers\Utils;
use Linksy\SearchAndReplace\Plugli\Standard\Helpers\Arr;
/**
 * Keywords class.
 */
class Keywords
{
    use TableFilters;
    use SEOPlugins;
    const SUPPORTED_PROVIDERS = ['rank_math' => ['name' => 'Rank Math', 'type' => 'postmeta', 'key' => 'rank_math_focus_keyword', 'plugin' => 'seo-by-rank-math/rank-math.php', 'extractor' => 'extract_rank_math_keywords'], 'yoast' => ['name' => 'Yoast SEO', 'type' => 'postmeta', 'key' => '_yoast_wpseo_focuskw', 'plugin' => 'wordpress-seo/wp-seo.php', 'extractor' => 'extract_yoast_keywords'], 'seopress' => ['name' => 'SEOPress', 'type' => 'postmeta', 'key' => '_seopress_analysis_target_kw', 'plugin' => 'wp-seopress/seopress.php', 'extractor' => 'extract_seopress_keywords'], 'boldgrid_easy' => ['name' => 'Boldgrid Easy SEO', 'type' => 'postmeta', 'key' => 'bgseo_custom_keyword', 'plugin' => 'boldgrid-easy-seo/boldgrid-easy-seo.php', 'extractor' => 'extract_boldgrid_easy_keywords'], 'squirrly' => ['name' => 'Squirrly SEO', 'type' => 'qss', 'key' => 'post,seo', 'plugin' => 'squirrly-seo/squirrly.php', 'extractor' => 'extract_squirrly_keywords'], 'aio' => ['name' => 'All in One SEO', 'type' => 'aioseo_posts', 'key' => 'post_id,keyphrases', 'plugin' => 'all-in-one-seo-pack/all_in_one_seo_pack.php', 'extractor' => 'extract_aio_keywords'], 'wpsso_core' => ['name' => 'WPSSO Core', 'type' => 'postmeta', 'key' => '_wpsso_meta', 'plugin' => 'wpsso/wpsso.php', 'extractor' => 'extract_wpsso_core_keywords'], 'smartcrawl' => ['name' => 'SmartCrawl', 'type' => 'postmeta', 'key' => '_wds_focus-keywords', 'plugin' => 'smartcrawl-seo/wpmu-dev-seo.php', 'extractor' => 'extract_smartcrawl_keywords'], 'seo_framework' => ['name' => 'SEO Framework', 'type' => 'postmeta', 'key' => '_tsfem-extension-post-meta', 'plugin' => 'autodescription/autodescription.php', 'extractor' => 'extract_seo_framework_keywords']];
    const SUPPORTED_GROUPBYS = ['post', 'provider'];
    private $opts = ['q' => '', 'is_active' => \true, 'post__in' => [], 'post__not_in' => [], 'new' => \false, 'groupby' => \false, 'limit' => \false];
    private $meta_keys = [];
    private $current_post = null;
    private $ai_generated_keywords = [];
    public function __construct($opts = [])
    {
        // Merge only supported options.
        $opts = array_intersect_key($opts, $this->opts);
        $this->opts = array_merge($this->opts, $opts);
    }
    private function is_provider_active($provider_plugin)
    {
        return Utils::is_plugin_active($provider_plugin);
    }
    private function get_provider($identity)
    {
        foreach (self::SUPPORTED_PROVIDERS as $provider => $opts) {
            if ($opts['type'] === $identity || in_array($identity, Arr::split($opts['key']))) {
                return $provider;
            }
        }
        return null;
    }
    public function get_active_chosen_provider()
    {
        $providers = [];
        $chosen_provider = Settings::get('seo_plugin', 'none');
        if ($chosen_provider === 'none') {
            return $providers;
        }
        foreach (self::SUPPORTED_PROVIDERS as $provider => $opts) {
            if ($provider === $chosen_provider && $this->opts['is_active'] && $this->is_provider_active($opts['plugin'])) {
                if ($opts['type'] === 'postmeta') {
                    $this->meta_keys = \array_merge($this->meta_keys, Arr::split($opts['key']));
                }
                $providers[$provider] = $opts;
            }
        }
        return $providers;
    }
    public function get_active_providers($only_names = \false)
    {
        $providers = [];
        foreach (self::SUPPORTED_PROVIDERS as $provider => $opts) {
            if ($this->opts['is_active'] && $this->is_provider_active($opts['plugin'])) {
                if ($opts['type'] === 'postmeta') {
                    $this->meta_keys = array_merge($this->meta_keys, Arr::split($opts['key']));
                }
                $providers[$provider] = $opts;
            }
        }
        if ($only_names) {
            return array_keys($providers);
        }
        return $providers;
    }
    private function get_tables()
    {
        $providers = $this->get_active_chosen_provider();
        $tables = [];
        // generate tablular-based queries
        foreach ($providers as $provider) {
            switch (strtolower($provider['type'])) {
                case 'qss':
                    $tables[] = $this->apply_squirrly_filters($provider['key'], $provider['type']);
                    break;
                case 'aioseo_posts':
                    $tables[] = $this->apply_aioseo_filters($provider['key'], $provider['type']);
                    break;
                default:
                    $tables[] = clone $this->apply_postmeta_filters($this->meta_keys);
                    break;
            }
        }
        // clear the meta_keys array
        $this->meta_keys = [];
        return $tables;
    }
    private function summarize_keyword($keyword)
    {
        $k = gettype($keyword) === 'string' ? ['keyword' => $keyword, 'score' => null] : $keyword;
        // check if keyword is already in linksy_keywords table
        $count = Database::table('linksy_keywords')->selectCount()->where('post_id', $this->current_post)->where('keyword', $k['keyword'])->getVar();
        return array_merge($k, ['post_id' => $this->current_post, 'is_old' => $count > 0, 'is_ai_generated' => in_array($k['keyword'], $this->ai_generated_keywords)]);
    }
    public function extract_keywords($provider, $record)
    {
        if (!isset(self::SUPPORTED_PROVIDERS[$provider])) {
            return [];
        }
        // Step 0: Extract keywords from the record based on the provider type
        $extractor = self::SUPPORTED_PROVIDERS[$provider]['extractor'];
        $result = $this->{$extractor}($record);
        if (empty($result)) {
            return [];
        }
        $this->current_post = $result[0];
        $keywords = $result[1];
        // Step 1: Retrieve existing keywords from the linksy_keywords table
        $linksy_keywords = Database::table('linksy_keywords')->select(['post_id', 'keyword', 'score', 'is_ai_generated'])->where('post_id', $this->current_post)->whereIn('keyword', $keywords)->where('provider', $provider)->get(\ARRAY_A);
        // Step 2: Create an array of existing keyword strings for comparison
        $existing_keyword_strings = array_column($linksy_keywords, 'keyword');
        // Step 3: Process existing keywords
        $existing_keywords = array_map(function ($k) use ($record) {
            return $this->summarize_keyword($k);
        }, $linksy_keywords);
        // Step 4: Identify new keywords that are not in the existing keywords
        $new_keywords = Arr::filter($keywords, function ($keyword) use ($existing_keyword_strings) {
            return !in_array($keyword, $existing_keyword_strings);
        });
        // Step 5: Process new keywords
        $processed_new_keywords = array_map(function ($k) use ($record) {
            return $this->summarize_keyword($k);
        }, $new_keywords);
        // Step 6: Merge existing and new keywords and return the result
        if ($this->opts['limit'] !== \false) {
            return array_slice(array_merge($existing_keywords, $processed_new_keywords), 0, $this->opts['limit']);
        }
        return array_merge($existing_keywords, $processed_new_keywords);
    }
    private function group_keywords($groupby, $groups, $keywords)
    {
        foreach ($keywords as $keyword) {
            $key = null;
            switch ($groupby) {
                case 'post':
                    $key = $keyword['post_id'];
                    break;
                case 'provider':
                    $key = $keyword['provider'];
                    break;
            }
            if (isset($groups[$key])) {
                $groups[$key][] = $keyword;
            } else {
                $groups[$key] = [$keyword];
            }
        }
        return $groups;
    }
    public function get()
    {
        global $wpdb;
        $results = [];
        $tracked_non_duplicates = [];
        $tables = $this->get_tables();
        foreach ($tables as $table) {
            // get the provider type from the table name
            $provider_type = substr($table->table, strlen($wpdb->prefix));
            // and then execute the query
            $records = $table->get();
            foreach ($records as $record) {
                $provider = $this->get_provider(isset($record->meta_key) ? $record->meta_key : $provider_type);
                if (is_null($provider)) {
                    continue;
                }
                $keywords = [];
                $extracted_keywords = $this->extract_keywords($provider, $record);
                foreach ($extracted_keywords as $k) {
                    if (isset($tracked_non_duplicates[$this->current_post])) {
                        if (in_array($k['keyword'], $tracked_non_duplicates[$this->current_post])) {
                            continue;
                        }
                        $tracked_non_duplicates[$this->current_post][] = $k['keyword'];
                    } else {
                        $tracked_non_duplicates[$this->current_post] = [$k['keyword']];
                    }
                    // check for new keyword if new is set
                    if ($this->opts['new'] && $k['is_old']) {
                        continue;
                    }
                    // if query is set and not seen in keyword, skip
                    if (!empty($this->opts['q']) && !Str::contains(trim($k['keyword']), $this->opts['q'])) {
                        continue;
                    }
                    $keywords[] = array_merge($k, ['provider' => $provider]);
                }
                if (count($keywords) === 0) {
                    continue;
                }
                $groupby = $this->opts['groupby'];
                if ($groupby && in_array($groupby, self::SUPPORTED_GROUPBYS)) {
                    $results = $this->group_keywords($groupby, $results, $keywords);
                    continue;
                }
                $results = array_merge($results, $keywords);
            }
        }
        return $results;
    }
    public static function get_ignored_tags_range($content)
    {
        $idx = 0;
        $content = strtolower($content);
        $ignored_tags = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'a', 'img'];
        $ignored_tags_range = [];
        foreach ($ignored_tags as $tag) {
            $idx = 0;
            $opening_tag = "<" . $tag;
            $closing_tag = in_array($tag, ['img']) ? '/>' : "</" . $tag . ">";
            while (strlen($content) > $idx) {
                $idx = strpos($content, $opening_tag, $idx);
                if ($idx === \false) {
                    break;
                }
                if (!in_array($content[$idx + strlen($opening_tag)], ['>', ' '])) {
                    break;
                }
                $closing_tag_idx = strpos($content, $closing_tag, $idx + strlen($opening_tag));
                if ($closing_tag_idx === \false) {
                    break;
                }
                $closing_tag_idx += strlen($closing_tag);
                if ($tag == 'a') {
                    $idx = $idx > 2 ? $idx - 2 : $idx;
                    $closing_tag_idx += 2;
                }
                $ignored_tags_range[] = [$idx, $closing_tag_idx, $tag];
                $idx = $closing_tag_idx;
            }
        }
        usort($ignored_tags_range, function ($a, $b) {
            return (int) ($a[0] > $b[0]);
        });
        return $ignored_tags_range;
    }
    private static function is_valid_portion($portion, $opts = [])
    {
        $opts = array_merge(['no_link' => 1, 'no_img' => 1, 'no_starting_block_tag' => 0, 'no_http_class' => 1], $opts);
        if ($opts['no_link']) {
            if (preg_match('/<a\b[^>]*?>/i', $portion) == 1) {
                return \false;
            }
        }
        if ($opts['no_img']) {
            if (preg_match('/<img\b[^>]*?>/i', $portion) == 1) {
                return \false;
            }
        }
        if ($opts['no_starting_block_tag']) {
            if (preg_match('/^\s*(<p\b[^>]*?>|[^<\s][^<]*?)/i', $portion) == 1) {
                return \false;
            }
        }
        if ($opts['no_http_class']) {
            if (preg_match('/class\s*=\s*"[^"]*(https?:)[^"]*"/i', $portion)) {
                return \false;
            }
        }
        return \true;
    }
    public static function get_keywords_range($keywords, $content, $opts = [])
    {
        $ranges = [];
        $cache = [];
        $original_content = $content;
        $content = strtolower($content);
        $ignoredTagsRange = self::get_ignored_tags_range($content);
        foreach ($keywords as $keyword) {
            $idx = $closing_idx = 0;
            $error = '';
            $keyword = strtolower($keyword);
            while (strlen($content) > $idx) {
                $idx = strpos($content, $keyword, $idx);
                if ($idx === \false) {
                    break;
                }
                $closing_idx = $idx + strlen($keyword) - 1;
                $isValidRange = \true;
                $phrase = '';
                // is whole word
                if (!preg_match("/\\b" . preg_quote($keyword, '/') . "\\b/i", substr($content, $idx > 0 ? $idx - 5 : $idx, strlen($keyword) + 10))) {
                    $error = 'is not a full word';
                    $isValidRange = \false;
                }
                // skips ignored tags
                if ($isValidRange) {
                    for ($i = 0; $i < count($ignoredTagsRange); $i++) {
                        $ignoredTagRange = $ignoredTagsRange[$i];
                        if ($idx >= $ignoredTagRange[0] && $idx <= $ignoredTagRange[1] || $closing_idx >= $ignoredTagRange[0] && $closing_idx <= $ignoredTagRange[1]) {
                            $isValidRange = \false;
                            $error = $ignoredTagRange[2];
                            break;
                        }
                    }
                }
                // skip used ranges and block
                if ($isValidRange) {
                    for ($i = 0; $i < count($ranges); $i++) {
                        $usedKeywordRange = $ranges[$i];
                        // is not immediately after
                        if ($usedKeywordRange['source_start_pos'] <= $idx && $usedKeywordRange['source_end_pos'] >= $closing_idx) {
                            if ($idx >= $usedKeywordRange['keyword_start_pos'] && $idx <= $usedKeywordRange['keyword_end_pos'] || $closing_idx >= $usedKeywordRange['keyword_start_pos'] && $closing_idx <= $usedKeywordRange['keyword_end_pos']) {
                                $isValidRange = \false;
                                $error = 'overlap with ' . substr($content, $usedKeywordRange['keyword_start_pos'], $usedKeywordRange['keyword_end_pos'] - $usedKeywordRange['keyword_start_pos']);
                                break;
                            }
                            if ($idx - 2 >= $usedKeywordRange['keyword_start_pos'] && $idx - 2 <= $usedKeywordRange['keyword_end_pos'] || $closing_idx + 2 >= $usedKeywordRange['keyword_start_pos'] && $closing_idx + 2 <= $usedKeywordRange['keyword_end_pos']) {
                                $isValidRange = \false;
                                $error = 'too close to ' . substr($content, $usedKeywordRange['keyword_start_pos'], $usedKeywordRange['keyword_end_pos'] - $usedKeywordRange['keyword_start_pos']);
                                break;
                            }
                        }
                    }
                }
                if ($isValidRange) {
                    $phrase = substr($original_content, $idx, strlen($keyword));
                    $origin = $idx;
                    while ($origin > 0) {
                        if (in_array($content[$origin - 1], ["\r", "\n"])) {
                            break;
                        }
                        $origin--;
                    }
                    $intercept = $closing_idx;
                    while ($intercept < strlen($content)) {
                        if (in_array($content[$intercept], ["\r", "\n"])) {
                            break;
                        }
                        $intercept++;
                    }
                    $portion = substr($original_content, $origin, $intercept - $origin);
                    if (self::is_valid_portion($portion)) {
                        if (isset($cache[$portion])) {
                            $portion_idx = $cache[$portion];
                            $ranges[$portion_idx]['more'][] = $keyword;
                        } else {
                            $cache[$portion] = count($ranges);
                            $ranges[] = ['keyword_start_pos' => $idx, 'highlight_start_pos' => $idx - $origin, 'keyword_end_pos' => $closing_idx, 'source_start_pos' => $origin, 'source_end_pos' => $intercept, 'phrase' => $phrase, 'source' => $portion, 'more' => []];
                        }
                        $closing_idx = $intercept;
                        if (!isset($opts['allow_multiple']) || !$opts['allow_multiple']) {
                            break;
                        }
                    }
                }
                $idx = $closing_idx + 1;
            }
        }
        return $ranges;
    }
}
