<?php
/**
 * SEO content strategy runner.
 *
 * @package SmartyPress_AI_Engine
 * @since 1.0.0
 */

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

/**
 * Runs registered content strategies for failed SEO tests.
 */
class SmartyPress_AI_SEO_Content_Strategy_Runner {

	/**
	 * Content strategy registry.
	 *
	 * @var SmartyPress_AI_SEO_Content_Strategy_Registry
	 */
	private $strategy_registry;

	/**
	 * 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 SmartyPress_AI_SEO_Content_Strategy_Registry $strategy_registry Strategy registry.
	 * @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( $strategy_registry, $prompt_builder, $json_generator ) {
		$this->strategy_registry = $strategy_registry;
		$this->prompt_builder    = $prompt_builder;
		$this->json_generator    = $json_generator;
	}

	/**
	 * Build a richer strategy payload according to its requested context scope.
	 *
	 * @param array                                 $payload  Strategy-selected payload.
	 * @param array                                 $context  Cached context.
	 * @param array                                 $analysis Full-context analysis.
	 * @param SmartyPress_AI_SEO_Strategy_Interface $strategy Strategy.
	 * @return array
	 */
	private function enrich_payload_for_scope( $payload, $context, $analysis, $strategy ) {
		$scope  = sanitize_key( $payload['context_scope'] ?? $strategy->get_context_scope() );
		$blocks = $this->flatten_blocks( $context['post']['blocks'] ?? array() );

		$payload['article_context'] = array(
			'scope'          => $scope,
			'analysis'       => array(
				'topic'           => sanitize_text_field( $analysis['topic'] ?? '' ),
				'search_intent'   => sanitize_key( $analysis['search_intent'] ?? '' ),
				'content_summary' => sanitize_textarea_field( $analysis['content_summary'] ?? '' ),
				'keyword_usage'   => $analysis['keyword_usage'] ?? array(),
				'content_map'     => $analysis['content_map'] ?? array(),
			),
			'headings'       => $this->collect_headings( $blocks ),
			'keyword_usage'  => $analysis['keyword_usage'] ?? array(),
		);

		if ( 'expanded' === $scope || 'full' === $scope ) {
			$target_ids = $this->collect_payload_block_ids( $payload );
			$payload['article_context']['neighbor_blocks'] = $this->select_neighbor_blocks( $blocks, $target_ids, 1 );
		}

		if ( 'full' === $scope ) {
			$payload['article_context']['content_plain_text'] = (string) ( $context['post']['content_plain_text'] ?? '' );
			$payload['article_context']['blocks']             = $this->compact_blocks( $blocks );
		}

		return $payload;
	}

	/**
	 * Flatten compact Gutenberg blocks while preserving reading order.
	 *
	 * @param array $blocks Blocks.
	 * @return array
	 */
	private function flatten_blocks( $blocks ) {
		$flat = array();

		foreach ( (array) $blocks as $block ) {
			if ( is_array( $block ) ) {
				$flat[] = $block;
				if ( ! empty( $block['innerBlocks'] ) ) {
					$flat = array_merge( $flat, $this->flatten_blocks( $block['innerBlocks'] ) );
				}
			}
		}

		return $flat;
	}

	/**
	 * Collect heading context for meaning-sensitive edits.
	 *
	 * @param array $blocks Flattened blocks.
	 * @return array
	 */
	private function collect_headings( $blocks ) {
		$headings = array();

		foreach ( $blocks as $block ) {
			if ( 'core/heading' !== ( $block['name'] ?? '' ) || empty( $block['text'] ) ) {
				continue;
			}

			$headings[] = array(
				'clientId' => sanitize_text_field( $block['clientId'] ?? '' ),
				'level'    => absint( $block['level'] ?? 2 ),
				'text'     => sanitize_text_field( $block['text'] ),
			);
		}

		return $headings;
	}

	/**
	 * Extract block IDs referenced by a strategy payload.
	 *
	 * @param array $payload Strategy payload.
	 * @return array
	 */
	private function collect_payload_block_ids( $payload ) {
		$ids = array();

		foreach ( array( 'target_block', 'target_block_id', 'blockClientId' ) as $key ) {
			if ( is_array( $payload[ $key ] ?? null ) && ! empty( $payload[ $key ]['clientId'] ) ) {
				$ids[] = (string) $payload[ $key ]['clientId'];
			} elseif ( ! is_array( $payload[ $key ] ?? null ) && ! empty( $payload[ $key ] ) ) {
				$ids[] = (string) $payload[ $key ];
			}
		}

		foreach ( array( 'candidate_blocks', 'headings' ) as $key ) {
			foreach ( (array) ( $payload[ $key ] ?? array() ) as $block ) {
				if ( is_array( $block ) && ! empty( $block['clientId'] ) ) {
					$ids[] = (string) $block['clientId'];
				}
			}
		}

		return array_values( array_unique( array_filter( array_map( 'sanitize_text_field', $ids ) ) ) );
	}

