<?php
/**
 * SEO result normalizer.
 *
 * @package SmartyPress_AI_Engine
 * @since 1.0.0
 */

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

/**
 * Normalizes SEO pipeline and legacy fix responses for the frontend.
 */
class SmartyPress_AI_SEO_Result_Normalizer extends SmartyPress_AI_SEO_Controller_Service_Base {

	/**
	 * Pipeline version.
	 *
	 * @var string
	 */
	private $pipeline_version;

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

	/**
	 * Content operation version.
	 *
	 * @var string
	 */
	private $content_op_version;

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

	/**
	 * Constructor.
	 *
	 * @param string                                      $pipeline_version  Pipeline version.
	 * @param string                                      $analysis_version  Analysis version.
	 * @param string                                      $content_op_version Content operation version.
	 * @param SmartyPress_AI_SEO_Content_Strategy_Registry $strategy_registry Strategy registry.
	 */
	public function __construct( $pipeline_version, $analysis_version, $content_op_version, $strategy_registry ) {
		$this->pipeline_version   = (string) $pipeline_version;
		$this->analysis_version   = (string) $analysis_version;
		$this->content_op_version = (string) $content_op_version;
		$this->strategy_registry  = $strategy_registry;
	}

	/**
	 * Merge all phase 1 pipeline results into the frontend legacy shape.
	 *
	 * @param array $context        Context.
	 * @param array $analysis       Analysis.
	 * @param array $meta_result    Meta result.
	 * @param array $content_result Strategy results.
	 * @return array
	 */
	public function merge_pipeline_results( $context, $analysis, $meta_result, $content_result ) {
		$warnings           = (array) ( $analysis['analysis_warnings'] ?? array() );
		$fixed_tests        = (array) ( $context['local_pre_checks']['fixed_tests'] ?? array() );
		$skipped_tests      = array();
		$content_operations = array();

		foreach ( (array) $content_result as $result ) {
			$content_operations = array_merge( $content_operations, (array) ( $result['content_operations'] ?? array() ) );
			$fixed_tests        = array_merge( $fixed_tests, (array) ( $result['fixed_tests'] ?? array() ) );
			$skipped_tests      = array_merge( $skipped_tests, (array) ( $result['skipped_tests'] ?? array() ) );
			$warnings           = array_merge( $warnings, (array) ( $result['warnings'] ?? array() ) );
		}

		foreach ( (array) ( $context['local_pre_checks']['manual_only'] ?? array() ) as $manual ) {
			$skipped_tests[] = array(
				'id'     => sanitize_text_field( $manual['testId'] ?? $manual['id'] ?? '' ),
				'reason' => sanitize_text_field( $manual['reason'] ?? '' ),
			);
		}

		foreach ( (array) ( $analysis['manual_only'] ?? array() ) as $manual ) {
			$manual_id = sanitize_text_field( $manual['testId'] ?? $manual['id'] ?? '' );
			if ( $this->is_automatable_content_test( $manual_id ) ) {
				continue;
			}

			$skipped_tests[] = array(
				'id'     => $manual_id,
				'reason' => sanitize_text_field( $manual['reason'] ?? '' ),
			);
		}

		$meta_result = is_array( $meta_result ) ? $meta_result : array();
		$warnings    = array_merge( $warnings, (array) ( $meta_result['warnings'] ?? array() ) );
		$fixed_tests = array_merge( $fixed_tests, (array) ( $meta_result['fixed_tests'] ?? array() ) );
		foreach ( (array) ( $meta_result['skipped_tests'] ?? array() ) as $skipped ) {
			$skipped_id = sanitize_text_field( $skipped['id'] ?? $skipped['testId'] ?? '' );
			if ( $this->is_non_meta_test( $skipped_id ) ) {
				continue;
			}

			$skipped_tests[] = array(
				'id'     => $skipped_id,
				'reason' => sanitize_text_field( $skipped['reason'] ?? '' ),
			);
		}
		$fixed_tests    = array_values( array_unique( array_filter( array_map( 'sanitize_text_field', $fixed_tests ) ) ) );
		$focus_keywords = $this->build_pipeline_focus_keywords( $meta_result, $analysis, $context );
		if ( ! empty( $focus_keywords[0] ) ) {
			$fixed_tests = array_values( array_unique( array_merge( $fixed_tests, array( 'keywordNotUsed' ) ) ) );
		}
		$skipped_tests = $this->normalize_skipped_tests( $skipped_tests, $fixed_tests );

		$normalized = $this->normalize_fix_response(
			array(
				'seo_title'          => $meta_result['seo_title'] ?? '',
				'meta_description'   => $meta_result['meta_description'] ?? '',
				'slug'               => $meta_result['slug'] ?? '',
				'post_title'         => $meta_result['post_title'] ?? '',
				'focus_keywords'     => $focus_keywords,
				'content_operations' => $content_operations,
				'fixed_tests'        => $fixed_tests,
				'skipped_tests'      => $skipped_tests,
				'warnings'           => $warnings,
				'recommendations'    => $analysis['recommendations'] ?? array(),
			)
		);

		$normalized['success']            = true;
		$normalized['pipeline_version']   = $this->pipeline_version;
		$normalized['analysis_version']   = $this->analysis_version;
		$normalized['content_op_version'] = $this->content_op_version;
		$normalized['analysis']           = $analysis;
		$normalized['strategy_results']   = $content_result;
		$normalized['local_pre_checks']   = $context['local_pre_checks'] ?? array();
		$normalized['recommendations']    = $analysis['recommendations'] ?? array();

		return $normalized;
	}

