<?php
/**
 * OpenAI client and ALT generation logic.
 *
 * @package Altify_AI
 */

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

/**
 * Handles AI requests and ALT text updates.
 */
class Altify_AI_Client {
	/**
	 * Logger instance.
	 *
	 * @var Altify_AI_Logger
	 */
	private $logger;

	/**
	 * WooCommerce helper.
	 *
	 * @var Altify_AI_WooCommerce|null
	 */
	private $woocommerce;

	/**
	 * Constructor.
	 *
	 * @param Altify_AI_Logger           $logger      Logger.
	 * @param Altify_AI_WooCommerce|null $woocommerce WooCommerce helper.
	 */
	public function __construct($logger, $woocommerce = null) {
		$this->logger      = $logger;
		$this->woocommerce = $woocommerce;
	}

	/**
	 * Generate and save ALT text for an attachment.
	 *
	 * @param int   $attachment_id Attachment ID.
	 * @param array $args          Additional context.
	 *
	 * @return array
	 */
	public function generate_and_save_alt($attachment_id, $args = array()) {
		$attachment_id = absint($attachment_id);
		$args          = is_array($args) ? $args : array();

		if (!$attachment_id || !altify_ai_is_image_attachment($attachment_id)) {
			return array(
				'status'  => 'skipped',
				'alt'     => '',
				'message' => __('Skipped non-image attachment.', 'altify-ai-auto-alt-text-generator'),
			);
		}

		$settings       = altify_ai_get_settings();
		$max_length     = max(20, absint($settings['max_alt_length']));
		$current_alt    = trim((string) get_post_meta($attachment_id, '_wp_attachment_image_alt', true));
		$overwrite      = !empty($settings['overwrite_existing_alt']) || !empty($args['force_overwrite']);
		$source         = !empty($args['source']) ? sanitize_key((string) $args['source']) : 'manual';
		$force_offline  = !empty($args['force_offline']);
		$requested_mode = !empty($args['generation_mode']) ? sanitize_key((string) $args['generation_mode']) : '';

		if ('' === $requested_mode) {
			$requested_mode = ($force_offline || !empty($settings['offline_mode'])) ? 'offline_only' : 'ai_first';
		}

		$generation_mode        = altify_ai_normalize_generation_mode($requested_mode, 'offline_first');
		$try_offline_first      = in_array($generation_mode, array('offline_first', 'offline_only'), true);
		$allow_ai_requests      = !in_array($generation_mode, array('offline_only'), true);
		$allow_offline_fallback = !in_array($generation_mode, array('ai_only'), true);
		$should_skip            = ('' !== $current_alt) && !$overwrite;

		if ($should_skip) {
			$message = sprintf(
				/* translators: %d: attachment id */
				__('Attachment #%d skipped because ALT already exists.', 'altify-ai-auto-alt-text-generator'),
				$attachment_id
			);
			$skip_method = $try_offline_first ? 'offline' : 'ai';
			$this->logger->log(
				'info',
				$message,
				array(
					'attachment_id'     => $attachment_id,
					'source'            => $source,
					'generation_method' => $skip_method,
					'alt_text'          => altify_ai_truncate_text($current_alt, $max_length),
				)
			);

			return array(
				'status'  => 'skipped',
				'alt'     => $current_alt,
				'generation_method' => $skip_method,
				'diagnostic' => '',
				'message' => $message,
			);
		}

		$product_context = array();
		if (!empty($args['product_name'])) {
			$product_context['product_name'] = $args['product_name'];
		}
		if (!empty($args['product_categories'])) {
			$product_context['product_categories'] = $args['product_categories'];
		}
		if (empty($product_context) && $this->woocommerce && $this->woocommerce->is_enabled()) {
			$product_context = $this->woocommerce->get_product_context_for_attachment($attachment_id);
		}

		$context   = altify_ai_get_attachment_context($attachment_id, $product_context);
		$language  = altify_ai_resolve_language(
			$settings['alt_language'],
			isset($settings['alt_language_custom']) ? $settings['alt_language_custom'] : ''
		);
		$prompt    = altify_ai_build_prompt($context, $language, $max_length);
		$hash_data = array(
			'context'      => $context,
			'language'     => $language,
			'max_length'   => $max_length,
			'model'        => sanitize_text_field((string) $settings['model']),
			'temperature'  => (float) $settings['temperature'],
			'max_tokens'   => absint($settings['max_tokens']),
			'generation_mode' => $generation_mode,
			'use_vision'   => !empty($settings['use_vision_analysis']) ? 1 : 0,
			'image_sig'    => !empty($settings['use_vision_analysis']) ? altify_ai_get_attachment_file_signature($attachment_id) : '',
		);
		$prompt_hash = altify_ai_hash_prompt_payload($hash_data);

		$cached_hash = (string) get_post_meta($attachment_id, '_altify_ai_last_prompt_hash', true);
		$cached_alt  = (string) get_post_meta($attachment_id, '_altify_ai_last_generated_alt', true);
		$used_cache    = false;
		$used_fallback = false;
		$diagnostic    = '';
		$generation_method = 'ai';
		$alt_text = '';

		if ('' !== $cached_alt && $cached_hash === $prompt_hash) {
			$alt_text   = altify_ai_sanitize_alt_text($cached_alt, $max_length);
			$used_cache = true;
			$generation_method = 'cache';
		} else {
			if ($try_offline_first) {
				$offline_alt = altify_ai_generate_offline_alt_text($context, $language, $max_length);
				$offline_alt = altify_ai_sanitize_alt_text($offline_alt, $max_length);
				$offline_ok  = '' !== $offline_alt;

				if ($offline_ok && $allow_ai_requests) {
					$offline_ok = !altify_ai_is_low_quality_alt_text($offline_alt);
				}

				if ($offline_ok) {
					$alt_text          = $offline_alt;
					$generation_method = 'offline';
					$diagnostic        = ('offline_only' === $generation_mode)
						? __('Offline-only mode enabled (no API request).', 'altify-ai-auto-alt-text-generator')
						: __('Offline-first mode used metadata-based ALT.', 'altify-ai-auto-alt-text-generator');
				} elseif ($allow_ai_requests) {
					$result = $this->request_alt_from_openai($prompt, $settings, $attachment_id);
					if (is_wp_error($result)) {
						$alt_text      = ('' !== $offline_alt) ? $offline_alt : altify_ai_fallback_alt_text($attachment_id, $context, $language, $max_length);
						$used_fallback = true;
						$diagnostic    = sanitize_text_field((string) $result->get_error_message());
						$generation_method = 'offline_fallback';
						$this->logger->log(
							'warning',
							sprintf(
								/* translators: 1: attachment id, 2: error message */
								__('OpenAI failed for attachment #%1$d: %2$s. Offline-first fallback used.', 'altify-ai-auto-alt-text-generator'),
								$attachment_id,
								$result->get_error_message()
							),
							array(
								'attachment_id'     => $attachment_id,
								'source'            => $source,
								'generation_method' => 'offline_fallback',
								'error'             => sanitize_text_field((string) $result->get_error_message()),
							)
						);
					} else {
						$alt_text = altify_ai_sanitize_alt_text($result, $max_length);
						$generation_method = 'ai';
					}
				} else {
					$alt_text = $offline_alt;
					$generation_method = 'offline';
				}
			} else {
				$result = $this->request_alt_from_openai($prompt, $settings, $attachment_id);
				if (is_wp_error($result)) {
					$diagnostic    = sanitize_text_field((string) $result->get_error_message());
					if ($allow_offline_fallback) {
						$alt_text      = altify_ai_fallback_alt_text($attachment_id, $context, $language, $max_length);
						$used_fallback = true;
						$generation_method = 'offline_fallback';
						$this->logger->log(
							'warning',
							sprintf(
								/* translators: 1: attachment id, 2: error message */
								__('OpenAI failed for attachment #%1$d: %2$s. Fallback ALT used.', 'altify-ai-auto-alt-text-generator'),
								$attachment_id,
								$result->get_error_message()
							),
							array(
								'attachment_id'     => $attachment_id,
								'source'            => $source,
								'generation_method' => 'offline_fallback',
								'error'             => sanitize_text_field((string) $result->get_error_message()),
							)
						);
					}
				} else {
					$alt_text = altify_ai_sanitize_alt_text($result, $max_length);
					$generation_method = 'ai';
				}
			}
		}

		if ('' === $alt_text && $allow_offline_fallback) {
			$alt_text      = altify_ai_fallback_alt_text($attachment_id, $context, $language, $max_length);
			$used_fallback = true;
			if ('cache' !== $generation_method) {
				$generation_method = 'offline_fallback';
			}
		}

		if ('' === $alt_text) {
			$message = sprintf(
				/* translators: %d: attachment id */
				__('Failed to generate ALT text for attachment #%d.', 'altify-ai-auto-alt-text-generator'),
				$attachment_id
			);
			$this->logger->log(
				'error',
				$message,
				array(
					'attachment_id'     => $attachment_id,
					'source'            => $source,
					'generation_method' => 'failed',
					'error'             => $diagnostic,
				)
			);

			return array(
				'status'  => 'failed',
				'alt'     => '',
				'generation_method' => 'failed',
				'diagnostic' => $diagnostic,
				'message' => $message,
			);
		}

		update_post_meta($attachment_id, '_wp_attachment_image_alt', $alt_text);
		update_post_meta($attachment_id, '_altify_ai_last_prompt_hash', $prompt_hash);
		update_post_meta($attachment_id, '_altify_ai_last_generated_alt', $alt_text);
		update_post_meta($attachment_id, '_altify_ai_last_generated_at', current_time('mysql', true));

		if ($used_cache) {
			$message = sprintf(
				/* translators: %d: attachment id */
				__('Attachment #%d updated from cache.', 'altify-ai-auto-alt-text-generator'),
				$attachment_id
			);
		} elseif ($used_fallback) {
			if ('' !== $diagnostic) {
				$message = sprintf(
					/* translators: 1: attachment id, 2: diagnostic reason */
					__('Attachment #%1$d updated with fallback ALT. Reason: %2$s', 'altify-ai-auto-alt-text-generator'),
					$attachment_id,
					$diagnostic
				);
			} else {
				$message = sprintf(
					/* translators: %d: attachment id */
					__('Attachment #%d updated with fallback ALT.', 'altify-ai-auto-alt-text-generator'),
					$attachment_id
				);
			}
		} else {
			if ('offline' === $generation_method) {
				$message = sprintf(
					/* translators: %d: attachment id */
					__('Attachment #%d updated with offline ALT.', 'altify-ai-auto-alt-text-generator'),
					$attachment_id
				);
			} else {
				$message = sprintf(
					/* translators: %d: attachment id */
					__('Attachment #%d updated with AI ALT.', 'altify-ai-auto-alt-text-generator'),
					$attachment_id
				);
			}
		}

		$this->logger->log(
			'info',
			$message,
			array(
				'attachment_id'     => $attachment_id,
				'source'            => $source,
				'generation_method' => $generation_method,
				'alt_text'          => altify_ai_truncate_text($alt_text, $max_length),
			)
		);

		return array(
			'status'        => 'success',
			'alt'           => $alt_text,
			'cached'        => $used_cache,
			'fallback_used' => $used_fallback,
			'generation_method' => $generation_method,
			'diagnostic'    => $diagnostic,
			'message'       => $message,
		);
	}

