<?php
/**
 * GEO readiness analyzer.
 *
 * Scores content for AI extractability — how well structured
 * the content is for citation by generative AI engines.
 *
 * @package MonitorCat\Modules\Seo\Analyzer
 */

declare(strict_types=1);

namespace MonitorCat\Modules\Seo\Analyzer;

defined( 'ABSPATH' ) || exit;

/**
 * Analyzes content for Generative Engine Optimization (GEO) readiness.
 */
final class GeoReadinessAnalyzer {

	/**
	 * Maximum sentences per paragraph for AI-friendly structure.
	 *
	 * @var int
	 */
	private const MAX_SENTENCES_PER_PARAGRAPH = 3;

	/**
	 * Months considered fresh for content.
	 *
	 * @var int
	 */
	private const FRESHNESS_PASS_MONTHS = 6;

	/**
	 * Months considered acceptable for content.
	 *
	 * @var int
	 */
	private const FRESHNESS_WARN_MONTHS = 12;

	/**
	 * Run the GEO readiness analysis on a post.
	 *
	 * @param int $post_id The post ID to analyze.
	 * @return array{score: int, status: string, checks: array<int, array{label: string, status: string, message: string, value: string|null}>}
	 */
	public function analyze( int $post_id ): array {
		$raw_content = get_post_field( 'post_content', $post_id );
		$raw_content = is_string( $raw_content ) ? $raw_content : '';
		$plain_text  = wp_strip_all_tags( $raw_content );
		$checks      = array();

		$checks[] = $this->check_content_structure( $raw_content );
		$checks[] = $this->check_qa_headings( $raw_content );
		$checks[] = $this->check_definition_leads( $raw_content );
		$checks[] = $this->check_data_density( $plain_text );
		$checks[] = $this->check_source_citations( $raw_content, $plain_text );
		$checks[] = $this->check_entity_clarity( $raw_content, $plain_text, $post_id );
		$checks[] = $this->check_content_freshness( $post_id );

		$score  = $this->calculate_score( $checks );
		$status = $this->determine_status( $score );

		return array(
			'score'  => $score,
			'status' => $status,
			'checks' => $checks,
		);
	}

	/**
	 * Check content structure: short paragraphs, lists, tables.
	 *
	 * AI engines extract content more reliably from well-structured pages
	 * with short paragraphs, bulleted/numbered lists, and tables.
	 *
	 * @param string $content Raw HTML content.
	 * @return array{label: string, status: string, message: string, value: string|null}
	 */
	private function check_content_structure( string $content ): array {
		$has_lists  = (bool) preg_match( '/<[ou]l[\s>]/i', $content );
		$has_tables = (bool) preg_match( '/<table[\s>]/i', $content );

		// Count paragraphs and check how many are short (<=3 sentences).
		preg_match_all( '/<p[^>]*>(.*?)<\/p>/is', $content, $matches );
		$paragraphs     = $matches[1];
		$total_paras    = count( $paragraphs );
		$short_paras    = 0;
		$elements_found = array();

		foreach ( $paragraphs as $para ) {
			$text           = wp_strip_all_tags( $para );
			$sentence_count = preg_match_all( '/[.!?]+(?:\s|$)/', $text );
			if ( $sentence_count <= self::MAX_SENTENCES_PER_PARAGRAPH ) {
				++$short_paras;
			}
		}

		if ( $has_lists ) {
			$elements_found[] = __( 'lists', 'monitor-cat' );
		}
		if ( $has_tables ) {
			$elements_found[] = __( 'tables', 'monitor-cat' );
		}

		$short_ratio = $total_paras > 0 ? $short_paras / $total_paras : 0;

		if ( $short_ratio >= 0.6 && ( $has_lists || $has_tables ) ) {
			return array(
				'label'   => __( 'Content Structure', 'monitor-cat' ),
				'status'  => 'pass',
				/* translators: %s: comma-separated list of elements found */
				'message' => sprintf( __( 'Content uses short paragraphs and includes %s — excellent for AI extraction.', 'monitor-cat' ), implode( ', ', $elements_found ) ),
				'value'   => sprintf( '%d%% short paragraphs', (int) round( $short_ratio * 100 ) ),
			);
		}

		if ( $short_ratio >= 0.3 || $has_lists || $has_tables ) {
			return array(
				'label'   => __( 'Content Structure', 'monitor-cat' ),
				'status'  => 'warning',
				'message' => __( 'Content structure is partially optimized for AI. Add more lists, tables, or break long paragraphs into shorter ones (2-3 sentences max).', 'monitor-cat' ),
				'value'   => sprintf( '%d%% short paragraphs', (int) round( $short_ratio * 100 ) ),
			);
		}

		return array(
			'label'   => __( 'Content Structure', 'monitor-cat' ),
			'status'  => 'fail',
			'message' => __( 'Content lacks AI-friendly structure. Use short paragraphs (2-3 sentences), bulleted lists, and tables to improve AI extractability.', 'monitor-cat' ),
			'value'   => null,
		);
	}

