<?php
/**
 * SEO Assessment Engine
 *
 * Runs per-post SEO checks and returns structured results with
 * status, label, and actionable messages. Used by both the SEO Bar
 * column (compact view) and the meta box assessment panel (detailed view).
 *
 * @package SEOvaultSEO
 */

namespace SEOvaultSEO\Utils;

use SEOvaultSEO\Core\PostMeta;

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

class SeoAssessment {

	const STATUS_GOOD    = 'good';
	const STATUS_OKAY    = 'okay';
	const STATUS_BAD     = 'bad';
	const STATUS_UNKNOWN = 'unknown';

	/**
	 * Per-request assessment cache for posts.
	 *
	 * @var array<int, array>
	 */
	private static $post_cache = array();

	/**
	 * Per-request assessment cache for terms.
	 *
	 * @var array<int, array>
	 */
	private static $term_cache = array();

	/**
	 * Clear per-request assessment caches.
	 *
	 * @return void
	 */
	public static function clear_cache() {
		self::$post_cache = array();
		self::$term_cache = array();
	}

	/**
	 * Clear cached assessment for a single term.
	 *
	 * @param int $term_id Term ID.
	 * @return void
	 */
	public static function clear_term_cache( $term_id ) {
		unset( self::$term_cache[ (int) $term_id ] );
	}

	/**
	 * Run all assessments for a given post ID.
	 *
	 * @param int $post_id Post ID.
	 * @return array {
	 *     @type string $overall  Overall status: good|okay|bad|unknown.
	 *     @type array  $checks   Array of individual check results.
	 * }
	 */
	public static function assess_post( $post_id ) {
		$post_id = (int) $post_id;
		if ( isset( self::$post_cache[ $post_id ] ) ) {
			return self::$post_cache[ $post_id ];
		}

		$post = get_post( $post_id );
		if ( ! $post ) {
			$result = array(
				'overall' => self::STATUS_UNKNOWN,
				'checks'  => array(),
			);
			self::$post_cache[ $post_id ] = $result;
			return $result;
		}

		$title       = PostMeta::get_seo_title( $post_id );
		$description = PostMeta::get_seo_description( $post_id );
		$keyword     = PostMeta::get_focus_keyword( $post_id );
		$noindex     = PostMeta::is_noindex( $post_id );
		$nofollow    = PostMeta::is_nofollow( $post_id );
		$canonical   = PostMeta::get_canonical_url( $post_id );

		// Fall back to post title if SEO title is empty (only in auto mode).
		if ( $title !== '' ) {
			$effective_title = $title;
		} elseif ( \SEOvaultSEO\Compatibility\Detector::is_auto_generation() ) {
			$effective_title = $post->post_title;
		} else {
			$effective_title = '';
		}

		$checks = array();

		$checks[] = self::check_title( $effective_title, $title === '' );
		$checks[] = self::check_description( $description );
		$checks[] = self::check_keyword( $keyword, $effective_title, $description );
		$checks[] = self::check_indexing( $noindex );
		$checks[] = self::check_following( $nofollow );
		$checks[] = self::check_canonical( $canonical, $post_id );
		$checks[] = self::check_branding( $effective_title );
		$checks[] = self::check_redirect( PostMeta::get_redirect_url( $post_id ) );

		$overall = self::compute_overall( $checks, $noindex );

		$result = array(
			'overall' => $overall,
			'checks'  => $checks,
		);
		self::$post_cache[ $post_id ] = $result;
		return $result;
	}

	/**
	 * Run assessments for a term ID.
	 *
	 * @param int $term_id Term ID.
	 * @return array
	 */
	public static function assess_term( $term_id ) {
		$term_id = (int) $term_id;
		if ( isset( self::$term_cache[ $term_id ] ) ) {
			return self::$term_cache[ $term_id ];
		}

		$title       = get_term_meta( $term_id, '_seovault_seo_title', true );
		$description = get_term_meta( $term_id, '_seovault_seo_description', true );
		$noindex     = (bool) get_term_meta( $term_id, '_seovault_robots_noindex', true );

		$term            = get_term( $term_id );
		$effective_title = $title !== '' ? $title : ( $term && ! is_wp_error( $term ) ? $term->name : '' );

		$checks = array();

		$checks[] = self::check_title( $effective_title, $title === '' );
		$checks[] = self::check_description( $description );
		$checks[] = self::check_indexing( $noindex );

		$overall = self::compute_overall( $checks, $noindex );

		$result = array(
			'overall' => $overall,
			'checks'  => $checks,
		);
		self::$term_cache[ $term_id ] = $result;
		return $result;
	}