	/**
	 * Generate and save ALT text using an explicit generation mode.
	 *
	 * @param int    $attachment_id   Attachment ID.
	 * @param string $generation_mode Generation mode.
	 * @param array  $args            Additional context.
	 *
	 * @return array
	 */
	public function generate_and_save_alt_with_mode($attachment_id, $generation_mode, $args = array()) {
		$args = is_array($args) ? $args : array();
		$args['generation_mode'] = altify_ai_normalize_generation_mode($generation_mode, 'offline_first');

		return $this->generate_and_save_alt($attachment_id, $args);
	}

	/**
	 * Test API connectivity from settings page.
	 *
	 * @return array
	 */
	public function test_api_connection() {
		$settings = altify_ai_get_settings();
		$api_key  = trim((string) $settings['openai_api_key']);

		if ('' === $api_key) {
			return array(
				'success' => false,
				'message' => __('OpenAI API key is missing.', 'altify-ai-auto-alt-text-generator'),
			);
		}

		$prompt = 'Reply with: API connection successful';
		$result = $this->request_alt_from_openai($prompt, $settings);
		if (is_wp_error($result)) {
			return array(
				'success' => false,
				'message' => $this->get_test_api_user_message($result),
			);
		}

		return array(
			'success' => true,
			'message' => __('OpenAI API test successful.', 'altify-ai-auto-alt-text-generator'),
		);
	}