	/**
	 * Check for Q&A-style headings.
	 *
	 * Headings framed as questions are 28-40% more likely to be cited
	 * by AI engines like ChatGPT and Perplexity.
	 *
	 * @param string $content Raw HTML content.
	 * @return array{label: string, status: string, message: string, value: string|null}
	 */
	private function check_qa_headings( string $content ): array {
		preg_match_all( '/<h[2-4][^>]*>(.*?)<\/h[2-4]>/is', $content, $matches );
		$headings       = $matches[1];
		$qa_count       = 0;
		$question_words = '/^(what|who|where|when|why|how|which|can|does|do|is|are|should|could|will|would)\b/i';

		foreach ( $headings as $heading ) {
			$text = trim( wp_strip_all_tags( $heading ) );
			if ( str_contains( $text, '?' ) || preg_match( $question_words, $text ) ) {
				++$qa_count;
			}
		}

		if ( $qa_count >= 2 ) {
			return array(
				'label'   => __( 'Q&A Headings', 'monitor-cat' ),
				'status'  => 'pass',
				/* translators: %d: number of Q&A headings */
				'message' => sprintf( __( 'Found %d question-style headings. AI engines prefer Q&A patterns for citation.', 'monitor-cat' ), $qa_count ),
				'value'   => (string) $qa_count,
			);
		}

		if ( 1 === $qa_count ) {
			return array(
				'label'   => __( 'Q&A Headings', 'monitor-cat' ),
				'status'  => 'warning',
				'message' => __( 'Only 1 question-style heading found. Use more Q&A headings (e.g. "What is...?", "How does...?") for better AI visibility.', 'monitor-cat' ),
				'value'   => '1',
			);
		}

		return array(
			'label'   => __( 'Q&A Headings', 'monitor-cat' ),
			'status'  => 'fail',
			'message' => __( 'No question-style headings found. AI engines are 28-40% more likely to cite content with Q&A headings.', 'monitor-cat' ),
			'value'   => '0',
		);
	}

