<?php
/**
 * SEO AI JSON generator.
 *
 * @package SmartyPress_AI_Engine
 * @since 1.0.0
 */

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

/**
 * Generates and parses JSON responses from configured AI providers.
 */
class SmartyPress_AI_SEO_AI_JSON_Generator extends SmartyPress_AI_SEO_Controller_Service_Base {

	/**
	 * Plugin settings.
	 *
	 * @var array
	 */
	private $settings;

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

	/**
	 * Constructor.
	 *
	 * @param array                                $settings       Plugin settings.
	 * @param SmartyPress_AI_SEO_Fix_Prompt_Builder $prompt_builder Prompt builder.
	 */
	public function __construct( $settings, $prompt_builder ) {
		$this->settings       = is_array( $settings ) ? $settings : array();
		$this->prompt_builder = $prompt_builder;
	}

	/**
	 * Keep JSON generation deterministic while respecting lower custom values.
	 *
	 * @param mixed $temperature Requested temperature.
	 * @return float
	 */
	public function get_json_temperature( $temperature ) {
		$temperature = null === $temperature ? 0.3 : (float) $temperature;

		return max( 0.1, min( 0.4, $temperature ) );
	}

	/**
	 * Get a bounded token budget for SEO JSON responses.
	 *
	 * @param string $model Model name.
	 * @return int
	 */
	public function get_seo_max_tokens( $model = '' ) {
		$settings_max = isset( $this->settings['max_tokens'] ) ? absint( $this->settings['max_tokens'] ) : 0;
		$model        = strtolower( (string) $model );

		if ( false !== strpos( $model, 'gpt-5.5' ) ) {
			return min( 16000, max( 12000, $settings_max ) );
		}

		return min( 8000, max( 4000, $settings_max ) );
	}

	/**
	 * Generate and parse JSON from an AI provider.
	 *
	 * @param object $provider    AI provider.
	 * @param string $prompt      Prompt.
	 * @param string $language    Language.
	 * @param string $model       Model.
	 * @param float  $temperature Temperature.
	 * @param array  $schema      JSON schema.
	 * @param string $schema_name Schema name.
	 * @param array  $log_context Optional log context.
	 * @return array|WP_Error
	 */
	public function generate_json( $provider, $prompt, $language, $model, $temperature, $schema, $schema_name, $log_context = array() ) {
		if ( class_exists( 'SmartyPress_AI_Token_Tracker' ) ) {
			SmartyPress_AI_Token_Tracker::set_module( 'seo-optimization' );
		}

		$max_tokens = $this->get_seo_max_tokens( $model );
		$response   = $provider->generate_content(
			$prompt,
			array(
				'system_prompt'   => $this->prompt_builder->build_system_prompt( $language ),
				'model'           => $model,
				'temperature'     => $temperature,
				'max_tokens'      => $max_tokens,
				'request_timeout' => 90,
				'response_format' => array(
					'type' => 'json_object',
				),
			)
		);

		if ( empty( $response['success'] ) ) {
			return new WP_Error(
				'smartypress_ai_seo_generation_failed',
				isset( $response['error'] ) ? $response['error'] : __( 'Unable to generate SEO fixes.', 'smartypress-ai-engine' ),
				array( 'status' => 500 )
			);
		}

		$data = $this->parse_json_response( $response['content'] ?? '' );
		if ( empty( $data ) ) {
			$this->log_invalid_json_response( $log_context, $model, $prompt, $response, $schema_name );

			return new WP_Error(
				'smartypress_ai_seo_invalid_response',
				__( 'AI response did not contain valid SEO fix JSON.', 'smartypress-ai-engine' ),
				array( 'status' => 502 )
			);
		}

		return $data;
	}