	/**
	 * Check title length and quality.
	 *
	 * @param string $title      Effective title (with fallback applied).
	 * @param bool   $is_fallback Whether title fell back to post title.
	 * @return array
	 */
	public static function check_title( $title, $is_fallback = false ) {
		$limits = Guidelines::get_title_limits();
		$length = mb_strlen( $title );
		$label  = Guidelines::length_label( $length, $limits );
		$pixels = Guidelines::estimate_pixel_width( $title );
		$trunc  = Guidelines::title_likely_truncated( $title );

		$status  = self::STATUS_GOOD;
		$message = sprintf(
			/* translators: 1: character count, 2: pixel estimate */
			__( '%1$d chars (~%2$dpx).', 'seovault-ai' ),
			$length,
			(int) $pixels
		);

		if ( 'empty' === $label ) {
			$status  = self::STATUS_BAD;
			$message = __( 'No title set. Add an SEO title for better search visibility.', 'seovault-ai' );
		} elseif ( 'too-short' === $label ) {
			$status  = self::STATUS_BAD;
			$message = sprintf(
				/* translators: 1: current character count, 2: recommended minimum, 3: recommended maximum */
				__( 'Title is %1$d chars — too short. Aim for %2$d–%3$d characters.', 'seovault-ai' ),
				$length,
				$limits['ideal_min'],
				$limits['ideal_max']
			);
		} elseif ( 'short' === $label ) {
			$status  = self::STATUS_OKAY;
			$message = sprintf(
				/* translators: 1: current character count, 2: recommended minimum, 3: recommended maximum */
				__( 'Title is %1$d chars. Could use more detail — aim for %2$d–%3$d characters.', 'seovault-ai' ),
				$length,
				$limits['ideal_min'],
				$limits['ideal_max']
			);
		} elseif ( 'long' === $label ) {
			$status  = self::STATUS_OKAY;
			$message = sprintf(
				/* translators: 1: current character count, 2: recommended minimum, 3: recommended maximum */
				__( 'Title is %1$d chars — slightly long. Google may truncate it. Aim for %2$d–%3$d characters.', 'seovault-ai' ),
				$length,
				$limits['ideal_min'],
				$limits['ideal_max']
			);
		} elseif ( 'too-long' === $label ) {
			$status  = self::STATUS_BAD;
			$message = sprintf(
				/* translators: 1: current character count, 2: maximum recommended characters */
				__( 'Title is %1$d chars — too long. Google will likely truncate it. Keep it under %2$d characters.', 'seovault-ai' ),
				$length,
				$limits['max']
			);
		}

		if ( $trunc && self::STATUS_GOOD === $status ) {
			$status   = self::STATUS_OKAY;
			$message .= ' ' . __( 'Pixel width suggests Google may truncate this title.', 'seovault-ai' );
		}

		if ( $is_fallback && $length > 0 ) {
			$message .= ' ' . __( 'Note: using post title as fallback — set a custom SEO title for more control.', 'seovault-ai' );
		}

		return array(
			'id'      => 'title',
			'label'   => __( 'Title', 'seovault-ai' ),
			'status'  => $status,
			'message' => $message,
			'length'  => $length,
		);
	}

