<?php
/**
 * Keyword density SEO strategy.
 *
 * @package SmartyPress_AI_Engine
 * @since 1.0.0
 */

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

/**
 * Adds the primary keyword to one relevant paragraph without keyword stuffing.
 */
class SmartyPress_AI_SEO_Strategy_Keyword_Density extends SmartyPress_AI_SEO_Strategy_Base {

	public function get_test_id() {
		return 'keywordDensity';
	}

	public function get_reason() {
		return 'keyword_density';
	}

	public function get_label() {
		return __( 'Focus keyword density', 'smartypress-ai-engine' );
	}

	public function get_context_scope() {
		return 'expanded';
	}

	public function get_strategy_rules() {
		return array_merge(
			parent::get_strategy_rules(),
			array(
				'Replace up to three relevant paragraphs if needed to reach the target keyword occurrence count.',
				'Insert primary_keyword exactly once per edited paragraph and exactly as written, with no inflection, declension, spelling changes, or added words inside the phrase.',
				'Use target_additional_occurrences to decide how many paragraphs need edits.',
				'Use the article_context to avoid keyword stuffing and choose paragraphs where the exact phrase reads most naturally.',
			)
		);
	}

	public function is_applicable( $context, $analysis ) {
		return '' !== $this->get_primary_keyword( $context, $analysis ) && ! empty( $this->select_context( $context, $analysis )['candidate_blocks'] );
	}

	public function select_context( $context, $analysis ) {
		$candidate_ids = (array) ( $analysis['content_map']['density_candidate_blocks'] ?? array() );
		$blocks        = $this->get_blocks( $context );
		$candidates    = array();

		foreach ( $candidate_ids as $client_id ) {
			$block = $this->find_block( $blocks, $client_id );
			if ( $block && ! empty( $block['text'] ) ) {
				$candidates[] = $block;
			}
		}

		if ( empty( $candidates ) ) {
			foreach ( $blocks as $block ) {
				if ( 'core/paragraph' === ( $block['name'] ?? '' ) && ! empty( $block['text'] ) ) {
					$candidates[] = $block;
				}

				if ( count( $candidates ) >= 3 ) {
					break;
				}
			}
		}

		$density_context = $this->get_density_context( $context, $analysis );

		return array(
			'testId'          => $this->get_test_id(),
			'reason'          => $this->get_reason(),
			'label'           => $this->get_label(),
			'context_scope'   => $this->get_context_scope(),
			'primary_keyword' => $this->get_primary_keyword( $context, $analysis ),
			'article_summary' => $analysis['content_summary'] ?? '',
			'keyword_usage'   => $analysis['keyword_usage'] ?? array(),
			'current_occurrences' => $density_context['occurrences'],
			'current_words'   => $density_context['words'],
			'target_occurrences' => $density_context['target_occurrences'],
			'target_additional_occurrences' => $density_context['target_additional_occurrences'],
			'candidate_blocks' => array_slice( $candidates, 0, 5 ),
		);
	}

	/**
	 * Calculate the minimum safe target for Rank Math keyword density.
	 *
	 * @param array $context Context.
	 * @param array $analysis Analysis.
	 * @return array
	 */
	private function get_density_context( $context, $analysis ) {
		$keyword = $this->get_primary_keyword( $context, $analysis );
		$content = (string) ( $context['post']['content_plain_text'] ?? '' );
		$words   = $this->count_words( $content );
		$target_words = $this->has_failed_test( $context, 'lengthContent' ) ? max( $words, 600 ) : $words;
		$occurrences = $this->count_keyword_occurrences( $content, $keyword );
		$target_occurrences = max( 3, (int) ceil( $target_words * 0.006 ) );

		return array(
			'words'                         => $words,
			'occurrences'                   => $occurrences,
			'target_occurrences'            => $target_occurrences,
			'target_additional_occurrences' => min( 3, max( 1, $target_occurrences - $occurrences ) ),
		);
	}

