<?php
/**
 * SEO analysis service.
 *
 * @package SmartyPress_AI_Engine
 * @since 1.0.0
 */

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

/**
 * Creates and normalizes the full-context SEO analysis.
 */
class SmartyPress_AI_SEO_Analysis_Service extends SmartyPress_AI_SEO_Controller_Service_Base {

	/**
	 * Analysis version.
	 *
	 * @var string
	 */
	private $analysis_version;

	/**
	 * Context TTL in seconds.
	 *
	 * @var int
	 */
	private $context_ttl;

	/**
	 * Prompt builder.
	 *
	 * @var SmartyPress_AI_SEO_Fix_Prompt_Builder
	 */
	private $prompt_builder;

	/**
	 * AI JSON generator.
	 *
	 * @var SmartyPress_AI_SEO_AI_JSON_Generator
	 */
	private $json_generator;

	/**
	 * Constructor.
	 *
	 * @param string                                  $analysis_version Analysis version.
	 * @param int                                     $context_ttl      Context TTL.
	 * @param SmartyPress_AI_SEO_Fix_Prompt_Builder   $prompt_builder   Prompt builder.
	 * @param SmartyPress_AI_SEO_AI_JSON_Generator    $json_generator   JSON generator.
	 */
	public function __construct( $analysis_version, $context_ttl, $prompt_builder, $json_generator ) {
		$this->analysis_version = (string) $analysis_version;
		$this->context_ttl      = absint( $context_ttl );
		$this->prompt_builder   = $prompt_builder;
		$this->json_generator   = $json_generator;
	}

	/**
	 * Get cached analysis or create it with a full-context AI request.
	 *
	 * @param string $context_id  Context transient key.
	 * @param array  $context     Cached context.
	 * @param object $provider    AI provider.
	 * @param string $language    Language.
	 * @param string $model       Model.
	 * @param float  $temperature Temperature.
	 * @return array|WP_Error
	 */
	public function get_or_create_analysis( $context_id, $context, $provider, $language, $model, $temperature ) {
		$analysis_id = $context_id . '_analysis';
		$analysis    = get_transient( $analysis_id );

		if ( is_array( $analysis ) && ( $analysis['analysis_version'] ?? '' ) === $this->analysis_version ) {
			return $analysis;
		}

		$prompt = $this->prompt_builder->build_analysis_prompt( $context );
		$response = $this->json_generator->generate_json(
			$provider,
			$prompt,
			$language,
			$model,
			$temperature,
			$this->get_analysis_response_schema(),
			'seo_fix_analysis',
			$context
		);

		if ( is_wp_error( $response ) ) {
			return $response;
		}

		$analysis = $this->normalize_analysis( $response, $context );
		if ( '' === (string) ( $analysis['primary_keyword'] ?? '' ) ) {
			$analysis = $this->recover_primary_keyword( $analysis, $response, $context, $provider, $language, $model, $temperature );
		}
		set_transient( $analysis_id, $analysis, $this->context_ttl );

		return $analysis;
	}

	/**
	 * Get analysis JSON schema.
	 *
	 * @return array
	 */
	public function get_analysis_response_schema() {
		return array(
			'type'                 => 'object',
			'additionalProperties' => false,
			'required'             => array( 'topic', 'search_intent', 'content_summary', 'primary_keyword', 'secondary_keywords', 'keyword_usage', 'content_map', 'fix_strategy', 'recommendations', 'manual_only' ),
			'properties'           => array(
				'topic'              => array( 'type' => 'string' ),
				'search_intent'      => array( 'type' => 'string' ),
				'content_summary'    => array( 'type' => 'string' ),
				'primary_keyword'    => array( 'type' => 'string' ),
				'secondary_keywords' => array( 'type' => 'array', 'items' => array( 'type' => 'string' ) ),
				'keyword_usage'      => array( 'type' => 'object' ),
				'content_map'        => array(
					'type'       => 'object',
					'properties' => array(
						'intro_block_id'           => array( 'type' => 'string' ),
						'first_heading_block_id'   => array( 'type' => 'string' ),
						'heading_candidates'       => array( 'type' => 'array', 'items' => array( 'type' => 'string' ) ),
						'density_candidate_blocks' => array( 'type' => 'array', 'items' => array( 'type' => 'string' ) ),
						'toc_available'            => array( 'type' => 'boolean' ),
						'toc_reason'               => array( 'type' => 'string' ),
						'toc_target_block_id'      => array( 'type' => 'string' ),
					),
				),
				'fix_strategy'       => array( 'type' => 'array', 'items' => array( 'type' => 'object' ) ),
				'recommendations'    => array( 'type' => 'array', 'items' => array( 'type' => 'object' ) ),
				'manual_only'        => array( 'type' => 'array', 'items' => array( 'type' => 'object' ) ),
			),
		);
	}