	/**
	 * Normalize generated data for the frontend.
	 *
	 * @param array $data Parsed data.
	 * @return array
	 */
	public function normalize_fix_response( $data ) {
		$warnings           = array_values( array_filter( array_map( 'sanitize_text_field', (array) ( $data['warnings'] ?? array() ) ) ) );
		$seo_title          = $this->normalize_seo_title( $data['seo_title'] ?? '', $warnings );
		$meta_description   = $this->normalize_meta_description( $data['meta_description'] ?? '', $warnings );
		$slug               = $this->normalize_slug( $data['slug'] ?? '', $warnings );
		$focus_keywords     = array_values( array_filter( array_map( 'sanitize_text_field', (array) ( $data['focus_keywords'] ?? array() ) ) ) );
		$primary_keyword    = $focus_keywords[0] ?? '';
		$seo_title          = $this->capitalize_line_start_keyword( $seo_title, $primary_keyword );
		$meta_description   = $this->capitalize_line_start_keyword( $meta_description, $primary_keyword );
		$content_operations = $this->normalize_content_operations( $data['content_operations'] ?? array(), $warnings, $primary_keyword );

		$primary_keyword_slug = '' !== $primary_keyword ? sanitize_title( $primary_keyword ) : '';

		return array(
			'seo_title'                => $seo_title,
			'meta_description'         => $meta_description,
			'slug'                     => $slug,
			'post_title'               => sanitize_text_field( $data['post_title'] ?? '' ),
			'focus_keywords'           => $focus_keywords,
			'primary_keyword_slug'     => $primary_keyword_slug,
			'content_operations'       => $content_operations,
			'internal_link_operations' => is_array( $data['internal_link_operations'] ?? null ) ? $data['internal_link_operations'] : array(),
			'image_alt_updates'        => is_array( $data['image_alt_updates'] ?? null ) ? $data['image_alt_updates'] : array(),
			'fixed_tests'              => array_values( array_filter( array_map( 'sanitize_text_field', (array) ( $data['fixed_tests'] ?? array() ) ) ) ),
			'skipped_tests'            => is_array( $data['skipped_tests'] ?? null ) ? $data['skipped_tests'] : array(),
			'warnings'                 => $warnings,
			'recommendations'          => is_array( $data['recommendations'] ?? null ) ? $this->sanitize_recursive( $data['recommendations'] ) : array(),
		);
	}

	/**
	 * Build final focus keywords from AI analysis and metadata response.
	 *
	 * @param array $meta_result Metadata step result.
	 * @param array $analysis    Full-context analysis.
	 * @param array $context     Cached context.
	 * @return array
	 */
	public function build_pipeline_focus_keywords( $meta_result, $analysis, $context ) {
		$post_text = (string) ( $context['post']['content_plain_text'] ?? '' );
		$primary   = sanitize_text_field( $analysis['primary_keyword'] ?? '' );

		if ( ! $this->is_keyword_present( $primary, $post_text ) ) {
			$primary = '';
		}

		$candidates = array_merge(
			(array) ( $meta_result['focus_keywords'] ?? array() ),
			(array) ( $analysis['secondary_keywords'] ?? array() ),
			(array) ( $context['seo']['focus_keywords'] ?? array() )
		);

		if ( '' === $primary ) {
			foreach ( $candidates as $candidate ) {
				$candidate = sanitize_text_field( $candidate );
				if ( $this->is_keyword_present( $candidate, $post_text ) ) {
					$primary = $candidate;
					break;
				}
			}
		}

		return array_merge( array_filter( array( $primary ) ), $this->normalize_keyword_list( $primary, $candidates, 4 ) );
	}