	/**
	 * Check description length and quality.
	 *
	 * @param string $description SEO description.
	 * @return array
	 */
	public static function check_description( $description ) {
		$limits = Guidelines::get_description_limits();
		$length = mb_strlen( $description );
		$label  = Guidelines::length_label( $length, $limits );

		$status  = self::STATUS_GOOD;
		$message = sprintf(
			/* translators: %d: character count */
			__( '%d chars.', 'seovault-ai' ),
			$length
		);

		if ( 'empty' === $label ) {
			$status  = self::STATUS_BAD;
			$message = __( 'No description set. Google will auto-generate one from your content.', 'seovault-ai' );
		} elseif ( 'too-short' === $label ) {
			$status  = self::STATUS_BAD;
			$message = sprintf(
				/* translators: 1: current character count, 2: recommended minimum, 3: recommended maximum */
				__( 'Description is %1$d chars — too short. Aim for %2$d–%3$d characters.', 'seovault-ai' ),
				$length,
				$limits['ideal_min'],
				$limits['ideal_max']
			);
		} elseif ( 'short' === $label ) {
			$status  = self::STATUS_OKAY;
			$message = sprintf(
				/* translators: 1: current character count, 2: recommended minimum, 3: recommended maximum */
				__( 'Description is %1$d chars. Add more detail — aim for %2$d–%3$d characters.', 'seovault-ai' ),
				$length,
				$limits['ideal_min'],
				$limits['ideal_max']
			);
		} elseif ( 'long' === $label ) {
			$status  = self::STATUS_OKAY;
			$message = sprintf(
				/* translators: 1: current character count, 2: recommended minimum, 3: recommended maximum */
				__( 'Description is %1$d chars — slightly long. Google may truncate it. Aim for %2$d–%3$d characters.', 'seovault-ai' ),
				$length,
				$limits['ideal_min'],
				$limits['ideal_max']
			);
		} elseif ( 'too-long' === $label ) {
			$status  = self::STATUS_BAD;
			$message = sprintf(
				/* translators: 1: current character count, 2: maximum recommended characters */
				__( 'Description is %1$d chars — too long. Google will truncate it. Keep it under %2$d characters.', 'seovault-ai' ),
				$length,
				$limits['max']
			);
		}

		return array(
			'id'      => 'description',
			'label'   => __( 'Description', 'seovault-ai' ),
			'status'  => $status,
			'message' => $message,
			'length'  => $length,
		);
	}

	/**
	 * Check focus keyword presence in title and description.
	 *
	 * @param string $keyword     Focus keyword.
	 * @param string $title       Effective title.
	 * @param string $description SEO description.
	 * @return array
	 */
	public static function check_keyword( $keyword, $title, $description ) {
		if ( '' === $keyword ) {
			return array(
				'id'      => 'keyword',
				'label'   => __( 'Focus Keyword', 'seovault-ai' ),
				'status'  => self::STATUS_BAD,
				'message' => __( 'No focus keyword set. Add one to track your target term.', 'seovault-ai' ),
				'length'  => 0,
			);
		}

		$kw_lower    = mb_strtolower( $keyword );
		$title_lower = mb_strtolower( $title );
		$desc_lower  = mb_strtolower( $description );

		$in_title = mb_strpos( $title_lower, $kw_lower ) !== false;
		$in_desc  = mb_strpos( $desc_lower, $kw_lower ) !== false;

		if ( $in_title && $in_desc ) {
			$status  = self::STATUS_GOOD;
			$message = __( 'Focus keyword appears in both title and description.', 'seovault-ai' );
		} elseif ( $in_title ) {
			$status  = self::STATUS_OKAY;
			$message = __( 'Focus keyword found in title but not in description.', 'seovault-ai' );
		} elseif ( $in_desc ) {
			$status  = self::STATUS_OKAY;
			$message = __( 'Focus keyword found in description but not in title.', 'seovault-ai' );
		} else {
			$status  = self::STATUS_BAD;
			$message = __( 'Focus keyword not found in title or description.', 'seovault-ai' );
		}

		return array(
			'id'      => 'keyword',
			'label'   => __( 'Focus Keyword', 'seovault-ai' ),
			'status'  => $status,
			'message' => $message,
			'length'  => mb_strlen( $keyword ),
		);
	}

