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

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

/**
 * Runs metadata generation in the SEO fix pipeline.
 */
class SmartyPress_AI_SEO_Meta_Step_Service extends SmartyPress_AI_SEO_Controller_Service_Base {

	/**
	 * 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_Fix_Prompt_Builder $prompt_builder Prompt builder.
	 * @param SmartyPress_AI_SEO_AI_JSON_Generator  $json_generator JSON generator.
	 */
	public function __construct( $prompt_builder, $json_generator ) {
		$this->prompt_builder = $prompt_builder;
		$this->json_generator = $json_generator;
	}

	/**
	 * Run the metadata generation step.
	 *
	 * @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 ) {
		$primary_keyword = $this->get_valid_primary_keyword( $analysis, $context );
		if ( '' === $primary_keyword ) {
			return array(
				'status'   => 'skipped',
				'warnings' => array( __( 'Metadata generation requires a valid 2-3 word primary keyword from the article.', 'smartypress-ai-engine' ) ),
			);
		}

		$payload  = array(
			'context'                   => $this->build_metadata_context( $context ),
			'analysis'                  => $analysis,
			'effective_primary_keyword' => $primary_keyword,
		);
		$response = $this->run_meta_attempt( $payload, $provider, $language, $model, $temperature, 'seo_fix_meta', false );

		if ( is_wp_error( $response ) ) {
			return array(
				'status'   => 'failed',
				'warnings' => array( $response->get_error_message() ),
			);
		}

		$failed_fields = $this->get_exact_keyword_failed_fields( $response, $primary_keyword );
		$this->log_metadata_attempt( 'seo_fix_meta', $primary_keyword, $failed_fields, $response, empty( $failed_fields ) );
		if ( ! empty( $failed_fields ) ) {
			$retry_payload = array_merge(
				$payload,
				array(
					'primary_keyword'   => $primary_keyword,
					'failed_fields'     => $failed_fields,
					'previous_response' => $response,
				)
			);
			$retry_response = $this->run_meta_attempt( $retry_payload, $provider, $language, $model, $temperature, 'seo_fix_meta_retry', true );

			if ( ! is_wp_error( $retry_response ) ) {
				$retry_failed_fields = $this->get_exact_keyword_failed_fields( $retry_response, $primary_keyword );
				$this->log_metadata_attempt( 'seo_fix_meta_retry', $primary_keyword, $retry_failed_fields, $retry_response, empty( $retry_failed_fields ) );
				$response = $retry_response;
				$failed_fields = $retry_failed_fields;
			}
		}

		if ( ! empty( $failed_fields ) ) {
			$response = $this->repair_metadata_keyword_fields( $response, $primary_keyword, $failed_fields, $context, $analysis );
			$failed_fields = $this->get_exact_keyword_failed_fields( $response, $primary_keyword );
			$this->log_metadata_attempt( 'seo_fix_meta_repair', $primary_keyword, $failed_fields, $response, empty( $failed_fields ) );
		}

		if ( ! empty( $failed_fields ) ) {
			$response['warnings'] = array_values(
				array_unique(
					array_merge(
						(array) ( $response['warnings'] ?? array() ),
						array( __( 'Metadata output still misses the exact primary keyword after retry and deterministic repair.', 'smartypress-ai-engine' ) )
					)
				)
			);
			foreach ( array( 'seo_title', 'meta_description' ) as $field ) {
				if ( in_array( $field, $failed_fields, true ) ) {
					$response[ $field ] = '';
				}
			}
		}

		return $response;
	}

	/**
	 * Run one metadata AI attempt.
	 *
	 * @param array  $payload     Prompt payload.
	 * @param object $provider    AI provider.
	 * @param string $language    Language.
	 * @param string $model       Model.
	 * @param float  $temperature Temperature.
	 * @param string $schema_name Schema name.
	 * @param bool   $is_retry    Whether this is a retry prompt.
	 * @return array|WP_Error
	 */
	private function run_meta_attempt( $payload, $provider, $language, $model, $temperature, $schema_name, $is_retry ) {
		$prompt = $is_retry ? $this->prompt_builder->build_meta_retry_prompt( $payload ) : $this->prompt_builder->build_meta_prompt( $payload );

		return $this->json_generator->generate_json(
			$provider,
			$prompt,
			$language,
			$model,
			$temperature,
			$this->get_meta_response_schema(),
			$schema_name,
			$payload['context'] ?? array()
		);
	}