	/**
	 * Deduplicate skipped tests and remove tests already fixed by the pipeline.
	 *
	 * @param array $skipped_tests Skipped test entries.
	 * @param array $fixed_tests   Fixed test IDs.
	 * @return array
	 */
	public function normalize_skipped_tests( $skipped_tests, $fixed_tests ) {
		$fixed_lookup = array_fill_keys( array_map( 'sanitize_text_field', (array) $fixed_tests ), true );
		$normalized   = array();

		foreach ( (array) $skipped_tests as $skipped ) {
			$id = sanitize_text_field( $skipped['id'] ?? $skipped['testId'] ?? '' );
			if ( '' === $id || isset( $fixed_lookup[ $id ] ) ) {
				continue;
			}

			if ( isset( $normalized[ $id ] ) ) {
				continue;
			}

			$reason = sanitize_text_field( $skipped['reason'] ?? '' );
			$normalized[ $id ] = array(
				'id'     => $id,
				'reason' => $reason,
			);
		}

		return array_values( $normalized );
	}

	/**
	 * Check whether a failed test is handled by a content strategy.
	 *
	 * @param string $test_id Test ID.
	 * @return bool
	 */
	private function is_automatable_content_test( $test_id ) {
		return null !== $this->strategy_registry->get( $test_id );
	}

	/**
	 * Check whether a test belongs to content/link/media scope, not metadata.
	 *
	 * @param string $test_id Test ID.
	 * @return bool
	 */
	private function is_non_meta_test( $test_id ) {
		return in_array(
			(string) $test_id,
			array(
				'keywordIn10Percent',
				'keywordInContent',
				'keywordInSubheadings',
				'keywordDensity',
				'contentHasTOC',
				'contentHasShortParagraphs',
				'contentHasAssets',
				'keywordInImageAlt',
				'linksHasInternal',
				'linksHasExternals',
				'linksNotAllExternals',
				'hasContentAI',
				'keywordNotUsed',
				'isReadable',
				'sentenceLengthInText',
				'transitionWords',
				'passiveVoice',
				'sentenceBeginnings',
				'subheadingDistribution',
				'wordComplexity',
				'contentHasLists',
				'contentStructure',
			),
			true
		);
	}

	/**
	 * Normalize generated SEO title and collect validation warnings.
	 *
	 * @param string $title    Generated title.
	 * @param array  $warnings Validation warnings.
	 * @return string
	 */
	private function normalize_seo_title( $title, &$warnings ) {
		$title = sanitize_text_field( $title );
		$title = preg_replace( '/\s*(?:%sep%|[-|–—])\s*%sitename%\s*$/i', '', $title );
		$title = trim( preg_replace( '/\s+/', ' ', $title ) );

		$length = $this->string_length( $title );

		if ( $length > 70 ) {
			$title = $this->truncate_text( $title, 65 );
			$warnings[] = __( 'SEO title was too long and had to be shortened. Consider rephrasing.', 'smartypress-ai-engine' );
		} elseif ( $length > 58 ) {
			$warnings[] = __( 'SEO title slightly exceeds the recommended 58-character limit.', 'smartypress-ai-engine' );
		}

		return $title;
	}

	/**
	 * Normalize generated meta description.
	 *
	 * @param string $description Generated description.
	 * @param array  $warnings    Validation warnings.
	 * @return string
	 */
	private function normalize_meta_description( $description, &$warnings ) {
		$description = sanitize_textarea_field( $description );
		$description = trim( preg_replace( '/\s+/', ' ', $description ) );

		$length = $this->string_length( $description );

		if ( $length > 170 ) {
			$description = $this->truncate_text( $description, 160 );
			$warnings[] = __( 'Meta description was too long and had to be shortened. Consider rephrasing.', 'smartypress-ai-engine' );
		} elseif ( $length > 155 ) {
			$warnings[] = __( 'Meta description slightly exceeds the recommended 155-character limit.', 'smartypress-ai-engine' );
		}

		return $description;
	}

	/**
	 * Capitalize the keyword when it starts a generated line.
	 *
	 * @param string $text    Text.
	 * @param string $keyword Primary keyword.
	 * @return string
	 */
	private function capitalize_line_start_keyword( $text, $keyword ) {
		$text    = (string) $text;
		$keyword = sanitize_text_field( $keyword );

		if ( '' === $text || '' === $keyword ) {
			return $this->mb_ucfirst( $text );
		}

		$normalized_text    = $this->normalize_match_text( $text );
		$normalized_keyword = $this->normalize_match_text( $keyword );

		if ( '' !== $normalized_keyword && 0 === strpos( $normalized_text, $normalized_keyword ) ) {
			$keyword_length = $this->string_length( $keyword );
			$actual_keyword = function_exists( 'mb_substr' ) ? mb_substr( $text, 0, $keyword_length ) : substr( $text, 0, $keyword_length );
			$rest           = function_exists( 'mb_substr' ) ? mb_substr( $text, $keyword_length ) : substr( $text, $keyword_length );

			return $this->mb_ucfirst( $actual_keyword ) . $rest;
		}

		return $this->mb_ucfirst( $text );
	}

