<?php
/**
 * Readability analyzer.
 *
 * Analyzes content readability metrics for SEO best practices.
 *
 * @package MonitorCat\Modules\Seo\Analyzer
 */

declare(strict_types=1);

namespace MonitorCat\Modules\Seo\Analyzer;

defined( 'ABSPATH' ) || exit;

/**
 * Analyzes post content readability using word count, sentence length,
 * paragraph structure, and Flesch-Kincaid approximation.
 */
final class ReadabilityAnalyzer {

	/**
	 * Minimum recommended word count for indexable content.
	 *
	 * @var int
	 */
	private const MIN_WORD_COUNT = 300;

	/**
	 * Maximum recommended average sentence length.
	 *
	 * @var int
	 */
	private const MAX_AVG_SENTENCE_LENGTH = 20;

	/**
	 * Run the readability 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 : '';

		// Use do_blocks + wpautop instead of apply_filters('the_content')
		// to avoid triggering expensive filters (oEmbed, shortcodes, etc.)
		// in REST context. We only need the text content for readability.
		$rendered   = function_exists( 'do_blocks' ) ? do_blocks( $raw_content ) : $raw_content;
		$rendered   = wpautop( $rendered );
		$plain_text = wp_strip_all_tags( $rendered );
		$checks     = array();

		$word_count      = str_word_count( $plain_text );
		$sentence_count  = $this->count_sentences( $plain_text );
		$paragraph_count = $this->count_paragraphs( $raw_content );
		$avg_sentence    = $sentence_count > 0 ? $word_count / $sentence_count : 0;
		$flesch_score    = $this->calculate_flesch_kincaid( $plain_text, $word_count, $sentence_count );

		$checks[] = $this->check_word_count( $word_count );
		$checks[] = $this->check_sentence_length( $avg_sentence );
		$checks[] = $this->check_paragraphs( $paragraph_count );
		$checks[] = $this->check_flesch_kincaid( $flesch_score );

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

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

	/**
	 * Count sentences in text by matching sentence-ending punctuation.
	 *
	 * @param string $text Plain text content.
	 * @return int Number of sentences.
	 */
	private function count_sentences( string $text ): int {
		$count = preg_match_all( '/[.!?]+(?:\s|$)/', $text );

		return max( $count ? $count : 0, 1 );
	}

	/**
	 * Count paragraphs in raw content.
	 *
	 * @param string $content Raw post content (HTML/blocks).
	 * @return int Number of paragraphs.
	 */
	private function count_paragraphs( string $content ): int {
		// Count <p> tags or double newlines.
		$p_count       = preg_match_all( '/<p[\s>]/i', $content );
		$newline_count = preg_match_all( '/\n\s*\n/', $content );

		$count = max( $p_count ? $p_count : 0, $newline_count ? $newline_count : 0 );

		return max( $count, 1 );
	}

	/**
	 * Calculate simplified Flesch-Kincaid readability score.
	 *
	 * Formula: 206.835 - (1.015 * avg_sentence_length) - (84.6 * avg_syllables_per_word)
	 *
	 * @param string $text           Plain text content.
	 * @param int    $word_count     Total word count.
	 * @param int    $sentence_count Total sentence count.
	 * @return float The Flesch readability score (higher = easier to read).
	 */
	private function calculate_flesch_kincaid( string $text, int $word_count, int $sentence_count ): float {
		if ( 0 === $word_count || 0 === $sentence_count ) {
			return 0.0;
		}

		$avg_sentence_length    = $word_count / $sentence_count;
		$total_syllables        = $this->count_syllables( $text );
		$avg_syllables_per_word = $total_syllables / $word_count;

		$score = 206.835 - ( 1.015 * $avg_sentence_length ) - ( 84.6 * $avg_syllables_per_word );

		return round( max( 0.0, min( 100.0, $score ) ), 1 );
	}

	/**
	 * Count syllables in text using vowel group approximation.
	 *
	 * @param string $text Plain text content.
	 * @return int Approximate syllable count.
	 */
	private function count_syllables( string $text ): int {
		$words = preg_split( '/\s+/', strtolower( $text ), -1, PREG_SPLIT_NO_EMPTY );
		$total = 0;

		if ( ! is_array( $words ) ) {
			return 0;
		}

		foreach ( $words as $word ) {
			$word = preg_replace( '/[^a-z]/', '', $word );

			if ( empty( $word ) ) {
				continue;
			}

			// Count vowel groups.
			$syllables = (int) preg_match_all( '/[aeiouy]+/', $word );

			// Subtract silent e at end.
			if ( preg_match( '/[^l]e$/', $word ) && $syllables > 1 ) {
				--$syllables;
			}

			$total += max( $syllables, 1 );
		}

		return $total;
	}

	/**
	 * Check word count against SEO minimum.
	 *
	 * @param int $word_count The word count.
	 * @return array{label: string, status: string, message: string, value: string|null}
	 */
	private function check_word_count( int $word_count ): array {
		if ( $word_count >= self::MIN_WORD_COUNT ) {
			return array(
				'label'   => __( 'Word Count', 'monitor-cat' ),
				'status'  => 'pass',
				/* translators: %d: number of words */
				'message' => sprintf( __( 'Content has %d words, meeting the minimum of 300 for SEO.', 'monitor-cat' ), $word_count ),
				'value'   => (string) $word_count,
			);
		}

		if ( $word_count > 0 ) {
			return array(
				'label'   => __( 'Word Count', 'monitor-cat' ),
				'status'  => 'warning',
				/* translators: %d: number of words */
				'message' => sprintf( __( 'Content has only %d words. Aim for at least 300 words for better SEO.', 'monitor-cat' ), $word_count ),
				'value'   => (string) $word_count,
			);
		}

		return array(
			'label'   => __( 'Word Count', 'monitor-cat' ),
			'status'  => 'fail',
			'message' => __( 'Content appears to be empty.', 'monitor-cat' ),
			'value'   => '0',
		);
	}