	/**
	 * Normalize a full-context analysis response.
	 *
	 * @param array $analysis Raw analysis.
	 * @param array $context  Cached context.
	 * @return array
	 */
	public function normalize_analysis( $analysis, $context ) {
		$keyword_usage = is_array( $analysis['keyword_usage'] ?? null ) ? $analysis['keyword_usage'] : array();
		$content_map   = is_array( $analysis['content_map'] ?? null ) ? $analysis['content_map'] : array();
		$post_text     = (string) ( $context['post']['content_plain_text'] ?? '' );
		$warnings      = array();
		$primary       = sanitize_text_field( $analysis['primary_keyword'] ?? '' );
		$secondary     = array_values( array_filter( array_map( 'sanitize_text_field', (array) ( $analysis['secondary_keywords'] ?? array() ) ) ) );

		if ( ! $this->is_valid_ai_keyword( $primary, $post_text ) ) {
			$warnings[] = sprintf(
				/* translators: %s: rejected primary keyword. */
				__( 'Primary keyword from SEO analysis was rejected because it must be an exact 2-3 word phrase from the article. Rejected value: %s', 'smartypress-ai-engine' ),
				'' !== $primary ? $primary : __( 'empty', 'smartypress-ai-engine' )
			);
			$primary    = '';
		}

		$secondary = array_values(
			array_filter(
				$secondary,
				function( $keyword ) use ( $post_text ) {
					return $this->is_valid_ai_keyword( $keyword, $post_text );
				}
			)
		);

		return array(
			'analysis_version'    => $this->analysis_version,
			'topic'               => sanitize_text_field( $analysis['topic'] ?? '' ),
			'search_intent'       => sanitize_key( $analysis['search_intent'] ?? 'informational' ),
			'content_summary'     => sanitize_textarea_field( $analysis['content_summary'] ?? '' ),
			'primary_keyword'     => $primary,
			'secondary_keywords'  => $this->normalize_keyword_list( $primary, $secondary, 4 ),
			'analysis_warnings'   => $warnings,
			'keyword_usage'       => array(
				'overused_phrases' => array_values( array_filter( array_map( 'sanitize_text_field', (array) ( $keyword_usage['overused_phrases'] ?? array() ) ) ) ),
				'safe_to_add'      => array_values( array_filter( array_map( 'sanitize_text_field', (array) ( $keyword_usage['safe_to_add'] ?? array() ) ) ) ),
				'avoid_repeating'  => array_values( array_filter( array_map( 'sanitize_text_field', (array) ( $keyword_usage['avoid_repeating'] ?? array() ) ) ) ),
			),
			'content_map'         => array(
				'intro_block_id'           => sanitize_text_field( $content_map['intro_block_id'] ?? '' ),
				'first_heading_block_id'   => sanitize_text_field( $content_map['first_heading_block_id'] ?? $content_map['toc_target_block_id'] ?? '' ),
				'heading_candidates'       => array_values( array_filter( array_map( 'sanitize_text_field', (array) ( $content_map['heading_candidates'] ?? array() ) ) ) ),
				'density_candidate_blocks' => array_values( array_filter( array_map( 'sanitize_text_field', (array) ( $content_map['density_candidate_blocks'] ?? array() ) ) ) ),
				'toc_available'            => ! empty( $content_map['toc_available'] ),
				'toc_reason'               => sanitize_text_field( $content_map['toc_reason'] ?? '' ),
				'toc_target_block_id'      => sanitize_text_field( $content_map['toc_target_block_id'] ?? $content_map['first_heading_block_id'] ?? '' ),
			),
			'fix_strategy'        => is_array( $analysis['fix_strategy'] ?? null ) ? $this->sanitize_recursive( $analysis['fix_strategy'] ) : array(),
			'recommendations'     => $this->normalize_recommendations( $analysis['recommendations'] ?? array(), $content_map ),
			'manual_only'         => is_array( $analysis['manual_only'] ?? null ) ? $this->sanitize_recursive( $analysis['manual_only'] ) : array(),
		);
	}

	/**
	 * Normalize optional AI recommendations used by the configure UI.
	 *
	 * @param array $recommendations Raw recommendations.
	 * @param array $content_map     Raw content map.
	 * @return array
	 */
	private function normalize_recommendations( $recommendations, $content_map ) {
		$normalized = array();

		foreach ( (array) $recommendations as $recommendation ) {
			$test_id = sanitize_text_field( $recommendation['testId'] ?? $recommendation['id'] ?? '' );
			$type    = sanitize_key( $recommendation['type'] ?? '' );

			if ( 'contentHasTOC' !== $test_id || 'optional_fix' !== $type || ! empty( $content_map['toc_available'] ) ) {
				continue;
			}

			$target_blocks = array_values( array_filter( array_map( 'sanitize_text_field', (array) ( $recommendation['target_blocks'] ?? array() ) ) ) );
			if ( empty( $target_blocks ) && ! empty( $content_map['toc_target_block_id'] ) ) {
				$target_blocks[] = sanitize_text_field( $content_map['toc_target_block_id'] );
			}

			$normalized[ $test_id ] = array(
				'testId'        => $test_id,
				'type'          => 'optional_fix',
				'reason'        => sanitize_key( $recommendation['reason'] ?? 'toc_recommended' ),
				'label'         => sanitize_text_field( $recommendation['label'] ?? __( 'Add a Table of Contents block', 'smartypress-ai-engine' ) ),
				'context_scope' => sanitize_key( $recommendation['context_scope'] ?? 'local' ),
				'target_blocks' => $target_blocks,
			);
		}

		return array_values( $normalized );
	}

