<?php

namespace Fotogenic_AI;

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

final class Api_Client {
    private static ?Api_Client $instance = null;
    private const PUBLIC_ENDPOINTS = [];
    private const BILLABLE_START_ENDPOINTS = [ 'images_generator', 'campaign_set_generator', 'enhance_images' ];
    private const DEFAULT_MULTIPART_MAX_BYTES = 268435456;
    private const HARD_MULTIPART_MAX_BYTES = 268435456;
    private const MULTIPART_MEMORY_RESERVE_BYTES = 8388608;

    private function __construct() {}

    public static function instance(): Api_Client {
        if ( null === self::$instance ) {
            self::$instance = new self();
        }

        return self::$instance;
    }

    public function get_default_config(): array {
        return Api_Endpoints::get_default_config();
    }

    public function request( string $method, string $endpoint_key, array $data = [], array $options = [] ) {
        $method  = strtoupper( $method );
        $context = $this->build_request_context( $method, $endpoint_key, $options );
        if ( is_wp_error( $context ) ) {
            return $context;
        }

        $request_args = $this->build_base_request_args( $method, $context['headers'], $context['timeout_seconds'] );
        if ( 'POST' === $method ) {
            $request_args['headers']['Content-Type'] = 'application/json';
            $request_args['body']                    = wp_json_encode( $data );
        }

        return $this->perform_request( $endpoint_key, $method, $data, [], $context, $request_args );
    }

    public function request_multipart( string $endpoint_key, array $fields = [], array $files = [], array $options = [] ) {
        $context = $this->build_request_context( 'POST', $endpoint_key, $options );
        if ( is_wp_error( $context ) ) {
            return $context;
        }

        $multipart = $this->build_multipart_request( $endpoint_key, $fields, $files );
        if ( is_wp_error( $multipart ) ) {
            return $multipart;
        }

        $request_args = $this->build_base_request_args( 'POST', $context['headers'], $context['timeout_seconds'] );
        $request_args['headers']['Content-Type'] = 'multipart/form-data; boundary=' . $multipart['boundary'];
        $request_args['body']                    = $multipart['body'];

        return $this->perform_request( $endpoint_key, 'POST', $fields, $files, $context, $request_args );
    }

    private function build_request_context( string $method, string $endpoint_key, array $options ) {
        $settings  = Settings::instance();
        $defaults  = $this->get_default_config()['endpoints'] ?? [];
        $endpoints = array_merge( $defaults, $settings->get_api_endpoints() );
        $endpoint  = $endpoints[ $endpoint_key ] ?? '';

        if ( '' === $endpoint ) {
            return Messages::error( 'invalid_endpoint', 400 );
        }
        if ( preg_match( '#^https?://#i', $endpoint ) ) {
            return Messages::error( 'invalid_endpoint', 400 );
        }

		$idempotency_key = is_string( $options['idempotency_key'] ?? null )
			? (string) $options['idempotency_key']
			: '';
		$billable_start = 'POST' === $method && $this->is_billable_start_endpoint( $endpoint_key );
		$idempotency_header_required = $billable_start || $this->is_reconciliation_endpoint( $endpoint_key, $method );
		if (
			( $idempotency_header_required && ! Upstream_Request_Identity::is_valid( $idempotency_key ) )
			|| ( ! $idempotency_header_required && '' !== $idempotency_key )
		) {
			return new \WP_Error(
				'fotogenic_ai_invalid_idempotency_key',
				__( 'The generation request identity is invalid.', 'fotogenic-ai' ),
				[
					'status'         => 400,
					'not_dispatched' => true,
				]
			);
		}

        $path_params = isset( $options['path_params'] ) && is_array( $options['path_params'] ) ? $options['path_params'] : [];
        foreach ( $path_params as $key => $value ) {
            $endpoint = str_replace( '{' . $key . '}', rawurlencode( (string) $value ), $endpoint );
        }
        if ( preg_match( '/\{[^}]+\}/', $endpoint ) ) {
            return Messages::error( 'missing_path_param', 400 );
        }

        $base_url  = rtrim( $settings->get_api_base_url(), '/' );
        $base_path = trim( (string) $settings->get_api_base_path(), '/' );
        if ( '' !== $base_path ) {
            $base_path = '/' . $base_path;
        }
        $endpoint = '/' . ltrim( $endpoint, '/' );
        $url      = $base_url . $base_path . $endpoint;

        $query_params = isset( $options['query_params'] ) && is_array( $options['query_params'] ) ? $options['query_params'] : [];
		$query_params = array_merge( $query_params, Api_Endpoints::get_fixed_query_params( $endpoint_key ) );
        if ( ! empty( $query_params ) ) {
            $url = add_query_arg( $query_params, $url );
        }

        $headers = [
            'Accept' => 'application/json',
        ];

        $requires_auth = $this->endpoint_requires_auth( $endpoint_key );
        if ( $requires_auth ) {
            $access_token = Auth_Session::instance()->get_access_token_for_request();
            if ( is_wp_error( $access_token ) ) {
                return $access_token;
            }
            $headers['Authorization'] = 'Bearer ' . $access_token;
        }
		if ( $idempotency_header_required ) {
			$headers['Idempotency-Key'] = $idempotency_key;
		}

		return [
            'url'             => $url,
            'headers'         => $headers,
            'timeout_seconds' => $this->get_timeout_seconds( $endpoint_key, $method ),
            'path_params'     => $path_params,
            'query_params'    => $query_params,
            'requires_auth'   => $requires_auth,
			'has_idempotency_key' => $idempotency_header_required,
			'billable_start'      => $billable_start,
				'strict_response'     => $billable_start || $this->is_strict_wire_endpoint( $endpoint_key, $method ),
            'request_id'      => $this->normalize_request_id( (string) ( $options['request_id'] ?? '' ) ),
            'run_id'          => $this->normalize_request_id( (string) ( $options['run_id'] ?? '' ) ),
        ];
    }

