<?php

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

final class NPATD_AI_Processor {
	private const AI_MAX_RETRY_ATTEMPTS = 3;
	private const AI_RETRY_BASE_DELAY_MICROSECONDS = 1000000;
	private const AI_RETRY_MAX_DELAY_MICROSECONDS = 8000000;
	private const SIGNED_URL_FETCH_TIMEOUT_SECONDS = 45;
	private const SIGNED_URL_MAX_REDIRECTS = 2;
	private const SIGNED_URL_MAX_RESPONSE_BYTES = 41943040;
	private const MAX_ATTACHMENT_PATH_LENGTH = 1024;
	private const DEFAULT_TEMPERATURE = 0.3;
	private const MIN_TEMPERATURE     = 0.0;
	private const MAX_TEMPERATURE     = 1.0;
	private const ESTIMATED_TOKENS_PER_MODEL_CALL = 1000;

	private static function estimated_tokens_per_model_call(): int {
		$estimated = (int) apply_filters( 'npatd_estimated_tokens_per_model_call', self::ESTIMATED_TOKENS_PER_MODEL_CALL );
		return max( 100, $estimated );
	}

	private static function ai_error( string $code, string $message ) {
		return NPATD_Ability_Contract::error_response( $code, $message );
	}

	private static function is_valid_attachment_path( $path ): bool {
		if ( ! is_string( $path ) ) {
			return false;
		}

		$path = trim( $path );
		if ( '' === $path ) {
			return false;
		}

		if ( strlen( $path ) > self::MAX_ATTACHMENT_PATH_LENGTH ) {
			return false;
		}

		if ( false !== strpos( $path, "\0" ) || false !== strpos( $path, "\n" ) || false !== strpos( $path, "\r" ) ) {
			return false;
		}

		return true;
	}

	private static function is_timeout_error( WP_Error $error ): bool {
		$message = strtolower( $error->get_error_message() );

		if ( false !== strpos( $message, 'curl error 28' ) ) {
			return true;
		}

		return false !== strpos( $message, 'timed out' );
	}

	private static function retryable_error_reason( WP_Error $error ): string {
		$code    = strtolower( (string) $error->get_error_code() );
		$message = strtolower( $error->get_error_message() );

		if ( self::is_timeout_error( $error ) ) {
			return 'timeout';
		}

		if ( false !== strpos( $code, 'rate_limit' ) || false !== strpos( $message, 'rate limit' ) || false !== strpos( $message, '429' ) ) {
			return 'rate_limit';
		}

		if ( false !== strpos( $message, '503' ) || false !== strpos( $message, 'service unavailable' ) || false !== strpos( $message, 'temporarily unavailable' ) ) {
			return 'service_unavailable';
		}

		if ( false !== strpos( $message, '502' ) || false !== strpos( $message, 'bad gateway' ) ) {
			return 'bad_gateway';
		}

		if ( false !== strpos( $message, '504' ) || false !== strpos( $message, 'gateway timeout' ) ) {
			return 'gateway_timeout';
		}

		return '';
	}

	private static function retry_delay_microseconds( int $attempt ): int {
		$exponent = max( 0, $attempt - 1 );
		$delay    = self::AI_RETRY_BASE_DELAY_MICROSECONDS * ( 2 ** $exponent );
		$delay    = min( $delay, self::AI_RETRY_MAX_DELAY_MICROSECONDS );

		// Add a small jitter to avoid synchronized retries.
		$jitter = random_int( 0, 300000 );

		return (int) ( $delay + $jitter );
	}

