<?php

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

/**
 * One canonical representation of the title and selected analysis body.
 *
 * The repository decides whether the body is stored or rendered. Every
 * downstream consumer uses this value and hash so embeddings cannot drift
 * from the content being analyzed.
 */
class Content_Miner_Analysis_Content {
	const MAX_EMBEDDING_WORDS = 4500;

	/**
	 * Build the canonical selected-content record for an inventory item.
	 *
	 * @param array<string,mixed> $item Content item.
	 * @return array{title:string,body:string,source:string,payload:string,hash:string}
	 */
	public static function from_item( array $item ) {
		$title   = isset( $item['analysis_title'] ) && '' !== trim( (string) $item['analysis_title'] )
			? (string) $item['analysis_title']
			: ( isset( $item['title'] ) ? (string) $item['title'] : '' );
		$body    = isset( $item['content'] ) ? (string) $item['content'] : '';
		$source  = isset( $item['analysis_content_source'] ) && in_array( $item['analysis_content_source'], array( 'rendered', 'stored_fallback', 'stored_nonpublic' ), true )
			? $item['analysis_content_source']
			: 'stored_fallback';
		$payload = self::bound_embedding_payload( trim( $title . "\n\n" . $body ) );

		return array(
			'title'   => $title,
			'body'    => $body,
			'source'  => $source,
			'payload' => $payload,
			'hash'    => hash( 'sha256', $payload ),
		);
	}

	/**
	 * Apply the provider safety bound once, before both hashing and sending.
	 *
	 * Payloads below the bound remain byte-for-byte unchanged so an unchanged
	 * selected body retains its existing vector.
	 */
	public static function bound_embedding_payload( $payload ) {
		$payload = (string) $payload;
		$words   = preg_split( '/\s+/u', trim( $payload ), -1, PREG_SPLIT_NO_EMPTY );
		if ( ! is_array( $words ) || count( $words ) <= self::MAX_EMBEDDING_WORDS ) {
			return $payload;
		}

		return implode( ' ', array_slice( $words, 0, self::MAX_EMBEDDING_WORDS ) );
	}
}