    private function build_base_request_args( string $method, array $headers, int $timeout_seconds ): array {
        return [
            'method'             => $method,
            'headers'            => $headers,
            'timeout'            => $timeout_seconds,
            'redirection'        => 0,
            'reject_unsafe_urls' => true,
            'user-agent'         => sprintf( 'FotogenicAI/%s; WordPress/%s', FOTOGENIC_AI_VERSION, get_bloginfo( 'version' ) ),
        ];
    }

    private function perform_request( string $endpoint_key, string $method, array $data, array $files, array $context, array $request_args ) {
        $logger       = Logger::instance();
        $request_id   = '' !== $context['request_id']
            ? $context['request_id']
            : substr( wp_generate_password( 10, false, false ), 0, 10 );
        $start        = microtime( true );
        $response     = null;
        $auth_retry   = 0;
        $request_meta = $this->extract_request_meta( $data, $endpoint_key, $files );
        if ( '' !== $context['run_id'] ) {
            $request_meta['run_id'] = $context['run_id'];
        }
        $request_body = $logger->enabled()
            ? $this->build_request_log_payload( $data, $files, $endpoint_key )
            : [];

        do {
            $response = $this->execute_request(
				$context['url'],
				$request_args,
				$endpoint_key,
				$method,
				$context['has_idempotency_key']
			);
            if ( '' !== $context['run_id'] ) {
                $logger->touch_run_api( $context['run_id'] );
            }
            if ( is_wp_error( $response ) ) {
                break;
            }

            $status = (int) wp_remote_retrieve_response_code( $response );
			if (
				$context['requires_auth']
				&& 401 === $status
					&& $auth_retry < Workflow_Operational_Limits::AUTH_REPLAY_LIMIT
				&& $this->can_retry_after_unauthorized( $endpoint_key, $method, $context['has_idempotency_key'] )
			) {
				$rejected_access_token = $this->extract_bearer_token( $request_args['headers'] );
				$fresh_token = Auth_Session::instance()->force_refresh_after_unauthorized( $rejected_access_token );
                if ( is_wp_error( $fresh_token ) ) {
                    return $fresh_token;
                }
                $request_args['headers']['Authorization'] = 'Bearer ' . $fresh_token;
                $auth_retry++;
                continue;
            }
            break;
		} while ( $auth_retry <= Workflow_Operational_Limits::AUTH_REPLAY_LIMIT );

        if ( is_wp_error( $response ) ) {
            if ( $logger->enabled() ) {
                $this->log_api_event( [
                    'request_id'      => $request_id,
                    'endpoint'        => $endpoint_key,
                    'method'          => $method,
                    'url'             => $context['url'],
                    'status'          => 0,
                    'duration_ms'     => (int) round( ( microtime( true ) - $start ) * 1000 ),
                    'timeout_seconds' => $context['timeout_seconds'],
                    'request_body'    => $request_body,
                    'request_meta'    => $request_meta,
                    'path_params'     => $context['path_params'],
                    'query_params'    => $context['query_params'],
                    'error'           => [
                        'code'    => $response->get_error_code(),
                        'message' => $response->get_error_message(),
                    ],
                ] );
                $logger->error( 'api_request_failed', [
                    'request_id' => $request_id,
                    'endpoint'   => $endpoint_key,
                ] );
            }

            $error_data = $this->is_billable_start_endpoint( $endpoint_key )
                ? [ 'uncertain' => true, 'requestId' => $request_id ]
                : [];
            return Messages::error( 'request_failed', 500, [], $error_data );
        }

        $status       = (int) wp_remote_retrieve_response_code( $response );
        $content_type = (string) wp_remote_retrieve_header( $response, 'content-type' );
        $server       = (string) wp_remote_retrieve_header( $response, 'server' );
        $body         = wp_remote_retrieve_body( $response );
        $body_length  = strlen( (string) $body );

        if ( $status >= 400 ) {
            if ( $logger->enabled() ) {
                $this->log_api_event( [
                    'request_id'      => $request_id,
                    'endpoint'        => $endpoint_key,
                    'method'          => $method,
                    'url'             => $context['url'],
                    'status'          => $status,
                    'duration_ms'     => (int) round( ( microtime( true ) - $start ) * 1000 ),
                    'timeout_seconds' => $context['timeout_seconds'],
                    'content_type'    => $content_type,
                    'server'          => $server,
                    'body_length'     => $body_length,
                    'request_body'    => $request_body,
                    'request_meta'    => $request_meta,
                    'path_params'     => $context['path_params'],
                    'query_params'    => $context['query_params'],
                ] );
                $logger->error( 'api_http_error', [
                    'request_id' => $request_id,
                    'endpoint'   => $endpoint_key,
                    'status'     => $status,
                ] );
            }

            if ( $context['requires_auth'] && 401 === $status ) {
                Auth_Session::instance()->expire_session();
                return Messages::error( 'auth_session_expired', 401 );
            }
			if ( 409 === $status && $this->is_billable_start_endpoint( $endpoint_key ) ) {
				return new \WP_Error(
					'fotogenic_ai_idempotency_conflict',
					__( 'This generation request conflicts with an earlier request identity. Review the request and try again.', 'fotogenic-ai' ),
					[ 'status' => 409 ]
				);
			}

            $error_data = [ 'status' => $status ];
            if ( $this->is_billable_start_endpoint( $endpoint_key ) && $status >= 500 ) {
                $error_data['uncertain'] = true;
                $error_data['requestId'] = $request_id;
            }
            return new \WP_Error( 'fotogenic_ai_http_error', $this->get_safe_http_message( $status ), $error_data );
        }

		if ( $context['strict_response'] && 200 !== $status ) {
			return $this->strict_response_error( $context, $request_id );
		}

        if ( '' === $body ) {
            if ( $logger->enabled() ) {
                $this->log_api_event( [
                    'request_id'      => $request_id,
                    'endpoint'        => $endpoint_key,
                    'method'          => $method,
                    'url'             => $context['url'],
                    'status'          => $status,
                    'duration_ms'     => (int) round( ( microtime( true ) - $start ) * 1000 ),
                    'timeout_seconds' => $context['timeout_seconds'],
                    'content_type'    => $content_type,
                    'server'          => $server,
                    'body_length'     => 0,
                    'request_body'    => $request_body,
                    'request_meta'    => $request_meta,
                    'path_params'     => $context['path_params'],
                    'query_params'    => $context['query_params'],
                ] );
            }

			return $context['strict_response']
				? $this->strict_response_error( $context, $request_id )
				: [];
        }

        $payload = trim( (string) $body );
        $payload = preg_replace( '/^\xEF\xBB\xBF/', '', $payload );
        $decoded = json_decode( $payload, true );

        if ( null === $decoded && JSON_ERROR_NONE !== json_last_error() ) {
            $json_error = json_last_error_msg();
            if ( $logger->enabled() ) {
                $this->log_api_event( [
                    'request_id'      => $request_id,
                    'endpoint'        => $endpoint_key,
                    'method'          => $method,
                    'url'             => $context['url'],
                    'status'          => $status,
                    'duration_ms'     => (int) round( ( microtime( true ) - $start ) * 1000 ),
                    'timeout_seconds' => $context['timeout_seconds'],
                    'content_type'    => $content_type,
                    'server'          => $server,
                    'body_length'     => $body_length,
                    'json_error'      => $json_error,
                    'request_body'    => $request_body,
                    'request_meta'    => $request_meta,
                    'path_params'     => $context['path_params'],
                    'query_params'    => $context['query_params'],
                ] );
                $logger->error( 'api_invalid_json', [
                    'request_id' => $request_id,
                    'endpoint'   => $endpoint_key,
                ] );
            }

			return $context['strict_response']
				? $this->strict_response_error( $context, $request_id )
				: Messages::error( 'invalid_response', 500 );
        }

		if ( $context['strict_response'] && ! $this->is_nonempty_associative_array( $decoded ) ) {
			return $this->strict_response_error( $context, $request_id );
		}

        if ( $logger->enabled() ) {
            $this->log_api_event( [
                'request_id'      => $request_id,
                'endpoint'        => $endpoint_key,
                'method'          => $method,
                'url'             => $context['url'],
                'status'          => $status,
                'duration_ms'     => (int) round( ( microtime( true ) - $start ) * 1000 ),
                'timeout_seconds' => $context['timeout_seconds'],
                'content_type'    => $content_type,
                'server'          => $server,
                'body_length'     => $body_length,
                'request_body'    => $request_body,
                'request_meta'    => $request_meta,
                'path_params'     => $context['path_params'],
                'query_params'    => $context['query_params'],
            ] );
        }

        return $decoded;
    }