	/**
	 * Select target blocks and nearby blocks for expanded context.
	 *
	 * @param array $blocks     Flattened blocks.
	 * @param array $target_ids Target block IDs.
	 * @param int   $radius     Neighbor radius.
	 * @return array
	 */
	private function select_neighbor_blocks( $blocks, $target_ids, $radius ) {
		$selected = array();
		$indexes  = array();

		foreach ( $blocks as $index => $block ) {
			if ( ! empty( $block['clientId'] ) && in_array( $block['clientId'], $target_ids, true ) ) {
				$indexes[] = $index;
			}
		}

		if ( empty( $indexes ) ) {
			$indexes = range( 0, min( count( $blocks ) - 1, 5 ) );
		}

		foreach ( $indexes as $index ) {
			for ( $i = max( 0, $index - $radius ); $i <= min( count( $blocks ) - 1, $index + $radius ); $i++ ) {
				if ( isset( $blocks[ $i ] ) ) {
					$selected[ $i ] = $this->compact_block( $blocks[ $i ] );
				}
			}
		}

		return array_values( array_filter( $selected ) );
	}

	/**
	 * Compact all blocks for full-context strategy prompts.
	 *
	 * @param array $blocks Flattened blocks.
	 * @return array
	 */
	private function compact_blocks( $blocks ) {
		$compact = array();

		foreach ( $blocks as $block ) {
			$compact_block = $this->compact_block( $block );
			if ( ! empty( $compact_block ) ) {
				$compact[] = $compact_block;
			}
		}

		return $compact;
	}

	/**
	 * Keep only fields that help the AI edit content safely.
	 *
	 * @param array $block Block.
	 * @return array
	 */
	private function compact_block( $block ) {
		if ( empty( $block['clientId'] ) ) {
			return array();
		}

		return array(
			'clientId' => sanitize_text_field( $block['clientId'] ?? '' ),
			'name'     => sanitize_text_field( $block['name'] ?? '' ),
			'level'    => absint( $block['level'] ?? 0 ),
			'text'     => sanitize_textarea_field( $block['text'] ?? '' ),
		);
	}

	/**
	 * Run a single strategy AI attempt and validate the response.
	 *
	 * @param SmartyPress_AI_SEO_Strategy_Interface $strategy    Strategy.
	 * @param array                                 $payload     Strategy payload.
	 * @param array                                 $context     Cached context.
	 * @param array                                 $analysis    Analysis.
	 * @param object                                $provider    AI provider.
	 * @param string                                $language    Language.
	 * @param string                                $model       Model.
	 * @param float                                 $temperature Temperature.
	 * @param string                                $schema_name Schema name.
	 * @return array|WP_Error
	 */
	private function run_strategy_attempt( $strategy, $payload, $context, $analysis, $provider, $language, $model, $temperature, $schema_name ) {
		$prompt   = $this->prompt_builder->build_strategy_prompt( $strategy, $payload );
		$response = $this->json_generator->generate_json(
			$provider,
			$prompt,
			$language,
			$model,
			$temperature,
			$strategy->get_response_schema(),
			$schema_name,
			$context
		);

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

		return $strategy->validate_response( $response, $context, $analysis );
	}

	/**
	 * Check whether a failed keyword strategy is worth retrying with a stricter prompt.
	 *
	 * @param array $result Strategy validation result.
	 * @return bool
	 */
	private function should_retry_keyword_failure( $result ) {
		$keyword_tests = array( 'keywordIn10Percent', 'keywordDensity', 'keywordInSubheadings', 'keywordInContent' );

		return is_array( $result )
			&& 'validation_failure' === ( $result['status'] ?? '' )
			&& in_array( $result['testId'] ?? '', $keyword_tests, true )
			&& empty( $result['content_operations'] );
	}

