<?php
/**
 * Keyword Extractor
 *
 * Extracts relevant keywords from post content using frequency analysis.
 * No external dependencies — pure PHP implementation.
 *
 * @package SEOvaultSEO
 */

namespace SEOvaultSEO\Utils;

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

/**
 * Extracts focus keyword suggestions from post content.
 *
 * Uses a simple TF-based approach with positional boosting:
 * - Words in the title get a 3x boost
 * - Words in headings (H1-H3) get a 2x boost
 * - Words in the first paragraph get a 1.5x boost
 * - Stop words are removed
 * - Multi-word phrases (2-3 words) are also extracted
 */
class KeywordExtractor {

	/**
	 * English stop words list.
	 *
	 * These are common words that carry little SEO meaning.
	 * Kept as a private property to allow future filtering.
	 *
	 * @var array
	 */
	private static $stop_words = array(
		'a',
		'an',
		'the',
		'and',
		'or',
		'but',
		'in',
		'on',
		'at',
		'to',
		'for',
		'of',
		'with',
		'by',
		'from',
		'is',
		'are',
		'was',
		'were',
		'be',
		'been',
		'being',
		'have',
		'has',
		'had',
		'do',
		'does',
		'did',
		'will',
		'would',
		'could',
		'should',
		'may',
		'might',
		'must',
		'can',
		'this',
		'that',
		'these',
		'those',
		'i',
		'you',
		'he',
		'she',
		'it',
		'we',
		'they',
		'what',
		'which',
		'who',
		'when',
		'where',
		'why',
		'how',
		'all',
		'each',
		'every',
		'both',
		'few',
		'more',
		'most',
		'other',
		'some',
		'such',
		'no',
		'nor',
		'not',
		'only',
		'own',
		'same',
		'so',
		'than',
		'too',
		'very',
		'just',
		'also',
		'as',
		'if',
		'about',
		'into',
		'through',
		'during',
		'before',
		'after',
		'above',
		'below',
		'up',
		'down',
		'out',
		'off',
		'over',
		'under',
		'again',
		'further',
		'then',
		'once',
		'here',
		'there',
		'any',
		'both',
		'each',
		'what',
		'which',
		'their',
		'them',
		'his',
		'her',
		'its',
		'our',
		'your',
		'my',
		'me',
		'him',
		'us',
		'am',
		'get',
		'got',
		'one',
		'two',
		'three',
		'new',
		'like',
		'want',
		'need',
		'know',
		'think',
		'feel',
		'go',
		'going',
		'gone',
		'make',
		'made',
		'see',
		'seen',
		'come',
		'came',
		'take',
		'took',
		'give',
		'gave',
		'find',
		'found',
		'tell',
		'told',
		'ask',
		'asked',
		'work',
		'worked',
		'seem',
		'seemed',
		'felt',
		'try',
		'tried',
		'leave',
		'left',
		'call',
		'called',
		'want',
		'wants',
		'let',
		'lets',
		'put',
		'puts',
		'mean',
		'means',
		'keep',
		'keeps',
		'begin',
		'begins',
		'seem',
		'seems',
		'help',
		'helps',
		'show',
		'shows',
		'hear',
		'hears',
		'play',
		'plays',
		'run',
		'runs',
		'move',
		'moves',
		'live',
		'lives',
		'believe',
		'believes',
		'hold',
		'holds',
		'bring',
		'brings',
		'happen',
		'happens',
		'write',
		'writes',
		'provide',
		'provides',
		'sit',
		'sits',
		'stand',
		'stands',
		'lose',
		'loses',
		'pay',
		'pays',
		'meet',
		'meets',
		'include',
		'includes',
		'continue',
		'continues',
		'set',
		'sets',
		'learn',
		'learns',
		'change',
		'changes',
		'lead',
		'leads',
		'understand',
		'understands',
		'watch',
		'watches',
		'follow',
		'follows',
		'stop',
		'stops',
		'create',
		'creates',
		'speak',
		'speaks',
		'read',
		'reads',
		'allow',
		'allows',
		'add',
		'adds',
		'spend',
		'spends',
		'grow',
		'grows',
		'open',
		'opens',
		'walk',
		'walks',
		'win',
		'wins',
		'offer',
		'offers',
		'remember',
		'remembers',
		'love',
		'loves',
		'consider',
		'considers',
		'buy',
		'buys',
		'wait',
		'waits',
		'serve',
		'serves',
		'die',
		'dies',
		'send',
		'sends',
		'expect',
		'expects',
		'build',
		'builds',
		'stay',
		'stays',
		'fall',
		'falls',
		'cut',
		'cuts',
		'reach',
		'reaches',
		'remain',
		'remains',
		'use',
		'used',
		'using',
		'get',
		'getting',
		'way',
		'ways',
		'thing',
		'things',
		'people',
		'person',
		'place',
		'places',
		'case',
		'cases',
		'part',
		'parts',
		'week',
		'weeks',
		'year',
		'years',
		'man',
		'men',
		'woman',
		'women',
		'child',
		'children',
		'world',
		'worlds',
		'life',
		'lives',
		'home',
		'homes',
		'hand',
		'hands',
		'eye',
		'eyes',
		'day',
		'days',
		'night',
		'nights',
		'water',
		'waters',
		'problem',
		'problems',
		'fact',
		'facts',
		'word',
		'words',
		'book',
		'books',
		'point',
		'points',
		'music',
		'area',
		'areas',
		'money',
		'story',
		'stories',
		'job',
		'jobs',
		'month',
		'months',
		'lot',
		'lots',
		'right',
		'study',
		'studies',
		'company',
		'companies',
		'kind',
		'kinds',
		'head',
		'heads',
		'friend',
		'friends',
		'father',
		'mother',
		'name',
		'names',
		'minute',
		'minutes',
		'idea',
		'ideas',
		'feet',
		'foot',
		'son',
		'daughter',
		'city',
		'cities',
		'class',
		'classes',
		'tree',
		'trees',
		'road',
		'roads',
		'room',
		'rooms',
		'business',
		'busineses',
		'hour',
		'hours',
		'title',
		'page',
		'pages',
		'post',
		'posts',
	);