	private static function generate_text_with_retry( $builder, string $stage, array $context = array() ) {
		$last_error = null;
		$last_retryable_reason = '';
		$attempts_used = 0;

		for ( $attempt = 1; $attempt <= self::AI_MAX_RETRY_ATTEMPTS; $attempt++ ) {
			$attempts_used = $attempt;
			$attempt_context = array_merge(
				$context,
				array(
					'stage'        => $stage,
					'attempt'      => $attempt,
					'max_attempts' => self::AI_MAX_RETRY_ATTEMPTS,
				)
			);

			$forced_result = apply_filters( 'npatd_ai_retry_fault_injection_error', null, $attempt_context );
			$result        = $forced_result instanceof WP_Error ? $forced_result : $builder->generate_text();

			if ( ! is_wp_error( $result ) ) {
				return $result;
			}

			$last_error = $result;
			$retryable_reason = self::retryable_error_reason( $result );
			$last_retryable_reason = $retryable_reason;

			if ( '' === $retryable_reason || $attempt >= self::AI_MAX_RETRY_ATTEMPTS ) {
				break;
			}

			NPATD_Observability::log_event(
				'ai_retry_attempt',
				array_merge(
					$attempt_context,
					array(
						'error_code'       => (string) $result->get_error_code(),
						'retryable_reason' => $retryable_reason,
						'error'            => $result->get_error_message(),
					)
				)
			);

			usleep( self::retry_delay_microseconds( $attempt ) );
		}

		if ( $last_error instanceof WP_Error && '' !== $last_retryable_reason ) {
			NPATD_Observability::log_event(
				'ai_retry_exhausted',
				array_merge(
					$context,
					array(
						'stage'            => $stage,
						'attempt'          => $attempts_used,
						'max_attempts'     => self::AI_MAX_RETRY_ATTEMPTS,
						'error_code'       => (string) $last_error->get_error_code(),
						'retryable_reason' => $last_retryable_reason,
						'error'            => $last_error->get_error_message(),
					)
				)
			);

			return self::ai_error(
				'ai_provider_unavailable',
				'AI provider temporarily unavailable while ' . $stage . '. Please retry in a few seconds.'
			);
		}

		if ( $last_error instanceof WP_Error ) {
			NPATD_Observability::log_event(
				'ai_non_retryable_error',
				array_merge(
					$context,
					array(
						'stage'        => $stage,
						'attempt'      => $attempts_used,
						'max_attempts' => self::AI_MAX_RETRY_ATTEMPTS,
						'error_code'   => (string) $last_error->get_error_code(),
						'error'        => $last_error->get_error_message(),
					)
				)
			);

			return self::ai_error( 'ai_provider_unavailable', $last_error->get_error_message() );
		}

		return self::ai_error( 'ai_provider_unavailable', 'AI provider failed without a detailed error.' );
	}

	private static function extract_usage_from_ai_result( $result ): array {
		$usage = array(
			'prompt_tokens'     => 0,
			'completion_tokens' => 0,
			'total_tokens'      => 0,
			'source'            => 'unknown',
			'connector'         => 'unknown',
		);

		if ( ! is_object( $result ) ) {
			return $usage;
		}

		if ( ! isset( $result->usage ) || ! is_object( $result->usage ) ) {
			return $usage;
		}

		$prompt = 0;
		if ( isset( $result->usage->input_tokens ) ) {
			$prompt = (int) $result->usage->input_tokens;
		} elseif ( isset( $result->usage->prompt_tokens ) ) {
			$prompt = (int) $result->usage->prompt_tokens;
		}

		$completion = 0;
		if ( isset( $result->usage->output_tokens ) ) {
			$completion = (int) $result->usage->output_tokens;
		} elseif ( isset( $result->usage->completion_tokens ) ) {
			$completion = (int) $result->usage->completion_tokens;
		}

		$total = isset( $result->usage->total_tokens ) ? (int) $result->usage->total_tokens : ( $prompt + $completion );

		$usage['prompt_tokens'] = max( 0, $prompt );
		$usage['completion_tokens'] = max( 0, $completion );
		$usage['total_tokens'] = max( 0, $total );
		$usage['source'] = 'actual';
		$usage['connector'] = self::extract_connector_from_ai_result( $result );

		return $usage;
	}

	private static function extract_connector_from_ai_result( $result ): string {
		if ( ! is_object( $result ) ) {
			return 'unknown';
		}

		$provider = '';
		$model    = '';

		if ( method_exists( $result, 'get_provider' ) ) {
			$provider = (string) $result->get_provider();
		}

		if ( method_exists( $result, 'get_model' ) ) {
			$model = (string) $result->get_model();
		}

		if ( '' === $provider && isset( $result->provider ) ) {
			$provider = (string) $result->provider;
		}

		if ( '' === $provider && isset( $result->connector ) ) {
			$provider = (string) $result->connector;
		}

		if ( '' === $model && isset( $result->model ) ) {
			$model = (string) $result->model;
		}

		if ( '' === $model && isset( $result->engine ) ) {
			$model = (string) $result->engine;
		}

		$provider = sanitize_key( trim( strtolower( $provider ) ) );
		$model    = sanitize_key( trim( strtolower( $model ) ) );

		if ( '' !== $provider && '' !== $model ) {
			return $provider . '/' . $model;
		}

		if ( '' !== $provider ) {
			return $provider;
		}

		if ( '' !== $model ) {
			return 'model/' . $model;
		}

		return 'unknown';
	}