    private function build_multipart_request( string $endpoint_key, array $fields, array $files ) {
        $boundary = '----FotogenicAI' . wp_generate_password( 24, false, false );
        $eol      = "\r\n";
        $body     = '';

        $preflight = $this->preflight_multipart_request( $endpoint_key, $boundary, $fields, $files );
        if ( is_wp_error( $preflight ) ) {
            return $preflight;
        }
        $files = $preflight['files'];

        foreach ( $fields as $name => $value ) {
            if ( null === $value ) {
                continue;
            }
            $body .= '--' . $boundary . $eol;
            $body .= 'Content-Disposition: form-data; name="' . $this->escape_multipart_name( (string) $name ) . '"' . $eol . $eol;
            $body .= $this->normalize_multipart_text_value( $value ) . $eol;
        }

        foreach ( $files as $name => $entry ) {
            $file_list = $this->normalize_multipart_file_list( $entry );
            foreach ( $file_list as $validated ) {
                // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Reads a validated local temp file path for multipart upload assembly.
                $contents = file_get_contents( $validated['path'] );
                if ( false === $contents ) {
                    return new \WP_Error(
                        'fotogenic_ai_invalid_image',
                        __( 'Unable to read the selected image.', 'fotogenic-ai' ),
                        [ 'status' => 400 ]
                    );
                }

                $body .= '--' . $boundary . $eol;
                $body .= 'Content-Disposition: form-data; name="' . $this->escape_multipart_name( (string) $name ) . '"; filename="' . $this->escape_multipart_name( $validated['filename'] ) . '"' . $eol;
                $body .= 'Content-Type: ' . $validated['mime'] . $eol . $eol;
                $body .= $contents . $eol;
            }
        }

        $body .= '--' . $boundary . '--' . $eol;

        return [
            'boundary' => $boundary,
            'body'     => $body,
        ];
    }

