<?php
/**
 * Meta tags analyzer.
 *
 * Analyzes title and description meta tags for SEO best practices.
 *
 * @package MonitorCat\Modules\Seo\Analyzer
 */

declare(strict_types=1);

namespace MonitorCat\Modules\Seo\Analyzer;

defined( 'ABSPATH' ) || exit;

/**
 * Analyzes post title and meta description for SEO quality.
 */
final class MetaAnalyzer {

	/**
	 * Ideal minimum title length.
	 *
	 * @var int
	 */
	private const TITLE_MIN_IDEAL = 30;

	/**
	 * Ideal maximum title length.
	 *
	 * @var int
	 */
	private const TITLE_MAX_IDEAL = 70;

	/**
	 * Ideal minimum description length.
	 *
	 * @var int
	 */
	private const DESC_MIN_IDEAL = 120;

	/**
	 * Ideal maximum description length.
	 *
	 * @var int
	 */
	private const DESC_MAX_IDEAL = 170;

	/**
	 * Run the meta 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 {
		$checks = array();

		$checks[] = $this->check_title( $post_id );
		$checks[] = $this->check_title_length( $post_id );
		$checks[] = $this->check_meta_description( $post_id );
		$checks[] = $this->check_description_length( $post_id );
		$checks[] = $this->check_duplicate_title( $post_id );

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

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

	/**
	 * Check if the post has a title.
	 *
	 * @param int $post_id The post ID.
	 * @return array{label: string, status: string, message: string, value: string|null}
	 */
	private function check_title( int $post_id ): array {
		$title = get_the_title( $post_id );

		if ( empty( $title ) ) {
			return array(
				'label'   => __( 'Post Title', 'monitor-cat' ),
				'status'  => 'fail',
				'message' => __( 'The post has no title. A title is essential for SEO.', 'monitor-cat' ),
				'value'   => null,
			);
		}

		return array(
			'label'   => __( 'Post Title', 'monitor-cat' ),
			'status'  => 'pass',
			'message' => __( 'The post has a title.', 'monitor-cat' ),
			'value'   => $title,
		);
	}

	/**
	 * Check title length against ideal range.
	 *
	 * @param int $post_id The post ID.
	 * @return array{label: string, status: string, message: string, value: string|null}
	 */
	private function check_title_length( int $post_id ): array {
		$title  = get_the_title( $post_id );
		$length = mb_strlen( $title );

		if ( 0 === $length ) {
			return array(
				'label'   => __( 'Title Length', 'monitor-cat' ),
				'status'  => 'fail',
				'message' => __( 'Title is empty.', 'monitor-cat' ),
				'value'   => '0',
			);
		}

		if ( $length >= self::TITLE_MIN_IDEAL && $length <= self::TITLE_MAX_IDEAL ) {
			return array(
				'label'   => __( 'Title Length', 'monitor-cat' ),
				'status'  => 'pass',
				/* translators: %d: number of characters */
				'message' => sprintf( __( 'Title length is %d characters, within the ideal range (30-70).', 'monitor-cat' ), $length ),
				'value'   => (string) $length,
			);
		}

		return array(
			'label'   => __( 'Title Length', 'monitor-cat' ),
			'status'  => 'warning',
			/* translators: %d: number of characters */
			'message' => sprintf( __( 'Title length is %d characters. Ideal range is 50-60 characters (acceptable: 30-70).', 'monitor-cat' ), $length ),
			'value'   => (string) $length,
		);
	}

	/**
	 * Check for meta description from SEO plugins or excerpt.
	 *
	 * @param int $post_id The post ID.
	 * @return array{label: string, status: string, message: string, value: string|null}
	 */
	private function check_meta_description( int $post_id ): array {
		$description = $this->get_meta_description( $post_id );

		if ( empty( $description ) ) {
			return array(
				'label'   => __( 'Meta Description', 'monitor-cat' ),
				'status'  => 'fail',
				'message' => __( 'No meta description found. Add one via an SEO plugin or set a post excerpt.', 'monitor-cat' ),
				'value'   => null,
			);
		}

		return array(
			'label'   => __( 'Meta Description', 'monitor-cat' ),
			'status'  => 'pass',
			'message' => __( 'Meta description is set.', 'monitor-cat' ),
			'value'   => $description,
		);
	}

