<?php

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

require_once __DIR__ . '/class-content-miner-scoring-exclusions.php';

/** Select and normalize a bounded, body-free page subset for whole-report prompts. */
class Content_Miner_Prompt_Page_Evidence {
	const PAGE_CAP = 20;
	const EVIDENCE_EXCERPT_CHAR_CAP = 280;

	/**
	 * Convert compact content memory into the fields allowed in a whole-report prompt.
	 *
	 * @param int                   $post_id Post ID.
	 * @param array<string,mixed>   $memory Compact content memory.
	 * @param array<string,mixed>   $search Search Console page metrics.
	 * @param array<string,string>  $issue_labels Readiness issue labels.
	 * @param array<string,mixed>   $search_context Active Search Console property and date window.
	 * @return array<string,mixed>
	 */
	public static function from_memory( $post_id, array $memory, array $search, array $issue_labels, array $search_context = array() ) {
		$readiness = isset( $memory['readiness'] ) && is_array( $memory['readiness'] ) ? $memory['readiness'] : array();
		$scores = isset( $readiness['scores'] ) && is_array( $readiness['scores'] ) ? $readiness['scores'] : array();
		$normalized_scores = array();
		foreach ( array( 'seo', 'aeo', 'geo', 'aio' ) as $lens ) {
			$normalized_scores[ $lens ] = max( 0, min( 100, (int) ( $scores[ $lens ] ?? 0 ) ) );
		}
		$overall = isset( $readiness['overall'] )
			? (int) $readiness['overall']
			: (int) round( array_sum( $normalized_scores ) / count( $normalized_scores ) );
		$issues = isset( $readiness['issues'] ) && is_array( $readiness['issues'] ) ? $readiness['issues'] : array();
		$top_issue = isset( $issues[0] ) ? (string) $issues[0] : '';
		$analysis = isset( $memory['analysis'] ) && is_array( $memory['analysis'] ) ? $memory['analysis'] : array();
		$signals = isset( $memory['signals'] ) && is_array( $memory['signals'] ) ? $memory['signals'] : array();
		$evidence = isset( $readiness['evidence'] ) && is_array( $readiness['evidence'] ) ? $readiness['evidence'] : array();
		$direct_answer_passed = array_key_exists( 'direct_answer_passed', $evidence ) ? $evidence['direct_answer_passed'] : null;
		$input_source = isset( $analysis['content_source'] )
			? (string) $analysis['content_source']
			: ( isset( $memory['analysis_content_source'] ) ? (string) $memory['analysis_content_source'] : 'stored_fallback' );
		$verification = isset( $memory['verification'] ) && is_array( $memory['verification'] ) ? $memory['verification'] : array();
		$search_available = ! empty( $search_context['available'] )
			&& '' !== (string) ( $search_context['property'] ?? '' )
			&& '' !== (string) ( $search_context['sync_run_id'] ?? '' )
			&& '' !== (string) ( $search_context['start_date'] ?? '' )
			&& '' !== (string) ( $search_context['end_date'] ?? '' )
			&& array_key_exists( 'clicks', $search )
			&& array_key_exists( 'impressions', $search )
			&& isset( $search['property'], $search['sync_run_id'], $search['start_date'], $search['end_date'] )
			&& hash_equals( (string) $search_context['property'], (string) $search['property'] )
			&& hash_equals( (string) $search_context['sync_run_id'], (string) $search['sync_run_id'] )
			&& hash_equals( (string) $search_context['start_date'], (string) $search['start_date'] )
			&& hash_equals( (string) $search_context['end_date'], (string) $search['end_date'] );

		return array(
			'id'                       => (int) $post_id,
			'title'                    => isset( $memory['title'] ) ? (string) $memory['title'] : '',
			'path'                     => '',
			'scores'                   => $normalized_scores,
			'overall'                  => max( 0, min( 100, $overall ) ),
			'age_days'                 => max( 0, (int) ( $analysis['age_days'] ?? 0 ) ),
			'top_gap'                  => isset( $issue_labels[ $top_issue ] ) ? (string) $issue_labels[ $top_issue ] : '',
			'input_source'              => $input_source,
			'fallback_reason_code'      => isset( $verification['fallback_reason_code'] ) ? (string) $verification['fallback_reason_code'] : '',
			'evidence'                  => array(
				'author_source'        => isset( $signals['author_source'] ) ? (string) $signals['author_source'] : '',
				'published_source'     => isset( $signals['published_source'] ) ? (string) $signals['published_source'] : '',
				'modified_source'      => isset( $signals['modified_source'] ) ? (string) $signals['modified_source'] : '',
				'direct_answer_span'   => null !== $direct_answer_passed && isset( $evidence['direct_answer_span'] ) ? self::excerpt( $evidence['direct_answer_span'] ) : '',
				'direct_answer_passed' => $direct_answer_passed,
				'direct_answer_reason' => isset( $evidence['direct_answer_reason'] ) ? (string) $evidence['direct_answer_reason'] : '',
			),
			'not_measured'              => isset( $readiness['not_measured'] ) && is_array( $readiness['not_measured'] ) ? $readiness['not_measured'] : array(),
			'search_metrics_available' => $search_available,
			'clicks'                   => $search_available ? max( 0, (int) $search['clicks'] ) : 0,
			'impressions'              => $search_available ? max( 0, (int) $search['impressions'] ) : 0,
			'scored'                   => true,
		);
	}