    private function preflight_multipart_request( string $endpoint_key, string $boundary, array $fields, array $files ) {
        $normalized_files = [];
        $estimated_bytes = strlen( $boundary ) + 16;

        foreach ( $fields as $name => $value ) {
            if ( null === $value ) {
                continue;
            }
            $estimated_bytes += strlen( (string) $name ) + strlen( $this->normalize_multipart_text_value( $value ) ) + 256;
        }

        foreach ( $files as $name => $entry ) {
            $file_list = $this->normalize_multipart_file_list( $entry );
            if ( empty( $file_list ) ) {
                continue;
            }
            foreach ( $file_list as $descriptor ) {
                $validated = $this->validate_file_descriptor( $descriptor );
                if ( is_wp_error( $validated ) ) {
                    return $validated;
                }
                $normalized_files[ $name ][] = $validated;
                $estimated_bytes += (int) $validated['bytes'] + strlen( (string) $name ) + strlen( $validated['filename'] ) + strlen( $validated['mime'] ) + 512;
            }
        }

        $default_limit = $this->is_billable_start_endpoint( $endpoint_key )
            ? Workflow_Operational_Limits::MAX_MULTIPART_BYTES
            : self::DEFAULT_MULTIPART_MAX_BYTES;
        $max_bytes = (int) apply_filters( 'fotogenic_ai_max_multipart_bytes', $default_limit, $endpoint_key );
		$hard_limit = $this->is_billable_start_endpoint( $endpoint_key )
			? Workflow_Operational_Limits::MAX_MULTIPART_BYTES
			: self::HARD_MULTIPART_MAX_BYTES;
        $max_bytes = max( 1048576, min( $hard_limit, $max_bytes ) );
        if ( $estimated_bytes > $max_bytes ) {
            return new \WP_Error(
                'fotogenic_ai_multipart_too_large',
                __( 'The selected images are too large to send together.', 'fotogenic-ai' ),
                [
                    'status'          => 413,
                    'estimated_bytes' => $estimated_bytes,
                    'max_bytes'       => $max_bytes,
                ]
            );
        }

        $memory_limit = $this->get_php_memory_limit_bytes();
        if ( $memory_limit > 0 ) {
            $available_memory = max( 0, $memory_limit - memory_get_usage( true ) );
            $required_memory = ( $estimated_bytes * 3 ) + self::MULTIPART_MEMORY_RESERVE_BYTES;
            if ( $required_memory > $available_memory ) {
                return new \WP_Error(
                    'fotogenic_ai_multipart_memory_unavailable',
                    __( 'There is not enough server memory to prepare these images safely.', 'fotogenic-ai' ),
                    [
                        'status'          => 413,
                        'estimated_bytes' => $estimated_bytes,
                    ]
                );
            }
        }

        return [
            'files'           => $normalized_files,
            'estimated_bytes' => $estimated_bytes,
        ];
    }

    private function validate_file_descriptor( array $descriptor ) {
        $path = isset( $descriptor['path'] ) ? (string) $descriptor['path'] : '';
        if ( '' === $path || ! is_readable( $path ) || ! is_file( $path ) ) {
            return new \WP_Error(
                'fotogenic_ai_invalid_image',
                __( 'Unable to read the selected image.', 'fotogenic-ai' ),
                [ 'status' => 400 ]
            );
        }

        $mime = isset( $descriptor['mime'] ) ? (string) $descriptor['mime'] : '';
        if ( '' === $mime ) {
            $mime = (string) wp_check_filetype( basename( $path ) )['type'];
        }
        if ( '' === $mime ) {
            $mime = 'application/octet-stream';
        }

        $filename = isset( $descriptor['filename'] ) ? sanitize_file_name( (string) $descriptor['filename'] ) : '';
        if ( '' === $filename ) {
            $filename = sanitize_file_name( wp_basename( $path ) );
        }
        if ( '' === $filename ) {
            $filename = 'upload.bin';
        }

        $bytes = filesize( $path );
        if ( false === $bytes || $bytes < 1 ) {
            return new \WP_Error(
                'fotogenic_ai_invalid_image',
                __( 'Invalid image data provided.', 'fotogenic-ai' ),
                [ 'status' => 400 ]
            );
        }
        $max_file_bytes = (int) apply_filters( 'fotogenic_ai_max_multipart_file_bytes', Workflow_Operational_Limits::MAX_FILE_BYTES );
        $max_file_bytes = max( 1048576, min( Workflow_Operational_Limits::MAX_FILE_BYTES, $max_file_bytes ) );
        if ( $bytes > $max_file_bytes ) {
            return new \WP_Error(
                'fotogenic_ai_image_too_large',
                __( 'Selected image is too large to process.', 'fotogenic-ai' ),
                [ 'status' => 413 ]
            );
        }

        return [
            'path'     => $path,
            'mime'     => $mime,
            'filename' => $filename,
            'bytes'    => $bytes,
        ];
    }

