<?php
/**
 * Structured generation error classification.
 *
 * @package AIAltTextGenerator
 * @since 2.4.2
 */

namespace AIAltTextGenerator;

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

/**
 * Maps raw provider / transport failures into stable codes and user-facing messages.
 *
 * @since 2.4.2
 */
class GenerationError {

	public const CODE_API_KEY          = 'api_credentials';
	public const CODE_PROVIDER         = 'ai_provider';
	public const CODE_MODEL            = 'model_unavailable';
	public const CODE_RATE_LIMIT       = 'rate_limit';
	public const CODE_IMAGE_ACCESS     = 'image_access';
	public const CODE_UNSUPPORTED_TYPE = 'unsupported_type';
	public const CODE_PERMISSION       = 'permissions';
	public const CODE_TIMEOUT          = 'timeout';
	public const CODE_SERVER           = 'server_config';
	public const CODE_INTERNAL         = 'internal';
	public const CODE_QUOTA            = 'quota';

	/** @var list<string> Error codes that should not be retried. */
	public const PERMANENT_CODES = [
		self::CODE_API_KEY,
		self::CODE_MODEL,
		self::CODE_UNSUPPORTED_TYPE,
		self::CODE_PERMISSION,
		self::CODE_QUOTA,
		'missing_api_key',
		'unsupported_format',
		'file_not_found',
		'file_too_large',
		'decorative',
		'monthly_limit',
		'provider_locked',
	];

	/**
	 * @return array{code: string, message: string, retryable: bool, http_status: int}
	 */
	public static function fromWpError( \WP_Error $error, int $http_status = 0 ): array {
		$code    = (string) $error->get_error_code();
		$message = (string) $error->get_error_message();
		$data    = $error->get_error_data();

		if ( is_array( $data ) && isset( $data['status'] ) ) {
			$http_status = (int) $data['status'];
		}

		return self::classify( $code, $message, $http_status );
	}

	/**
	 * @return array{code: string, message: string, retryable: bool, http_status: int}
	 */
	public static function classify( string $code, string $message, int $http_status = 0 ): array {
		$lower = strtolower( $message . ' ' . $code );

		if ( $http_status === 401 || $http_status === 403 || self::containsAny( $lower, [ 'invalid api key', 'incorrect api key', 'authentication', 'unauthorized', 'missing_api_key', 'api key' ] ) ) {
			return self::pack( self::CODE_API_KEY, self::messageFor( self::CODE_API_KEY, $message ), false, $http_status ?: 401 );
		}

		if ( $http_status === 429 || self::containsAny( $lower, [ 'rate limit', 'too many requests', 'quota exceeded', 'resource_exhausted' ] ) ) {
			$is_quota = self::containsAny( $lower, [ 'quota', 'billing', 'insufficient_quota' ] );
			$mapped   = $is_quota ? self::CODE_QUOTA : self::CODE_RATE_LIMIT;

			return self::pack( $mapped, self::messageFor( $mapped, $message ), ! $is_quota, $http_status ?: 429 );
		}

		if ( $http_status === 404 || self::containsAny( $lower, [ 'model_not_found', 'does not exist', 'model not found', 'unknown model' ] ) ) {
			return self::pack( self::CODE_MODEL, self::messageFor( self::CODE_MODEL, $message ), false, $http_status ?: 404 );
		}

		if ( self::containsAny( $lower, [ 'unsupported_format', 'unsupported', 'mime', 'file type' ] ) ) {
			return self::pack( self::CODE_UNSUPPORTED_TYPE, self::messageFor( self::CODE_UNSUPPORTED_TYPE, $message ), false, $http_status );
		}

		if ( self::containsAny( $lower, [ 'file_not_found', 'could not read', 'image file not found', 'no_editor', 'load_failed' ] ) ) {
			return self::pack( self::CODE_IMAGE_ACCESS, self::messageFor( self::CODE_IMAGE_ACCESS, $message ), false, $http_status );
		}

		if ( self::containsAny( $lower, [ 'permission', 'forbidden', 'not allowed' ] ) ) {
			return self::pack( self::CODE_PERMISSION, self::messageFor( self::CODE_PERMISSION, $message ), false, $http_status ?: 403 );
		}

		if ( $http_status === 408 || self::containsAny( $lower, [ 'timed out', 'timeout', 'cURL error 28' ] ) ) {
			return self::pack( self::CODE_TIMEOUT, self::messageFor( self::CODE_TIMEOUT, $message ), true, $http_status ?: 408 );
		}

		if ( $http_status >= 500 || self::containsAny( $lower, [ 'server error', 'internal server', 'bad gateway', 'service unavailable' ] ) ) {
			return self::pack( self::CODE_PROVIDER, self::messageFor( self::CODE_PROVIDER, $message ), true, $http_status ?: 502 );
		}

		if ( in_array( $code, self::PERMANENT_CODES, true ) ) {
			return self::pack( $code, $message !== '' ? $message : self::messageFor( self::CODE_INTERNAL, '' ), false, $http_status );
		}

		$retryable = $http_status === 0 || $http_status >= 500 || $http_status === 429;

		return self::pack(
			$code !== '' ? $code : self::CODE_INTERNAL,
			$message !== '' ? $message : self::messageFor( self::CODE_INTERNAL, '' ),
			$retryable,
			$http_status
		);
	}

