<?php
/**
 * Audit Scorer.
 *
 * Turns a post's on-page health + Search Console signals into a single
 * recommendation (Keep / Refresh / Merge / Delete) plus a plain-English
 * reason and a 0-100 health score for sorting.
 *
 * The logic is deliberately transparent and conservative: it only flags a
 * post for deletion when it is thin, old, AND earns essentially no search
 * traffic. Everything is a recommendation — nothing here deletes anything.
 *
 * @package Platform\Helm\Modules\Content_Auditor
 */

declare( strict_types = 1 );

namespace Platform\Helm\Modules\Content_Auditor;

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

final class AuditScorer {

	public const KEEP    = 'keep';
	public const REFRESH = 'refresh';
	public const MERGE   = 'merge';
	public const DELETE  = 'delete';

	// Thresholds (tweakable in one place).
	private const THIN_WORDS         = 300;
	private const SHORT_WORDS        = 600;
	private const MERGE_WORDS        = 400;
	private const OLD_DAYS           = 365;
	private const VERY_OLD_DAYS      = 730;
	private const STRIKING_MIN_POS   = 5.0;
	private const STRIKING_MAX_POS   = 20.0;
	private const LOW_CTR            = 0.02;
	private const MIN_IMPR_REFRESH   = 50;
	private const MIN_IMPR_LOW_CTR   = 100;

	/**
	 * @param array{words:int,has_image:bool,has_meta:bool,age_days:int,clicks:int,impressions:int,ctr:float,position:float} $m
	 * @return array{recommendation:string,score:int,reasons:string}
	 */
	public static function evaluate( array $m ): array {
		$words   = (int) $m['words'];
		$clicks  = (int) $m['clicks'];
		$impr    = (int) $m['impressions'];
		$ctr     = (float) $m['ctr'];
		$pos     = (float) $m['position'];
		$age     = (int) $m['age_days'];
		$has_img = (bool) $m['has_image'];
		$has_meta = (bool) $m['has_meta'];

		$rec     = self::KEEP;
		$reasons = [];

		if ( $words < self::THIN_WORDS && 0 === $clicks && $impr < 10 && $age > self::OLD_DAYS ) {
			$rec       = self::DELETE;
			$reasons[] = sprintf( 'Thin (%dw), no clicks, negligible impressions, not updated in over a year. De-index + 301-redirect, then remove.', $words );
		} elseif ( $impr >= self::MIN_IMPR_REFRESH && $pos >= self::STRIKING_MIN_POS && $pos <= self::STRIKING_MAX_POS ) {
			$rec       = self::REFRESH;
			$reasons[] = sprintf( 'Striking distance: ranks ~#%.0f with %s impressions. On-page tweaks could push it onto page 1.', $pos, number_format_i18n( $impr ) );
		} elseif ( $impr >= self::MIN_IMPR_LOW_CTR && $ctr < self::LOW_CTR ) {
			$rec       = self::REFRESH;
			$reasons[] = sprintf( 'High impressions (%s) but low CTR (%.1f%%). Rewrite the title/meta to win more clicks.', number_format_i18n( $impr ), $ctr * 100 );
		} elseif ( $clicks >= 5 && ( $age > self::VERY_OLD_DAYS || $words < self::SHORT_WORDS ) ) {
			$rec       = self::REFRESH;
			$reasons[] = ( $age > self::VERY_OLD_DAYS )
				? sprintf( 'Still earns %s clicks but is %d+ years old. Refresh and re-date.', number_format_i18n( $clicks ), (int) floor( $age / 365 ) )
				: sprintf( 'Earns %s clicks on only %dw. Expand it into a stronger piece.', number_format_i18n( $clicks ), $words );
		} elseif ( $words < self::MERGE_WORDS && $clicks < 5 && $impr >= 10 ) {
			$rec       = self::MERGE;
			$reasons[] = sprintf( 'Thin (%dw) with minor impressions. Merge into a stronger related post and redirect.', $words );
		} else {
			$rec       = self::KEEP;
			$reasons[] = ( $clicks >= 5 || ( $words >= self::SHORT_WORDS && $has_img ) )
				? 'Healthy and performing — leave as is.'
				: 'Low signal but not a removal candidate — monitor.';
		}

		// Secondary health flags (apply to any bucket).
		if ( ! $has_img ) {
			$reasons[] = 'No featured image.';
		}
		if ( ! $has_meta ) {
			$reasons[] = 'Missing SEO meta description.';
		}

		return [
			'recommendation' => $rec,
			'score'          => self::health_score( $m ),
			'reasons'        => implode( ' ', $reasons ),
		];
	}

	/** 0-100 health score (higher = healthier), used for sorting. */
	private static function health_score( array $m ): int {
		$score  = 0.0;
		$score += min( 60, (int) $m['clicks'] * 2 );
		$score += min( 15, (int) $m['impressions'] * 0.05 );
		$score += min( 12, min( (int) $m['words'], 1500 ) * 0.008 );
		$score += $m['has_image'] ? 8 : 0;
		$score += $m['has_meta'] ? 5 : 0;
		$age    = (int) $m['age_days'];
		$score += $age < 180 ? 8 : ( $age < 365 ? 4 : 0 );
		return (int) max( 0, min( 100, round( $score ) ) );
	}
}