	/**
	 * Capitalize generated content when it starts with the primary keyword.
	 *
	 * @param string $html    Operation HTML.
	 * @param string $keyword Primary keyword.
	 * @return string
	 */
	private function capitalize_operation_html_start( $html, $keyword ) {
		$html = (string) $html;
		if ( '' === $html ) {
			return '';
		}

		if ( ! $this->starts_with_normalized_keyword_or_text( $html, $keyword ) ) {
			return $html;
		}

		return preg_replace_callback(
			'/^(<h[1-6][^>]*>\s*)?([^<])/iu',
			function ( $matches ) {
				$prefix = $matches[1] ?? '';
				$char   = $matches[2] ?? '';
				$rest   = substr( $matches[0], strlen( $prefix . $char ) );

				return $prefix . $this->mb_ucfirst( $char ) . $rest;
			},
			$html,
			1
		);
	}

	/**
	 * Check whether operation text starts with the normalized primary keyword.
	 *
	 * @param string $html    HTML/text.
	 * @param string $keyword Primary keyword.
	 * @return bool
	 */
	private function starts_with_normalized_keyword_or_text( $html, $keyword ) {
		$plain = trim( preg_replace( '/\s+/', ' ', wp_strip_all_tags( (string) $html ) ) );
		if ( '' === $plain ) {
			return false;
		}

		$normalized_plain   = $this->normalize_match_text( $plain );
		$normalized_keyword = $this->normalize_match_text( $keyword );

		return '' !== $normalized_keyword && 0 === strpos( $normalized_plain, $normalized_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 );
	}

	/**
	 * Normalize generated slug.
	 *
	 * @param string $slug     Generated slug.
	 * @param array  $warnings Validation warnings.
	 * @return string
	 */
	private function normalize_slug( $slug, &$warnings ) {
		$slug = trim( (string) $slug );

		if ( preg_match( '#^https?://#i', $slug ) ) {
			$path  = (string) wp_parse_url( $slug, PHP_URL_PATH );
			$parts = array_filter( explode( '/', $path ) );
			$slug  = end( $parts ) ?: $slug;
		}

		$slug = sanitize_title( $slug );
		if ( $this->string_length( $slug ) > 65 ) {
			$slug = preg_replace( '/-[^-]*$/', '', substr( $slug, 0, 65 ) );
			$slug = trim( (string) $slug, '-' );
			$warnings[] = __( 'Slug was too long and had to be shortened.', 'smartypress-ai-engine' );
		}

		return $slug;
	}

	/**
	 * Normalize generated content operations.
	 *
	 * @param array $operations Operations.
	 * @param array $warnings   Warnings.
	 * @return array
	 */
	private function normalize_content_operations( $operations, &$warnings, $primary_keyword = '' ) {
		$normalized = array();

		foreach ( (array) $operations as $operation ) {
			if ( ! is_array( $operation ) ) {
				continue;
			}

			$type = sanitize_key( $operation['type'] ?? '' );
			if ( ! in_array( $type, array( 'replace', 'insert_before', 'insert_after' ), true ) ) {
				continue;
			}

			$html     = wp_kses_post( $operation['html'] ?? '' );
			$html     = $this->capitalize_operation_html_start( $html, $primary_keyword );
			$block_id = sanitize_text_field( $operation['blockClientId'] ?? '' );
			if ( '' === $html || '' === $block_id ) {
				continue;
			}

			$normalized[] = array(
				'type'          => $type,
				'reason'        => sanitize_key( $operation['reason'] ?? '' ),
				'testId'        => preg_replace( '/[^A-Za-z0-9_-]/', '', (string) ( $operation['testId'] ?? '' ) ),
				'label'         => sanitize_text_field( $operation['label'] ?? '' ),
				'blockClientId' => $block_id,
				'html'          => $html,
				'plainSummary'  => sanitize_text_field( $operation['plainSummary'] ?? '' ),
			);
		}

		if ( empty( $normalized ) && ! empty( $operations ) ) {
			$warnings[] = __( 'Some content operations were rejected because they were incomplete or unsafe.', 'smartypress-ai-engine' );
		}

		return $normalized;
	}

	/**
	 * Check keyword quality and presence in the article.
	 *
	 * @param string $keyword Keyword.
	 * @param string $post_text Article text.
	 * @return bool
	 */
	private function is_keyword_present( $keyword, $post_text ) {
		if ( ! $this->is_concise_keyword( $keyword ) ) {
			return false;
		}

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