	/**
	 * Check average sentence length.
	 *
	 * @param float $avg_sentence_length Average words per sentence.
	 * @return array{label: string, status: string, message: string, value: string|null}
	 */
	private function check_sentence_length( float $avg_sentence_length ): array {
		$rounded = round( $avg_sentence_length, 1 );

		if ( $avg_sentence_length <= self::MAX_AVG_SENTENCE_LENGTH ) {
			return array(
				'label'   => __( 'Sentence Length', 'monitor-cat' ),
				'status'  => 'pass',
				/* translators: %s: average sentence length */
				'message' => sprintf( __( 'Average sentence length is %s words, within the recommended limit of 20.', 'monitor-cat' ), (string) $rounded ),
				'value'   => (string) $rounded,
			);
		}

		return array(
			'label'   => __( 'Sentence Length', 'monitor-cat' ),
			'status'  => 'warning',
			/* translators: %s: average sentence length */
			'message' => sprintf( __( 'Average sentence length is %s words. Aim for under 20 words per sentence for better readability.', 'monitor-cat' ), (string) $rounded ),
			'value'   => (string) $rounded,
		);
	}

	/**
	 * Check paragraph structure.
	 *
	 * @param int $paragraph_count Number of paragraphs.
	 * @return array{label: string, status: string, message: string, value: string|null}
	 */
	private function check_paragraphs( int $paragraph_count ): array {
		if ( $paragraph_count > 1 ) {
			return array(
				'label'   => __( 'Paragraph Structure', 'monitor-cat' ),
				'status'  => 'pass',
				/* translators: %d: number of paragraphs */
				'message' => sprintf( __( 'Content has %d paragraphs, providing good visual structure.', 'monitor-cat' ), $paragraph_count ),
				'value'   => (string) $paragraph_count,
			);
		}

		return array(
			'label'   => __( 'Paragraph Structure', 'monitor-cat' ),
			'status'  => 'warning',
			'message' => __( 'Content appears to be a single block of text. Break it into paragraphs for better readability.', 'monitor-cat' ),
			'value'   => '1',
		);
	}

	/**
	 * Check Flesch-Kincaid readability score.
	 *
	 * Score ranges:
	 * 90-100: Very Easy
	 * 80-89:  Easy
	 * 70-79:  Fairly Easy
	 * 60-69:  Standard
	 * 50-59:  Fairly Difficult
	 * 30-49:  Difficult
	 * 0-29:   Very Difficult
	 *
	 * @param float $flesch_score The Flesch readability score.
	 * @return array{label: string, status: string, message: string, value: string|null}
	 */
	private function check_flesch_kincaid( float $flesch_score ): array {
		$grade = $this->get_readability_grade( $flesch_score );

		if ( $flesch_score >= 60.0 ) {
			return array(
				'label'   => __( 'Readability Score', 'monitor-cat' ),
				'status'  => 'pass',
				/* translators: 1: Flesch score, 2: readability grade */
				'message' => sprintf( __( 'Flesch readability score: %1$s (%2$s). Content is easy to read.', 'monitor-cat' ), (string) $flesch_score, $grade ),
				'value'   => (string) $flesch_score,
			);
		}

		if ( $flesch_score >= 40.0 ) {
			return array(
				'label'   => __( 'Readability Score', 'monitor-cat' ),
				'status'  => 'warning',
				/* translators: 1: Flesch score, 2: readability grade */
				'message' => sprintf( __( 'Flesch readability score: %1$s (%2$s). Consider simplifying sentences and using shorter words.', 'monitor-cat' ), (string) $flesch_score, $grade ),
				'value'   => (string) $flesch_score,
			);
		}

		return array(
			'label'   => __( 'Readability Score', 'monitor-cat' ),
			'status'  => 'fail',
			/* translators: 1: Flesch score, 2: readability grade */
			'message' => sprintf( __( 'Flesch readability score: %1$s (%2$s). Content is difficult to read for a general audience.', 'monitor-cat' ), (string) $flesch_score, $grade ),
			'value'   => (string) $flesch_score,
		);
	}

	/**
	 * Get a human-readable grade for the Flesch score.
	 *
	 * @param float $score The Flesch readability score.
	 * @return string The readability grade.
	 */
	private function get_readability_grade( float $score ): string {
		if ( $score >= 90.0 ) {
			return __( 'Very Easy', 'monitor-cat' );
		}

		if ( $score >= 80.0 ) {
			return __( 'Easy', 'monitor-cat' );
		}

		if ( $score >= 70.0 ) {
			return __( 'Fairly Easy', 'monitor-cat' );
		}

		if ( $score >= 60.0 ) {
			return __( 'Standard', 'monitor-cat' );
		}

		if ( $score >= 50.0 ) {
			return __( 'Fairly Difficult', 'monitor-cat' );
		}

		if ( $score >= 30.0 ) {
			return __( 'Difficult', 'monitor-cat' );
		}

		return __( 'Very Difficult', 'monitor-cat' );
	}

	/**
	 * Calculate weighted score from checks.
	 *
	 * @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( 30, 25, 15, 30 );
		$total   = 0;

		foreach ( $checks as $index => $check ) {
			$weight     = $weights[ $index ] ?? 25;
			$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';
	}
}
