<?php

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

class Niwa_AI_OpenAI_Client implements Niwa_AI_AI_Client_Interface {
    const RESPONSES_ENDPOINT = 'https://api.openai.com/v1/responses';
    const CODEX_RESPONSES_ENDPOINT = 'https://chatgpt.com/backend-api/codex/responses';
    const CODEX_ORIGINATOR = 'codex_cli_rs';
    const WHISPER_ENDPOINT = 'https://api.openai.com/v1/audio/transcriptions';
    const DEFAULT_CODEX_INSTRUCTIONS = 'You are Codex, a coding assistant.';
    const TRANSIENT_RETRY_DELAY_MS = 2500;
    const MAX_TRANSIENT_REQUEST_ATTEMPTS = 2;
    const LIGHTWEIGHT_CODEX_MIN_TIMEOUT = 35;
    const LIGHTWEIGHT_CODEX_MAX_TIMEOUT = 55;
    const REFERENCE_IMAGE_MAX_BYTES = 6291456;
    const REFERENCE_IMAGE_FETCH_TIMEOUT = 20;
    const DEFAULT_MAX_OUTPUT_TOKENS = 16384;
    private $profile = 'default';

    public function __construct( $profile = 'default' ) {
        $this->profile = $this->sanitize_profile( $profile );
    }

    public function get_profile() {
        return $this->profile;
    }

    public function generate( $model, $input, $instructions, $auth_mode, $creds, $timeout = 30 ) {
        $token = $this->resolve_token( $auth_mode, $creds );
        if ( empty( $token ) ) {
            return array( 'success' => false, 'error' => 'Missing auth token', 'answer' => '' );
        }

        $headers = $this->build_headers( $auth_mode, $creds, $token );
        $request_model = $this->normalize_request_model( $model );
        $reasoning     = $this->reasoning_options_for_model( $model, array() );

        if ( $this->is_oauth_mode( $auth_mode ) ) {
            $message_item = $this->build_response_message_item( 'user', $input );
            $payload = array(
                'model' => $request_model,
                'input' => ! empty( $message_item ) ? array( $message_item ) : array(),
                'instructions' => $this->ensure_codex_instructions( (string) $instructions ),
                'store' => false,
            );
            if ( ! empty( $reasoning ) ) {
                $payload['reasoning'] = $reasoning;
            }
            $response = $this->post_json(
                self::CODEX_RESPONSES_ENDPOINT,
                $headers,
                $payload,
                $timeout
            );
        } else {
            $message_item = $this->build_response_message_item( 'user', $input );
            $payload = array(
                'model' => $request_model,
                'input' => ! empty( $message_item ) ? array( $message_item ) : array(),
                'instructions' => (string) $instructions,
                'max_output_tokens' => self::DEFAULT_MAX_OUTPUT_TOKENS,
            );
            if ( ! empty( $reasoning ) ) {
                $payload['reasoning'] = $reasoning;
            }
            $response = $this->post_json(
                self::RESPONSES_ENDPOINT,
                $headers,
                $payload,
                $timeout
            );
        }

        if ( empty( $response['success'] ) ) {
            return array( 'success' => false, 'error' => isset( $response['error'] ) ? $response['error'] : 'Request failed', 'answer' => '' );
        }

        $body   = isset( $response['body'] ) ? $response['body'] : array();
        $answer = $this->extract_text( $body );
        return array(
            'success' => ! empty( $answer ),
            'error' => '',
            'answer' => $answer,
            'http_ok' => true,
            'usage' => $this->extract_responses_usage( isset( $body['usage'] ) ? $body['usage'] : array() ),
        );
    }

    public function generate_image( $prompt, $auth_mode, $creds, $quality = 'medium', $size = '1024x1024', $timeout = 180, $reference_image_urls = array() ) {
        $prompt = trim( (string) $prompt );
        if ( '' === $prompt ) {
            return array( 'success' => false, 'error' => 'Image prompt is required.', 'image_b64' => '' );
        }

        if ( ! $this->is_oauth_mode( $auth_mode ) ) {
            return array( 'success' => false, 'error' => 'Codex OAuth is required for image generation.', 'image_b64' => '' );
        }

        $token = $this->resolve_token( $auth_mode, $creds );
        if ( '' === $token ) {
            return array( 'success' => false, 'error' => 'Missing OAuth token for image generation.', 'image_b64' => '' );
        }

        $quality          = $this->normalize_image_quality( $quality );
        $size             = $this->normalize_image_size( $size );
        $headers          = $this->build_headers( $auth_mode, $creds, $token );
        $reference_inputs = $this->resolve_reference_image_inputs( $reference_image_urls );
        $payload          = $this->build_codex_image_generation_payload( $prompt, $quality, $size, $reference_inputs );

        $response = $this->post_json( self::CODEX_RESPONSES_ENDPOINT, $headers, $payload, $timeout );
        if ( empty( $response['success'] ) ) {
            return array(
                'success'   => false,
                'error'     => isset( $response['error'] ) ? (string) $response['error'] : 'Image generation request failed.',
                'image_b64' => '',
            );
        }

        $body      = isset( $response['body'] ) && is_array( $response['body'] ) ? $response['body'] : array();
        $image_b64 = $this->extract_image_b64( $body );
        if ( '' === $image_b64 ) {
            return array( 'success' => false, 'error' => 'Codex image generation returned no image.', 'image_b64' => '' );
        }

        return array(
            'success'               => true,
            'error'                 => '',
            'image_b64'             => $image_b64,
            'mime_type'             => 'image/png',
            'quality'               => $quality,
            'size'                  => $size,
            'reference_images_used' => count( $reference_inputs ),
            'usage'                 => $this->extract_responses_usage( isset( $body['usage'] ) ? $body['usage'] : array() ),
            'output_model'          => 'gpt-image-2',
        );
    }

    public function generate_with_tools( $model, $input, $instructions, $tools, $auth_mode, $creds, $timeout = 30 ) {
        return $this->responses_generate_with_tools( $model, $input, $instructions, $tools, $auth_mode, $creds, $timeout );
    }

    public function chat_complete( $model, $messages, $auth_mode, $creds, $timeout = 30, $tools = null, $tool_choice = null ) {
        $token = $this->resolve_token( $auth_mode, $creds );
        if ( empty( $token ) ) {
            return array( 'success' => false, 'error' => 'Missing auth token', 'answer' => '' );
        }

        return $this->responses_chat_complete( $model, $messages, $auth_mode, $creds, $timeout, $tools, $tool_choice );
    }

    private function responses_generate_with_tools( $model, $input, $instructions, $tools, $auth_mode, $creds, $timeout ) {
        $token = $this->resolve_token( $auth_mode, $creds );
        if ( '' === $token ) {
            return array( 'success' => false, 'error' => 'Missing auth token', 'answer' => '' );
        }

        $headers = $this->build_headers( $auth_mode, $creds, $token );
        if ( is_array( $input ) ) {
            $converted_input = $this->convert_messages_to_responses_payload( $input );
            $request_input   = isset( $converted_input['input'] ) ? $converted_input['input'] : array();
        } else {
            $message_item  = $this->build_response_message_item( 'user', $input );
            $request_input = ! empty( $message_item ) ? array( $message_item ) : array();
        }
        $request_model = $this->normalize_request_model( $model );
        $payload = array(
            'model' => $request_model,
            'instructions' => $this->is_oauth_mode( $auth_mode ) ? $this->ensure_codex_instructions( (string) $instructions ) : (string) $instructions,
            'input' => $request_input,
            'store' => false,
            'include' => array( 'reasoning.encrypted_content' ),
            'reasoning' => $this->reasoning_options_for_model( $model, array( 'summary' => 'auto' ) ),
        );

        $response_tools = $this->convert_tools_to_responses( $tools );
        if ( ! empty( $response_tools ) ) {
            $payload['tools'] = $response_tools;
            $payload['tool_choice'] = 'auto';
        }

        $response = $this->post_json(
            $this->is_oauth_mode( $auth_mode ) ? self::CODEX_RESPONSES_ENDPOINT : self::RESPONSES_ENDPOINT,
            $headers,
            $payload,
            $timeout
        );
        if ( empty( $response['success'] ) ) {
            return array( 'success' => false, 'error' => isset( $response['error'] ) ? $response['error'] : 'Codex request failed', 'answer' => '' );
        }

        $body = isset( $response['body'] ) ? $response['body'] : array();
        $answer = $this->extract_text( $body );
        $response_items = $this->extract_responses_replay_items( $body );
        $tool_calls = $this->extract_responses_tool_calls( $body );
        $usage = $this->extract_responses_usage( isset( $body['usage'] ) ? $body['usage'] : array() );
        if ( ! empty( $tool_calls ) ) {
            return array(
                'success' => true,
                'tool_calls' => $tool_calls,
                'assistant_message' => $this->build_assistant_message( $answer, $tool_calls, $response_items ),
                'answer' => '',
                'usage' => $usage,
            );
        }

        return array(
            'success' => true,
            'tool_calls' => array(),
            'assistant_message' => $this->build_assistant_message( $answer, array(), $response_items ),
            'answer' => $answer,
            'usage' => $usage,
        );
    }