	/**
	 * Convert technical API errors into user-friendly Test API messages.
	 *
	 * @param WP_Error $error API error.
	 *
	 * @return string
	 */
	private function get_test_api_user_message($error) {
		if (!is_wp_error($error)) {
			return __('API test failed.', 'altify-ai-auto-alt-text-generator');
		}

		$code    = (string) $error->get_error_code();
		$message = strtolower((string) $error->get_error_message());

		if (false !== strpos($message, 'insufficient_quota')) {
			return __('Your OpenAI API quota is exhausted. Add billing/credits in your OpenAI account, then test again.', 'altify-ai-auto-alt-text-generator');
		}

		if ('altify_ai_rate_limited' === $code) {
			return __('Plugin rate limit reached. Wait about 1 minute or lower Requests per minute, then test again.', 'altify-ai-auto-alt-text-generator');
		}

		if (false !== strpos($message, 'http 429') || false !== strpos($message, 'rate_limit')) {
			return __('OpenAI is rate-limiting requests right now. Wait about 1 minute, lower Requests per minute, and try again.', 'altify-ai-auto-alt-text-generator');
		}

		if (false !== strpos($message, 'invalid_api_key') || false !== strpos($message, 'incorrect api key')) {
			return __('API key is invalid. Check the key and make sure it belongs to the correct OpenAI project.', 'altify-ai-auto-alt-text-generator');
		}

		if (false !== strpos($message, 'model_not_found') || false !== strpos($message, 'does not exist')) {
			return __('Selected model is not available for this API key. Try gpt-4o-mini or another enabled model.', 'altify-ai-auto-alt-text-generator');
		}

		return __('OpenAI request failed. Check API key, model access, and billing/quota in your OpenAI account.', 'altify-ai-auto-alt-text-generator');
	}