	/**
	 * Recover a missing primary keyword with a focused keyword-selection request.
	 *
	 * @param array  $analysis    Normalized analysis.
	 * @param array  $raw_analysis Raw analysis response.
	 * @param array  $context     Cached context.
	 * @param object $provider    AI provider.
	 * @param string $language    Language.
	 * @param string $model       Model.
	 * @param float  $temperature Temperature.
	 * @return array
	 */
	private function recover_primary_keyword( $analysis, $raw_analysis, $context, $provider, $language, $model, $temperature ) {
		$payload = array(
			'context'                  => $context,
			'analysis'                 => $analysis,
			'rejected_primary_keyword' => sanitize_text_field( $raw_analysis['primary_keyword'] ?? '' ),
			'failure_reason'           => 'missing_or_invalid_primary_keyword',
		);

		$response = $this->run_keyword_selection_attempt( $payload, $provider, $language, $model, $temperature, false );
		if ( is_wp_error( $response ) || ! $this->is_valid_keyword_selection( $response, $context ) ) {
			$payload['previous_response'] = is_wp_error( $response ) ? array( 'error' => $response->get_error_message() ) : $response;
			$response = $this->run_keyword_selection_attempt( $payload, $provider, $language, $model, $temperature, true );
		}

		if ( is_wp_error( $response ) || ! $this->is_valid_keyword_selection( $response, $context ) ) {
			$analysis['analysis_warnings'][] = __( 'AI could not select a valid 2-3 word primary keyword from the article after an additional keyword-selection request.', 'smartypress-ai-engine' );
			return $analysis;
		}

		$primary   = sanitize_text_field( $response['primary_keyword'] ?? '' );
		$secondary = array_values( array_filter( array_map( 'sanitize_text_field', (array) ( $response['secondary_keywords'] ?? array() ) ) ) );

		$analysis['primary_keyword']     = $primary;
		$analysis['secondary_keywords']  = $this->normalize_keyword_list( $primary, array_merge( $secondary, (array) ( $analysis['secondary_keywords'] ?? array() ) ), 4 );
		$analysis['analysis_warnings'][] = __( 'Primary keyword was selected by an additional keyword-selection request because the full analysis returned an invalid keyword.', 'smartypress-ai-engine' );
		$analysis['keyword_selection']   = array(
			'source' => 'fallback_ai_request',
			'reason' => sanitize_text_field( $response['reason'] ?? '' ),
		);

		return $analysis;
	}

	/**
	 * Run one focused keyword-selection attempt.
	 *
	 * @param array  $payload     Prompt payload.
	 * @param object $provider    AI provider.
	 * @param string $language    Language.
	 * @param string $model       Model.
	 * @param float  $temperature Temperature.
	 * @param bool   $is_retry    Whether this is a retry request.
	 * @return array|WP_Error
	 */
	private function run_keyword_selection_attempt( $payload, $provider, $language, $model, $temperature, $is_retry ) {
		$prompt = $is_retry ? $this->prompt_builder->build_keyword_selection_retry_prompt( $payload ) : $this->prompt_builder->build_keyword_selection_prompt( $payload );

		return $this->json_generator->generate_json(
			$provider,
			$prompt,
			$language,
			$model,
			$temperature,
			$this->get_keyword_selection_response_schema(),
			$is_retry ? 'seo_keyword_selection_retry' : 'seo_keyword_selection',
			$payload['context'] ?? array()
		);
	}

	/**
	 * Get keyword-selection JSON schema.
	 *
	 * @return array
	 */
	private function get_keyword_selection_response_schema() {
		return array(
			'type'                 => 'object',
			'additionalProperties' => false,
			'required'             => array( 'primary_keyword', 'secondary_keywords', 'reason' ),
			'properties'           => array(
				'primary_keyword'    => array( 'type' => 'string' ),
				'secondary_keywords' => array( 'type' => 'array', 'items' => array( 'type' => 'string' ) ),
				'reason'             => array( 'type' => 'string' ),
			),
		);
	}

	/**
	 * Validate focused keyword-selection response.
	 *
	 * @param array $response Keyword-selection response.
	 * @param array $context  Cached context.
	 * @return bool
	 */
	private function is_valid_keyword_selection( $response, $context ) {
		if ( ! is_array( $response ) ) {
			return false;
		}

		return $this->is_valid_ai_keyword(
			$response['primary_keyword'] ?? '',
			(string) ( $context['post']['content_plain_text'] ?? '' )
		);
	}

	/**
	 * Validate AI-selected keyword without language-specific stop word lists.
	 *
	 * @param string $keyword Keyword.
	 * @param string $post_text Article text.
	 * @return bool
	 */
	private function is_valid_ai_keyword( $keyword, $post_text ) {
		$keyword = sanitize_text_field( $keyword );
		if ( ! $this->is_concise_keyword( $keyword ) ) {
			return false;
		}

		return '' !== $this->normalize_match_text( $keyword );
	}
}