    private function responses_chat_complete( $model, $messages, $auth_mode, $creds, $timeout, $tools = null, $tool_choice = null ) {
        $token = $this->resolve_token( $auth_mode, $creds );
        if ( '' === $token ) {
            return array( 'success' => false, 'error' => 'Missing OAuth token', 'answer' => '' );
        }

        $headers = $this->build_headers( $auth_mode, $creds, $token );
        $converted = $this->convert_messages_to_responses_payload( $messages );
        $request_model = $this->normalize_request_model( $model );

        $payload = array(
            'model' => $request_model,
            'input' => isset( $converted['input'] ) ? $converted['input'] : array(),
            'store' => false,
            'include' => array( 'reasoning.encrypted_content' ),
            'reasoning' => $this->reasoning_options_for_model( $model, array( 'summary' => 'auto' ) ),
        );
        $instructions = isset( $converted['instructions'] ) ? trim( (string) $converted['instructions'] ) : '';
        if ( $this->is_oauth_mode( $auth_mode ) ) {
            $payload['instructions'] = $this->ensure_codex_instructions( $instructions );
        } elseif ( '' !== $instructions ) {
            $payload['instructions'] = $instructions;
        }

        // Include tools definitions even when tool_choice is 'none' if the input
        // contains function_call items — the Codex backend may reject function_call
        // items without a matching tools definition.
        $has_function_call_in_input = $this->input_contains_function_call( isset( $payload['input'] ) ? $payload['input'] : array() );
        if ( ! empty( $tools ) && ( 'none' !== $tool_choice || $has_function_call_in_input ) ) {
            $response_tools = $this->convert_tools_to_responses( $tools );
            if ( ! empty( $response_tools ) ) {
                $payload['tools'] = $response_tools;
            }
        }
        if ( ! empty( $tool_choice ) && 'none' !== $tool_choice ) {
            $payload['tool_choice'] = $tool_choice;
        }

        $response = $this->post_json(
            $this->is_oauth_mode( $auth_mode ) ? self::CODEX_RESPONSES_ENDPOINT : self::RESPONSES_ENDPOINT,
            $headers,
            $payload,
            $timeout
        );
        if (
            empty( $response['success'] )
            && $this->should_retry_missing_tool_output_error( isset( $response['error'] ) ? $response['error'] : '' )
        ) {
            $this->log_request_failure(
                'responses_missing_tool_output_retry',
                array(
                    'model' => (string) $model,
                    'message_count' => is_array( $messages ) ? count( $messages ) : 0,
                    'input_count' => is_array( $payload['input'] ) ? count( $payload['input'] ) : 0,
                    'error' => isset( $response['error'] ) ? (string) $response['error'] : '',
                )
            );
            $retry_converted = $this->convert_messages_to_responses_payload( $messages, true );
            $retry_payload   = array(
                'model' => $request_model,
                'input' => isset( $retry_converted['input'] ) ? $retry_converted['input'] : array(),
                'store' => false,
                'include' => array( 'reasoning.encrypted_content' ),
                'reasoning' => $this->reasoning_options_for_model( $model, array( 'summary' => 'auto' ) ),
            );
            $retry_instructions = isset( $retry_converted['instructions'] ) ? trim( (string) $retry_converted['instructions'] ) : '';
            if ( $this->is_oauth_mode( $auth_mode ) ) {
                $retry_payload['instructions'] = $this->ensure_codex_instructions( $retry_instructions );
            } elseif ( '' !== $retry_instructions ) {
                $retry_payload['instructions'] = $retry_instructions;
            }
            $retry_has_function_call = $this->input_contains_function_call( isset( $retry_payload['input'] ) ? $retry_payload['input'] : array() );
            if ( ! empty( $tools ) && ( 'none' !== $tool_choice || $retry_has_function_call ) ) {
                $retry_tools = $this->convert_tools_to_responses( $tools );
                if ( ! empty( $retry_tools ) ) {
                    $retry_payload['tools'] = $retry_tools;
                }
            }
            if ( ! empty( $tool_choice ) && 'none' !== $tool_choice ) {
                $retry_payload['tool_choice'] = $tool_choice;
            }
            $response = $this->post_json(
                $this->is_oauth_mode( $auth_mode ) ? self::CODEX_RESPONSES_ENDPOINT : self::RESPONSES_ENDPOINT,
                $headers,
                $retry_payload,
                $timeout
            );
            if ( empty( $response['success'] ) ) {
                $this->log_request_failure(
                    'responses_missing_tool_output_retry_failed',
                    array(
                        'model' => (string) $model,
                        'message_count' => is_array( $messages ) ? count( $messages ) : 0,
                        'retry_input_count' => is_array( $retry_payload['input'] ) ? count( $retry_payload['input'] ) : 0,
                        'error' => isset( $response['error'] ) ? (string) $response['error'] : '',
                    )
                );
            }
        }
        if ( empty( $response['success'] ) ) {
            return array( 'success' => false, 'error' => isset( $response['error'] ) ? $response['error'] : 'Codex request failed', 'answer' => '' );
        }

        $body = isset( $response['body'] ) ? $response['body'] : array();
        $answer = $this->extract_text( $body );
        $response_items = $this->extract_responses_replay_items( $body );
        $tool_calls = $this->extract_responses_tool_calls( $body );
        $usage = $this->extract_responses_usage( isset( $body['usage'] ) ? $body['usage'] : array() );

        return array(
            'success' => true,
            'answer' => $answer,
            'message' => $this->build_assistant_message( $answer, $tool_calls, $response_items ),
            'tool_calls' => $tool_calls,
            'usage' => $usage,
            'response_id' => isset( $body['id'] ) ? (string) $body['id'] : '',
        );
    }

    private function post_json( $endpoint, $headers, $payload, $timeout ) {
        $request_headers = is_array( $headers ) ? $headers : array();
        $request_payload = is_array( $payload ) ? $payload : array();
        $request_timeout = max( 5, intval( $timeout ) );
        $audit_id        = $this->new_audit_id();
        $audit_started   = microtime( true );

        if ( $this->is_codex_endpoint( $endpoint ) ) {
            $request_payload['stream'] = true;
            $request_headers['Accept'] = 'text/event-stream';
        }

        $request_timeout = $this->adjust_timeout_for_profile( $endpoint, $request_timeout, $request_payload );

        for ( $attempt = 1; $attempt <= self::MAX_TRANSIENT_REQUEST_ATTEMPTS; $attempt++ ) {
            $result = $this->dispatch_json_request( $endpoint, $request_headers, $request_payload, $request_timeout );
            $this->record_json_audit_event( $audit_id, $endpoint, $request_payload, $request_timeout, $attempt, $audit_started, $result );

            if ( ! empty( $result['success'] ) ) {
                if ( empty( $result['body_is_json'] ) ) {
                    $this->log_request_failure(
                        'request_non_json_success_body',
                        array(
                            'endpoint' => $endpoint,
                            'profile' => $this->profile,
                            'status' => isset( $result['status'] ) ? intval( $result['status'] ) : 0,
                            'body_excerpt' => isset( $result['body_excerpt'] ) ? (string) $result['body_excerpt'] : '',
                        )
                    );
                }

                return array(
                    'success' => true,
                    'error' => '',
                    'body' => isset( $result['body'] ) && is_array( $result['body'] ) ? $result['body'] : array(),
                );
            }

            $error_type = isset( $result['error_type'] ) ? (string) $result['error_type'] : 'http_error';
            $error      = isset( $result['error'] ) ? (string) $result['error'] : 'Request failed';

            if ( $this->should_retry_request_result( $result, $attempt, $request_timeout ) ) {
                $this->log_request_failure(
                    'request_transient_retry',
                    array(
                        'endpoint' => $endpoint,
                        'profile' => $this->profile,
                        'attempt' => $attempt,
                        'error_type' => $error_type,
                        'error' => $error,
                    )
                );
                $this->sleep_for_transient_retry();
                continue;
            }

            if ( 'transport_error' === $error_type ) {
                $this->log_request_failure(
                    'request_transport_error',
                    array(
                        'endpoint' => $endpoint,
                        'profile' => $this->profile,
                        'attempt' => $attempt,
                        'error' => $error,
                    )
                );
            } else {
                $this->log_request_failure(
                    'request_http_error',
                    array(
                        'endpoint' => $endpoint,
                        'profile' => $this->profile,
                        'attempt' => $attempt,
                        'status' => isset( $result['status'] ) ? intval( $result['status'] ) : 0,
                        'error' => $error,
                        'body_excerpt' => isset( $result['body_excerpt'] ) ? (string) $result['body_excerpt'] : '',
                        'has_account_id' => ! empty( $request_headers['ChatGPT-Account-Id'] ),
                    )
                );
            }

            return array(
                'success' => false,
                'error' => $error,
                'body' => isset( $result['body'] ) && is_array( $result['body'] ) ? $result['body'] : array(),
            );
        }

        return array(
            'success' => false,
            'error' => 'Request failed after retries.',
            'body' => array(),
        );
    }