    private function normalize_multipart_text_value( $value ): string {
        if ( is_bool( $value ) ) {
            return $value ? 'true' : 'false';
        }
        if ( is_scalar( $value ) ) {
            return (string) $value;
        }
        return (string) wp_json_encode( $value );
    }

    private function escape_multipart_name( string $value ): string {
        return str_replace( [ "\r", "\n", '"' ], [ '', '', '' ], $value );
    }

    private function normalize_multipart_file_list( $entry ): array {
        if ( ! is_array( $entry ) ) {
            return [];
        }
        if ( isset( $entry['path'] ) || isset( $entry['filename'] ) || isset( $entry['mime'] ) || isset( $entry['bytes'] ) ) {
            return [ $entry ];
        }

        return array_values(
            array_filter(
                $entry,
                static fn( $item ) => is_array( $item )
            )
        );
    }

    private function execute_request( string $url, array $request_args, string $endpoint_key, string $method, bool $has_idempotency_key ) {
        $response    = null;
        $attempt     = 0;
        $max_retries = $this->get_retry_count( $endpoint_key, $method, $has_idempotency_key );

        do {
            $attempt++;
            $response = wp_remote_request( $url, $request_args );
            if ( ! is_wp_error( $response ) ) {
                $status = (int) wp_remote_retrieve_response_code( $response );
                if ( $this->should_retry_http( $endpoint_key, $method, $status, $has_idempotency_key ) && $attempt <= $max_retries ) {
                    usleep( $this->get_retry_delay_ms( $attempt, $response ) * 1000 );
                    continue;
                }
                break;
            }

            if ( ! $this->should_retry( $endpoint_key, $method, $response, $has_idempotency_key ) || $attempt > $max_retries ) {
                break;
            }

            usleep( $this->get_retry_delay_ms( $attempt ) * 1000 );
        } while ( $attempt <= $max_retries );

        return $response;
    }

    private function endpoint_requires_auth( string $endpoint_key ): bool {
        return ! in_array( $endpoint_key, self::PUBLIC_ENDPOINTS, true );
    }

    private function get_safe_http_message( int $status ): string {
		$messages = [
			400 => 'bad_request',
			401 => 'unauthorized',
			402 => 'payment_required',
			403 => 'forbidden',
			422 => 'unprocessable',
			500 => 'server_error',
			503 => 'service_unavailable',
		];

		return Messages::t( $messages[ $status ] ?? 'unexpected_api_error' );
    }

    private function get_timeout_seconds( string $endpoint_key, string $method ): int {
        if ( 'POST' === $method && 'configuration_generator' === $endpoint_key ) {
            return 60;
        }
        if ( 'POST' === $method && in_array( $endpoint_key, [ 'images_generator', 'campaign_set_generator', 'enhance_images' ], true ) ) {
			return Workflow_Operational_Limits::BILLABLE_HTTP_TIMEOUT_SECONDS;
        }
		if ( 'POST' === $method && 'jobs_batch' === $endpoint_key ) {
			return Workflow_Operational_Limits::RETRYING_HTTP_TIMEOUT_SECONDS;
		}
		if ( 'GET' === $method && 'job' === $endpoint_key ) {
			return Workflow_Operational_Limits::RETRYING_HTTP_TIMEOUT_SECONDS;
		}
		return Workflow_Operational_Limits::RETRYING_HTTP_TIMEOUT_SECONDS;
    }

    private function get_retry_count( string $endpoint_key, string $method, bool $has_idempotency_key ): int {
		if ( ! $this->is_retry_allowed( $endpoint_key, $method, $has_idempotency_key ) ) {
			return 0;
		}
		return 'POST' === $method && $this->is_billable_start_endpoint( $endpoint_key )
			? Workflow_Operational_Limits::BILLABLE_HTTP_MAX_RETRIES
			: Workflow_Operational_Limits::RETRYING_HTTP_MAX_RETRIES;
    }

    private function should_retry( string $endpoint_key, string $method, \WP_Error $error, bool $has_idempotency_key ): bool {
		if ( 'POST' === $method && $this->is_billable_start_endpoint( $endpoint_key ) ) {
			return false;
		}
        if ( ! $this->is_retry_allowed( $endpoint_key, $method, $has_idempotency_key ) ) {
            return false;
        }
        $code = $error->get_error_code();
        if ( 'http_request_failed' !== $code ) {
            return false;
        }
        $message   = strtolower( (string) $error->get_error_message() );
        $retryable = [ 'timed out', 'timeout', 'could not resolve', 'name resolution', 'connection refused', 'connection reset' ];
        foreach ( $retryable as $needle ) {
            if ( fotogenic_ai_str_contains( $message, $needle ) ) {
                return true;
            }
        }
        return false;
    }