	/**
	 * Extract keyword suggestions from post content.
	 *
	 * @param string $content         Post content (HTML).
	 * @param string $title           Post title.
	 * @param int    $max_suggestions Maximum number of suggestions to return.
	 * @return array Array of suggested keywords (strings), sorted by score descending.
	 */
	public static function extract( $content, $title = '', $max_suggestions = 5 ) {
		if ( empty( $content ) ) {
			return array();
		}

		$heading_text    = self::extract_heading_text( $content );
		$first_paragraph = self::extract_first_paragraph( $content );

		$text = \strip_shortcodes( $content );
		$text = \wp_strip_all_tags( $text );
		$text = html_entity_decode( $text, ENT_QUOTES, 'UTF-8' );

		$words = self::tokenize( $text );
		if ( empty( $words ) ) {
			return array();
		}

		$words = self::remove_stop_words( $words );
		if ( empty( $words ) ) {
			return array();
		}

		$word_scores = array_count_values( $words );

		$title_words      = self::remove_stop_words( self::tokenize( $title ) );
		$heading_words    = self::remove_stop_words( self::tokenize( $heading_text ) );
		$first_para_words = self::remove_stop_words( self::tokenize( $first_paragraph ) );

		foreach ( $word_scores as $word => $count ) {
			if ( in_array( $word, $title_words, true ) ) {
				$word_scores[ $word ] = $count * 3;
			}

			if ( in_array( $word, $heading_words, true ) ) {
				$word_scores[ $word ] = $word_scores[ $word ] * 2;
			}

			if ( in_array( $word, $first_para_words, true ) ) {
				$word_scores[ $word ] = (int) round( $word_scores[ $word ] * 1.5 );
			}
		}

		arsort( $word_scores );

		$phrase_budget = min( 3, max( 1, (int) floor( $max_suggestions / 2 ) ) );
		$word_budget   = $max_suggestions - $phrase_budget;
		$suggestions   = array_slice( array_keys( $word_scores ), 0, $word_budget );

		$top_words   = array_slice( array_keys( $word_scores ), 0, $max_suggestions );
		$phrases     = self::extract_phrases( $text, array_merge( $title_words, $top_words ), $phrase_budget );
		$suggestions = array_merge( $suggestions, $phrases );

		$suggestions = array_values( array_unique( $suggestions ) );
		$suggestions = array_slice( $suggestions, 0, $max_suggestions );

		return $suggestions;
	}