    private function dispatch_json_request( $endpoint, $headers, $payload, $timeout ) {
        $response = wp_remote_post(
            $endpoint,
            array(
                'headers' => $headers,
                'body' => wp_json_encode( $payload ),
                'timeout' => $timeout,
            )
        );

        if ( is_wp_error( $response ) ) {
            return array(
                'success' => false,
                'error_type' => 'transport_error',
                'error' => $response->get_error_message(),
                'body' => array(),
                'status' => 0,
                'body_excerpt' => '',
            );
        }

        $code     = wp_remote_retrieve_response_code( $response );
        $body_raw = wp_remote_retrieve_body( $response );
        $body     = $this->decode_response_body( $endpoint, $body_raw );

        // Some Codex OAuth sessions fail when ChatGPT-Account-Id is stale/mismatched.
        // Retry once without that header for 4xx responses.
        if (
            $code >= 400 && $code < 500
            && isset( $headers['ChatGPT-Account-Id'] )
            && '' !== trim( (string) $headers['ChatGPT-Account-Id'] )
        ) {
            $retry_headers = $headers;
            unset( $retry_headers['ChatGPT-Account-Id'] );

            $retry = wp_remote_post(
                $endpoint,
                array(
                    'headers' => $retry_headers,
                    'body' => wp_json_encode( $payload ),
                    'timeout' => $timeout,
                )
            );

            if ( ! is_wp_error( $retry ) ) {
                $retry_code     = wp_remote_retrieve_response_code( $retry );
                $retry_body_raw = wp_remote_retrieve_body( $retry );
                $retry_body     = $this->decode_response_body( $endpoint, $retry_body_raw );

                if ( $retry_code >= 200 && $retry_code < 300 ) {
                    $this->log_request_failure(
                        'request_retry_without_account_id_succeeded',
                        array(
                            'endpoint' => $endpoint,
                            'profile' => $this->profile,
                            'status' => $code,
                            'retry_status' => $retry_code,
                        )
                    );

                    return array(
                        'success' => true,
                        'error' => '',
                        'body' => is_array( $retry_body ) ? $retry_body : array(),
                        'status' => $retry_code,
                        'body_is_json' => is_array( $retry_body ),
                        'body_excerpt' => is_string( $retry_body_raw ) ? substr( preg_replace( '/\s+/', ' ', $retry_body_raw ), 0, 500 ) : '',
                    );
                }

                if ( is_array( $retry_body ) && ! empty( $retry_body ) ) {
                    $code     = $retry_code;
                    $body     = $retry_body;
                    $body_raw = $retry_body_raw;
                }
            }
        }

        if ( $code < 200 || $code >= 300 ) {
            $error = 'HTTP ' . $code;
            if ( is_array( $body ) ) {
                if ( ! empty( $body['error']['message'] ) ) {
                    $error .= ': ' . (string) $body['error']['message'];
                } elseif ( ! empty( $body['detail'] ) && is_string( $body['detail'] ) ) {
                    $error .= ': ' . $body['detail'];
                }
            }

            return array(
                'success' => false,
                'error_type' => 'http_error',
                'error' => $error,
                'body' => is_array( $body ) ? $body : array(),
                'status' => $code,
                'body_excerpt' => is_string( $body_raw ) ? substr( preg_replace( '/\s+/', ' ', $body_raw ), 0, 500 ) : '',
            );
        }

        return array(
            'success' => true,
            'error' => '',
            'body' => is_array( $body ) ? $body : array(),
            'status' => $code,
            'body_is_json' => is_array( $body ),
            'body_excerpt' => is_string( $body_raw ) ? substr( preg_replace( '/\s+/', ' ', $body_raw ), 0, 500 ) : '',
        );
    }

    private function should_retry_request_result( $result, $attempt, $request_timeout = 0 ) {
        if ( $attempt >= self::MAX_TRANSIENT_REQUEST_ATTEMPTS ) {
            return false;
        }

        $request_timeout = intval( $request_timeout );
        if ( $request_timeout >= 90 && $request_timeout < 120 ) {
            return false;
        }

        $error_type = isset( $result['error_type'] ) ? (string) $result['error_type'] : '';
        if ( 'transport_error' === $error_type ) {
            $message = isset( $result['error'] ) ? (string) $result['error'] : '';
            return (bool) preg_match( '/timeout|timed out|connection|temporar(?:ily)? unavailable|could not resolve|econn|stream/i', $message );
        }

        $status = isset( $result['status'] ) ? intval( $result['status'] ) : 0;
        return in_array( $status, array( 408, 409, 425, 429, 500, 502, 503, 504, 520, 521, 522, 523, 524 ), true );
    }

    private function sleep_for_transient_retry() {
        if ( function_exists( 'usleep' ) ) {
            usleep( self::TRANSIENT_RETRY_DELAY_MS * 1000 );
        }
    }

    private function adjust_timeout_for_profile( $endpoint, $timeout, $payload = array() ) {
        $timeout = max( 5, intval( $timeout ) );
        $profile = $this->sanitize_profile( $this->profile );

        if ( $this->is_codex_endpoint( $endpoint ) ) {
            if ( $this->is_lightweight_codex_payload( $payload ) ) {
                $timeout = max( self::LIGHTWEIGHT_CODEX_MIN_TIMEOUT, min( self::LIGHTWEIGHT_CODEX_MAX_TIMEOUT, $timeout ) );
            } elseif ( 'frontend_fast' === $profile ) {
                // Allow more time when tools are included — Spark tool-calling
                // round-trips through SSE need a wider budget than simple completions.
                $has_tools = ! empty( $payload['tools'] );
                $min_timeout = $has_tools ? 65 : 45;
                $max_timeout = $has_tools ? 90 : 75;
                $timeout = max( $min_timeout, min( $max_timeout, $timeout ) );
            } else {
                // Codex SSE often takes materially longer than the plugin's legacy
                // 45s budget before the final buffered response is complete.
                // If the agent loop has already reduced the timeout into the
                // 90-119s band, treat that as a runtime soft-deadline cap.
                // Raising it back to 120s can consume the whole LSAPI worker
                // window and leave only a checkpoint instead of a useful turn.
                if ( $timeout < 90 ) {
                    $timeout = 120;
                }
            }
        } elseif ( 'frontend_fast' === $profile ) {
            $timeout = min( 25, $timeout );
        } elseif ( 'founder_deep' === $profile ) {
            // Founder/heartbeat tasks send large snapshot payloads to Responses API;
            // ensure at least 60s so they do not time out on complex prompts.
            $timeout = max( 60, $timeout );
        }

        if ( function_exists( 'apply_filters' ) ) {
            $timeout = (int) apply_filters( 'niwa_ai_openai_request_timeout', $timeout, $profile, $endpoint, $payload );
        }

        return max( 5, $timeout );
    }

    private function is_lightweight_codex_payload( $payload ) {
        if ( ! is_array( $payload ) ) {
            return false;
        }

        if ( ! empty( $payload['tools'] ) || ! empty( $payload['reasoning'] ) || ! empty( $payload['include'] ) ) {
            return false;
        }

        $instructions = isset( $payload['instructions'] ) ? trim( (string) $payload['instructions'] ) : '';
        if ( $this->utf8_length( $instructions ) > 1200 ) {
            return false;
        }

        $input = isset( $payload['input'] ) && is_array( $payload['input'] ) ? $payload['input'] : array();
        if ( 1 !== count( $input ) ) {
            return false;
        }

        return $this->utf8_length( $this->flatten_codex_input_text( $input ) ) <= 2400;
    }

    private function flatten_codex_input_text( $input ) {
        if ( ! is_array( $input ) ) {
            return '';
        }

        $parts = array();
        foreach ( $input as $item ) {
            if ( ! is_array( $item ) ) {
                continue;
            }

            if ( isset( $item['content'] ) && is_string( $item['content'] ) ) {
                $parts[] = (string) $item['content'];
                continue;
            }

            if ( empty( $item['content'] ) || ! is_array( $item['content'] ) ) {
                continue;
            }

            foreach ( $item['content'] as $content_item ) {
                if ( ! is_array( $content_item ) ) {
                    continue;
                }

                if ( isset( $content_item['text'] ) && is_string( $content_item['text'] ) ) {
                    $parts[] = (string) $content_item['text'];
                } elseif ( isset( $content_item['content'] ) && is_string( $content_item['content'] ) ) {
                    $parts[] = (string) $content_item['content'];
                }
            }
        }

        return implode( "\n", $parts );
    }

    private function utf8_length( $text ) {
        return function_exists( 'mb_strlen' ) ? mb_strlen( (string) $text, 'UTF-8' ) : strlen( (string) $text );
    }

    private function sanitize_profile( $profile ) {
        $profile = sanitize_key( (string) $profile );
        if ( in_array( $profile, array( 'frontend_fast', 'founder_deep', 'subagent', 'background', 'default' ), true ) ) {
            return $profile;
        }

        return 'default';
    }

    private function resolve_token( $auth_mode, $creds ) {
        if ( $this->is_oauth_mode( $auth_mode ) ) {
            return isset( $creds['access_token'] ) ? trim( (string) $creds['access_token'] ) : '';
        }
        return isset( $creds['api_key'] ) ? trim( (string) $creds['api_key'] ) : '';
    }

    private function build_headers( $auth_mode, $creds, $token ) {
        $headers = array(
            'Content-Type' => 'application/json',
            'Authorization' => 'Bearer ' . $token,
            'Accept' => 'application/json',
        );

        if ( $this->is_oauth_mode( $auth_mode ) ) {
            $headers['originator'] = self::CODEX_ORIGINATOR;
            $headers['OpenAI-Beta'] = 'responses=experimental';
            $headers['User-Agent'] = 'CodexBar';
            $account_id = isset( $creds['account_id'] ) ? trim( (string) $creds['account_id'] ) : '';
            if ( '' !== $account_id ) {
                $headers['ChatGPT-Account-Id'] = $account_id;
            }
        }

        return $headers;
    }

    private function is_oauth_mode( $auth_mode ) {
        return 'oauth' === strtolower( trim( (string) $auth_mode ) );
    }

    private function ensure_codex_instructions( $instructions ) {
        $instructions = trim( (string) $instructions );
        return '' !== $instructions ? $instructions : self::DEFAULT_CODEX_INSTRUCTIONS;
    }

    private function normalize_request_model( $model ) {
        if ( class_exists( 'Niwa_AI_Model_Catalog' ) && method_exists( 'Niwa_AI_Model_Catalog', 'normalize_request_model' ) ) {
            return (string) Niwa_AI_Model_Catalog::normalize_request_model( $model );
        }

        return (string) $model;
    }

    private function reasoning_options_for_model( $model, $reasoning ) {
        if ( class_exists( 'Niwa_AI_Model_Catalog' ) && method_exists( 'Niwa_AI_Model_Catalog', 'with_reasoning_effort' ) ) {
            return Niwa_AI_Model_Catalog::with_reasoning_effort( $model, $reasoning );
        }

        return is_array( $reasoning ) ? $reasoning : array();
    }