	/**
	 * Check live page-builder body content.
	 *
	 * This check is added only when a live editor explicitly supplies content,
	 * so the existing classic metabox assessment remains unchanged.
	 *
	 * @param string $content Raw editor HTML or text.
	 * @param string $keyword Optional focus keyword.
	 * @return array
	 */
	public static function check_content( $content, $keyword = '' ) {
		$text  = Formatter::clean_text( (string) $content );
		$words = '' === $text ? array() : preg_split( '/\s+/u', trim( $text ) );
		$count = is_array( $words ) ? count( array_filter( $words, 'strlen' ) ) : 0;

		if ( 0 === $count ) {
			return array(
				'id'      => 'content',
				'label'   => __( 'Live Content', 'seovault-ai' ),
				'status'  => self::STATUS_BAD,
				'message' => __( 'No editable page content was detected.', 'seovault-ai' ),
				'length'  => 0,
			);
		}

		$keyword_found = '' === $keyword || false !== mb_strpos( mb_strtolower( $text ), mb_strtolower( $keyword ) );

		if ( ! $keyword_found ) {
			$status  = self::STATUS_BAD;
			$message = sprintf(
				/* translators: %d: number of words detected in live page-builder content */
				__( '%d words detected. The focus keyword is not present in the page content.', 'seovault-ai' ),
				$count
			);
		} elseif ( $count < 50 ) {
			$status  = self::STATUS_BAD;
			$message = sprintf(
				/* translators: %d: number of words detected in live page-builder content */
				__( '%d words detected. Add more useful page content.', 'seovault-ai' ),
				$count
			);
		} elseif ( $count < 300 ) {
			$status  = self::STATUS_OKAY;
			$message = sprintf(
				/* translators: %d: number of words detected in live page-builder content */
				__( '%d words detected in the live page content.', 'seovault-ai' ),
				$count
			);
		} else {
			$status  = self::STATUS_GOOD;
			$message = sprintf(
				/* translators: %d: number of words detected in live page-builder content */
				__( '%d words detected in the live page content.', 'seovault-ai' ),
				$count
			);
		}

		return array(
			'id'      => 'content',
			'label'   => __( 'Live Content', 'seovault-ai' ),
			'status'  => $status,
			'message' => $message,
			'length'  => $count,
		);
	}

	/**
	 * Check indexing state.
	 *
	 * @param bool $noindex Whether noindex is set.
	 * @return array
	 */
	public static function check_indexing( $noindex ) {
		if ( $noindex ) {
			return array(
				'id'      => 'indexing',
				'label'   => __( 'Indexing', 'seovault-ai' ),
				'status'  => self::STATUS_BAD,
				'message' => __( 'This post is set to noindex — search engines are told not to index it.', 'seovault-ai' ),
				'length'  => 0,
			);
		}

		return array(
			'id'      => 'indexing',
			'label'   => __( 'Indexing', 'seovault-ai' ),
			'status'  => self::STATUS_GOOD,
			'message' => __( 'Post is indexable by search engines.', 'seovault-ai' ),
			'length'  => 0,
		);
	}

	/**
	 * Check following state.
	 *
	 * @param bool $nofollow Whether nofollow is set.
	 * @return array
	 */
	public static function check_following( $nofollow ) {
		if ( $nofollow ) {
			return array(
				'id'      => 'following',
				'label'   => __( 'Following', 'seovault-ai' ),
				'status'  => self::STATUS_OKAY,
				'message' => __( 'Nofollow is enabled — search engines won\'t follow links on this post.', 'seovault-ai' ),
				'length'  => 0,
			);
		}

		return array(
			'id'      => 'following',
			'label'   => __( 'Following', 'seovault-ai' ),
			'status'  => self::STATUS_GOOD,
			'message' => __( 'Links on this post are followable.', 'seovault-ai' ),
			'length'  => 0,
		);
	}

	/**
	 * Check canonical URL state.
	 *
	 * @param string $canonical Custom canonical URL.
	 * @param int    $post_id   Post ID.
	 * @return array
	 */
	public static function check_canonical( $canonical, $post_id ) {
		if ( '' === $canonical ) {
			return array(
				'id'      => 'canonical',
				'label'   => __( 'Canonical', 'seovault-ai' ),
				'status'  => self::STATUS_GOOD,
				'message' => __( 'Using default permalink as canonical URL.', 'seovault-ai' ),
				'length'  => 0,
			);
		}

		$permalink = get_permalink( $post_id );
		if ( $canonical === $permalink ) {
			return array(
				'id'      => 'canonical',
				'label'   => __( 'Canonical', 'seovault-ai' ),
				'status'  => self::STATUS_GOOD,
				'message' => __( 'Canonical URL matches permalink.', 'seovault-ai' ),
				'length'  => mb_strlen( $canonical ),
			);
		}

		return array(
			'id'      => 'canonical',
			'label'   => __( 'Canonical', 'seovault-ai' ),
			'status'  => self::STATUS_OKAY,
			'message' => __( 'Custom canonical URL set — this post points to a different URL.', 'seovault-ai' ),
			'length'  => mb_strlen( $canonical ),
		);
	}