	/**
	 * Add retry-specific instructions after keyword validation failure.
	 *
	 * @param array $payload Strategy payload.
	 * @param array $result  Previous validation result.
	 * @return array
	 */
	private function build_retry_payload( $payload, $result ) {
		$keyword = sanitize_text_field( $payload['primary_keyword'] ?? '' );

		$payload['retry_attempt']     = true;
		$payload['previous_warnings'] = array_values( array_filter( array_map( 'sanitize_text_field', (array) ( $result['warnings'] ?? array() ) ) ) );
		$payload['validation_failure'] = array(
			'failed_field'    => 'html',
			'reason'          => 'missing_exact_primary_keyword',
			'primary_keyword' => $keyword,
			'required_rule'   => 'The output must contain primary_keyword exactly as provided, character by character. Do not change word forms, endings, spelling, spacing, word order, or any character in the keyword.',
		);
		$payload['retry_instruction'] = 'The generated result failed validation because it did not contain primary_keyword as an exact substring. You must include primary_keyword exactly as provided, character by character. Do not change word forms, endings, spelling, spacing, punctuation inside the keyword, word order, or any character in the keyword. The output must contain a 100% exact occurrence of primary_keyword. If the sentence sounds awkward, rewrite the surrounding sentence around the unchanged primary_keyword.';

		return $payload;
	}

	/**
	 * Convert a provider error into a strategy result.
	 *
	 * @param WP_Error                              $error    Error.
	 * @param string                                $test_id  Test ID.
	 * @param SmartyPress_AI_SEO_Strategy_Interface $strategy Strategy.
	 * @return array
	 */
	private function build_failed_result( $error, $test_id, $strategy ) {
		return array(
			'status'        => 'failed',
			'testId'        => $test_id,
			'reason'        => $strategy->get_reason(),
			'label'         => $strategy->get_label(),
			'skipped_tests' => array(
				array(
					'id'     => $test_id,
					'reason' => $error->get_error_message(),
				),
			),
			'warnings'      => array( $error->get_error_message() ),
		);
	}

	/**
	 * Run content strategies sequentially for phase 1.
	 *
	 * @param array  $context     Cached context.
	 * @param array  $analysis    Analysis.
	 * @param object $provider    AI provider.
	 * @param string $language    Language.
	 * @param string $model       Model.
	 * @param float  $temperature Temperature.
	 * @return array
	 */
	public function run( $context, $analysis, $provider, $language, $model, $temperature ) {
		$results  = array();
		$needs_ai = (array) ( $context['local_pre_checks']['needs_ai'] ?? $context['failed_tests'] ?? array() );
		$strategy_ids = array_keys( $this->strategy_registry->all() );

		foreach ( $needs_ai as $test ) {
			$test_id  = $test['id'] ?? '';
			$strategy = $this->strategy_registry->get( $test_id );
			if ( ! $strategy ) {
				continue;
			}

			if ( 'contentHasTOC' === $test_id && ! empty( $analysis['content_map']['toc_available'] ) ) {
				$results[] = array(
					'status'             => 'success',
					'testId'             => $test_id,
					'reason'             => 'toc_already_available',
					'label'              => $strategy->get_label(),
					'content_operations' => array(),
					'fixed_tests'        => array( $test_id ),
					'skipped_tests'      => array(),
					'warnings'           => array(),
				);
				continue;
			}

			if ( ! $strategy->is_applicable( $context, $analysis ) ) {
				$results[] = array(
					'status'        => 'skipped',
					'testId'        => $test_id,
					'reason'        => 'not_applicable',
					'label'         => $strategy->get_label(),
					'skipped_tests' => array(
						array(
							'id'     => $test_id,
							'reason' => __( 'Not enough suitable content context for this automatic fix.', 'smartypress-ai-engine' ),
						),
					),
					'warnings'      => array(),
				);
				continue;
			}

			$payload = $this->enrich_payload_for_scope( $strategy->select_context( $context, $analysis ), $context, $analysis, $strategy );
			$result  = $this->run_strategy_attempt( $strategy, $payload, $context, $analysis, $provider, $language, $model, $temperature, 'seo_fix_' . $strategy->get_reason() );

			if ( is_wp_error( $result ) ) {
				$results[] = $this->build_failed_result( $result, $test_id, $strategy );
				continue;
			}

			if ( $this->should_retry_keyword_failure( $result ) ) {
				$retry_payload = $this->build_retry_payload( $payload, $result );
				$retry_result  = $this->run_strategy_attempt( $strategy, $retry_payload, $context, $analysis, $provider, $language, $model, $temperature, 'seo_fix_' . $strategy->get_reason() . '_retry' );

				if ( ! is_wp_error( $retry_result ) ) {
					if ( 'success' !== ( $retry_result['status'] ?? '' ) ) {
						$retry_result['warnings'] = array_values(
							array_unique(
								array_merge(
									(array) ( $result['warnings'] ?? array() ),
									(array) ( $retry_result['warnings'] ?? array() )
								)
							)
						);
					}
					$result = $retry_result;
				}
			}

			$results[] = $result;
		}

		return $results;
	}
}