	public static function isPermanent( string $code ): bool {
		return in_array( $code, self::PERMANENT_CODES, true );
	}

	public static function isPermanentMessage( string $message ): bool {
		$classified = self::classify( '', $message );

		return ! $classified['retryable'];
	}

	/**
	 * @return array{code: string, message: string, retryable: bool, http_status: int}
	 */
	private static function pack( string $code, string $message, bool $retryable, int $http_status ): array {
		return [
			'code'        => $code,
			'message'     => $message,
			'retryable'   => $retryable,
			'http_status' => $http_status,
		];
	}

	private static function messageFor( string $code, string $fallback ): string {
		$defaults = [
			self::CODE_API_KEY          => __( 'API credentials are missing or invalid. Check Settings → AI Connections.', 'wp-powersuite' ),
			self::CODE_PROVIDER         => __( 'The AI provider returned an error. Try again in a moment.', 'wp-powersuite' ),
			self::CODE_MODEL            => __( 'The selected AI model is unavailable. Choose a different vision model in Settings.', 'wp-powersuite' ),
			self::CODE_RATE_LIMIT       => __( 'AI provider rate limit reached. Waiting and retrying is recommended.', 'wp-powersuite' ),
			self::CODE_IMAGE_ACCESS     => __( 'The image file could not be read. It may be missing, private, or in an unsupported format.', 'wp-powersuite' ),
			self::CODE_UNSUPPORTED_TYPE => __( 'This file type is not supported for AI vision analysis.', 'wp-powersuite' ),
			self::CODE_PERMISSION       => __( 'You do not have permission to edit this attachment.', 'wp-powersuite' ),
			self::CODE_TIMEOUT          => __( 'The AI request timed out. Try a smaller batch size or increase the request timeout in Settings.', 'wp-powersuite' ),
			self::CODE_SERVER           => __( 'A server configuration issue blocked the request (memory, execution time, or outbound HTTP).', 'wp-powersuite' ),
			self::CODE_QUOTA            => __( 'AI provider quota or billing limit reached. Check your provider account.', 'wp-powersuite' ),
			self::CODE_INTERNAL         => __( 'An internal plugin error occurred while generating alt text.', 'wp-powersuite' ),
		];

		if ( isset( $defaults[ $code ] ) ) {
			// Prefer the classified message when the raw message is empty or generic.
			if ( $fallback === '' || self::isGenericMessage( $fallback ) ) {
				return $defaults[ $code ];
			}

			return $fallback;
		}

		return $fallback !== '' ? $fallback : $defaults[ self::CODE_INTERNAL ];
	}

	private static function isGenericMessage( string $message ): bool {
		$generic = [
			'something went wrong',
			'generation failed',
			'unknown error',
			'server error',
			'unknown api error',
			'api error',
		];

		return in_array( strtolower( trim( $message ) ), $generic, true );
	}

	/**
	 * @param list<string> $needles
	 */
	private static function containsAny( string $haystack, array $needles ): bool {
		foreach ( $needles as $needle ) {
			if ( $needle !== '' && str_contains( $haystack, $needle ) ) {
				return true;
			}
		}

		return false;
	}
}