    public function supports_native_web_search( $model, $auth_mode = 'api_key' ) {
        $model = trim( (string) $model );
        if ( '' === $model ) {
            return false;
        }

        return (bool) preg_match( '/^(gpt|o[1-9]|codex)/i', $model );
    }

    private function convert_tools_to_responses( $tools ) {
        if ( ! is_array( $tools ) ) {
            return array();
        }

        $converted = array();
        foreach ( $tools as $tool ) {
            if ( ! is_array( $tool ) || empty( $tool['type'] ) ) {
                continue;
            }

            $type = (string) $tool['type'];
            if ( in_array( $type, array( 'web_search', 'web_search_preview' ), true ) ) {
                $entry = array(
                    'type' => 'web_search',
                );
                if ( ! empty( $tool['search_context_size'] ) ) {
                    $entry['search_context_size'] = (string) $tool['search_context_size'];
                }
                $converted[] = $entry;
                continue;
            }

            if ( 'function' !== $type ) {
                continue;
            }
            if ( empty( $tool['function'] ) || ! is_array( $tool['function'] ) ) {
                continue;
            }

            $name = isset( $tool['function']['name'] ) ? trim( (string) $tool['function']['name'] ) : '';
            if ( '' === $name ) {
                continue;
            }

            $entry = array(
                'type' => 'function',
                'name' => $name,
                'parameters' => $this->normalize_json_schema_for_api(
                    isset( $tool['function']['parameters'] ) ? $tool['function']['parameters'] : (object) array()
                ),
                'strict' => false,
            );
            if ( ! empty( $tool['function']['description'] ) ) {
                $entry['description'] = (string) $tool['function']['description'];
            }
            $converted[] = $entry;
        }
        return $converted;
    }

    private function normalize_json_schema_for_api( $schema ) {
        if ( ! is_array( $schema ) ) {
            return $schema;
        }

        $object_map_keys = array( 'properties', 'patternProperties', 'definitions', '$defs', 'dependentSchemas' );

        foreach ( $schema as $key => $value ) {
            if ( is_array( $value ) ) {
                $schema[ $key ] = $this->normalize_json_schema_for_api( $value );
                if ( in_array( $key, $object_map_keys, true ) && empty( $schema[ $key ] ) ) {
                    $schema[ $key ] = new \stdClass();
                }
            }
        }

        return $schema;
    }

    private function convert_messages_to_responses_payload( $messages, $strip_response_items = false ) {
        $instructions = '';
        $input = array();

        if ( ! is_array( $messages ) ) {
            return array(
                'instructions' => '',
                'input' => array(),
            );
        }

        foreach ( $messages as $message ) {
            if ( ! is_array( $message ) ) {
                continue;
            }
            $role = isset( $message['role'] ) ? strtolower( trim( (string) $message['role'] ) ) : 'user';
            $content = $this->stringify_message_content( isset( $message['content'] ) ? $message['content'] : '' );

            if ( 'system' === $role ) {
                if ( '' !== $content ) {
                    $instructions = '' === $instructions ? $content : ( $instructions . "\n\n" . $content );
                }
                continue;
            }

            if ( 'assistant' === $role ) {
                // With include:["reasoning.encrypted_content"] + reasoning.summary:"auto",
                // reasoning items carry encrypted_content and summary that enable replay
                // without store:true.  Use response_items when available (saves tokens),
                // otherwise rebuild from tool_calls.
                $function_call_items     = array();
                $has_message_item        = false;
                $has_replayable_response = false;

                if ( ! $strip_response_items && ! empty( $message['response_items'] ) && is_array( $message['response_items'] ) ) {
                    $response_items = $this->normalize_response_replay_items( $message['response_items'] );
                    if ( ! empty( $response_items ) ) {
                        foreach ( $response_items as $response_item ) {
                            $type = isset( $response_item['type'] ) ? (string) $response_item['type'] : '';
                            if ( 'function_call' === $type ) {
                                $function_call_items[] = $response_item;
                                continue;
                            }
                            if ( 'message' === $type ) {
                                $has_message_item = true;
                            }
                            if ( 'reasoning' === $type && ! isset( $response_item['summary'] ) ) {
                                $response_item['summary'] = array();
                            }
                            $input[] = $response_item;
                            $has_replayable_response = true;
                        }
                    }
                }

                if ( ! empty( $message['tool_calls'] ) && is_array( $message['tool_calls'] ) ) {
                    if ( ! $has_message_item && '' !== $content ) {
                        $assistant_item = $this->build_response_message_item( 'assistant', isset( $message['content'] ) ? $message['content'] : $content );
                        if ( ! empty( $assistant_item ) ) {
                            $input[] = $assistant_item;
                        }
                    }

                    if ( empty( $function_call_items ) ) {
                        foreach ( $message['tool_calls'] as $call ) {
                            $resolved = $this->normalize_chat_tool_call( $call );
                            if ( empty( $resolved ) ) {
                                continue;
                            }
                            $function_call_items[] = array(
                                'type'      => 'function_call',
                                'call_id'   => $resolved['id'],
                                'name'      => $resolved['name'],
                                'arguments' => $resolved['arguments'],
                            );
                        }
                    }

                    foreach ( $function_call_items as $function_call_item ) {
                        $input[] = $function_call_item;
                    }
                    continue;
                }

                if ( $has_replayable_response ) {
                    if ( ! $has_message_item && '' !== $content ) {
                        $assistant_item = $this->build_response_message_item( 'assistant', isset( $message['content'] ) ? $message['content'] : $content );
                        if ( ! empty( $assistant_item ) ) {
                            $input[] = $assistant_item;
                        }
                    }
                    continue;
                }
            }

            if ( 'tool' === $role || ( 'user' === $role && ! empty( $message['tool_call_id'] ) ) ) {
                $call_id = isset( $message['tool_call_id'] ) ? trim( (string) $message['tool_call_id'] ) : '';
                if ( '' === $call_id ) {
                    continue;
                }
                $input[] = array(
                    'type' => 'function_call_output',
                    'call_id' => $call_id,
                    'output' => $content,
                );
                continue;
            }

            if ( in_array( $role, array( 'user', 'assistant' ), true ) ) {
                $message_item = $this->build_response_message_item( $role, isset( $message['content'] ) ? $message['content'] : $content );
                if ( ! empty( $message_item ) ) {
                    $input[] = $message_item;
                }
            }
        }

        $input = $this->prune_orphan_function_calls_from_input( $input );

        return array(
            'instructions' => $instructions,
            'input' => $input,
        );
    }

    private function should_retry_missing_tool_output_error( $error ) {
        $error = strtolower( trim( (string) $error ) );
        return '' !== $error && false !== strpos( $error, 'no tool output found for function call' );
    }

    /**
     * Check if a Responses API input array contains function_call items.
     */
    private function input_contains_function_call( $input ) {
        if ( ! is_array( $input ) ) {
            return false;
        }
        foreach ( $input as $item ) {
            if ( is_array( $item ) && isset( $item['type'] ) && 'function_call' === (string) $item['type'] ) {
                return true;
            }
        }
        return false;
    }

    private function build_response_message_item( $role, $content ) {
        $parts = $this->normalize_content_for_responses_message( $content, $role );
        if ( empty( $parts ) ) {
            return array();
        }

        return array(
            'type'    => 'message',
            'role'    => $role,
            'content' => $parts,
        );
    }

    private function normalize_content_for_responses_message( $content, $role = 'user' ) {
        return $this->normalize_content_parts( $content, 'assistant' === $role ? 'responses_assistant' : 'responses' );
    }

    private function normalize_content_parts( $content, $target = 'responses' ) {
        if ( ! in_array( $target, array( 'responses', 'responses_assistant' ), true ) ) {
            $target = 'responses';
        }

        if ( is_string( $content ) || is_scalar( $content ) ) {
            $text = (string) $content;
            if ( '' === $text ) {
                return array();
            }
            return array( $this->build_text_part( $text, $target ) );
        }

        if ( ! is_array( $content ) ) {
            return array();
        }

        $items = $this->is_assoc_array( $content ) ? array( $content ) : $content;
        $parts = array();
        foreach ( $items as $item ) {
            foreach ( $this->normalize_single_content_item( $item, $target ) as $part ) {
                if ( ! empty( $part ) ) {
                    $parts[] = $part;
                }
            }
        }

        return $parts;
    }

    private function normalize_single_content_item( $item, $target ) {
        if ( is_string( $item ) || is_scalar( $item ) ) {
            $text = (string) $item;
            return '' === $text ? array() : array( $this->build_text_part( $text, $target ) );
        }

        if ( ! is_array( $item ) ) {
            return array();
        }

        $parts = array();
        $type  = isset( $item['type'] ) ? strtolower( trim( (string) $item['type'] ) ) : '';

        if ( in_array( $type, array( 'text', 'input_text', 'output_text' ), true ) ) {
            $text = isset( $item['text'] ) ? (string) $item['text'] : ( isset( $item['content'] ) ? (string) $item['content'] : '' );
            if ( '' !== $text ) {
                $parts[] = $this->build_text_part( $text, $target );
            }
            return $parts;
        }

        $image = $this->extract_image_part_data( $item );
        if ( ! empty( $image['url'] ) ) {
            $parts[] = $this->build_image_part( $image['url'], $target, isset( $image['detail'] ) ? $image['detail'] : '' );
        }

        if ( empty( $parts ) && ! empty( $item['text'] ) && is_string( $item['text'] ) ) {
            $parts[] = $this->build_text_part( (string) $item['text'], $target );
        }

        return $parts;
    }