	/**
	 * Tokenize text into lowercase words.
	 *
	 * @param string $text Input text.
	 * @return array Array of words.
	 */
	protected static function tokenize( $text ) {
		if ( empty( $text ) ) {
			return array();
		}

		$words = preg_split( '/[^\p{L}\p{N}]+/u', $text, -1, PREG_SPLIT_NO_EMPTY );

		if ( empty( $words ) ) {
			return array();
		}

		$result = array();
		foreach ( $words as $word ) {
			$word = mb_strtolower( $word, 'UTF-8' );
			if ( mb_strlen( $word, 'UTF-8' ) >= 3 ) {
				$result[] = $word;
			}
		}

		return $result;
	}

	/**
	 * Remove stop words from an array of words.
	 *
	 * @param array $words Array of lowercase words.
	 * @return array Filtered array.
	 */
	protected static function remove_stop_words( $words ) {
		$stop_words = self::get_stop_words();

		return array_values(
			array_filter(
				$words,
				function ( $word ) use ( $stop_words ) {
					return ! in_array( $word, $stop_words, true );
				}
			)
		);
	}

	/**
	 * Get the stop words list, allowing filtering.
	 *
	 * @return array
	 */
	public static function get_stop_words() {
		/**
		 * Filter: allow modifying the stop words list.
		 *
		 * @param array $stop_words
		 */
		return \apply_filters( 'seovaultseo/keyword_extractor/stop_words', self::$stop_words );
	}

	/**
	 * Extract text from H1-H3 headings in HTML content.
	 *
	 * @param string $content HTML content.
	 * @return string Extracted heading text.
	 */
	protected static function extract_heading_text( $content ) {
		if ( ! preg_match_all( '/<h[1-3][^>]*>(.*?)<\/h[1-3]>/is', $content, $matches ) ) {
			return '';
		}

		return implode( ' ', $matches[1] );
	}

	/**
	 * Extract the first paragraph text from HTML content.
	 *
	 * @param string $content HTML content.
	 * @return string First paragraph text.
	 */
	protected static function extract_first_paragraph( $content ) {
		if ( ! preg_match( '/<p[^>]*>(.*?)<\/p>/is', $content, $matches ) ) {
			$text = \wp_strip_all_tags( $content );
			return mb_substr( $text, 0, 200, 'UTF-8' );
		}

		return $matches[1];
	}

	/**
	 * Extract 2-word phrases that contain at least one top word.
	 *
	 * @param string $text      Full text (stripped of HTML).
	 * @param array  $top_words Top scoring words to look for in phrases.
	 * @param int    $max       Maximum phrases to return.
	 * @return array Array of phrase strings.
	 */
	protected static function extract_phrases( $text, $top_words, $max = 3 ) {
		$words   = self::tokenize( $text );
		$phrases = array();

		for ( $i = 0; $i < count( $words ) - 1; $i++ ) {
			$phrase = $words[ $i ] . ' ' . $words[ $i + 1 ];

			if ( in_array( $words[ $i ], $top_words, true ) || in_array( $words[ $i + 1 ], $top_words, true ) ) {
				$stop_words = self::get_stop_words();
				if ( in_array( $words[ $i ], $stop_words, true ) || in_array( $words[ $i + 1 ], $stop_words, true ) ) {
					continue;
				}
				$phrases[] = $phrase;
			}
		}

		$phrase_counts = array_count_values( $phrases );
		arsort( $phrase_counts );

		return array_slice( array_keys( $phrase_counts ), 0, $max );
	}
}
