<?php
/**
 * Heading hierarchy analyzer.
 *
 * Analyzes heading structure in post content for SEO best practices.
 *
 * @package MonitorCat\Modules\Seo\Analyzer
 */

declare(strict_types=1);

namespace MonitorCat\Modules\Seo\Analyzer;

defined( 'ABSPATH' ) || exit;

/**
 * Analyzes heading hierarchy and structure in post content.
 */
final class HeadingAnalyzer {

	/**
	 * Minimum recommended words per heading.
	 *
	 * @var int
	 */
	private const WORDS_PER_HEADING = 300;

	/**
	 * Run the heading 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 {
		$content  = get_post_field( 'post_content', $post_id );
		$content  = is_string( $content ) ? $content : '';
		$headings = $this->parse_headings( $content );
		$checks   = array();

		$checks[] = $this->check_h1_usage( $headings );
		$checks[] = $this->check_hierarchy( $headings );
		$checks[] = $this->check_has_h2( $headings );
		$checks[] = $this->check_heading_density( $content, $headings );
		$checks[] = $this->report_structure( $headings );

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

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

	/**
	 * Parse all headings from content.
	 *
	 * @param string $content The post content.
	 * @return array<int, array{level: int, text: string}> Array of headings with level and text.
	 */
	private function parse_headings( string $content ): array {
		$headings = array();

		if ( preg_match_all( '/<h([1-6])[^>]*>(.*?)<\/h\1>/is', $content, $matches, PREG_SET_ORDER ) ) {
			foreach ( $matches as $match ) {
				$headings[] = array(
					'level' => (int) $match[1],
					'text'  => wp_strip_all_tags( $match[2] ),
				);
			}
		}

		return $headings;
	}

	/**
	 * Check H1 usage in content.
	 *
	 * Most themes render the post title as H1, so content should not contain H1 tags.
	 *
	 * @param array<int, array{level: int, text: string}> $headings Parsed headings.
	 * @return array{label: string, status: string, message: string, value: string|null}
	 */
	private function check_h1_usage( array $headings ): array {
		$h1_count = 0;

		foreach ( $headings as $heading ) {
			if ( 1 === $heading['level'] ) {
				++$h1_count;
			}
		}

		if ( 0 === $h1_count ) {
			return array(
				'label'   => __( 'H1 Usage', 'monitor-cat' ),
				'status'  => 'pass',
				'message' => __( 'No H1 in content body. The post title should serve as the only H1.', 'monitor-cat' ),
				'value'   => '0',
			);
		}

		if ( 1 === $h1_count ) {
			return array(
				'label'   => __( 'H1 Usage', 'monitor-cat' ),
				'status'  => 'warning',
				'message' => __( 'Found 1 H1 tag in content. Most themes already use the post title as H1, which could result in duplicate H1 tags.', 'monitor-cat' ),
				'value'   => '1',
			);
		}

		return array(
			'label'   => __( 'H1 Usage', 'monitor-cat' ),
			'status'  => 'fail',
			/* translators: %d: number of H1 tags */
			'message' => sprintf( __( 'Found %d H1 tags in content. There should be only one H1 per page.', 'monitor-cat' ), $h1_count ),
			'value'   => (string) $h1_count,
		);
	}

	/**
	 * Check that headings do not skip levels.
	 *
	 * @param array<int, array{level: int, text: string}> $headings Parsed headings.
	 * @return array{label: string, status: string, message: string, value: string|null}
	 */
	private function check_hierarchy( array $headings ): array {
		if ( empty( $headings ) ) {
			return array(
				'label'   => __( 'Heading Hierarchy', 'monitor-cat' ),
				'status'  => 'warning',
				'message' => __( 'No headings found in content to evaluate hierarchy.', 'monitor-cat' ),
				'value'   => null,
			);
		}

		$skipped_levels = array();
		$previous_level = 1; // Assume post title is H1.

		foreach ( $headings as $heading ) {
			$level = $heading['level'];

			if ( $level > $previous_level + 1 ) {
				$skipped_levels[] = sprintf( 'H%d to H%d', $previous_level, $level );
			}

			$previous_level = $level;
		}

		if ( ! empty( $skipped_levels ) ) {
			return array(
				'label'   => __( 'Heading Hierarchy', 'monitor-cat' ),
				'status'  => 'warning',
				/* translators: %s: list of skipped heading levels */
				'message' => sprintf( __( 'Heading levels are skipped: %s. Use sequential heading levels for better accessibility and SEO.', 'monitor-cat' ), implode( ', ', $skipped_levels ) ),
				'value'   => implode( ', ', $skipped_levels ),
			);
		}

		return array(
			'label'   => __( 'Heading Hierarchy', 'monitor-cat' ),
			'status'  => 'pass',
			'message' => __( 'Heading hierarchy is sequential with no skipped levels.', 'monitor-cat' ),
			'value'   => null,
		);
	}