    private function extract_image_part_data( $item ) {
        if ( ! is_array( $item ) ) {
            return array();
        }

        $detail = isset( $item['detail'] ) ? trim( (string) $item['detail'] ) : '';
        if ( isset( $item['image_url'] ) ) {
            if ( is_string( $item['image_url'] ) ) {
                return array(
                    'url'    => trim( (string) $item['image_url'] ),
                    'detail' => $detail,
                );
            }
            if ( is_array( $item['image_url'] ) && ! empty( $item['image_url']['url'] ) ) {
                return array(
                    'url'    => trim( (string) $item['image_url']['url'] ),
                    'detail' => ! empty( $item['image_url']['detail'] ) ? trim( (string) $item['image_url']['detail'] ) : $detail,
                );
            }
        }

        foreach ( array( 'url', 'src', 'file_url' ) as $key ) {
            if ( ! empty( $item[ $key ] ) && is_string( $item[ $key ] ) ) {
                return array(
                    'url'    => trim( (string) $item[ $key ] ),
                    'detail' => $detail,
                );
            }
        }

        return array();
    }

    private function build_text_part( $text, $target ) {
        if ( 'responses_assistant' === $target ) {
            $type = 'output_text';
        } else {
            $type = 'input_text';
        }

        return array(
            'type' => $type,
            'text' => (string) $text,
        );
    }

    private function build_image_part( $url, $target, $detail = '' ) {
        $url = trim( (string) $url );
        if ( '' === $url ) {
            return array();
        }

        $image = array(
            'type'      => 'input_image',
            'image_url' => $url,
        );
        if ( '' !== $detail ) {
            $image['detail'] = $detail;
        }

        return $image;
    }

    private function is_assoc_array( $value ) {
        if ( ! is_array( $value ) ) {
            return false;
        }

        return array_keys( $value ) !== range( 0, count( $value ) - 1 );
    }

    private function prune_orphan_function_calls_from_input( $input ) {
        if ( ! is_array( $input ) || empty( $input ) ) {
            return array();
        }

        $output_ids = array();
        foreach ( $input as $item ) {
            if ( ! is_array( $item ) ) {
                continue;
            }
            if ( 'function_call_output' !== ( isset( $item['type'] ) ? (string) $item['type'] : '' ) ) {
                continue;
            }
            $call_id = isset( $item['call_id'] ) ? trim( (string) $item['call_id'] ) : '';
            if ( '' !== $call_id ) {
                $output_ids[ $call_id ] = true;
            }
        }

        if ( empty( $output_ids ) ) {
            return array_values(
                array_filter(
                    $input,
                    function ( $item ) {
                        return ! is_array( $item ) || 'function_call' !== ( isset( $item['type'] ) ? (string) $item['type'] : '' );
                    }
                )
            );
        }

        $pruned = array();
        foreach ( $input as $item ) {
            if ( ! is_array( $item ) ) {
                continue;
            }
            if ( 'function_call' === ( isset( $item['type'] ) ? (string) $item['type'] : '' ) ) {
                $call_id = isset( $item['call_id'] ) ? trim( (string) $item['call_id'] ) : '';
                if ( '' === $call_id || empty( $output_ids[ $call_id ] ) ) {
                    continue;
                }
            }
            $pruned[] = $item;
        }

        return $pruned;
    }

    private function normalize_chat_tool_call( $call ) {
        if ( ! is_array( $call ) ) {
            return array();
        }

        $id = isset( $call['id'] ) ? trim( (string) $call['id'] ) : '';
        $name = '';
        $arguments = '';

        if ( ! empty( $call['function'] ) && is_array( $call['function'] ) ) {
            $name = isset( $call['function']['name'] ) ? trim( (string) $call['function']['name'] ) : '';
            $arguments = isset( $call['function']['arguments'] ) ? (string) $call['function']['arguments'] : '';
        }
        if ( '' === $name && ! empty( $call['name'] ) ) {
            $name = trim( (string) $call['name'] );
        }
        if ( '' === $arguments && isset( $call['arguments'] ) ) {
            if ( is_string( $call['arguments'] ) ) {
                $arguments = $call['arguments'];
            } elseif ( is_array( $call['arguments'] ) ) {
                $arguments = wp_json_encode( $call['arguments'] );
            }
        }

        if ( '' === $id ) {
            $id = 'call_' . wp_generate_uuid4();
        }
        if ( '' === $name ) {
            return array();
        }

        return array(
            'id' => $id,
            'name' => $name,
            'arguments' => '' !== $arguments ? $arguments : '{}',
        );
    }

    private function extract_responses_tool_calls( $body ) {
        if ( ! is_array( $body ) || empty( $body['output'] ) || ! is_array( $body['output'] ) ) {
            return array();
        }

        $tool_calls = array();
        foreach ( $body['output'] as $item ) {
            if ( ! is_array( $item ) ) {
                continue;
            }
            if ( empty( $item['type'] ) || 'function_call' !== $item['type'] ) {
                continue;
            }

            $id = isset( $item['call_id'] ) ? trim( (string) $item['call_id'] ) : '';
            if ( '' === $id && ! empty( $item['id'] ) ) {
                $id = trim( (string) $item['id'] );
            }
            if ( '' === $id ) {
                $id = 'call_' . wp_generate_uuid4();
            }

            $name = isset( $item['name'] ) ? trim( (string) $item['name'] ) : '';
            if ( '' === $name ) {
                continue;
            }

            $arguments = '';
            if ( isset( $item['arguments'] ) ) {
                if ( is_string( $item['arguments'] ) ) {
                    $arguments = $item['arguments'];
                } elseif ( is_array( $item['arguments'] ) ) {
                    $arguments = wp_json_encode( $item['arguments'] );
                }
            }
            if ( '' === $arguments ) {
                $arguments = '{}';
            }

            $tool_calls[] = array(
                'id' => $id,
                'name' => $name,
                'arguments' => $arguments,
            );
        }

        return $tool_calls;
    }

    private function extract_responses_replay_items( $body ) {
        if ( ! is_array( $body ) || empty( $body['output'] ) || ! is_array( $body['output'] ) ) {
            return array();
        }

        return $this->normalize_response_replay_items( $body['output'] );
    }

    private function normalize_response_replay_items( $items ) {
        if ( ! is_array( $items ) ) {
            return array();
        }

        $normalized = array();
        foreach ( $items as $item ) {
            $resolved = $this->normalize_response_replay_item( $item );
            if ( ! empty( $resolved ) ) {
                $normalized[] = $resolved;
            }
        }

        return $normalized;
    }

    private function normalize_response_replay_item( $item ) {
        if ( ! is_array( $item ) || empty( $item['type'] ) ) {
            return array();
        }

        $type = trim( (string) $item['type'] );
        if ( 'message' === $type ) {
            $role = isset( $item['role'] ) ? strtolower( trim( (string) $item['role'] ) ) : 'assistant';
            if ( ! in_array( $role, array( 'assistant', 'user' ), true ) ) {
                return array();
            }

            $content = $this->normalize_response_message_content( isset( $item['content'] ) ? $item['content'] : '' );
            if ( '' === $content || array() === $content ) {
                return array();
            }

            return array(
                'type' => 'message',
                'role' => $role,
                'content' => $content,
            );
        }

        if ( 'function_call' === $type ) {
            $call_id = isset( $item['call_id'] ) ? trim( (string) $item['call_id'] ) : '';
            $name = isset( $item['name'] ) ? trim( (string) $item['name'] ) : '';
            if ( '' === $call_id || '' === $name ) {
                return array();
            }

            $arguments = '{}';
            if ( isset( $item['arguments'] ) ) {
                if ( is_string( $item['arguments'] ) ) {
                    $arguments = $item['arguments'];
                } elseif ( is_array( $item['arguments'] ) ) {
                    $arguments = wp_json_encode( $item['arguments'] );
                }
            }

            $resolved = array(
                'type' => 'function_call',
                'call_id' => $call_id,
                'name' => $name,
                'arguments' => $arguments,
            );

            $response_id = isset( $item['id'] ) ? trim( (string) $item['id'] ) : '';
            if ( '' !== $response_id ) {
                $resolved['id'] = $response_id;
            }

            return $resolved;
        }

        if ( 'reasoning' === $type ) {
            $reasoning_id = isset( $item['id'] ) ? trim( (string) $item['id'] ) : '';
            if ( '' === $reasoning_id ) {
                return array();
            }

            $resolved = array(
                'type' => 'reasoning',
                'id' => $reasoning_id,
            );
            // encrypted_content enables replay without store:true — the opaque
            // payload carries all reasoning state needed for multi-turn continuity.
            if ( isset( $item['encrypted_content'] ) && is_string( $item['encrypted_content'] ) ) {
                $resolved['encrypted_content'] = $item['encrypted_content'];
            }
            if ( isset( $item['content'] ) && is_string( $item['content'] ) && '' !== $item['content'] ) {
                $resolved['content'] = $item['content'];
            }
            // Summary — required field for reasoning items in input.
            if ( isset( $item['summary'] ) && is_array( $item['summary'] ) ) {
                $resolved['summary'] = $item['summary'];
            } elseif ( isset( $item['summary'] ) && is_string( $item['summary'] ) && '' !== $item['summary'] ) {
                $resolved['summary'] = array( array( 'type' => 'summary_text', 'text' => $item['summary'] ) );
            } else {
                $resolved['summary'] = array();
            }

            return $resolved;
        }

        if ( 'web_search_call' === $type ) {
            return $item;
        }

        return array();
    }