	/**
	 * Check for definition-lead sentences.
	 *
	 * Sections that begin with a concise, self-contained definition
	 * are more likely to be extracted by AI for direct answers.
	 *
	 * @param string $content Raw HTML content.
	 * @return array{label: string, status: string, message: string, value: string|null}
	 */
	private function check_definition_leads( string $content ): array {
		// Find paragraphs that follow headings (allow optional whitespace/blocks between).
		preg_match_all( '/<\/h[2-6]>\s*(?:<[^p][^>]*>.*?<\/[^p][^>]*>\s*)*<p[^>]*>(.*?)<\/p>/is', $content, $matches );
		$lead_paragraphs    = $matches[1];
		$definition_count   = 0;
		$definition_pattern = '/\b(is|are|refers?\s+to|means?|describes?|represents?|involves?|provides?|defines?|consists?\s+of|enables?)\b/i';

		foreach ( $lead_paragraphs as $para ) {
			$text = trim( wp_strip_all_tags( $para ) );
			// Get first sentence.
			$first_sentence = '';
			if ( preg_match( '/^[^.!?]+[.!?]/', $text, $sentence_match ) ) {
				$first_sentence = $sentence_match[0];
			} else {
				$first_sentence = $text;
			}

			$word_count = str_word_count( $first_sentence );
			// Definition lead: under 35 words and contains a definition verb.
			if ( $word_count > 0 && $word_count <= 35 && preg_match( $definition_pattern, $first_sentence ) ) {
				++$definition_count;
			}
		}

		if ( $definition_count >= 2 ) {
			return array(
				'label'   => __( 'Definition Leads', 'monitor-cat' ),
				'status'  => 'pass',
				/* translators: %d: number of definition-lead sections */
				'message' => sprintf( __( 'Found %d sections with definition-lead sentences. These are ideal for AI citation.', 'monitor-cat' ), $definition_count ),
				'value'   => (string) $definition_count,
			);
		}

		if ( 1 === $definition_count ) {
			return array(
				'label'   => __( 'Definition Leads', 'monitor-cat' ),
				'status'  => 'warning',
				'message' => __( 'Only 1 definition-lead found. Start more sections with concise definitions (e.g. "[Topic] is a [category] that...").', 'monitor-cat' ),
				'value'   => '1',
			);
		}

		return array(
			'label'   => __( 'Definition Leads', 'monitor-cat' ),
			'status'  => 'fail',
			'message' => __( 'No definition-lead sentences found. AI engines prefer sections that begin with concise, self-contained definitions.', 'monitor-cat' ),
			'value'   => '0',
		);
	}

	/**
	 * Check data density (statistics, numbers, percentages).
	 *
	 * Content with specific statistics and data points has 30-40%
	 * higher visibility in AI-generated responses.
	 *
	 * Uses targeted patterns to avoid false positives from dates,
	 * version numbers, and other non-statistical numbers.
	 *
	 * @param string $plain_text Plain text content.
	 * @return array{label: string, status: string, message: string, value: string|null}
	 */
	private function check_data_density( string $plain_text ): array {
		// Match meaningful data points, not arbitrary numbers.
		$patterns = array(
			'/\b\d+(?:\.\d+)?\s*%/',                           // Percentages: 42%, 3.5%.
			'/\b\d+(?:\.\d+)?\s*(?:percent|million|billion|trillion|thousand)\b/i', // Written units.
			'/\$\s*\d+(?:[,.]?\d+)*/',                          // Currency: $100, $1,000.00.
			'/\b\d{1,3}(?:,\d{3})+\b/',                         // Large formatted numbers: 1,000 or 10,000.
			'/\b\d+(?:\.\d+)?x\b/i',                            // Multipliers: 3x, 2.5x.
			'/\b\d+(?:\.\d+)?\s*(?:times|fold)\b/i',            // "5 times", "10-fold".
			'/\b(?:over|under|about|approximately|nearly|more than|less than|up to|at least)\s+\d+/i', // Quantified claims.
		);

		$data_count = 0;
		foreach ( $patterns as $pattern ) {
			$data_count += (int) preg_match_all( $pattern, $plain_text );
		}

		if ( $data_count >= 3 ) {
			return array(
				'label'   => __( 'Data Density', 'monitor-cat' ),
				'status'  => 'pass',
				/* translators: %d: number of data points */
				'message' => sprintf( __( 'Found %d data points (statistics, percentages, figures). Data-rich content has 30-40%% higher AI visibility.', 'monitor-cat' ), $data_count ),
				'value'   => (string) $data_count,
			);
		}

		if ( $data_count >= 1 ) {
			return array(
				'label'   => __( 'Data Density', 'monitor-cat' ),
				'status'  => 'warning',
				/* translators: %d: number of data points */
				'message' => sprintf( __( 'Only %d data point(s) found. Add more specific statistics, percentages, and figures to boost AI citation likelihood.', 'monitor-cat' ), $data_count ),
				'value'   => (string) $data_count,
			);
		}

		return array(
			'label'   => __( 'Data Density', 'monitor-cat' ),
			'status'  => 'fail',
			'message' => __( 'No data points found. Include statistics, percentages, and specific numbers to significantly improve AI visibility.', 'monitor-cat' ),
			'value'   => '0',
		);
	}