	/**
	 * Call OpenAI with retry/backoff.
	 *
	 * @param string $prompt   Prompt.
	 * @param array  $settings Plugin settings.
	 * @param int    $attachment_id Attachment ID for optional vision analysis.
	 *
	 * @return string|WP_Error
	 */
	private function request_alt_from_openai($prompt, $settings, $attachment_id = 0) {
		$api_key = trim((string) $settings['openai_api_key']);
		if ('' === $api_key) {
			return new WP_Error('altify_ai_no_api_key', __('OpenAI API key is not configured.', 'altify-ai-auto-alt-text-generator'));
		}

		$model       = sanitize_text_field((string) $settings['model']);
		$model       = ('' === $model) ? 'gpt-4o-mini' : $model;
		$temperature = (float) $settings['temperature'];
		$max_tokens  = max(1, absint($settings['max_tokens']));
		$rpm_limit   = max(1, absint($settings['requests_per_minute']));
		$attachment_id = absint($attachment_id);

		$body = array(
			'model'       => $model,
			'temperature' => $temperature,
			'max_tokens'  => $max_tokens,
			'messages'    => array(
				array(
					'role'    => 'system',
					'content' => 'You generate concise ALT text for images. Return plain text only.',
				),
				array(
					'role'    => 'user',
					'content' => (string) $prompt,
				),
			),
		);

		$use_vision = !empty($settings['use_vision_analysis']) && $attachment_id > 0 && altify_ai_is_image_attachment($attachment_id);
		if ($use_vision) {
			$vision_payload = $this->build_vision_image_payload($attachment_id);
			if (is_wp_error($vision_payload)) {
				$this->logger->log(
					'warning',
					sprintf(
						/* translators: 1: attachment id, 2: error message */
						__('Vision payload unavailable for attachment #%1$d. Falling back to metadata-only prompt. Reason: %2$s', 'altify-ai-auto-alt-text-generator'),
						$attachment_id,
						$vision_payload->get_error_message()
					),
					array('attachment_id' => $attachment_id, 'source' => 'vision')
				);
			} else {
				$body['messages'][1]['content'] = array(
					array(
						'type' => 'text',
						'text' => (string) $prompt . "\n" . 'Use the image content as primary context. Use metadata only as support.',
					),
					array(
						'type' => 'image_url',
						'image_url' => array(
							'url' => $vision_payload,
						),
					),
				);
			}
		}

		$max_attempts = 4;
		$delay_sec    = 1;

		for ($attempt = 1; $attempt <= $max_attempts; $attempt++) {
			if (!$this->consume_rate_limit_slot($rpm_limit)) {
				return new WP_Error('altify_ai_rate_limited', __('Rate limit reached (requests per minute). Try again shortly.', 'altify-ai-auto-alt-text-generator'));
			}

			$response = wp_remote_post(
				'https://api.openai.com/v1/chat/completions',
				array(
					'timeout' => 25,
					'headers' => array(
						'Authorization' => 'Bearer ' . $api_key,
						'Content-Type'  => 'application/json',
					),
					'body'    => wp_json_encode($body),
				)
			);

			if (is_wp_error($response)) {
				if ($attempt < $max_attempts) {
					sleep($delay_sec);
					$delay_sec *= 2;
					continue;
				}
				return $response;
			}

			$http_code      = absint(wp_remote_retrieve_response_code($response));
			$response_body  = wp_remote_retrieve_body($response);
			$response_array = json_decode($response_body, true);
			$error_details  = $this->build_openai_error_details($response_array);

			if (429 === $http_code || $http_code >= 500) {
				if ($attempt < $max_attempts) {
					$sleep_for = $delay_sec;
					if (429 === $http_code) {
						$retry_after = $this->get_retry_after_seconds($response);
						if ($retry_after > 0) {
							$sleep_for = min($retry_after, 30);
						}
					}
					sleep($sleep_for);
					$delay_sec = min($delay_sec * 2, 30);
					continue;
				}

				$error_message = sprintf(
					/* translators: %d: HTTP code */
					__('OpenAI request failed after retries (HTTP %d).', 'altify-ai-auto-alt-text-generator'),
					$http_code
				);
				if ('' !== $error_details) {
					$error_message = sprintf(
						/* translators: 1: HTTP code, 2: OpenAI error details */
						__('OpenAI request failed after retries (HTTP %1$d): %2$s', 'altify-ai-auto-alt-text-generator'),
						$http_code,
						$error_details
					);
				}

				return new WP_Error(
					'altify_ai_openai_retry_failed',
					$error_message
				);
			}

			if ($http_code < 200 || $http_code >= 300) {
				$error_message = __('OpenAI API returned an error.', 'altify-ai-auto-alt-text-generator');
				if ('' !== $error_details) {
					$error_message = $error_details;
				}
				return new WP_Error('altify_ai_openai_http_error', $error_message);
			}

			if (!is_array($response_array)) {
				return new WP_Error('altify_ai_invalid_response', __('Invalid OpenAI response.', 'altify-ai-auto-alt-text-generator'));
			}

			$content = '';
			if (!empty($response_array['choices'][0]['message']['content'])) {
				$content = (string) $response_array['choices'][0]['message']['content'];
			}

			$content = trim($content);
			if ('' === $content) {
				return new WP_Error('altify_ai_empty_response', __('OpenAI returned empty content.', 'altify-ai-auto-alt-text-generator'));
			}

			return $content;
		}

		return new WP_Error('altify_ai_unknown_error', __('Unable to generate ALT text.', 'altify-ai-auto-alt-text-generator'));
	}