    private function normalize_response_message_content( $content ) {
        if ( is_string( $content ) ) {
            return $content;
        }

        if ( ! is_array( $content ) ) {
            return '';
        }

        $parts = array();
        foreach ( $content as $part ) {
            if ( ! is_array( $part ) ) {
                continue;
            }

            $type = isset( $part['type'] ) ? trim( (string) $part['type'] ) : 'output_text';
            $text = isset( $part['text'] ) ? (string) $part['text'] : '';
            if ( '' === $text ) {
                continue;
            }

            if ( ! in_array( $type, array( 'input_text', 'output_text' ), true ) ) {
                $type = 'output_text';
            }

            $parts[] = array(
                'type' => $type,
                'text' => $text,
            );
        }

        if ( ! empty( $parts ) ) {
            return $parts;
        }

        return $this->stringify_message_content( $content );
    }

    private function build_assistant_message( $answer, $tool_calls, $response_items = array() ) {
        $message = array(
            'role' => 'assistant',
        );
        if ( '' !== (string) $answer ) {
            $message['content'] = (string) $answer;
        }

        $response_items = $this->normalize_response_replay_items( $response_items );
        if ( ! empty( $response_items ) ) {
            $message['response_items'] = $response_items;
        }

        if ( empty( $tool_calls ) || ! is_array( $tool_calls ) ) {
            return $message;
        }

        $message['tool_calls'] = array();
        foreach ( $tool_calls as $call ) {
            if ( ! is_array( $call ) || empty( $call['name'] ) ) {
                continue;
            }
            $id = isset( $call['id'] ) ? trim( (string) $call['id'] ) : '';
            if ( '' === $id ) {
                $id = 'call_' . wp_generate_uuid4();
            }
            $message['tool_calls'][] = array(
                'id' => $id,
                'type' => 'function',
                'function' => array(
                    'name' => (string) $call['name'],
                    'arguments' => isset( $call['arguments'] ) ? (string) $call['arguments'] : '{}',
                ),
            );
        }

        return $message;
    }

    private function stringify_message_content( $content ) {
        if ( is_string( $content ) ) {
            return $content;
        }
        if ( is_scalar( $content ) ) {
            return (string) $content;
        }
        if ( is_array( $content ) ) {
            $parts = array();
            foreach ( $content as $item ) {
                if ( is_string( $item ) ) {
                    $parts[] = $item;
                    continue;
                }
                if ( is_array( $item ) ) {
                    if ( ! empty( $item['text'] ) && is_string( $item['text'] ) ) {
                        $parts[] = $item['text'];
                        continue;
                    }
                    if ( ! empty( $item['image_url'] ) ) {
                        if ( is_string( $item['image_url'] ) ) {
                            $parts[] = '[image] ' . $item['image_url'];
                            continue;
                        }
                        if ( is_array( $item['image_url'] ) && ! empty( $item['image_url']['url'] ) ) {
                            $parts[] = '[image] ' . (string) $item['image_url']['url'];
                            continue;
                        }
                    }
                    foreach ( array( 'url', 'src', 'file_url' ) as $key ) {
                        if ( ! empty( $item[ $key ] ) && is_string( $item[ $key ] ) ) {
                            $parts[] = '[image] ' . (string) $item[ $key ];
                            continue 2;
                        }
                    }
                    if ( ! empty( $item['content'] ) && is_string( $item['content'] ) ) {
                        $parts[] = $item['content'];
                    }
                }
            }
            if ( ! empty( $parts ) ) {
                return implode( "\n", $parts );
            }
            return wp_json_encode( $content );
        }
        return '';
    }

	    private function extract_text( $body ) {
	        if ( ! is_array( $body ) ) {
	            return '';
	        }

        if ( ! empty( $body['output_text'] ) ) {
            return is_array( $body['output_text'] ) ? implode( "\n", $body['output_text'] ) : (string) $body['output_text'];
        }

	        if ( ! empty( $body['output'] ) && is_array( $body['output'] ) ) {
	            $parts = array();
	            foreach ( $body['output'] as $out ) {
	                if ( ! is_array( $out ) ) {
	                    continue;
	                }
	                if (
	                    ! empty( $out['type'] )
	                    && 'output_text' === $out['type']
	                    && isset( $out['text'] )
	                    && is_string( $out['text'] )
	                    && '' !== $out['text']
	                ) {
	                    $parts[] = $out['text'];
	                    continue;
	                }
	                if ( ! empty( $out['content'] ) && is_array( $out['content'] ) ) {
	                    foreach ( $out['content'] as $c ) {
	                        if ( is_array( $c ) && ! empty( $c['refusal'] ) && 'refusal' === (string) ( $c['type'] ?? '' ) ) {
	                            $parts[] = (string) $c['refusal'];
	                            continue;
	                        }
	                        if (
	                            is_array( $c )
	                            && ! empty( $c['text'] )
	                            && (
	                                empty( $c['type'] )
	                                || in_array( (string) $c['type'], array( 'text', 'output_text', 'input_text' ), true )
	                            )
	                        ) {
	                            $parts[] = (string) $c['text'];
	                        }
	                    }
	                }
	                if ( ! empty( $out['content'] ) && is_string( $out['content'] ) ) {
	                    $parts[] = (string) $out['content'];
	                }
	                if ( ! empty( $out['text'] ) ) {
	                    $parts[] = (string) $out['text'];
	                }
	            }
	            if ( ! empty( $parts ) ) {
	                return implode( "\n", $parts );
	            }
	        }

	        return '';
	    }

	    private function is_codex_endpoint( $endpoint ) {
	        return false !== strpos( strtolower( (string) $endpoint ), '/backend-api/codex/responses' );
	    }

	    private function decode_response_body( $endpoint, $body_raw ) {
	        if ( ! is_string( $body_raw ) || '' === $body_raw ) {
	            return array();
	        }

	        $decoded = json_decode( $body_raw, true );
	        if ( is_array( $decoded ) ) {
	            return $decoded;
	        }

	        if ( $this->is_codex_endpoint( $endpoint ) ) {
	            return $this->parse_sse_response_body( $body_raw );
	        }

	        return array();
	    }

	    private function extract_responses_usage( $usage ) {
	        if ( ! is_array( $usage ) ) {
	            return array();
	        }

	        $prompt     = isset( $usage['input_tokens'] ) ? intval( $usage['input_tokens'] ) : 0;
	        $completion = isset( $usage['output_tokens'] ) ? intval( $usage['output_tokens'] ) : 0;
	        $cached     = 0;

	        if ( 0 === $prompt && isset( $usage['prompt_tokens'] ) ) {
	            $prompt = intval( $usage['prompt_tokens'] );
	        }
	        if ( 0 === $completion && isset( $usage['completion_tokens'] ) ) {
	            $completion = intval( $usage['completion_tokens'] );
	        }

	        if ( isset( $usage['input_tokens_details'] ) && is_array( $usage['input_tokens_details'] ) && isset( $usage['input_tokens_details']['cached_tokens'] ) ) {
	            $cached = intval( $usage['input_tokens_details']['cached_tokens'] );
	        } elseif ( isset( $usage['prompt_tokens_details'] ) && is_array( $usage['prompt_tokens_details'] ) && isset( $usage['prompt_tokens_details']['cached_tokens'] ) ) {
	            $cached = intval( $usage['prompt_tokens_details']['cached_tokens'] );
	        } elseif ( isset( $usage['cached_tokens'] ) ) {
	            $cached = intval( $usage['cached_tokens'] );
	        }

	        $total  = isset( $usage['total_tokens'] ) ? intval( $usage['total_tokens'] ) : ( $prompt + $completion );
	        $cached = max( 0, min( $cached, $prompt ) );

	        return array(
	            'prompt_tokens'     => $prompt,
	            'completion_tokens' => $completion,
	            'total_tokens'      => $total,
	            'cached_tokens'     => $cached,
	            'cache_hit_rate'    => $prompt > 0 ? round( $cached / $prompt, 4 ) : 0.0,
	        );
	    }

    private function build_codex_image_generation_payload( $prompt, $quality, $size, $reference_image_urls = array() ) {
        $content = array(
            array(
                'type' => 'input_text',
                'text' => (string) $prompt,
            ),
        );

        foreach ( $this->normalize_reference_image_urls( $reference_image_urls ) as $url ) {
            $content[] = array(
                'type'      => 'input_image',
                'image_url' => $url,
            );
        }

        return array(
            'model'        => 'gpt-5.5',
            'store'        => false,
            'instructions' => 'You are an assistant that must fulfill image generation requests by using the image_generation tool when provided.',
            'input'        => array(
                array(
                    'type'    => 'message',
                    'role'    => 'user',
                    'content' => $content,
                ),
            ),
            'tools'        => array(
                array(
                    'type'           => 'image_generation',
                    'model'          => 'gpt-image-2',
                    'size'           => $this->normalize_image_size( $size ),
                    'quality'        => $this->normalize_image_quality( $quality ),
                    'output_format'  => 'png',
                    'background'     => 'opaque',
                    'partial_images' => 1,
                ),
            ),
            'tool_choice'  => array(
                'type'  => 'allowed_tools',
                'mode'  => 'required',
                'tools' => array(
                    array( 'type' => 'image_generation' ),
                ),
            ),
            'stream'       => true,
        );
    }

    private function normalize_reference_image_urls( $reference_image_urls ) {
        $clean = array();
        foreach ( (array) $reference_image_urls as $url ) {
            $url = trim( (string) $url );
            if ( '' === $url || ! preg_match( '#^(https?://|data:image/)#i', $url ) ) {
                continue;
            }
            $clean[] = $url;
            if ( count( $clean ) >= 3 ) {
                break;
            }
        }

        return array_values( array_unique( $clean ) );
    }