	/**
	 * Check for source citations and references.
	 *
	 * Content that cites authoritative sources is significantly more
	 * likely to be cited by AI engines. Studies show cited content
	 * gets 20-30% more AI visibility.
	 *
	 * @param string $content    Raw HTML content.
	 * @param string $plain_text Plain text content.
	 * @return array{label: string, status: string, message: string, value: string|null}
	 */
	private function check_source_citations( string $content, string $plain_text ): array {
		$citation_count = 0;

		// External links (outbound references to other domains).
		$site_host = wp_parse_url( home_url(), PHP_URL_HOST );
		preg_match_all( '/<a[^>]+href=["\']?(https?:\/\/[^"\'>\s]+)/i', $content, $link_matches );
		foreach ( $link_matches[1] as $url ) {
			$link_host = wp_parse_url( $url, PHP_URL_HOST );
			if ( $link_host && $link_host !== $site_host ) {
				++$citation_count;
			}
		}

		// Citation language patterns (e.g. "according to", "research shows").
		$citation_patterns = array(
			'/\baccording\s+to\b/i',
			'/\b(?:research|studies|data|surveys?|reports?)\s+(?:show|indicate|suggest|reveal|found|demonstrate)/i',
			'/\b(?:source|published|cited|referenced)\b/i',
			'/\b(?:university|institute|journal|study)\s+(?:of|by|from|published)/i',
		);

		foreach ( $citation_patterns as $pattern ) {
			$citation_count += (int) preg_match_all( $pattern, $plain_text );
		}

		// Blockquotes (often used for citations).
		$citation_count += (int) preg_match_all( '/<blockquote[\s>]/i', $content );

		if ( $citation_count >= 3 ) {
			return array(
				'label'   => __( 'Source Citations', 'monitor-cat' ),
				'status'  => 'pass',
				/* translators: %d: number of citations/references */
				'message' => sprintf( __( 'Found %d source citations and references. Well-cited content gets 20-30%% more AI visibility.', 'monitor-cat' ), $citation_count ),
				'value'   => (string) $citation_count,
			);
		}

		if ( $citation_count >= 1 ) {
			return array(
				'label'   => __( 'Source Citations', 'monitor-cat' ),
				'status'  => 'warning',
				/* translators: %d: number of citations/references */
				'message' => sprintf( __( 'Only %d citation(s) found. Add external references, link to authoritative sources, and cite research to boost credibility with AI engines.', 'monitor-cat' ), $citation_count ),
				'value'   => (string) $citation_count,
			);
		}

		return array(
			'label'   => __( 'Source Citations', 'monitor-cat' ),
			'status'  => 'fail',
			'message' => __( 'No source citations found. AI engines strongly favor content that references authoritative sources, research, and external links.', 'monitor-cat' ),
			'value'   => '0',
		);
	}