	/**
	 * Check if content has at least one H2.
	 *
	 * @param array<int, array{level: int, text: string}> $headings Parsed headings.
	 * @return array{label: string, status: string, message: string, value: string|null}
	 */
	private function check_has_h2( array $headings ): array {
		$h2_count = 0;

		foreach ( $headings as $heading ) {
			if ( 2 === $heading['level'] ) {
				++$h2_count;
			}
		}

		if ( $h2_count > 0 ) {
			return array(
				'label'   => __( 'H2 Subheadings', 'monitor-cat' ),
				'status'  => 'pass',
				/* translators: %d: number of H2 headings */
				'message' => sprintf( __( 'Found %d H2 subheading(s) for content structure.', 'monitor-cat' ), $h2_count ),
				'value'   => (string) $h2_count,
			);
		}

		return array(
			'label'   => __( 'H2 Subheadings', 'monitor-cat' ),
			'status'  => 'warning',
			'message' => __( 'No H2 subheadings found. Use H2 tags to structure your content.', 'monitor-cat' ),
			'value'   => '0',
		);
	}

	/**
	 * Check if heading count is proportional to content length.
	 *
	 * @param string                                       $content  The post content.
	 * @param array<int, array{level: int, text: string}> $headings Parsed headings.
	 * @return array{label: string, status: string, message: string, value: string|null}
	 */
	private function check_heading_density( string $content, array $headings ): array {
		$word_count    = str_word_count( wp_strip_all_tags( $content ) );
		$heading_count = count( $headings );

		if ( $word_count < self::WORDS_PER_HEADING ) {
			return array(
				'label'   => __( 'Heading Density', 'monitor-cat' ),
				'status'  => 'pass',
				'message' => __( 'Content is short enough that minimal headings are acceptable.', 'monitor-cat' ),
				'value'   => sprintf( '%d headings / %d words', $heading_count, $word_count ),
			);
		}

		$expected = (int) floor( $word_count / self::WORDS_PER_HEADING );

		if ( $heading_count >= $expected ) {
			return array(
				'label'   => __( 'Heading Density', 'monitor-cat' ),
				'status'  => 'pass',
				/* translators: 1: heading count, 2: word count */
				'message' => sprintf( __( 'Good heading density: %1$d headings for %2$d words.', 'monitor-cat' ), $heading_count, $word_count ),
				'value'   => sprintf( '%d headings / %d words', $heading_count, $word_count ),
			);
		}

		return array(
			'label'   => __( 'Heading Density', 'monitor-cat' ),
			'status'  => 'warning',
			/* translators: 1: heading count, 2: word count, 3: recommended count */
			'message' => sprintf( __( 'Low heading density: %1$d headings for %2$d words. Consider adding at least %3$d headings.', 'monitor-cat' ), $heading_count, $word_count, $expected ),
			'value'   => sprintf( '%d headings / %d words', $heading_count, $word_count ),
		);
	}

	/**
	 * Report the heading structure found.
	 *
	 * @param array<int, array{level: int, text: string}> $headings Parsed headings.
	 * @return array{label: string, status: string, message: string, value: string|null}
	 */
	private function report_structure( array $headings ): array {
		if ( empty( $headings ) ) {
			return array(
				'label'   => __( 'Heading Structure', 'monitor-cat' ),
				'status'  => 'warning',
				'message' => __( 'No headings found in the content.', 'monitor-cat' ),
				'value'   => null,
			);
		}

		$structure = array();
		foreach ( $headings as $heading ) {
			$structure[] = sprintf( 'H%d: %s', $heading['level'], $heading['text'] );
		}

		return array(
			'label'   => __( 'Heading Structure', 'monitor-cat' ),
			'status'  => 'pass',
			/* translators: %d: number of headings */
			'message' => sprintf( __( 'Found %d headings in content.', 'monitor-cat' ), count( $headings ) ),
			'value'   => implode( "\n", $structure ),
		);
	}

	/**
	 * 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( 25, 25, 25, 15, 10 );
		$total   = 0;

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