    /**
     * The Codex backend does not fetch external URLs for input_image parts
     * (codex CLI always inlines images as base64 data URLs), so remote
     * references are downloaded server-side and inlined. A failed download
     * skips that reference instead of failing the whole generation.
     */
    private function resolve_reference_image_inputs( $reference_image_urls ) {
        $inputs = array();
        foreach ( $this->normalize_reference_image_urls( $reference_image_urls ) as $url ) {
            if ( 0 === stripos( $url, 'data:image/' ) ) {
                $inputs[] = $url;
                continue;
            }

            $data_url = $this->download_reference_image_as_data_url( $url );
            if ( '' !== $data_url ) {
                $inputs[] = $data_url;
            }
        }

        return $inputs;
    }

    private function download_reference_image_as_data_url( $url ) {
        $fetch = function_exists( 'wp_safe_remote_get' ) ? 'wp_safe_remote_get' : ( function_exists( 'wp_remote_get' ) ? 'wp_remote_get' : '' );
        if ( '' === $fetch ) {
            return '';
        }

        $response = call_user_func(
            $fetch,
            $url,
            array(
                'timeout'     => self::REFERENCE_IMAGE_FETCH_TIMEOUT,
                'redirection' => 3,
            )
        );
        if ( function_exists( 'is_wp_error' ) && is_wp_error( $response ) ) {
            return '';
        }

        $status = function_exists( 'wp_remote_retrieve_response_code' ) ? intval( wp_remote_retrieve_response_code( $response ) ) : 0;
        $body   = function_exists( 'wp_remote_retrieve_body' ) ? (string) wp_remote_retrieve_body( $response ) : '';
        if ( 200 !== $status || '' === $body || strlen( $body ) > self::REFERENCE_IMAGE_MAX_BYTES ) {
            return '';
        }

        $mime = $this->sniff_image_mime( $body );
        if ( '' === $mime ) {
            return '';
        }

        return 'data:' . $mime . ';base64,' . base64_encode( $body );
    }

    private function sniff_image_mime( $bytes ) {
        if ( 0 === strncmp( $bytes, "\x89PNG\r\n\x1a\n", 8 ) ) {
            return 'image/png';
        }
        if ( 0 === strncmp( $bytes, "\xFF\xD8\xFF", 3 ) ) {
            return 'image/jpeg';
        }
        if ( 0 === strncmp( $bytes, 'GIF8', 4 ) ) {
            return 'image/gif';
        }
        if ( 0 === strncmp( $bytes, 'RIFF', 4 ) && 'WEBP' === substr( $bytes, 8, 4 ) ) {
            return 'image/webp';
        }
        return '';
    }

    private function normalize_image_quality( $quality ) {
        $quality = strtolower( trim( (string) $quality ) );
        return in_array( $quality, array( 'low', 'medium', 'high' ), true ) ? $quality : 'medium';
    }

    private function normalize_image_size( $size ) {
        $size = strtolower( trim( (string) $size ) );
        $allowed = array( '1024x1024', '1536x1024', '1024x1536' );
        return in_array( $size, $allowed, true ) ? $size : '1024x1024';
    }

    private function extract_image_b64( $body ) {
        if ( isset( $body['output'] ) && is_array( $body['output'] ) ) {
            foreach ( $body['output'] as $item ) {
                if ( ! is_array( $item ) ) {
                    continue;
                }
                if ( ! empty( $item['result'] ) && is_string( $item['result'] ) ) {
                    return (string) $item['result'];
                }
                if ( isset( $item['content'] ) && is_array( $item['content'] ) ) {
                    foreach ( $item['content'] as $content ) {
                        if ( is_array( $content ) && ! empty( $content['result'] ) && is_string( $content['result'] ) ) {
                            return (string) $content['result'];
                        }
                    }
                }
            }
        }
        if ( ! empty( $body['result'] ) && is_string( $body['result'] ) ) {
            return (string) $body['result'];
        }
        return '';
    }

	    private function parse_sse_response_body( $body_raw ) {
	        $body_raw = (string) $body_raw;
	        if ( '' === trim( $body_raw ) ) {
	            return array();
	        }

	        $chunks = preg_split( "/\r\n\r\n|\n\n|\r\r/", $body_raw );
	        if ( ! is_array( $chunks ) ) {
	            return array();
	        }

	        $response = array();
	        $output_items = array();
	        $output_text = '';

	        foreach ( $chunks as $chunk ) {
	            $parsed = $this->parse_sse_event_chunk( $chunk );
	            if ( empty( $parsed ) || empty( $parsed['data'] ) ) {
	                continue;
	            }

	            $data_raw = trim( (string) $parsed['data'] );
	            if ( '' === $data_raw || '[DONE]' === $data_raw ) {
	                continue;
	            }

	            $event = json_decode( $data_raw, true );
	            if ( ! is_array( $event ) ) {
	                continue;
	            }

	            $event_type = isset( $event['type'] ) ? (string) $event['type'] : ( isset( $parsed['event'] ) ? (string) $parsed['event'] : '' );

	            if (
	                in_array( $event_type, array( 'response.completed', 'response.failed', 'response.incomplete' ), true )
	                && ! empty( $event['response'] )
	                && is_array( $event['response'] )
	            ) {
	                $response = $event['response'];
	                break;
	            }

	            if ( 'response.output_item.done' === $event_type && ! empty( $event['item'] ) && is_array( $event['item'] ) ) {
	                $output_items[] = $event['item'];
	                continue;
	            }

	            if ( 'response.output_text.delta' === $event_type && isset( $event['delta'] ) && is_string( $event['delta'] ) ) {
	                $output_text .= $event['delta'];
	                continue;
	            }

	            if ( 'response.output_text.done' === $event_type && isset( $event['text'] ) && is_string( $event['text'] ) ) {
	                // Only use the done text if we haven't accumulated anything from
	                // deltas yet — avoids overwriting a complete delta-assembled response
	                // when the done event carries a shorter/different payload.
	                if ( '' === $output_text ) {
	                    $output_text = (string) $event['text'];
	                }
	            }
	        }

	        if ( ! empty( $response ) ) {
	            $response_has_text = '' !== $this->extract_text( $response );
	            if ( ! $response_has_text && '' !== $output_text ) {
	                $response['output_text'] = $output_text;
	            }
	            if ( ( ! isset( $response['output'] ) || ! is_array( $response['output'] ) || empty( $response['output'] ) ) && ! empty( $output_items ) ) {
	                $response['output'] = $output_items;
	            }
	            return $response;
	        }

	        if ( '' !== $output_text || ! empty( $output_items ) ) {
	            return array(
	                'output_text' => $output_text,
	                'output' => $output_items,
	            );
	        }

	        return array();
	    }

	    private function parse_sse_event_chunk( $chunk ) {
	        $chunk = trim( (string) $chunk );
	        if ( '' === $chunk ) {
	            return array();
	        }

	        $event = '';
	        $data_lines = array();
	        $lines = preg_split( "/\r\n|\n|\r/", $chunk );
	        if ( ! is_array( $lines ) ) {
	            return array();
	        }

	        foreach ( $lines as $line ) {
	            if ( 0 === strpos( $line, 'event:' ) ) {
	                $event = trim( substr( $line, 6 ) );
	                continue;
	            }
	            if ( 0 === strpos( $line, 'data:' ) ) {
	                $data_lines[] = ltrim( substr( $line, 5 ) );
	            }
	        }

	        return array(
	            'event' => $event,
	            'data' => implode( "\n", $data_lines ),
	        );
	    }