	/**
	 * Build OpenAI-compatible image payload (data URL) for vision analysis.
	 *
	 * @param int $attachment_id Attachment ID.
	 *
	 * @return string|WP_Error
	 */
	private function build_vision_image_payload($attachment_id) {
		$attachment_id = absint($attachment_id);
		if (!$attachment_id) {
			return new WP_Error('altify_ai_vision_invalid_attachment', __('Invalid attachment for image analysis.', 'altify-ai-auto-alt-text-generator'));
		}

		$file_path = get_attached_file($attachment_id);
		if (!$file_path) {
			$file_path = (string) get_post_meta($attachment_id, '_wp_attached_file', true);
		}
		$file_path = $this->normalize_attachment_file_path((string) $file_path);

		if ('' === $file_path || !file_exists($file_path) || !is_readable($file_path)) {
			return new WP_Error('altify_ai_vision_missing_file', __('Attachment file is not accessible for image analysis.', 'altify-ai-auto-alt-text-generator'));
		}

		$editor = wp_get_image_editor($file_path);
		if (is_wp_error($editor)) {
			$mime = $this->detect_file_mime_type($attachment_id, $file_path);
			return $this->build_data_url_from_file($file_path, $mime, 1048576);
		}

		$size = $editor->get_size();
		$width  = !empty($size['width']) ? absint($size['width']) : 0;
		$height = !empty($size['height']) ? absint($size['height']) : 0;
		if (($width > 1280 || $height > 1280) && method_exists($editor, 'resize')) {
			$editor->resize(1280, 1280, false);
		}

		if (method_exists($editor, 'set_quality')) {
			$editor->set_quality(75);
		}

		$temp_file = wp_tempnam('altify-ai-vision.jpg');
		if (!$temp_file) {
			$mime = $this->detect_file_mime_type($attachment_id, $file_path);
			return $this->build_data_url_from_file($file_path, $mime, 1048576);
		}

		$saved = $editor->save($temp_file, 'image/jpeg');
		if (is_wp_error($saved) || empty($saved['path']) || !is_readable($saved['path'])) {
			if (file_exists($temp_file)) {
				wp_delete_file($temp_file);
			}
			$mime = $this->detect_file_mime_type($attachment_id, $file_path);
			return $this->build_data_url_from_file($file_path, $mime, 1048576);
		}

		$data_url = $this->build_data_url_from_file((string) $saved['path'], 'image/jpeg', 1048576);
		wp_delete_file((string) $saved['path']);

		return $data_url;
	}