	/**
	 * Check entity clarity — whether the content clearly defines
	 * its main subject within the first paragraph.
	 *
	 * AI knowledge graphs prefer content where the primary entity
	 * is clearly named and defined early, making it easier to
	 * attribute facts to the correct topic.
	 *
	 * @param string $content    Raw HTML content.
	 * @param string $plain_text Plain text content.
	 * @param int    $post_id    The post ID.
	 * @return array{label: string, status: string, message: string, value: string|null}
	 */
	private function check_entity_clarity( string $content, string $plain_text, int $post_id ): array {
		$post_title = get_the_title( $post_id );
		if ( empty( $post_title ) || empty( $plain_text ) ) {
			return array(
				'label'   => __( 'Entity Clarity', 'monitor-cat' ),
				'status'  => 'warning',
				'message' => __( 'Could not evaluate entity clarity — post title or content is empty.', 'monitor-cat' ),
				'value'   => null,
			);
		}

		// Extract key terms from title (words 4+ chars, excluding stop words).
		$stop_words  = array( 'about', 'also', 'been', 'best', 'does', 'each', 'even', 'from', 'have', 'here', 'into', 'just', 'know', 'like', 'make', 'many', 'more', 'most', 'much', 'must', 'need', 'only', 'other', 'over', 'some', 'such', 'than', 'that', 'the', 'them', 'then', 'these', 'they', 'this', 'those', 'very', 'want', 'well', 'were', 'what', 'when', 'will', 'with', 'your' );
		$title_words = preg_split( '/[\s\-_:,]+/', strtolower( $post_title ) );
		$title_words = is_array( $title_words ) ? $title_words : array();
		$key_terms   = array_filter(
			$title_words,
			function ( string $word ) use ( $stop_words ): bool {
				return mb_strlen( $word ) >= 4 && ! in_array( $word, $stop_words, true );
			}
		);

		if ( empty( $key_terms ) ) {
			return array(
				'label'   => __( 'Entity Clarity', 'monitor-cat' ),
				'status'  => 'warning',
				'message' => __( 'Title is too short or generic to assess entity clarity. Use a descriptive title for better AI indexing.', 'monitor-cat' ),
				'value'   => null,
			);
		}

		// Get first 2 paragraphs of text.
		preg_match_all( '/<p[^>]*>(.*?)<\/p>/is', $content, $para_matches );
		$first_paragraphs = array_slice( $para_matches[1], 0, 2 );
		$intro_text       = strtolower( wp_strip_all_tags( implode( ' ', $first_paragraphs ) ) );

		// Check how many key terms appear in the introduction.
		$matched_terms = 0;
		foreach ( $key_terms as $term ) {
			if ( str_contains( $intro_text, $term ) ) {
				++$matched_terms;
			}
		}

		$total_terms = count( $key_terms );
		$match_ratio = $total_terms > 0 ? $matched_terms / $total_terms : 0;

		// Also check if the exact title (or close variant) appears in intro.
		$title_lower     = strtolower( $post_title );
		$has_exact_match = str_contains( $intro_text, $title_lower );

		if ( $has_exact_match || $match_ratio >= 0.75 ) {
			return array(
				'label'   => __( 'Entity Clarity', 'monitor-cat' ),
				'status'  => 'pass',
				'message' => __( 'Content clearly establishes its main topic in the opening paragraphs. AI engines can easily identify and attribute this content.', 'monitor-cat' ),
				'value'   => sprintf(
					/* translators: %d: number of matched terms, %d: total terms */
					__( '%1$d/%2$d key terms in intro', 'monitor-cat' ),
					$matched_terms,
					$total_terms
				),
			);
		}

		if ( $match_ratio >= 0.4 ) {
			return array(
				'label'   => __( 'Entity Clarity', 'monitor-cat' ),
				'status'  => 'warning',
				'message' => __( 'Topic is partially established in the introduction. Mention your main subject explicitly in the first paragraph for better AI entity recognition.', 'monitor-cat' ),
				'value'   => sprintf(
					/* translators: %d: number of matched terms, %d: total terms */
					__( '%1$d/%2$d key terms in intro', 'monitor-cat' ),
					$matched_terms,
					$total_terms
				),
			);
		}

		return array(
			'label'   => __( 'Entity Clarity', 'monitor-cat' ),
			'status'  => 'fail',
			'message' => __( 'Main topic is not clearly established in the opening paragraphs. AI engines struggle to attribute content that does not name its subject early.', 'monitor-cat' ),
			'value'   => sprintf(
				/* translators: %d: number of matched terms, %d: total terms */
				__( '%1$d/%2$d key terms in intro', 'monitor-cat' ),
				$matched_terms,
				$total_terms
			),
		);
	}