	    /**
	     * Transcribe audio using OpenAI Whisper API.
	     *
	     * @param string $file_path Local audio file path.
	     * @param string $auth_mode 'api_key' or 'oauth'.
	     * @param array  $creds     Credentials array.
	     * @param string $language  Optional ISO 639-1 language code. Empty = auto-detect.
	     * @param int    $timeout   Request timeout in seconds. Default 120.
	     * @return array { success: bool, text: string, error: string }
	     */
	    public function transcribe_audio( $file_path, $auth_mode, $creds, $language = '', $timeout = 120 ) {
	        $token = $this->resolve_token( $auth_mode, $creds );
	        if ( empty( $token ) ) {
	            return array( 'success' => false, 'text' => '', 'error' => 'Missing auth token for transcription.' );
	        }

	        if ( ! is_file( $file_path ) || ! is_readable( $file_path ) ) {
	            return array( 'success' => false, 'text' => '', 'error' => 'Audio file not found or unreadable.' );
	        }

	        if ( ! class_exists( 'CURLFile' ) || ! function_exists( 'curl_init' ) ) {
	            return array( 'success' => false, 'text' => '', 'error' => 'cURL with CURLFile required for audio transcription.' );
	        }

	        // Detect MIME — try magic bytes first, then php built-in.
	        $mime = '';
	        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen
	        $fh = fopen( $file_path, 'rb' );
	        if ( $fh ) {
	            $header = fread( $fh, 16 );
	            fclose( $fh );
	            if ( is_string( $header ) && strlen( $header ) >= 4 ) {
	                if ( 'OggS' === substr( $header, 0, 4 ) ) {
	                    $mime = 'audio/ogg';
	                } elseif ( 'ID3' === substr( $header, 0, 3 ) ) {
	                    $mime = 'audio/mpeg';
	                } elseif ( "\xFF" === $header[0] && in_array( $header[1], array( "\xFB", "\xF3", "\xF2" ), true ) ) {
	                    $mime = 'audio/mpeg';
	                } elseif ( 'RIFF' === substr( $header, 0, 4 ) && strlen( $header ) >= 12 && 'WAVE' === substr( $header, 8, 4 ) ) {
	                    $mime = 'audio/wav';
	                } elseif ( strlen( $header ) >= 8 && 'ftyp' === substr( $header, 4, 4 ) ) {
	                    $mime = 'audio/mp4';
	                } elseif ( "\x1A\x45\xDF\xA3" === substr( $header, 0, 4 ) ) {
	                    $mime = 'audio/webm';
	                } elseif ( 'fLaC' === substr( $header, 0, 4 ) ) {
	                    $mime = 'audio/flac';
	                }
	            }
	        }
	        if ( '' === $mime && function_exists( 'mime_content_type' ) ) {
	            $detected = mime_content_type( $file_path );
	            if ( is_string( $detected ) && '' !== $detected ) {
	                $mime = $detected;
	            }
	        }
	        if ( '' === $mime ) {
	            $mime = 'audio/ogg';
	        }
	        $audit_id      = $this->new_audit_id();
	        $audit_started = microtime( true );

	        // Map MIME to a file extension Whisper recognizes.
	        // Whisper infers format from the filename extension, not from Content-Type.
	        $ext_map = array(
	            'audio/ogg'     => 'ogg',
	            'audio/mpeg'    => 'mp3',
	            'audio/mp3'     => 'mp3',
	            'audio/mp4'     => 'mp4',
	            'audio/m4a'     => 'm4a',
	            'audio/x-m4a'   => 'm4a',
	            'audio/wav'     => 'wav',
	            'audio/webm'    => 'webm',
	            'audio/flac'    => 'flac',
	            'audio/mpga'    => 'mpga',
	            'video/mp4'     => 'mp4',
	            'video/webm'    => 'webm',
	        );
	        $ext      = isset( $ext_map[ $mime ] ) ? $ext_map[ $mime ] : 'ogg';
	        $filename = 'audio.' . $ext;

	        $body = array(
	            'file'            => new CURLFile( $file_path, $mime, $filename ),
	            'model'           => 'whisper-1',
	            'response_format' => 'json',
	        );

	        $language = trim( (string) $language );
	        if ( '' !== $language ) {
	            $body['language'] = $language;
	        }

	        $ch = curl_init( self::WHISPER_ENDPOINT );
	        curl_setopt( $ch, CURLOPT_POST, true );
	        curl_setopt( $ch, CURLOPT_POSTFIELDS, $body );
	        curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
	        curl_setopt( $ch, CURLOPT_TIMEOUT, max( 30, intval( $timeout ) ) );
	        curl_setopt( $ch, CURLOPT_HTTPHEADER, array(
	            'Authorization: Bearer ' . $token,
	        ) );

	        $raw = curl_exec( $ch );
	        if ( false === $raw ) {
	            $error = curl_error( $ch );
	            curl_close( $ch );
	            $this->log_request_failure( 'whisper_curl_error', array( 'error' => $error ) );
	            $this->record_audio_audit_event(
	                $audit_id,
	                $file_path,
	                $mime,
	                $language,
	                $timeout,
	                $audit_started,
	                array( 'success' => false, 'error' => 'Whisper cURL failed: ' . $error, 'status' => 0, 'text' => '' )
	            );
	            return array( 'success' => false, 'text' => '', 'error' => 'Whisper cURL failed: ' . $error );
	        }

	        $status = curl_getinfo( $ch, CURLINFO_HTTP_CODE );
	        curl_close( $ch );

	        $data = json_decode( (string) $raw, true );

	        if ( $status < 200 || $status >= 300 ) {
	            $error_msg = 'Whisper HTTP ' . $status;
	            if ( is_array( $data ) && ! empty( $data['error']['message'] ) ) {
	                $error_msg .= ': ' . (string) $data['error']['message'];
	            }
	            $this->log_request_failure( 'whisper_http_error', array( 'status' => $status, 'error' => $error_msg ) );
	            $this->record_audio_audit_event(
	                $audit_id,
	                $file_path,
	                $mime,
	                $language,
	                $timeout,
	                $audit_started,
	                array( 'success' => false, 'error' => $error_msg, 'status' => $status, 'body' => $data, 'text' => '' )
	            );
	            return array( 'success' => false, 'text' => '', 'error' => $error_msg );
	        }

	        $text = isset( $data['text'] ) ? trim( (string) $data['text'] ) : '';
	        if ( '' === $text ) {
	            $this->log_request_failure( 'whisper_empty_transcription', array( 'status' => $status ) );
	            $this->record_audio_audit_event(
	                $audit_id,
	                $file_path,
	                $mime,
	                $language,
	                $timeout,
	                $audit_started,
	                array( 'success' => false, 'error' => 'Whisper returned empty transcription.', 'status' => $status, 'body' => $data, 'text' => '' )
	            );
	            return array( 'success' => false, 'text' => '', 'error' => 'Whisper returned empty transcription.' );
	        }

	        $this->record_audio_audit_event(
	            $audit_id,
	            $file_path,
	            $mime,
	            $language,
	            $timeout,
	            $audit_started,
	            array( 'success' => true, 'error' => '', 'status' => $status, 'body' => $data, 'text' => $text )
	        );

	        return array( 'success' => true, 'text' => $text, 'error' => '' );
	    }

	    private function new_audit_id() {
	        return class_exists( 'Niwa_AI_LLM_Audit_Logger' )
	            ? Niwa_AI_LLM_Audit_Logger::new_event_id()
	            : '';
	    }

	    private function record_json_audit_event( $audit_id, $endpoint, $payload, $timeout, $attempt, $started_at, $result ) {
	        if ( ! class_exists( 'Niwa_AI_LLM_Audit_Logger' ) || ! Niwa_AI_LLM_Audit_Logger::is_enabled() ) {
	            return;
	        }

	        $body = isset( $result['body'] ) && is_array( $result['body'] ) ? $result['body'] : array();
	        Niwa_AI_LLM_Audit_Logger::record_event(
	            'llm_responses_request',
	            array(
	                'audit_id' => $audit_id,
	                'profile' => $this->profile,
	                'endpoint' => $this->classify_endpoint( $endpoint ),
	                'endpoint_url' => $endpoint,
	                'attempt' => intval( $attempt ),
	                'timeout_seconds' => intval( $timeout ),
	                'duration_ms' => round( ( microtime( true ) - floatval( $started_at ) ) * 1000, 2 ),
	                'request' => array(
	                    'model' => isset( $payload['model'] ) ? (string) $payload['model'] : '',
	                    'has_tools' => ! empty( $payload['tools'] ),
	                    'tool_choice' => isset( $payload['tool_choice'] ) ? $payload['tool_choice'] : '',
	                    'payload' => $payload,
	                ),
	                'response' => array(
	                    'success' => ! empty( $result['success'] ),
	                    'status' => isset( $result['status'] ) ? intval( $result['status'] ) : 0,
	                    'error_type' => isset( $result['error_type'] ) ? (string) $result['error_type'] : '',
	                    'error' => isset( $result['error'] ) ? (string) $result['error'] : '',
	                    'output_text' => $this->extract_text( $body ),
	                    'usage' => $this->extract_responses_usage( isset( $body['usage'] ) ? $body['usage'] : array() ),
	                    'body' => $body,
	                    'body_excerpt' => isset( $result['body_excerpt'] ) ? (string) $result['body_excerpt'] : '',
	                ),
	            )
	        );
	    }

	    private function record_audio_audit_event( $audit_id, $file_path, $mime, $language, $timeout, $started_at, $result ) {
	        if ( ! class_exists( 'Niwa_AI_LLM_Audit_Logger' ) || ! Niwa_AI_LLM_Audit_Logger::is_enabled() ) {
	            return;
	        }

	        Niwa_AI_LLM_Audit_Logger::record_event(
	            'llm_audio_transcription_request',
	            array(
	                'audit_id' => $audit_id,
	                'profile' => $this->profile,
	                'endpoint' => 'openai_audio_transcriptions',
	                'endpoint_url' => self::WHISPER_ENDPOINT,
	                'timeout_seconds' => intval( $timeout ),
	                'duration_ms' => round( ( microtime( true ) - floatval( $started_at ) ) * 1000, 2 ),
	                'request' => array(
	                    'model' => 'whisper-1',
	                    'file_basename' => basename( (string) $file_path ),
	                    'file_size_bytes' => is_file( $file_path ) ? filesize( $file_path ) : 0,
	                    'mime' => (string) $mime,
	                    'language' => (string) $language,
	                ),
	                'response' => $result,
	            )
	        );
	    }

	    private function classify_endpoint( $endpoint ) {
	        if ( $this->is_codex_endpoint( $endpoint ) ) {
	            return 'codex_responses';
	        }
	        if ( false !== strpos( strtolower( (string) $endpoint ), '/v1/responses' ) ) {
	            return 'openai_responses';
	        }

	        return 'http_json';
	    }

	    private function log_request_failure( $event, $context = array() ) {
	        $event_lc = strtolower( (string) $event );
	        if ( false === strpos( $event_lc, 'error' )
	            && false === strpos( $event_lc, 'failed' )
	            && false === strpos( $event_lc, 'empty' )
	        ) {
	            return;
	        }

	        $enabled = ( defined( 'WP_DEBUG' ) && WP_DEBUG ) || ( defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG );
	        if ( ! $enabled ) {
	            return;
	        }

	        if ( ! is_array( $context ) ) {
	            $context = array( 'value' => $context );
	        }

	        foreach ( $context as $key => $value ) {
	            if ( is_scalar( $value ) || null === $value ) {
	                $context[ $key ] = (string) $value;
	                continue;
	            }
	            $encoded = wp_json_encode( $value );
	            $context[ $key ] = false !== $encoded ? $encoded : '[unserializable]';
	        }

	        error_log( 'Niwa AI OpenAI Client [' . sanitize_key( (string) $event ) . '] ' . wp_json_encode( $context ) );
	    }
	}