	/**
	 * Parse JSON from an AI response.
	 *
	 * @param string $content Response content.
	 * @return array
	 */
	public function parse_json_response( $content ) {
		$content = trim( (string) $content );
		$content = preg_replace( '/^```(?:json)?\s*/i', '', $content );
		$content = preg_replace( '/\s*```$/', '', $content );

		$data = json_decode( $content, true );
		if ( is_array( $data ) ) {
			return $data;
		}

		$first_error = json_last_error_msg();
		if ( false !== stripos( $first_error, 'Control character' ) ) {
			$sanitized = $this->sanitize_json_control_characters( $content );
			$data      = json_decode( $sanitized, true );
			if ( is_array( $data ) ) {
				return $data;
			}
		}

		$json = $this->extract_first_json_object( $content );
		if ( '' === $json ) {
			return array();
		}

		$data = json_decode( $json, true );
		if ( is_array( $data ) ) {
			return $data;
		}

		$object_error = json_last_error_msg();
		if ( false !== stripos( $object_error, 'Control character' ) ) {
			$sanitized = $this->sanitize_json_control_characters( $json );
			$data      = json_decode( $sanitized, true );
			if ( is_array( $data ) ) {
				return $data;
			}
		}

		return array();
	}

	/**
	 * Remove literal control characters that make otherwise valid JSON unparsable.
	 *
	 * @param string $json JSON string.
	 * @return string
	 */
	private function sanitize_json_control_characters( $json ) {
		$json      = (string) $json;
		$output    = '';
		$in_string = false;
		$escape    = false;
		$length    = strlen( $json );

		for ( $i = 0; $i < $length; $i++ ) {
			$char = $json[ $i ];
			$ord  = ord( $char );

			if ( $escape ) {
				$output .= $char;
				$escape  = false;
				continue;
			}

			if ( '\\' === $char ) {
				$output .= $char;
				$escape  = true;
				continue;
			}

			if ( '"' === $char ) {
				$output   .= $char;
				$in_string = ! $in_string;
				continue;
			}

			if ( $in_string && $ord < 32 ) {
				if ( 10 === $ord ) {
					$output .= '\n';
				} elseif ( 13 === $ord ) {
					$output .= '\r';
				} elseif ( 9 === $ord ) {
					$output .= '\t';
				}
				continue;
			}

			if ( ! $in_string && $ord < 32 && ! in_array( $ord, array( 9, 10, 13 ), true ) ) {
				continue;
			}

			$output .= $char;
		}

		return $output;
	}

	/**
	 * Extract the first balanced JSON object from text.
	 *
	 * @param string $content Raw content.
	 * @return string
	 */
	public function extract_first_json_object( $content ) {
		$start = strpos( $content, '{' );
		if ( false === $start ) {
			return '';
		}

		$depth     = 0;
		$in_string = false;
		$escape    = false;
		$length    = strlen( $content );

		for ( $i = $start; $i < $length; $i++ ) {
			$char = $content[ $i ];

			if ( $escape ) {
				$escape = false;
				continue;
			}

			if ( '\\' === $char ) {
				$escape = true;
				continue;
			}

			if ( '"' === $char ) {
				$in_string = ! $in_string;
				continue;
			}

			if ( $in_string ) {
				continue;
			}

			if ( '{' === $char ) {
				$depth++;
			} elseif ( '}' === $char ) {
				$depth--;
				if ( 0 === $depth ) {
					return substr( $content, $start, $i - $start + 1 );
				}
			}
		}

		return '';
	}

	/**
	 * Log enough context to diagnose invalid JSON without storing article text.
	 *
	 * @param array  $params      Request parameters.
	 * @param string $model       Requested model.
	 * @param string $user_prompt Built user prompt.
	 * @param array  $response    Provider response.
	 * @param string $schema_name Schema name.
	 * @return void
	 */
	public function log_invalid_json_response( $params, $model, $user_prompt, $response, $schema_name = '' ) {
		if ( ! class_exists( 'SmartyPress_AI_Logger' ) ) {
			return;
		}

		$content = (string) ( $response['content'] ?? '' );
		$excerpt = preg_replace( '/\s+/', ' ', wp_strip_all_tags( $content ) );

		SmartyPress_AI_Logger::log_module_event(
			'seo_optimization',
			'seo_fix_invalid_json',
			array(
				'provider'        => sanitize_key( $params['ai_provider'] ?? $params['provider_name'] ?? '' ),
				'seo_provider'    => sanitize_key( $params['provider'] ?? '' ),
				'model'           => sanitize_text_field( $model ),
				'schema'          => sanitize_key( $schema_name ),
				'prompt_length'   => strlen( $user_prompt ),
				'response_length' => strlen( $content ),
				'response_start'  => substr( $excerpt, 0, 500 ),
				'json_error'      => json_last_error_msg(),
			),
			false
		);
	}
}