	/**
	 * Check if the site name/brand appears in the title.
	 *
	 * @param string $title Effective title.
	 * @return array
	 */
	public static function check_branding( $title ) {
		$site_name = get_bloginfo( 'name' );

		if ( '' === $site_name || '' === $title ) {
			return array(
				'id'      => 'branding',
				'label'   => __( 'Branding', 'seovault-ai' ),
				'status'  => self::STATUS_UNKNOWN,
				'message' => __( 'Unable to check branding — no site name set or title is empty.', 'seovault-ai' ),
				'length'  => 0,
			);
		}

		$site_name_lower = mb_strtolower( $site_name );
		$title_lower     = mb_strtolower( $title );

		if ( mb_strpos( $title_lower, $site_name_lower ) !== false ) {
			// Check for duplicate branding (site name appearing more than once).
			$occurrences = substr_count( $title_lower, $site_name_lower );
			if ( $occurrences > 1 ) {
				return array(
					'id'      => 'branding',
					'label'   => __( 'Branding', 'seovault-ai' ),
					'status'  => self::STATUS_OKAY,
					'message' => __( 'Site name appears multiple times in the title — consider removing duplicates.', 'seovault-ai' ),
					'length'  => 0,
				);
			}

			return array(
				'id'      => 'branding',
				'label'   => __( 'Branding', 'seovault-ai' ),
				'status'  => self::STATUS_GOOD,
				'message' => __( 'Site name is present in the title.', 'seovault-ai' ),
				'length'  => 0,
			);
		}

		return array(
			'id'      => 'branding',
			'label'   => __( 'Branding', 'seovault-ai' ),
			'status'  => self::STATUS_OKAY,
			'message' => __( 'Site name not found in title — adding it can improve brand recognition in SERPs.', 'seovault-ai' ),
			'length'  => 0,
		);
	}

	/**
	 * Check if a redirect is set on this post, which would prevent indexing.
	 *
	 * @param string $redirect_url Redirect URL.
	 * @return array
	 */
	public static function check_redirect( $redirect_url ) {
		if ( '' === $redirect_url ) {
			return array(
				'id'      => 'redirect',
				'label'   => __( 'Redirect', 'seovault-ai' ),
				'status'  => self::STATUS_GOOD,
				'message' => __( 'No redirect set — this post is directly accessible.', 'seovault-ai' ),
				'length'  => 0,
			);
		}

		return array(
			'id'      => 'redirect',
			'label'   => __( 'Redirect', 'seovault-ai' ),
			'status'  => self::STATUS_BAD,
			'message' => __( 'A redirect is set on this post — visitors will be forwarded and this URL won\'t be indexed.', 'seovault-ai' ),
			'length'  => mb_strlen( $redirect_url ),
		);
	}

	/**
	 * Compute overall status from individual checks.
	 *
	 * @param array $checks  Array of check results.
	 * @param bool  $noindex Whether noindex overrides everything.
	 * @return string
	 */
	private static function compute_overall( $checks, $noindex = false ) {
		if ( $noindex ) {
			return self::STATUS_BAD;
		}

		$has_bad  = false;
		$has_okay = false;

		foreach ( $checks as $check ) {
			if ( self::STATUS_BAD === $check['status'] ) {
				$has_bad = true;
			} elseif ( self::STATUS_OKAY === $check['status'] ) {
				$has_okay = true;
			}
		}

		if ( $has_bad ) {
			return self::STATUS_BAD;
		}
		if ( $has_okay ) {
			return self::STATUS_OKAY;
		}
		return self::STATUS_GOOD;
	}

	/**
	 * Map assessment status to SEO Bar badge status.
	 *
	 * @param string $assessment_status good|okay|bad|unknown.
	 * @return string good|ok|warning|empty|noindex
	 */
	public static function to_bar_status( $assessment_status ) {
		$map = array(
			self::STATUS_GOOD    => 'good',
			self::STATUS_OKAY    => 'ok',
			self::STATUS_BAD     => 'warning',
			self::STATUS_UNKNOWN => 'empty',
		);

		return $map[ $assessment_status ] ?? 'empty';
	}
}