	/**
	 * Convert a local image file into a data URL for OpenAI vision input.
	 *
	 * @param string $file_path  File path.
	 * @param string $mime       MIME type.
	 * @param int    $max_bytes  Max source bytes.
	 *
	 * @return string|WP_Error
	 */
	private function build_data_url_from_file($file_path, $mime, $max_bytes) {
		$file_path = (string) $file_path;
		$mime      = sanitize_mime_type((string) $mime);
		$max_bytes = max(1024, absint($max_bytes));

		if ('' === $mime || 0 !== strpos($mime, 'image/')) {
			$mime = 'image/jpeg';
		}

		if (!file_exists($file_path) || !is_readable($file_path)) {
			return new WP_Error('altify_ai_vision_file_unreadable', __('Image file is not readable for vision analysis.', 'altify-ai-auto-alt-text-generator'));
		}

		$file_size = filesize($file_path);
		if (false === $file_size || $file_size <= 0) {
			return new WP_Error('altify_ai_vision_file_empty', __('Image file is empty for vision analysis.', 'altify-ai-auto-alt-text-generator'));
		}

		if ($file_size > $max_bytes) {
			return new WP_Error(
				'altify_ai_vision_file_too_large',
				__('Image file is too large for vision analysis payload. Try a smaller image.', 'altify-ai-auto-alt-text-generator')
			);
		}

		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Needed to read local image bytes for optional vision request payload.
		$binary = file_get_contents($file_path);
		if (false === $binary || '' === $binary) {
			return new WP_Error('altify_ai_vision_read_failed', __('Unable to read image bytes for vision analysis.', 'altify-ai-auto-alt-text-generator'));
		}

		return 'data:' . $mime . ';base64,' . base64_encode($binary); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
	}