	private static function usage_accumulator_default(): array {
		return array(
			'prompt_tokens'     => 0,
			'completion_tokens' => 0,
			'total_tokens'      => 0,
			'model_calls'       => 0,
			'has_actual'        => false,
			'has_estimated'     => false,
			'connectors'        => array(),
		);
	}

	private static function usage_accumulator_add( array $accumulator, array $usage ): array {
		$accumulator['prompt_tokens'] += (int) $usage['prompt_tokens'];
		$accumulator['completion_tokens'] += (int) $usage['completion_tokens'];
		$accumulator['total_tokens'] += (int) $usage['total_tokens'];
		$accumulator['model_calls']++;

		if ( 'actual' === $usage['source'] ) {
			$accumulator['has_actual'] = true;
		}

		if ( 'estimated' === $usage['source'] ) {
			$accumulator['has_estimated'] = true;
		}

		$connector = isset( $usage['connector'] ) && is_string( $usage['connector'] ) ? $usage['connector'] : 'unknown';
		if ( '' === $connector ) {
			$connector = 'unknown';
		}

		if ( ! isset( $accumulator['connectors'][ $connector ] ) ) {
			$accumulator['connectors'][ $connector ] = 0;
		}
		$accumulator['connectors'][ $connector ]++;

		return $accumulator;
	}

	private static function primary_connector_from_usage( array $accumulator ): string {
		if ( empty( $accumulator['connectors'] ) || ! is_array( $accumulator['connectors'] ) ) {
			return 'unknown';
		}

		if ( 1 === count( $accumulator['connectors'] ) ) {
			$keys = array_keys( $accumulator['connectors'] );
			return (string) $keys[0];
		}

		arsort( $accumulator['connectors'] );
		$keys = array_keys( $accumulator['connectors'] );
		$top  = isset( $keys[0] ) ? (string) $keys[0] : 'unknown';

		return 'unknown' === $top ? 'mixed' : 'mixed/' . $top;
	}

	private static function usage_accumulator_finalize( array $accumulator ): array {
		$source = 'unknown';
		if ( ! empty( $accumulator['has_actual'] ) && empty( $accumulator['has_estimated'] ) ) {
			$source = 'actual';
		} elseif ( ! empty( $accumulator['has_estimated'] ) ) {
			$source = 'estimated';
		} elseif ( ! empty( $accumulator['has_actual'] ) ) {
			$source = 'actual';
		}

		return array(
			'prompt_tokens'     => max( 0, (int) $accumulator['prompt_tokens'] ),
			'completion_tokens' => max( 0, (int) $accumulator['completion_tokens'] ),
			'total_tokens'      => max( 0, (int) $accumulator['total_tokens'] ),
			'model_calls'       => max( 0, (int) $accumulator['model_calls'] ),
			'source'            => $source,
			'connector'         => self::primary_connector_from_usage( $accumulator ),
		);
	}

	private static function normalize_generate_text_result( $result, array $usage_fallback ): array {
		if ( is_wp_error( $result ) ) {
			return array(
				'ok'    => false,
				'error' => $result,
				'text'  => '',
				'usage' => $usage_fallback,
			);
		}

		if ( is_object( $result ) && method_exists( $result, 'get_text' ) ) {
			$text = $result->get_text();
			$usage = self::extract_usage_from_ai_result( $result );
			if ( 'unknown' === $usage['source'] ) {
				$usage = $usage_fallback;
				if ( ! isset( $usage['connector'] ) ) {
					$usage['connector'] = 'unknown';
				}
			}
			return array(
				'ok'    => true,
				'error' => null,
				'text'  => is_string( $text ) ? $text : (string) $text,
				'usage' => $usage,
			);
		}

		if ( is_string( $result ) ) {
			return array(
				'ok'    => true,
				'error' => null,
				'text'  => $result,
				'usage' => $usage_fallback,
			);
		}

		return array(
			'ok'    => true,
			'error' => null,
			'text'  => (string) $result,
			'usage' => $usage_fallback,
		);
	}

