<?php
/**
 * OpenAI API Client
 *
 * Handles communication with the OpenAI API (Models + Responses).
 *
 * @package BoostBot
 * @since 1.0.0
 */

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

/**
 * Class BoostBot_OpenAI_Client
 */
class BoostBot_OpenAI_Client {

    /**
     * OpenAI base URL.
     *
     * @var string
     */
    private $base_url = 'https://api.openai.com/v1';

    /**
     * API key.
     *
     * @var string
     */
    private $api_key;

    /**
     * Default model.
     *
     * @var string
     */
    private $model = 'gpt-4o-mini';

    /**
     * Maximum retries.
     *
     * @var int
     */
    private $max_retries = 3;

    /**
     * Request timeout.
     *
     * @var int
     */
    private $timeout = 60;

    /**
     * Constructor.
     *
     * @param string|null $api_key Optional API key. If not provided, uses settings.
     * @throws Exception If API key is not configured.
     */
    public function __construct( $api_key = null ) {
        $this->api_key = $api_key ?: BoostBot_Settings::get( 'openai_api_key' );

        if ( empty( $this->api_key ) ) {
            throw new Exception( esc_html( __( 'OpenAI API key not configured', 'boostbot' ) ) );
        }

        $configured_model = BoostBot_Settings::get( 'openai_model' );
        if ( ! empty( $configured_model ) ) {
            $this->model = $configured_model;
        }
    }

    /**
     * Validate an OpenAI API key using /v1/models.
     *
     * @param string $key API key to validate.
     * @return bool True if valid.
     */
    public static function validate_api_key( string $key ): bool {
        try {
            $response = wp_remote_get( 'https://api.openai.com/v1/models', array(
                'timeout' => 15,
                'headers' => array(
                    'Authorization' => 'Bearer ' . $key,
                    'Content-Type'  => 'application/json',
                ),
            ) );

            if ( is_wp_error( $response ) ) {
                return false;
            }

            $code = wp_remote_retrieve_response_code( $response );
            return $code === 200;
        } catch ( Exception $e ) {
            return false;
        }
    }

    /**
     * Get available OpenAI models from API (cached).
     *
     * @param bool $force_refresh Force refresh from API instead of cache.
     * @return array List of available models (normalized for UI).
     */
    public static function get_available_models( bool $force_refresh = false ): array {
        $cache_key      = 'boostbot_openai_models';
        $cache_duration = 24 * HOUR_IN_SECONDS;

        if ( ! $force_refresh ) {
            $cached = get_transient( $cache_key );
            if ( false !== $cached && is_array( $cached ) ) {
                return $cached;
            }
        }

        $api_key = BoostBot_Settings::get( 'openai_api_key' );
        if ( empty( $api_key ) ) {
            return self::get_fallback_models();
        }

        try {
            $response = wp_remote_get( 'https://api.openai.com/v1/models', array(
                'timeout' => 15,
                'headers' => array(
                    'Authorization' => 'Bearer ' . $api_key,
                    'Content-Type'  => 'application/json',
                ),
            ) );

            if ( is_wp_error( $response ) ) {
                return self::get_fallback_models();
            }

            $code = wp_remote_retrieve_response_code( $response );
            $body = json_decode( wp_remote_retrieve_body( $response ), true );

            if ( $code !== 200 || empty( $body['data'] ) || ! is_array( $body['data'] ) ) {
                return self::get_fallback_models();
            }

            $models = self::parse_models_response( $body['data'] );

            set_transient( $cache_key, $models, $cache_duration );

            return $models;
        } catch ( Exception $e ) {
            return self::get_fallback_models();
        }
    }

    /**
     * Clear models cache.
     *
     * @return bool True if cache was cleared.
     */
    public static function clear_models_cache(): bool {
        return delete_transient( 'boostbot_openai_models' );
    }