    private function should_retry_http( string $endpoint_key, string $method, int $status, bool $has_idempotency_key ): bool {
        if ( ! $this->is_retry_allowed( $endpoint_key, $method, $has_idempotency_key ) ) {
            return false;
        }
		if ( 'POST' === $method && $this->is_billable_start_endpoint( $endpoint_key ) ) {
			return 429 === $status;
		}
        return in_array( $status, [ 429, 502, 503 ], true );
    }

    private function is_retry_allowed( string $endpoint_key, string $method, bool $has_idempotency_key ): bool {
		if ( 'POST' === $method && $this->is_billable_start_endpoint( $endpoint_key ) ) {
			return $has_idempotency_key;
		}
        if ( 'POST' === $method && 'jobs_batch' === $endpoint_key ) {
            return true;
        }
		if ( 'GET' === $method && in_array( $endpoint_key, [ 'job', 'credits_balance', 'requests_reconcile' ], true ) ) {
            return true;
        }
        return false;
    }

    private function can_retry_after_unauthorized( string $endpoint_key, string $method, bool $has_idempotency_key ): bool {
		if ( 'POST' === $method && $this->is_billable_start_endpoint( $endpoint_key ) ) {
			return $has_idempotency_key;
		}

		return true;
    }

    private function is_billable_start_endpoint( string $endpoint_key ): bool {
        return in_array( $endpoint_key, self::BILLABLE_START_ENDPOINTS, true );
    }

	/**
	 * Generation wire endpoints need a complete JSON object as their evidence
	 * record. Unrelated endpoints retain the tolerant decoder until their own
	 * response contracts are documented.
	 */
	private function is_nonempty_associative_array( $value ): bool {
		if ( ! is_array( $value ) || [] === $value ) {
			return false;
		}

		$expected_index = 0;
		foreach ( array_keys( $value ) as $key ) {
			if ( $key !== $expected_index ) {
				return true;
			}
			$expected_index++;
		}

		return false;
	}

	private function extract_bearer_token( array $headers ): string {
		$authorization = trim( (string) ( $headers['Authorization'] ?? '' ) );
		if ( 0 !== strpos( $authorization, 'Bearer ' ) ) {
			return '';
		}

		return trim( substr( $authorization, 7 ) );
	}

	private function strict_response_error( array $context, string $request_id ): \WP_Error {
		$error_data = [];
		if ( ! empty( $context['billable_start'] ) ) {
			$error_data = [
				'uncertain' => true,
				'requestId' => $request_id,
			];
		}

		return Messages::error( 'invalid_response', 502, [], $error_data );
	}

	private function is_strict_wire_endpoint( string $endpoint_key, string $method ): bool {
		return ( 'GET' === $method && in_array( $endpoint_key, [ 'requests_reconcile', 'job' ], true ) )
			|| ( 'POST' === $method && 'jobs_batch' === $endpoint_key );
	}

	private function is_reconciliation_endpoint( string $endpoint_key, string $method ): bool {
		return 'GET' === $method && 'requests_reconcile' === $endpoint_key;
	}

    private function get_retry_delay_ms( int $attempt, $response = null ): int {
		if ( is_array( $response ) && 429 === (int) wp_remote_retrieve_response_code( $response ) ) {
			$retry_after = trim( (string) wp_remote_retrieve_header( $response, 'retry-after' ) );
			if ( ctype_digit( $retry_after ) ) {
				return min( Workflow_Operational_Limits::HTTP_MAX_RETRY_AFTER_SECONDS * 1000, (int) $retry_after * 1000 );
			}
			$retry_at = '' !== $retry_after ? strtotime( $retry_after ) : false;
			if ( false !== $retry_at ) {
				return min( Workflow_Operational_Limits::HTTP_MAX_RETRY_AFTER_SECONDS * 1000, max( 0, $retry_at - time() ) * 1000 );
			}
		}
        $base   = 1 === $attempt ? 1000 : 2000;
        $jitter = random_int( 0, 250 );
        return $base + $jitter;
    }

    private function normalize_request_id( string $request_id ): string {
        $request_id = trim( $request_id );
        if ( '' === $request_id || strlen( $request_id ) > 64 ) {
            return '';
        }
        return preg_match( '/^[A-Za-z0-9._:-]+$/', $request_id ) ? $request_id : '';
    }

    private function get_php_memory_limit_bytes(): int {
        $raw = trim( (string) ini_get( 'memory_limit' ) );
        if ( '' === $raw || '-1' === $raw ) {
            return -1;
        }
        if ( is_numeric( $raw ) ) {
            return (int) $raw;
        }

        $unit = strtolower( substr( $raw, -1 ) );
        $value = (float) substr( $raw, 0, -1 );
		$multipliers = [
			'g' => 1073741824,
			'm' => 1048576,
			'k' => 1024,
		];

		return isset( $multipliers[ $unit ] )
			? (int) round( $value * $multipliers[ $unit ] )
			: (int) $raw;
    }