	private static function resolve_from_media_id( int $media_id ) {
		if ( $media_id < 1 ) {
			return self::ai_error( 'invalid_input', 'media_id must reference an audio attachment.' );
		}

		$mime_type = (string) get_post_mime_type( $media_id );
		if ( '' === $mime_type || 0 !== strpos( $mime_type, 'audio/' ) ) {
			return self::ai_error( 'invalid_input', 'media_id must reference an audio attachment.' );
		}

		$path = get_attached_file( $media_id );
		if ( ! self::is_valid_attachment_path( $path ) || ! file_exists( $path ) ) {
			return self::ai_error( 'invalid_input', 'Audio attachment file not found on server.' );
		}

		$bytes = file_get_contents( $path );
		if ( false === $bytes ) {
			return self::ai_error( 'ai_provider_unavailable', 'Unable to read audio attachment bytes.' );
		}

		return array(
			'base64'    => base64_encode( $bytes ),
			'mime_type' => $mime_type,
		);
	}

	private static function resolve_from_signed_url( string $signed_url ) {
		$validation = self::validate_signed_url( $signed_url );
		if ( is_wp_error( $validation ) ) {
			return $validation;
		}

		$response = wp_safe_remote_get(
			$signed_url,
			array(
				'timeout'             => self::SIGNED_URL_FETCH_TIMEOUT_SECONDS,
				'redirection'         => self::SIGNED_URL_MAX_REDIRECTS,
				'reject_unsafe_urls'  => true,
				'limit_response_size' => self::SIGNED_URL_MAX_RESPONSE_BYTES,
			)
		);

		if ( is_wp_error( $response ) ) {
			return self::ai_error( 'ai_provider_unavailable', 'Unable to fetch signed_url audio payload.' );
		}

		$status = (int) wp_remote_retrieve_response_code( $response );
		if ( $status < 200 || $status >= 300 ) {
			return self::ai_error( 'ai_provider_unavailable', 'signed_url returned non-success HTTP status.' );
		}

		$body = wp_remote_retrieve_body( $response );
		if ( ! is_string( $body ) || '' === $body ) {
			return self::ai_error( 'ai_provider_unavailable', 'signed_url response body is empty.' );
		}

		$mime_type = (string) wp_remote_retrieve_header( $response, 'content-type' );
		if ( '' === $mime_type || 0 !== strpos( $mime_type, 'audio/' ) ) {
			$mime_type = 'audio/mpeg';
		}

		return array(
			'base64'    => base64_encode( $body ),
			'mime_type' => $mime_type,
		);
	}

	private static function validate_signed_url( string $signed_url ) {
		$parts = wp_parse_url( $signed_url );
		if ( ! is_array( $parts ) || empty( $parts['scheme'] ) || empty( $parts['host'] ) ) {
			return self::ai_error( 'invalid_input', 'signed_url must be a valid absolute URL.' );
		}

		$scheme          = strtolower( (string) $parts['scheme'] );
		$host            = strtolower( (string) $parts['host'] );
		$allowed_schemes = apply_filters( 'npatd_signed_url_allowed_schemes', array( 'https' ) );

		if ( ! is_array( $allowed_schemes ) ) {
			$allowed_schemes = array( 'https' );
		}

		$allowed_schemes = array_map( 'strtolower', array_map( 'strval', $allowed_schemes ) );
		if ( ! in_array( $scheme, $allowed_schemes, true ) ) {
			return self::ai_error( 'invalid_input', 'signed_url scheme is not allowed.' );
		}

		if ( isset( $parts['port'] ) ) {
			$port = (int) $parts['port'];
			if ( 'https' === $scheme && 443 !== $port ) {
				return self::ai_error( 'invalid_input', 'signed_url port is not allowed for HTTPS.' );
			}

			if ( 'http' === $scheme && 80 !== $port ) {
				return self::ai_error( 'invalid_input', 'signed_url port is not allowed for HTTP.' );
			}
		}

		$allowlist = apply_filters( 'npatd_signed_url_host_allowlist', array() );
		if ( is_array( $allowlist ) && ! empty( $allowlist ) && ! self::host_matches_allowlist( $host, $allowlist ) ) {
			return self::ai_error( 'invalid_input', 'signed_url host is not in the allowed host list.' );
		}

		$ips = self::resolve_host_ips( $host );
		if ( empty( $ips ) ) {
			return self::ai_error( 'ai_provider_unavailable', 'signed_url host could not be resolved.' );
		}

		foreach ( $ips as $ip ) {
			if ( ! self::is_public_ip( $ip ) ) {
				return self::ai_error( 'invalid_input', 'signed_url resolves to a private or reserved IP, which is not allowed.' );
			}
		}

		return true;
	}