	/**
	 * Rank scored pages and return the bounded prompt subset.
	 *
	 * Search-connected reports put pages with matched evidence first, ordered
	 * by impressions descending. Both paths then use readiness ascending, age
	 * descending, and post ID ascending for deterministic ties.
	 *
	 * @param array<int,array<string,mixed>> $pages Candidate pages.
	 * @param bool                           $search_available Whether completed site-level Search Console evidence is available.
	 * @param int                            $limit Maximum returned pages.
	 * @return array<int,array<string,mixed>>
	 */
	public static function rank( array $pages, $search_available, $limit = self::PAGE_CAP ) {
		$pages = array_values(
			array_filter(
				$pages,
				function ( $page ) {
					if ( ! is_array( $page ) ) {
						return false;
					}
					if ( array_key_exists( 'scored', $page ) && true !== $page['scored'] ) {
						return false;
					}
					return Content_Miner_Scoring_Exclusions::is_in_scope( $page );
				}
			)
		);
		usort(
			$pages,
			function ( $left, $right ) use ( $search_available ) {
				if ( $search_available ) {
					$left_evidence = ! empty( $left['search_metrics_available'] );
					$right_evidence = ! empty( $right['search_metrics_available'] );
					if ( $left_evidence !== $right_evidence ) {
						return $right_evidence <=> $left_evidence;
					}
					$impressions = (int) ( $right['impressions'] ?? 0 ) <=> (int) ( $left['impressions'] ?? 0 );
					if ( 0 !== $impressions ) {
						return $impressions;
					}
				}
				$readiness = (int) ( $left['overall'] ?? 0 ) <=> (int) ( $right['overall'] ?? 0 );
				if ( 0 !== $readiness ) {
					return $readiness;
				}
				$age = (int) ( $right['age_days'] ?? 0 ) <=> (int) ( $left['age_days'] ?? 0 );
				if ( 0 !== $age ) {
					return $age;
				}
				$id = (int) ( $left['id'] ?? 0 ) <=> (int) ( $right['id'] ?? 0 );
				if ( 0 !== $id ) {
					return $id;
				}
				return strcmp( (string) ( $left['title'] ?? '' ), (string) ( $right['title'] ?? '' ) );
			}
		);

		return array_slice( $pages, 0, min( self::PAGE_CAP, max( 0, (int) $limit ) ) );
	}

	/** Return only a public URL path, never its scheme, host, query, or fragment. */
	public static function path_from_url( $url ) {
		$path = wp_parse_url( (string) $url, PHP_URL_PATH );
		if ( ! is_string( $path ) || '' === trim( $path ) ) {
			return '/';
		}
		return '/' . ltrim( $path, '/' );
	}

	/** Return a tag-free, whitespace-normalized excerpt with a hard character cap. */
	public static function excerpt( $value, $limit = self::EVIDENCE_EXCERPT_CHAR_CAP ) {
		$value = wp_strip_all_tags( (string) $value );
		$value = trim( preg_replace( '/\s+/u', ' ', $value ) );
		$limit = max( 0, (int) $limit );
		if ( '' === $value || 0 === $limit ) {
			return '';
		}
		if ( self::strlen( $value ) <= $limit ) {
			return $value;
		}
		if ( $limit <= 3 ) {
			return self::substr( $value, 0, $limit );
		}
		return rtrim( self::substr( $value, 0, $limit - 3 ) ) . '...';
	}

	/** @return int */
	private static function strlen( $value ) {
		return function_exists( 'mb_strlen' ) ? mb_strlen( $value, 'UTF-8' ) : strlen( $value );
	}

	/** @return string */
	private static function substr( $value, $start, $length ) {
		return function_exists( 'mb_substr' ) ? mb_substr( $value, $start, $length, 'UTF-8' ) : substr( $value, $start, $length );
	}
}