	/**
	 * Check meta description length against ideal range.
	 *
	 * @param int $post_id The post ID.
	 * @return array{label: string, status: string, message: string, value: string|null}
	 */
	private function check_description_length( int $post_id ): array {
		$description = $this->get_meta_description( $post_id );
		$length      = mb_strlen( $description );

		if ( 0 === $length ) {
			return array(
				'label'   => __( 'Description Length', 'monitor-cat' ),
				'status'  => 'fail',
				'message' => __( 'No meta description to measure.', 'monitor-cat' ),
				'value'   => '0',
			);
		}

		if ( $length >= self::DESC_MIN_IDEAL && $length <= self::DESC_MAX_IDEAL ) {
			return array(
				'label'   => __( 'Description Length', 'monitor-cat' ),
				'status'  => 'pass',
				/* translators: %d: number of characters */
				'message' => sprintf( __( 'Description length is %d characters, within the ideal range (120-170).', 'monitor-cat' ), $length ),
				'value'   => (string) $length,
			);
		}

		return array(
			'label'   => __( 'Description Length', 'monitor-cat' ),
			'status'  => 'warning',
			/* translators: %d: number of characters */
			'message' => sprintf( __( 'Description length is %d characters. Ideal range is 150-160 characters (acceptable: 120-170).', 'monitor-cat' ), $length ),
			'value'   => (string) $length,
		);
	}

	/**
	 * Check for duplicate titles across published posts.
	 *
	 * @param int $post_id The post ID.
	 * @return array{label: string, status: string, message: string, value: string|null}
	 */
	private function check_duplicate_title( int $post_id ): array {
		$title = get_the_title( $post_id );

		if ( empty( $title ) ) {
			return array(
				'label'   => __( 'Duplicate Title', 'monitor-cat' ),
				'status'  => 'fail',
				'message' => __( 'Cannot check for duplicates without a title.', 'monitor-cat' ),
				'value'   => null,
			);
		}

		$cache_key       = 'monitorcat_dup_title_' . md5( $title ) . '_' . $post_id;
		$duplicate_count = wp_cache_get( $cache_key, 'monitorcat' );

		if ( false === $duplicate_count ) {
			global $wpdb;

			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- No WP API for duplicate title count.
			$duplicate_count = (int) $wpdb->get_var(
				$wpdb->prepare(
					"SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_title = %s AND post_status = 'publish' AND ID != %d",
					$title,
					$post_id
				)
			);

			wp_cache_set( $cache_key, $duplicate_count, 'monitorcat', HOUR_IN_SECONDS );
		}

		$duplicate_count = (int) $duplicate_count;

		if ( $duplicate_count > 0 ) {
			return array(
				'label'   => __( 'Duplicate Title', 'monitor-cat' ),
				'status'  => 'warning',
				/* translators: %d: number of duplicate posts */
				'message' => sprintf( __( 'Found %d other published post(s) with the same title. Unique titles improve SEO.', 'monitor-cat' ), $duplicate_count ),
				'value'   => (string) $duplicate_count,
			);
		}

		return array(
			'label'   => __( 'Duplicate Title', 'monitor-cat' ),
			'status'  => 'pass',
			'message' => __( 'No other published posts share this title.', 'monitor-cat' ),
			'value'   => '0',
		);
	}

	/**
	 * Retrieve meta description from SEO plugins or post excerpt.
	 *
	 * @param int $post_id The post ID.
	 * @return string The meta description, or empty string if none found.
	 */
	private function get_meta_description( int $post_id ): string {
		$meta_keys = array(
			'_yoast_wpseo_metadesc',
			'_aioseo_description',
			'rank_math_description',
		);

		foreach ( $meta_keys as $key ) {
			$value = get_post_meta( $post_id, $key, true );
			if ( ! empty( $value ) && is_string( $value ) ) {
				return $value;
			}
		}

		$post = get_post( $post_id );
		if ( $post && ! empty( $post->post_excerpt ) ) {
			return $post->post_excerpt;
		}

		// Check if Monitor.Cat auto meta tags are generating a description.
		$mc_settings = get_option( 'monitorcat_settings', array() );
		$mc_settings = is_array( $mc_settings ) ? $mc_settings : array();
		$auto_meta   = $mc_settings['seo']['auto_meta_tags'] ?? true;

		if ( $auto_meta && ! defined( 'WPSEO_VERSION' ) && ! defined( 'AIOSEO_VERSION' ) && ! defined( 'RANK_MATH_VERSION' ) ) {
			$content = get_post_field( 'post_content', $post_id );
			$content = is_string( $content ) ? $content : '';
			$text    = wp_strip_all_tags( $content );
			$text    = trim( (string) preg_replace( '/\s+/', ' ', $text ) );

			if ( ! empty( $text ) ) {
				return mb_strlen( $text ) > 160 ? mb_substr( $text, 0, 157 ) . '...' : $text;
			}
		}

		return '';
	}

	/**
	 * 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( 20, 20, 25, 20, 15 );
		$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';
	}
}