	private static function host_matches_allowlist( string $host, array $allowlist ): bool {
		$host = strtolower( trim( $host ) );

		foreach ( $allowlist as $pattern ) {
			$pattern = strtolower( trim( (string) $pattern ) );
			if ( '' === $pattern ) {
				continue;
			}

			if ( 0 === strpos( $pattern, '*.' ) ) {
				$suffix = substr( $pattern, 1 );
				if ( '' !== $suffix && str_ends_with( $host, $suffix ) ) {
					return true;
				}
				continue;
			}

			if ( $host === $pattern ) {
				return true;
			}
		}

		return false;
	}

	private static function resolve_host_ips( string $host ): array {
		$ips = array();

		if ( filter_var( $host, FILTER_VALIDATE_IP ) ) {
			return array( $host );
		}

		$ipv4 = gethostbynamel( $host );
		if ( is_array( $ipv4 ) ) {
			$ips = array_merge( $ips, $ipv4 );
		}

		if ( function_exists( 'dns_get_record' ) ) {
			$aaaa = dns_get_record( $host, DNS_AAAA );
			if ( is_array( $aaaa ) ) {
				foreach ( $aaaa as $record ) {
					if ( ! empty( $record['ipv6'] ) ) {
						$ips[] = (string) $record['ipv6'];
					}
				}
			}
		}

		$ips = array_values( array_unique( array_filter( $ips ) ) );

		return $ips;
	}

	private static function is_public_ip( string $ip ): bool {
		return false !== filter_var(
			$ip,
			FILTER_VALIDATE_IP,
			FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
		);
	}

	private static function resolve_from_base64( array $audio ) {
		$base64    = isset( $audio['base64'] ) ? (string) $audio['base64'] : '';
		$mime_type = isset( $audio['mime_type'] ) ? (string) $audio['mime_type'] : '';

		if ( '' === $base64 || '' === $mime_type ) {
			return self::ai_error( 'invalid_input', 'base64 audio mode requires base64 and mime_type.' );
		}

		if ( 0 !== strpos( $mime_type, 'audio/' ) ) {
			return self::ai_error( 'invalid_input', 'mime_type must start with audio/.' );
		}

		$decoded = base64_decode( $base64, true );
		if ( false === $decoded || '' === $decoded ) {
			return self::ai_error( 'invalid_input', 'Invalid base64 audio payload.' );
		}

		return array(
			'base64'    => $base64,
			'mime_type' => $mime_type,
		);
	}

	private static function resolve_audio_payload( array $payload ) {
		$audio = $payload['audio'];

		if ( isset( $audio['media_id'] ) ) {
			return self::resolve_from_media_id( (int) $audio['media_id'] );
		}

		if ( isset( $audio['signed_url'] ) ) {
			return self::resolve_from_signed_url( (string) $audio['signed_url'] );
		}

		return self::resolve_from_base64( $audio );
	}

	private static function output_language( array $payload ): string {
		if ( isset( $payload['editorial_options']['language'] ) && is_string( $payload['editorial_options']['language'] ) ) {
			return $payload['editorial_options']['language'];
		}

		if ( function_exists( 'get_locale' ) ) {
			return (string) get_locale();
		}

		return 'en-US';
	}

	private static function audio_context_hint( array $payload ): string {
		if ( isset( $payload['audio']['media_id'] ) ) {
			return 'Audio source: WordPress media library attachment.';
		}

		if ( isset( $payload['audio']['signed_url'] ) ) {
			return 'Audio source: signed URL attachment.';
		}

		return 'Audio source: base64 payload.';
	}

	private static function target_length_range( array $payload ): array {
		$target = isset( $payload['editorial_options']['target_length'] ) ? (string) $payload['editorial_options']['target_length'] : 'medium';

		if ( 'short' === $target ) {
			return array(
				'mode' => 'short',
				'min'  => 250,
				'max'  => 450,
			);
		}

		if ( 'long' === $target ) {
			return array(
				'mode' => 'long',
				'min'  => 900,
				'max'  => 1400,
			);
		}

		return array(
			'mode' => 'medium',
			'min'  => 500,
			'max'  => 850,
		);
	}