	/**
	 * Keep the metadata prompt focused on metadata-related failed tests.
	 *
	 * @param array $context Cached context.
	 * @return array
	 */
	private function build_metadata_context( $context ) {
		$context = is_array( $context ) ? $context : array();

		$context['failed_tests'] = array_values(
			array_filter(
				(array) ( $context['failed_tests'] ?? array() ),
				function( $test ) {
					return $this->is_metadata_test( $test['id'] ?? '' );
				}
			)
		);

		if ( isset( $context['local_pre_checks']['needs_ai'] ) ) {
			$context['local_pre_checks']['needs_ai'] = array_values(
				array_filter(
					(array) $context['local_pre_checks']['needs_ai'],
					function( $test ) {
						return $this->is_metadata_test( $test['id'] ?? '' );
					}
				)
			);
		}

		return $context;
	}

	/**
	 * Check whether a test belongs to the metadata step.
	 *
	 * @param string $test_id Test ID.
	 * @return bool
	 */
	private function is_metadata_test( $test_id ) {
		return in_array(
			(string) $test_id,
			array(
				'keywordInTitle',
				'titleStartWithKeyword',
				'titleHasNumber',
				'titleHasPowerWords',
				'titleSentiment',
				'titleLength',
				'keywordInMetaDescription',
				'metaDescriptionLength',
				'keywordInPermalink',
				'keywordInUrl',
				'lengthPermalink',
				'postTitlePresence',
			),
			true
		);
	}

	/**
	 * Find metadata fields missing the exact primary keyword.
	 *
	 * @param array $response Metadata response.
	 * @param string $keyword Primary keyword.
	 * @return array
	 */
	private function get_exact_keyword_failed_fields( $response, $keyword ) {
		$keyword = sanitize_text_field( $keyword );
		if ( '' === $keyword ) {
			return array();
		}

		$failed = array();
		foreach ( array( 'seo_title', 'meta_description' ) as $field ) {
			if ( ! $this->text_contains_exact_keyword( $response[ $field ] ?? '', $keyword ) ) {
				$failed[] = $field;
			}
		}

		$focus_keywords = (array) ( $response['focus_keywords'] ?? array() );
		if ( ! $this->text_contains_exact_keyword( $focus_keywords[0] ?? '', $keyword ) ) {
			$failed[] = 'focus_keywords';
		}

		return $failed;
	}

	/**
	 * Check whether generated text contains the exact normalized keyword.
	 *
	 * @param string $text    Text.
	 * @param string $keyword Keyword.
	 * @return bool
	 */
	private function text_contains_exact_keyword( $text, $keyword ) {
		$text    = $this->normalize_match_text( $text );
		$keyword = $this->normalize_match_text( $keyword );

		return '' !== $keyword && false !== strpos( $text, $keyword );
	}

	/**
	 * Repair metadata fields deterministically when AI keeps inflecting keyword.
	 *
	 * @param array  $response      Metadata response.
	 * @param string $keyword       Primary keyword.
	 * @param array  $failed_fields Failed fields.
	 * @param array  $context       Cached context.
	 * @param array  $analysis      Analysis.
	 * @return array
	 */
	private function repair_metadata_keyword_fields( $response, $keyword, $failed_fields, $context, $analysis ) {
		$keyword = sanitize_text_field( $keyword );
		if ( '' === $keyword ) {
			return $response;
		}

		if ( in_array( 'seo_title', $failed_fields, true ) ) {
			$response['seo_title'] = $this->repair_seo_title( $response['seo_title'] ?? '', $keyword, $context, $analysis );
		}

		if ( in_array( 'meta_description', $failed_fields, true ) ) {
			$response['meta_description'] = $this->repair_meta_description( $response['meta_description'] ?? '', $keyword, $context, $analysis );
		}

		if ( in_array( 'focus_keywords', $failed_fields, true ) ) {
			$focus_keywords = array_values( array_filter( array_map( 'sanitize_text_field', (array) ( $response['focus_keywords'] ?? array() ) ) ) );
			array_unshift( $focus_keywords, $keyword );
			$response['focus_keywords'] = array_values( array_unique( $focus_keywords ) );
		}

		$response['warnings'] = array_values(
			array_unique(
				array_merge(
					(array) ( $response['warnings'] ?? array() ),
					array( __( 'Metadata fields were repaired locally to preserve the exact primary keyword.', 'smartypress-ai-engine' ) )
				)
			)
		);

		return $response;
	}