    /**
     * Send a message using OpenAI Responses API.
     *
     * Normalizes result into:
     * - content: string
     * - usage: [input_tokens, output_tokens]
     * - stop_reason: string
     *
     * @param array $messages Messages in BoostBot internal format [{role, content}...].
     * @param array $options Options (model, system, max_tokens, temperature, bot_id, conversation_id).
     * @return array Normalized response.
     * @throws Exception On API error.
     */
    public function send_message( array $messages, array $options = array() ): array {
        $model_used = $options['model'] ?? $this->model;

        // Build Responses API input: keep it simple as role/content text.
        $input = array();

        // System prompt (if provided) goes first as a system role.
        if ( ! empty( $options['system'] ) ) {
            $input[] = array(
                'role'    => 'system',
                'content' => array(
                    array(
                        'type' => 'input_text',
                        'text' => (string) $options['system'],
                    ),
                ),
            );
        }

        foreach ( $messages as $msg ) {
            $role = $msg['role'] ?? 'user';
            $content = (string) ( $msg['content'] ?? '' );
            if ( $content === '' ) {
                continue;
            }

            // Map to supported roles for Responses API.
            if ( $role !== 'user' && $role !== 'assistant' && $role !== 'system' ) {
                $role = 'user';
            }

            $input[] = array(
                'role'    => $role,
                'content' => array(
                    array(
                        'type' => 'input_text',
                        'text' => $content,
                    ),
                ),
            );
        }

        $payload = array(
            'model' => $model_used,
            'input' => $input,
        );

        // Token + temperature controls (best-effort; OpenAI may ignore unsupported fields per model).
        if ( isset( $options['temperature'] ) ) {
            $payload['temperature'] = (float) $options['temperature'];
        }
        if ( isset( $options['max_tokens'] ) ) {
            // Responses API commonly uses max_output_tokens.
            $payload['max_output_tokens'] = (int) $options['max_tokens'];
        }

        $raw = $this->make_request( '/responses', $payload );

        $content_text = $this->extract_output_text( $raw );
        $usage = $raw['usage'] ?? array();

        $input_tokens  = (int) ( $usage['input_tokens'] ?? 0 );
        $output_tokens = (int) ( $usage['output_tokens'] ?? 0 );

        return array(
            'content'     => $content_text,
            'usage'       => array(
                'input_tokens'  => $input_tokens,
                'output_tokens' => $output_tokens,
            ),
            'stop_reason' => $raw['status'] ?? ( $raw['stop_reason'] ?? 'unknown' ),
        );
    }

    /**
     * Make an HTTP request to OpenAI.
     *
     * @param string $path API path (e.g. /responses).
     * @param array  $payload JSON payload.
     * @param int    $attempt Attempt number.
     * @return array Parsed body.
     * @throws Exception On API error.
     */
    private function make_request( string $path, array $payload, int $attempt = 1 ): array {
        $url = rtrim( $this->base_url, '/' ) . $path;

        try {
            $response = wp_remote_post( $url, array(
                'timeout' => $this->timeout,
                'headers' => array(
                    'Content-Type'  => 'application/json',
                    'Authorization' => 'Bearer ' . $this->api_key,
                ),
                'body'    => wp_json_encode( $payload ),
            ) );

            if ( is_wp_error( $response ) ) {
                throw new Exception( $response->get_error_message() );
            }

            $code = wp_remote_retrieve_response_code( $response );
            $body = json_decode( wp_remote_retrieve_body( $response ), true );

            if ( $code >= 400 ) {
                $message = $body['error']['message'] ?? __( 'OpenAI API Error', 'boostbot' );
                throw new Exception( $message, $code );
            }

            return is_array( $body ) ? $body : array();

        } catch ( Exception $e ) {
            if ( $attempt < $this->max_retries && $this->is_retryable( $e ) ) {
                // Bounded backoff (1s, 2s) — never block the request path for long.
                sleep( min( $attempt, 2 ) );
                return $this->make_request( $path, $payload, $attempt + 1 );
            }

            throw $e;
        }
    }