	private static function structured_word_count( array $structured ): int {
		$parts = array();

		if ( isset( $structured['title'] ) && is_string( $structured['title'] ) ) {
			$parts[] = $structured['title'];
		}

		if ( isset( $structured['sections'] ) && is_array( $structured['sections'] ) ) {
			foreach ( $structured['sections'] as $section ) {
				if ( ! is_array( $section ) ) {
					continue;
				}

				if ( isset( $section['heading'] ) && is_string( $section['heading'] ) ) {
					$parts[] = $section['heading'];
				}

				if ( isset( $section['paragraphs'] ) && is_array( $section['paragraphs'] ) ) {
					foreach ( $section['paragraphs'] as $paragraph ) {
						if ( is_string( $paragraph ) ) {
							$parts[] = $paragraph;
						}
					}
				}

				if ( isset( $section['bullet_points'] ) && is_array( $section['bullet_points'] ) ) {
					foreach ( $section['bullet_points'] as $bullet ) {
						if ( is_string( $bullet ) ) {
							$parts[] = $bullet;
						}
					}
				}
			}
		}

		$text = trim( preg_replace( '/\s+/', ' ', implode( ' ', $parts ) ) );
		if ( '' === $text ) {
			return 0;
		}

		return count( preg_split( '/\s+/', $text ) );
	}

	private static function decode_structured_json( $structured_json ) {
		$structured = json_decode( (string) $structured_json, true );
		if ( ! is_array( $structured ) ) {
			return NPATD_Ability_Contract::error_response( 'ai_provider_unavailable', 'Could not parse AI structured output.' );
		}

		return $structured;
	}

	private static function clamp_temperature( float $value ): float {
		if ( $value < self::MIN_TEMPERATURE ) {
			return self::MIN_TEMPERATURE;
		}

		if ( $value > self::MAX_TEMPERATURE ) {
			return self::MAX_TEMPERATURE;
		}

		return $value;
	}

	private static function generation_temperature( array $payload ): float {
		$default = (float) self::DEFAULT_TEMPERATURE;

		$requested = $default;
		if ( isset( $payload['editorial_options']['temperature'] ) && is_numeric( $payload['editorial_options']['temperature'] ) ) {
			$requested = (float) $payload['editorial_options']['temperature'];
		}

		return self::clamp_temperature( $requested );
	}

	private static function temperature_observability_context( array $payload, float $effective ): array {
		$has_requested = isset( $payload['editorial_options']['temperature'] ) && is_numeric( $payload['editorial_options']['temperature'] );
		$requested     = $has_requested ? (float) $payload['editorial_options']['temperature'] : (float) self::DEFAULT_TEMPERATURE;

		return array(
			'has_requested'     => $has_requested,
			'requested'         => $requested,
			'effective'         => $effective,
			'external_run_id'   => isset( $payload['external_run_id'] ) ? (string) $payload['external_run_id'] : '',
		);
	}