    private function extract_request_meta( array $data, string $endpoint_key = '', array $files = [] ): array {
        if ( 'campaign_set_generator' === $endpoint_key ) {
            $product_images = $this->normalize_multipart_file_list( $files['productImages'] ?? [] );
            $subject_included = ! empty( $this->normalize_multipart_file_list( $files['subjectImage'] ?? [] ) );
            $environment_included = ! empty( $this->normalize_multipart_file_list( $files['environmentImage'] ?? [] ) );
            return [
                'request_format'          => 'multipart',
                'workflow'                => 'campaign_set',
                'primary_images_count'    => count( $product_images ),
                'subject_included'        => $subject_included,
                'environment_included'    => $environment_included,
                'images_count_input'      => count( $product_images ) + ( $subject_included ? 1 : 0 ) + ( $environment_included ? 1 : 0 ),
                'images_bytes_total'      => $this->sum_file_bytes( $files ),
                'output_count'            => isset( $data['count'] ) ? (int) $data['count'] : 0,
                'quality'                 => (string) ( $data['quality'] ?? '' ),
                'style_included'          => '' !== trim( (string) ( $data['style'] ?? '' ) ),
                'aspect_ratio_included'   => '' !== trim( (string) ( $data['aspectRatio'] ?? '' ) ),
                'instructions_included'   => '' !== trim( (string) ( $data['customInstructions'] ?? '' ) ),
                'instructions_length'     => strlen( (string) ( $data['customInstructions'] ?? '' ) ),
            ];
        }

        if ( 'enhance_images' === $endpoint_key ) {
            $images = $this->normalize_multipart_file_list( $files['image'] ?? [] );
            return [
                'request_format'     => 'multipart',
                'workflow'           => 'increase_resolution',
                'images_count_input' => count( $images ),
                'images_bytes_total' => $this->sum_file_bytes( $files ),
            ];
        }

        if ( 'configuration_generator' === $endpoint_key ) {
            $product_images      = $this->normalize_multipart_file_list( $files['productImages'] ?? [] );
            $subject_included    = ! empty( $this->normalize_multipart_file_list( $files['subjectImage'] ?? [] ) );
            $environment_included = ! empty( $this->normalize_multipart_file_list( $files['environmentImage'] ?? [] ) );
            $inspiration_included = ! empty( $this->normalize_multipart_file_list( $files['inspirationImage'] ?? [] ) );
            $images_count_input  = count( $product_images ) + ( $subject_included ? 1 : 0 ) + ( $environment_included ? 1 : 0 ) + ( $inspiration_included ? 1 : 0 );
            return [
                'request_format'       => ! empty( $files ) ? 'multipart' : 'json',
                'images_count_input'   => $images_count_input,
                'primary_images_count' => count( $product_images ),
                'subject_included'     => $subject_included,
                'environment_included' => $environment_included,
                'inspiration_included' => $inspiration_included,
                'images_bytes_total'   => $this->sum_file_bytes( $files ),
                'payload_summary'      => [
                    'subject_included'     => $subject_included,
                    'environment_included' => $environment_included,
                    'inspiration_included' => $inspiration_included,
                ],
            ];
        }

        if ( 'images_generator' === $endpoint_key ) {
            $product_images       = $this->normalize_multipart_file_list( $files['productImages'] ?? [] );
            $subject_included     = ! empty( $this->normalize_multipart_file_list( $files['subjectImage'] ?? [] ) );
            $environment_included = ! empty( $this->normalize_multipart_file_list( $files['environmentImage'] ?? [] ) );
            $inspiration_included = ! empty( $this->normalize_multipart_file_list( $files['inspirationImage'] ?? [] ) );
            $selections           = $this->decode_selection_payload( $data['selections'] ?? [] );
            return [
                'request_format'         => ! empty( $files ) ? 'multipart' : 'json',
                'quality'                => $data['quality'] ?? '',
                'count'                  => isset( $data['count'] ) ? (int) $data['count'] : 0,
                'selections_count'       => count( $selections ),
                'has_custom_instructions'=> ! empty( $data['customInstructions'] ) && is_string( $data['customInstructions'] ),
                'subject_included'       => $subject_included,
                'environment_included'   => $environment_included,
                'inspiration_included'   => $inspiration_included,
                'images_count_input'     => count( $product_images ) + ( $subject_included ? 1 : 0 ) + ( $environment_included ? 1 : 0 ) + ( $inspiration_included ? 1 : 0 ),
                'primary_images_count'   => count( $product_images ),
                'images_bytes_total'     => $this->sum_file_bytes( $files ),
                'payload_images_roles'   => [
                    'subject_included'     => $subject_included,
                    'environment_included' => $environment_included,
                    'inspiration_included' => $inspiration_included,
                    'primary_count'        => count( $product_images ),
                ],
                'payload_summary'        => [
                    'quality'                 => $data['quality'] ?? '',
                    'count'                   => isset( $data['count'] ) ? (int) $data['count'] : 0,
                    'selections_count'        => count( $selections ),
                    'has_custom_instructions' => ! empty( $data['customInstructions'] ) && is_string( $data['customInstructions'] ),
                    'subject_included'        => $subject_included,
                    'environment_included'    => $environment_included,
                    'inspiration_included'    => $inspiration_included,
                ],
            ];
        }

        $meta = [];
        if ( ! empty( $files ) ) {
            foreach ( $files as $key => $entry ) {
                $file_list = $this->normalize_multipart_file_list( $entry );
                if ( empty( $file_list ) ) {
                    continue;
                }
                $meta[ $key . '_count' ] = count( $file_list );
                $meta[ $key . '_bytes' ] = array_sum( array_map( static fn( $item ) => (int) ( $item['bytes'] ?? 0 ), $file_list ) );
            }
            if ( ! empty( $meta ) ) {
                $counts = array_filter( $meta, static fn( $value, $key ) => fotogenic_ai_str_ends_with( $key, '_count' ), ARRAY_FILTER_USE_BOTH );
                $bytes  = array_filter( $meta, static fn( $value, $key ) => fotogenic_ai_str_ends_with( $key, '_bytes' ), ARRAY_FILTER_USE_BOTH );
                $meta['images_count']   = array_sum( $counts );
                $meta['images_bytes']   = array_sum( $bytes );
                $meta['request_format'] = 'multipart';
            }
            return $meta;
        }

        return [];
    }