	/**
	 * Check content freshness.
	 *
	 * AI engines heavily favor recently updated content — pages not
	 * updated quarterly lose AI citations at 3x the normal rate.
	 *
	 * @param int $post_id The post ID.
	 * @return array{label: string, status: string, message: string, value: string|null}
	 */
	private function check_content_freshness( int $post_id ): array {
		$post = get_post( $post_id );

		if ( ! $post ) {
			return array(
				'label'   => __( 'Content Freshness', 'monitor-cat' ),
				'status'  => 'fail',
				'message' => __( 'Post not found.', 'monitor-cat' ),
				'value'   => null,
			);
		}

		$modified   = strtotime( $post->post_modified );
		$now        = time();
		$diff       = $now - $modified;
		$months_ago = $diff / ( 30 * DAY_IN_SECONDS );
		$age_label  = $this->format_time_ago( $diff );

		if ( $months_ago <= self::FRESHNESS_PASS_MONTHS ) {
			return array(
				'label'   => __( 'Content Freshness', 'monitor-cat' ),
				'status'  => 'pass',
				/* translators: %s: human-readable time ago */
				'message' => sprintf( __( 'Content was last updated %s — fresh content is strongly favored by AI engines.', 'monitor-cat' ), $age_label ),
				'value'   => $age_label,
			);
		}

		if ( $months_ago <= self::FRESHNESS_WARN_MONTHS ) {
			return array(
				'label'   => __( 'Content Freshness', 'monitor-cat' ),
				'status'  => 'warning',
				/* translators: %s: human-readable time ago */
				'message' => sprintf( __( 'Content was last updated %s. Consider refreshing — AI engines prefer content updated within 6 months.', 'monitor-cat' ), $age_label ),
				'value'   => $age_label,
			);
		}

		return array(
			'label'   => __( 'Content Freshness', 'monitor-cat' ),
			'status'  => 'fail',
			/* translators: %s: human-readable time ago */
			'message' => sprintf( __( 'Content was last updated %s. Stale content loses AI citations at 3x the normal rate.', 'monitor-cat' ), $age_label ),
			'value'   => $age_label,
		);
	}

	/**
	 * Format a time difference into human-readable form.
	 *
	 * @param int $seconds Time difference in seconds.
	 * @return string Human-readable time ago string.
	 */
	private function format_time_ago( int $seconds ): string {
		$days   = (int) floor( $seconds / DAY_IN_SECONDS );
		$months = (int) floor( $days / 30 );
		$years  = (int) floor( $days / 365 );

		if ( $days < 1 ) {
			return __( 'today', 'monitor-cat' );
		}

		if ( $days < 30 ) {
			/* translators: %d: number of days */
			return sprintf( _n( '%d day ago', '%d days ago', $days, 'monitor-cat' ), $days );
		}

		if ( $months < 12 ) {
			/* translators: %d: number of months */
			return sprintf( _n( '%d month ago', '%d months ago', $months, 'monitor-cat' ), $months );
		}

		/* translators: %d: number of years */
		return sprintf( _n( '%d year ago', '%d years ago', $years, 'monitor-cat' ), $years );
	}

	/**
	 * Calculate weighted score from checks.
	 *
	 * Weights: Structure 20, Q&A 15, Definitions 15, Data 15,
	 * Citations 15, Entity 10, Freshness 10.
	 *
	 * @param array<int, array{label: string, status: string, message: string, value: string|null}> $checks The checks array.
	 * @return int Score from 0 to 100.
	 */
	private function calculate_score( array $checks ): int {
		if ( empty( $checks ) ) {
			return 0;
		}

		$weights = array( 20, 15, 15, 15, 15, 10, 10 );
		$total   = 0;

		foreach ( $checks as $index => $check ) {
			$weight     = $weights[ $index ] ?? 10;
			$multiplier = 'pass' === $check['status'] ? 1.0 : ( 'warning' === $check['status'] ? 0.5 : 0.0 );
			$total     += $weight * $multiplier;
		}

		return (int) round( $total );
	}

	/**
	 * Determine overall status from score.
	 *
	 * @param int $score The calculated score.
	 * @return string 'pass', 'warning', or 'fail'.
	 */
	private function determine_status( int $score ): string {
		if ( $score >= 80 ) {
			return 'pass';
		}

		if ( $score >= 50 ) {
			return 'warning';
		}

		return 'fail';
	}
}