	public static function transcribe_and_structure( array $payload ) {
		if ( ! function_exists( 'wp_ai_client_prompt' ) ) {
			return NPATD_Ability_Contract::error_response( 'ai_provider_unavailable', 'WordPress AI Client is not available.' );
		}

		$usage_accumulator = self::usage_accumulator_default();

		$audio_payload = self::resolve_audio_payload( $payload );
		if ( is_wp_error( $audio_payload ) ) {
			return $audio_payload;
		}

		$language = self::output_language( $payload );
		$hint     = self::audio_context_hint( $payload );
		$length   = self::target_length_range( $payload );
		$generation_temperature = self::generation_temperature( $payload );

		NPATD_Observability::log_event(
			'ai_generation_temperature_resolved',
			self::temperature_observability_context( $payload, $generation_temperature )
		);

		$transcription_builder = wp_ai_client_prompt( "Transcribe this audio note accurately in {$language}. Return plain text only." )
			->with_file( $audio_payload['base64'], $audio_payload['mime_type'] )
			->using_temperature( 0.1 );

		if ( method_exists( $transcription_builder, 'is_supported_for_text_generation' ) && ! $transcription_builder->is_supported_for_text_generation() ) {
			return self::ai_error( 'ai_provider_unavailable', 'No configured AI model supports this audio transcription request.' );
		}

		$retry_context = array(
			'external_run_id' => isset( $payload['external_run_id'] ) ? (string) $payload['external_run_id'] : '',
			'source'          => isset( $payload['source'] ) ? (string) $payload['source'] : '',
		);

		$transcript_result = self::normalize_generate_text_result(
			self::generate_text_with_retry( $transcription_builder, 'transcribing audio', $retry_context ),
			array(
				'prompt_tokens'     => 0,
				'completion_tokens' => 0,
				'total_tokens'      => self::estimated_tokens_per_model_call(),
				'source'            => 'estimated',
				'connector'         => 'unknown',
			)
		);

		if ( ! $transcript_result['ok'] ) {
			return $transcript_result['error'];
		}

		$transcript = (string) $transcript_result['text'];
		$usage_accumulator = self::usage_accumulator_add( $usage_accumulator, $transcript_result['usage'] );

		$schema = array(
			'type'       => 'object',
			'properties' => array(
				'title'    => array( 'type' => 'string' ),
				'sections' => array(
					'type'  => 'array',
					'items' => array(
						'type'       => 'object',
						'properties' => array(
							'heading'       => array( 'type' => 'string' ),
							'level'         => array( 'type' => 'integer' ),
							'paragraphs'    => array( 'type' => 'array', 'items' => array( 'type' => 'string' ) ),
							'bullet_points' => array( 'type' => 'array', 'items' => array( 'type' => 'string' ) ),
						),
						'required'   => array( 'heading', 'level', 'paragraphs' ),
					),
				),
			),
			'required'   => array( 'title', 'sections' ),
		);

		$prompt =
			"Create a concise blog-ready draft in {$language}.\n" .
			"Target length mode: {$length['mode']}.\n" .
			"Required word count range: {$length['min']} to {$length['max']} words.\n" .
			"Hard requirement: produce at least {$length['min']} words.\n" .
			"Return JSON with title and sections.\n" .
			"Each section must include heading, level (2 or 3), paragraphs, optional bullet_points.\n" .
			"{$hint}\n" .
			"Use factual and clear writing.\n" .
			"Do not add fabricated facts.\n\n" .
			"Transcript:\n" . (string) $transcript;

		$structured_builder = wp_ai_client_prompt( $prompt )
			->using_temperature( $generation_temperature )
			->as_json_response( $schema );

		$structured_result = self::normalize_generate_text_result(
			self::generate_text_with_retry( $structured_builder, 'generating structured draft', $retry_context ),
			array(
				'prompt_tokens'     => 0,
				'completion_tokens' => 0,
				'total_tokens'      => self::estimated_tokens_per_model_call(),
				'source'            => 'estimated',
				'connector'         => 'unknown',
			)
		);

		if ( ! $structured_result['ok'] ) {
			return $structured_result['error'];
		}

		$structured_json = (string) $structured_result['text'];
		$usage_accumulator = self::usage_accumulator_add( $usage_accumulator, $structured_result['usage'] );

		$structured = self::decode_structured_json( $structured_json );
		if ( is_wp_error( $structured ) ) {
			return $structured;
		}

		$current_word_count = self::structured_word_count( $structured );
		if ( $current_word_count < $length['min'] ) {
			$expansion_prompt =
				"Expand and rewrite the following draft in {$language}.\n" .
				"Required word count range: {$length['min']} to {$length['max']} words.\n" .
				"Current draft is about {$current_word_count} words, so it is too short.\n" .
				"Keep facts consistent with the transcript and avoid fabricated details.\n" .
				"Return JSON with the same schema: title and sections (heading, level, paragraphs, optional bullet_points).\n\n" .
				"Transcript:\n" . (string) $transcript . "\n\n" .
				"Current draft JSON:\n" . wp_json_encode( $structured );

			$expanded_builder = wp_ai_client_prompt( $expansion_prompt )
				->using_temperature( $generation_temperature )
				->as_json_response( $schema );

			$expanded_result = self::normalize_generate_text_result(
				self::generate_text_with_retry( $expanded_builder, 'expanding structured draft to target length', $retry_context ),
				array(
					'prompt_tokens'     => 0,
					'completion_tokens' => 0,
					'total_tokens'      => self::estimated_tokens_per_model_call(),
					'source'            => 'estimated',
					'connector'         => 'unknown',
				)
			);

			if ( $expanded_result['ok'] ) {
				$usage_accumulator = self::usage_accumulator_add( $usage_accumulator, $expanded_result['usage'] );
				$expanded_structured = self::decode_structured_json( $expanded_result['text'] );
				if ( ! is_wp_error( $expanded_structured ) ) {
					$structured = $expanded_structured;
				}
			}
		}

		return array(
			'structured' => $structured,
			'ai_usage'   => self::usage_accumulator_finalize( $usage_accumulator ),
		);
	}
}