    private function build_request_log_payload( array $data, array $files, string $endpoint_key ) {
        if ( $this->is_billable_start_endpoint( $endpoint_key ) ) {
            return [
                'summary' => $this->extract_request_meta( $data, $endpoint_key, $files ),
            ];
        }
        if ( empty( $files ) ) {
            return $this->redact_payload_for_log( $data );
        }

        return [
            'fields' => $this->redact_payload_for_log( $data ),
            'files'  => $this->summarize_files_for_log( $files ),
        ];
    }

    private function summarize_files_for_log( array $files ): array {
        $summary = [];
        foreach ( $files as $key => $entry ) {
            $file_list = $this->normalize_multipart_file_list( $entry );
            if ( empty( $file_list ) ) {
                continue;
            }
            $summary[ $key ] = array_map(
                static function ( array $item ): array {
                    return [
                        'mime'     => (string) ( $item['mime'] ?? '' ),
                        'bytes'    => (int) ( $item['bytes'] ?? 0 ),
                    ];
                },
                $file_list
            );
        }

        return $summary;
    }

    private function sum_file_bytes( array $files ): int {
        $total = 0;
        foreach ( $files as $entry ) {
            foreach ( $this->normalize_multipart_file_list( $entry ) as $file ) {
                $total += (int) ( $file['bytes'] ?? 0 );
            }
        }
        return $total;
    }

    private function decode_selection_payload( $raw ): array {
        if ( is_array( $raw ) ) {
            return $raw;
        }
        if ( is_string( $raw ) && '' !== $raw ) {
            $decoded = json_decode( $raw, true );
            if ( is_array( $decoded ) ) {
                return $decoded;
            }
        }
        return [];
    }

    private function redact_payload_for_log( $data ) {
        if ( is_array( $data ) ) {
            $redacted = [];
            foreach ( $data as $key => $value ) {
                $key_str = strtolower( (string) $key );
                if ( $this->is_binary_key( $key_str ) || in_array( $key_str, [ 'custominstructions', 'instructions' ], true ) ) {
                    $redacted[ $key ] = '[omitted]';
                    continue;
                }
                $redacted[ $key ] = $this->redact_payload_for_log( $value );
            }
            return $redacted;
        }
        if ( is_string( $data ) ) {
            if ( strlen( $data ) > 200 ) {
                return sprintf( '[omitted len=%d]', strlen( $data ) );
            }
            return $data;
        }
        if ( is_bool( $data ) || is_numeric( $data ) || null === $data ) {
            return $data;
        }
        if ( is_object( $data ) ) {
            return $this->redact_payload_for_log( (array) $data );
        }
        return '[omitted]';
    }

    private function log_api_event( array $event ): void {
        $logger = Logger::instance();
        if ( ! $logger->enabled() ) {
            return;
        }
        if ( isset( $event['endpoint'] ) && 'credits_balance' === $event['endpoint'] ) {
            return;
        }
        unset( $event['url'], $event['path_params'], $event['query_params'], $event['body_preview'] );
        if ( empty( $event['run_id'] ) && ! empty( $event['request_meta']['run_id'] ) ) {
            $event['run_id'] = (string) $event['request_meta']['run_id'];
        }
        $event['request_body'] = $this->redact_payload_for_log( $event['request_body'] ?? [] );
        $logger->api( $event );
    }

    public function redact_for_export( array $event ): array {
        unset( $event['url'], $event['path_params'], $event['query_params'], $event['body_preview'] );
        if ( isset( $event['request_body'] ) ) {
            $event['request_body'] = $this->redact_payload_for_log( $event['request_body'] ?? [] );
        }
        return $event;
    }

    private function is_binary_key( string $key ): bool {
        return in_array(
            $key,
            [
                'productimages',
                'images',
                'subjectimage',
                'environmentimage',
                'inspirationimage',
                'inputimages',
                'image',
                'productimage',
                'inputimage',
            ],
            true
        );
    }
}