	/**
	 * Detect image MIME type with attachment-first fallback.
	 *
	 * @param int    $attachment_id Attachment ID.
	 * @param string $file_path     File path.
	 *
	 * @return string
	 */
	private function detect_file_mime_type($attachment_id, $file_path) {
		$attachment_id = absint($attachment_id);
		$file_path     = (string) $file_path;

		$mime = get_post_mime_type($attachment_id);
		$mime = is_string($mime) ? sanitize_mime_type($mime) : '';
		if ('' !== $mime && 0 === strpos($mime, 'image/')) {
			return $mime;
		}

		$file_type = wp_check_filetype($file_path);
		if (!empty($file_type['type']) && 0 === strpos((string) $file_type['type'], 'image/')) {
			return sanitize_mime_type((string) $file_type['type']);
		}

		return 'image/jpeg';
	}

	/**
	 * Normalize attachment file path into an absolute local path.
	 *
	 * @param string $file_path Raw attachment file path.
	 *
	 * @return string
	 */
	private function normalize_attachment_file_path($file_path) {
		$file_path = trim((string) $file_path);
		if ('' === $file_path) {
			return '';
		}

		if (function_exists('path_is_absolute') && path_is_absolute($file_path)) {
			return $file_path;
		}

		$uploads = wp_get_upload_dir();
		if (!empty($uploads['basedir']) && is_string($uploads['basedir'])) {
			return trailingslashit($uploads['basedir']) . ltrim($file_path, '/\\');
		}

		return $file_path;
	}

	/**
	 * Build a readable error message from OpenAI response payload.
	 *
	 * @param array|null $response_array Decoded response body.
	 *
	 * @return string
	 */
	private function build_openai_error_details($response_array) {
		if (!is_array($response_array) || empty($response_array['error']) || !is_array($response_array['error'])) {
			return '';
		}

		$error_parts = array();

		if (!empty($response_array['error']['type'])) {
			$error_parts[] = sprintf(
				/* translators: %s: OpenAI error type. */
				__('type: %s', 'altify-ai-auto-alt-text-generator'),
				sanitize_text_field((string) $response_array['error']['type'])
			);
		}

		if (!empty($response_array['error']['code'])) {
			$error_parts[] = sprintf(
				/* translators: %s: OpenAI error code. */
				__('code: %s', 'altify-ai-auto-alt-text-generator'),
				sanitize_text_field((string) $response_array['error']['code'])
			);
		}

		if (!empty($response_array['error']['message'])) {
			$error_parts[] = sanitize_text_field((string) $response_array['error']['message']);
		}

		return implode(' | ', $error_parts);
	}

	/**
	 * Parse Retry-After header value into seconds.
	 *
	 * @param array $response HTTP response.
	 *
	 * @return int
	 */
	private function get_retry_after_seconds($response) {
		$retry_after = wp_remote_retrieve_header($response, 'retry-after');
		if (is_array($retry_after)) {
			$retry_after = reset($retry_after);
		}

		$retry_after = trim((string) $retry_after);
		if ('' === $retry_after) {
			return 0;
		}

		if (is_numeric($retry_after)) {
			return max(0, absint($retry_after));
		}

		$retry_timestamp = strtotime($retry_after);
		if (false === $retry_timestamp) {
			return 0;
		}

		$current_timestamp = (int) current_time('timestamp', true);
		$seconds           = (int) ($retry_timestamp - $current_timestamp);

		return max(0, $seconds);
	}

	/**
	 * Consume one token from local request-per-minute bucket.
	 *
	 * @param int $rpm_limit Requests per minute limit.
	 *
	 * @return bool
	 */
	private function consume_rate_limit_slot($rpm_limit) {
		$rpm_limit = max(1, absint($rpm_limit));
		$key       = 'altify_ai_rpm_' . gmdate('YmdHi');
		$count     = (int) get_transient($key);

		if ($count >= $rpm_limit) {
			return false;
		}

		$count++;
		set_transient($key, $count, MINUTE_IN_SECONDS + 5);
		return true;
	}
}