	/**
	 * Repair SEO title with exact keyword.
	 *
	 * @param string $title   Generated title.
	 * @param string $keyword Primary keyword.
	 * @param array  $context Cached context.
	 * @param array  $analysis Analysis.
	 * @return string
	 */
	private function repair_seo_title( $title, $keyword, $context, $analysis ) {
		$title = sanitize_text_field( $title );
		if ( $this->text_contains_exact_keyword( $title, $keyword ) ) {
			return $title;
		}

		$topic = sanitize_text_field( $analysis['topic'] ?? $context['post']['title'] ?? '' );
		$topic = trim( preg_replace( '/\s+/', ' ', $topic ) );
		$display_keyword = $this->mb_ucfirst( $keyword );
		$title = '' !== $topic ? sprintf( '%1$s: %2$s', $display_keyword, $topic ) : $display_keyword;

		if ( $this->string_length( $title ) > 58 ) {
			$title = $this->truncate_text_preserving_keyword( $title, $keyword, 58 );
		}

		return $title;
	}

	/**
	 * Repair meta description with exact keyword.
	 *
	 * @param string $description Generated description.
	 * @param string $keyword     Primary keyword.
	 * @param array  $context     Cached context.
	 * @param array  $analysis    Analysis.
	 * @return string
	 */
	private function repair_meta_description( $description, $keyword, $context, $analysis ) {
		$description = sanitize_textarea_field( $description );
		$description = trim( preg_replace( '/\s+/', ' ', $description ) );
		if ( $this->text_contains_exact_keyword( $description, $keyword ) ) {
			return $description;
		}

		$summary = sanitize_textarea_field( $analysis['content_summary'] ?? '' );
		$source  = '' !== $description ? $description : $summary;
		$source  = trim( preg_replace( '/\s+/', ' ', $source ) );
		$display_keyword = $this->mb_ucfirst( $keyword );
		$description = '' !== $source ? sprintf( '%1$s: %2$s', $display_keyword, $source ) : sprintf( '%s: %s', $display_keyword, sanitize_text_field( $context['post']['title'] ?? '' ) );

		if ( $this->string_length( $description ) > 155 ) {
			$description = $this->truncate_text_preserving_keyword( $description, $keyword, 155 );
		}

		if ( $this->string_length( $description ) < 140 ) {
			$tail = __( 'Learn the key facts, context, and practical details in this article.', 'smartypress-ai-engine' );
			$description = trim( $description . ' ' . $tail );
			if ( $this->string_length( $description ) > 155 ) {
				$description = $this->truncate_text_preserving_keyword( $description, $keyword, 155 );
			}
		}

		return $description;
	}

	/**
	 * Truncate text while keeping exact keyword intact.
	 *
	 * @param string $text    Text.
	 * @param string $keyword Keyword.
	 * @param int    $limit   Character limit.
	 * @return string
	 */
	private function truncate_text_preserving_keyword( $text, $keyword, $limit ) {
		$text = trim( preg_replace( '/\s+/', ' ', (string) $text ) );
		if ( $this->string_length( $text ) <= $limit ) {
			return $text;
		}

		$keyword = sanitize_text_field( $keyword );
		if ( 0 === strpos( $this->normalize_match_text( $text ), $this->normalize_match_text( $keyword ) ) ) {
			$keyword_len  = $this->string_length( $keyword );
			$actual_kw    = function_exists( 'mb_substr' ) ? mb_substr( $text, 0, $keyword_len ) : substr( $text, 0, $keyword_len );
			$suffix_limit = max( 0, $limit - $keyword_len - 2 );
			$suffix       = trim( (string) preg_replace( '/^.{' . $keyword_len . '}\s*[:-]?\s*/su', '', $text ) );
			$suffix       = $this->truncate_text( $suffix, $suffix_limit );

			return trim( $actual_kw . ( '' !== $suffix ? ': ' . $suffix : '' ) );
		}

		return $this->truncate_text( $text, $limit );
	}