    /**
     * Best-effort output text extraction from Responses API response.
     *
     * @param array $raw Raw API response.
     * @return string Output text.
     */
    private function extract_output_text( array $raw ): string {
        // Some responses include output_text directly.
        if ( ! empty( $raw['output_text'] ) && is_string( $raw['output_text'] ) ) {
            return $raw['output_text'];
        }

        // Try to traverse output content items.
        if ( ! empty( $raw['output'] ) && is_array( $raw['output'] ) ) {
            $parts = array();
            foreach ( $raw['output'] as $out ) {
                if ( empty( $out['content'] ) || ! is_array( $out['content'] ) ) {
                    continue;
                }
                foreach ( $out['content'] as $content ) {
                    if ( ! is_array( $content ) ) {
                        continue;
                    }
                    // Typical: {type: "output_text", text: "..."}
                    if ( isset( $content['text'] ) && is_string( $content['text'] ) ) {
                        $parts[] = $content['text'];
                    }
                }
            }
            $text = trim( implode( "\n", $parts ) );
            if ( $text !== '' ) {
                return $text;
            }
        }

        return '';
    }

    /**
     * Parse models from OpenAI API response.
     *
     * @param array $data Raw models array from /v1/models.
     * @return array Normalized models keyed by id.
     */
    private static function parse_models_response( array $data ): array {
        $pricing = self::get_pricing_map();
        $models  = array();

        foreach ( $data as $model ) {
            $id = $model['id'] ?? '';
            if ( empty( $id ) ) {
                continue;
            }

            $category = 'other';
            if ( strpos( $id, 'gpt' ) === 0 ) {
                $category = 'gpt';
            } elseif ( strpos( $id, 'o1' ) === 0 || strpos( $id, 'o3' ) === 0 ) {
                $category = 'reasoning';
            }

            $display_name = strtoupper( $id );
            $input_price  = isset( $pricing[ $id ] ) ? (float) $pricing[ $id ]['input'] : 0.0;
            $output_price = isset( $pricing[ $id ] ) ? (float) $pricing[ $id ]['output'] : 0.0;

            $models[ $id ] = array(
                'id'           => $id,
                'display_name' => $display_name,
                'category'     => $category,
                'input_price'  => $input_price,
                'output_price' => $output_price,
                'created_at'   => isset( $model['created'] ) ? gmdate( 'c', (int) $model['created'] ) : null,
            );
        }

        // Sort: gpt first, then reasoning, then other. Within category, alphabetical.
        uasort( $models, function( $a, $b ) {
            $priority = array( 'gpt' => 1, 'reasoning' => 2, 'other' => 3 );
            $ap = $priority[ $a['category'] ] ?? 3;
            $bp = $priority[ $b['category'] ] ?? 3;
            if ( $ap !== $bp ) {
                return $ap - $bp;
            }
            return strcmp( $a['id'], $b['id'] );
        } );

        return $models;
    }

    /**
     * Local pricing map (per 1M tokens, USD).
     *
     * Note: OpenAI does not provide prices via API. Keep this list curated.
     *
     * @return array
     */
    private static function get_pricing_map(): array {
        return array(
            // Common defaults (best-effort, may change over time).
            'gpt-4o'      => array( 'input' => 5.0,  'output' => 15.0 ),
            'gpt-4o-mini' => array( 'input' => 0.15, 'output' => 0.60 ),
        );
    }

    /**
     * Fallback models when API is unavailable.
     *
     * @return array
     */
    private static function get_fallback_models(): array {
        $pricing = self::get_pricing_map();
        return array(
            'gpt-4o-mini' => array(
                'id'           => 'gpt-4o-mini',
                'display_name' => 'GPT-4O-MINI',
                'category'     => 'gpt',
                'input_price'  => (float) $pricing['gpt-4o-mini']['input'],
                'output_price' => (float) $pricing['gpt-4o-mini']['output'],
                'created_at'   => null,
            ),
            'gpt-4o' => array(
                'id'           => 'gpt-4o',
                'display_name' => 'GPT-4O',
                'category'     => 'gpt',
                'input_price'  => (float) $pricing['gpt-4o']['input'],
                'output_price' => (float) $pricing['gpt-4o']['output'],
                'created_at'   => null,
            ),
        );
    }

    /**
     * Check if error is retryable.
     *
     * @param Exception $exception The exception.
     * @return bool
     */
    private function is_retryable( Exception $exception ): bool {
        $code = $exception->getCode();
        // Retry on transport failures (code 0, e.g. a WP_Error with no HTTP
        // status), rate limit (429), or server errors (5xx).
        return $code === 0 || $code === 429 || $code >= 500;
    }
}