	private function has_failed_test( $context, $test_id ) {
		foreach ( (array) ( $context['failed_tests'] ?? array() ) as $test ) {
			if ( $test_id === ( $test['id'] ?? '' ) ) {
				return true;
			}
		}

		return false;
	}

	private function count_words( $text ) {
		$words = preg_split( '/\s+/u', $this->normalize_text( $text ), -1, PREG_SPLIT_NO_EMPTY );

		return is_array( $words ) ? count( $words ) : 0;
	}

	private function count_keyword_occurrences( $text, $keyword ) {
		$text    = $this->normalize_text( $text );
		$keyword = $this->normalize_text( $keyword );

		return '' !== $keyword ? substr_count( $text, $keyword ) : 0;
	}

	/**
	 * Build a deterministic density repair when AI returns a no-op.
	 *
	 * @param array  $operation Operation.
	 * @param string $keyword   Primary keyword.
	 * @return array
	 */
	private function repair_noop_density_operation( $operation, $keyword ) {
		$html = trim( wp_strip_all_tags( (string) ( $operation['html'] ?? '' ) ) );
		if ( '' === $html || '' === $keyword ) {
			return $operation;
		}

		$sentence = sprintf(
			/* translators: %s: primary keyword. */
			__( 'This paragraph additionally explains why %s is important for understanding the article topic.', 'smartypress-ai-engine' ),
			$keyword
		);

		if ( false === strpos( $this->normalize_text( $html ), $this->normalize_text( $sentence ) ) ) {
			$operation['html'] = trim( $html . ' ' . $sentence );
		}

		return $operation;
	}

	public function validate_response( $response, $context, $analysis ) {
		$operations = array();
		$warnings   = array_values( array_filter( array_map( 'sanitize_text_field', (array) ( $response['warnings'] ?? array() ) ) ) );
		$keyword    = $this->get_primary_keyword( $context, $analysis );
		$target     = $this->get_density_context( $context, $analysis );
		$max_operations = max( 1, (int) $target['target_additional_occurrences'] );
		$used_blocks = array();

		foreach ( (array) ( $response['content_operations'] ?? array() ) as $operation ) {
			$operation = $this->normalize_operation( $operation );
			if ( 'replace' !== $operation['type'] || '' === $operation['blockClientId'] || '' === $operation['html'] ) {
				continue;
			}

			if ( isset( $used_blocks[ $operation['blockClientId'] ] ) ) {
				continue;
			}

			if ( ! $this->includes_keyword( $operation['html'], $keyword ) ) {
				$warnings[] = __( 'The generated density fix does not include the exact focus keyword.', 'smartypress-ai-engine' );
				continue;
			}

			if ( ! $this->has_meaningful_change( $context, $operation['blockClientId'], $operation['html'] ) && $this->includes_keyword( $operation['html'], $keyword ) ) {
				$operation = $this->repair_noop_density_operation( $operation, $keyword );
			}

			if ( ! $this->has_meaningful_change( $context, $operation['blockClientId'], $operation['html'] ) || $this->has_adjacent_word_duplication( $operation['html'] ) ) {
				$warnings[] = __( 'The generated density fix was rejected because it did not make a meaningful clean edit.', 'smartypress-ai-engine' );
				continue;
			}

			$operations[] = $operation;
			$used_blocks[ $operation['blockClientId'] ] = true;

			if ( count( $operations ) >= $max_operations ) {
				break;
			}
		}

		return array(
			'status'             => ! empty( $operations ) ? 'success' : 'validation_failure',
			'testId'             => $this->get_test_id(),
			'reason'             => $this->get_reason(),
			'label'              => $this->get_label(),
			'content_operations' => $operations,
			'fixed_tests'        => ! empty( $operations ) ? array( $this->get_test_id() ) : array(),
			'skipped_tests'      => empty( $operations ) ? array( array( 'id' => $this->get_test_id(), 'reason' => $this->get_failure_reason( $warnings, __( 'AI could not safely improve focus keyword density without keyword stuffing.', 'smartypress-ai-engine' ) ) ) ) : array(),
			'warnings'           => array_values( array_unique( $warnings ) ),
		);
	}
}