	/**
	 * Log sanitized metadata attempt diagnostics in debug mode.
	 *
	 * @param string $schema_name   Schema/attempt name.
	 * @param string $keyword       Primary keyword.
	 * @param array  $failed_fields Failed fields.
	 * @param array  $response      Metadata response.
	 * @param bool   $success       Whether validation passed.
	 * @return void
	 */
	private function log_metadata_attempt( $schema_name, $keyword, $failed_fields, $response, $success ) {
		if ( ! class_exists( 'SmartyPress_AI_Logger' ) ) {
			return;
		}

		SmartyPress_AI_Logger::log_module_event(
			'seo_optimization',
			'metadata_' . sanitize_key( $schema_name ),
			array(
				'primary_keyword'  => sanitize_text_field( $keyword ),
				'failed_fields'    => array_values( array_filter( array_map( 'sanitize_key', (array) $failed_fields ) ) ),
				'seo_title'        => sanitize_text_field( $response['seo_title'] ?? '' ),
				'meta_description' => sanitize_textarea_field( $response['meta_description'] ?? '' ),
				'focus_keywords'   => array_values( array_filter( array_map( 'sanitize_text_field', (array) ( $response['focus_keywords'] ?? array() ) ) ) ),
			),
			(bool) $success
		);
	}

	/**
	 * Resolve a valid metadata primary keyword.
	 *
	 * @param array $analysis Analysis.
	 * @param array $context  Cached context.
	 * @return string
	 */
	private function get_valid_primary_keyword( $analysis, $context ) {
		$keyword   = sanitize_text_field( $analysis['primary_keyword'] ?? '' );
		$post_text = (string) ( $context['post']['content_plain_text'] ?? '' );

		if ( ! $this->is_concise_keyword( $keyword ) ) {
			return '';
		}

		$needle   = $this->normalize_match_text( $keyword );
		$haystack = $this->normalize_match_text( $post_text );

		return '' !== $needle && false !== strpos( $haystack, $needle ) ? $keyword : '';
	}

	/**
	 * Capitalize the first letter of a UTF-8 string.
	 *
	 * @param string $value Input string.
	 * @return string
	 */
	private function mb_ucfirst( $value ) {
		$value = (string) $value;
		if ( '' === $value ) {
			return '';
		}

		if ( function_exists( 'mb_strtoupper' ) && function_exists( 'mb_substr' ) ) {
			return mb_strtoupper( mb_substr( $value, 0, 1 ) ) . mb_substr( $value, 1 );
		}

		return ucfirst( $value );
	}

	/**
	 * Get metadata JSON schema.
	 *
	 * @return array
	 */
	public function get_meta_response_schema() {
		return array(
			'type'                 => 'object',
			'additionalProperties' => false,
			'required'             => array( 'seo_title', 'meta_description', 'slug', 'post_title', 'focus_keywords', 'fixed_tests', 'skipped_tests', 'warnings' ),
			'properties'           => array(
				'seo_title'        => array( 'type' => 'string' ),
				'meta_description' => array( 'type' => 'string' ),
				'slug'             => array( 'type' => 'string' ),
				'post_title'       => array( 'type' => 'string' ),
				'focus_keywords'   => array( 'type' => 'array', 'items' => array( 'type' => 'string' ) ),
				'fixed_tests'      => array( 'type' => 'array', 'items' => array( 'type' => 'string' ) ),
				'skipped_tests'    => array( 'type' => 'array', 'items' => array( 'type' => 'object' ) ),
				'warnings'         => array( 'type' => 'array', 'items' => array( 'type' => 'string' ) ),
			),
		);
	}
}
