<?php
/**
 * Plugin AI API functions.
 *
 * @package mind
 */

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

/**
 * Mind AI API class.
 */
class Mind_AI_API {
    /**
     * Buffer for streaming response.
     *
     * @var string
     */
    private $buffer = '';

    /**
     * Last time the buffer was sent.
     *
     * @var int
     */
    private $last_send_time = 0;

    /**
     * Buffer threshold.
     *
     * @var int
     */
    private const BUFFER_THRESHOLD = 150;

    /**
     * Minimum send interval.
     *
     * @var float
     */
    private const MIN_SEND_INTERVAL = 0.05;

    /**
     * The single class instance.
     *
     * @var null
     */
    private static $instance = null;

    /**
     * Main Instance
     * Ensures only one instance of this class exists in memory at any one time.
     */
    public static function instance() {
        if ( is_null( self::$instance ) ) {
            self::$instance = new self();
            self::$instance->init();
        }
        return self::$instance;
    }

    /**
     * Initialize the class.
     */
    public function init() {
        add_action( 'rest_api_init', array( $this, 'register_routes' ) );
    }

    /**
     * Get connected model.
     *
     * @return array|bool
     */
    public function get_connected_model() {
        $settings = get_option( 'mind_settings', array() );
        $ai_model = $settings['ai_model'] ?? '';
        $result   = false;

        if ( $ai_model ) {
            switch ( $ai_model ) {
                case 'gpt-4o':
                    if ( ! empty( $settings['openai_api_key'] ) ) {
                        $result = [
                            'name' => 'gpt-4o',
                            'key'  => $settings['openai_api_key'],
                        ];
                    }
                    break;

                case 'deepseek':
                    if ( ! empty( $settings['deepseek_api_key'] ) ) {
                        $result = [
                            'name' => 'deepseek-chat',
                            'key'  => $settings['deepseek_api_key'],
                        ];
                    }
                    break;

                case 'claude-3-5-haiku':
                case 'claude-3-7-sonnet':
                    if ( ! empty( $settings['anthropic_api_key'] ) ) {
                        $result = [
                            'name' => $ai_model,
                            'key'  => $settings['anthropic_api_key'],
                        ];
                    }
                    break;
            }
        }

        return $result;
    }

    /**
     * Send request to API.
     *
     * @param string $request request text.
     * @param string $selected_blocks selected blocks context.
     * @param string $page_blocks page blocks context.
     * @param string $page_context page context.
     *
     * @return mixed
     */
    public function request( $request, $selected_blocks = '', $page_blocks = '', $page_context = '' ) {
        // Set headers for streaming.
        header( 'Content-Type: text/event-stream' );
        header( 'Cache-Control: no-cache' );
        header( 'Connection: keep-alive' );
        header( 'X-Accel-Buffering: no' );

        ob_implicit_flush( true );
        ob_end_flush();

        if ( ! $request ) {
            $this->send_stream_error( 'no_request', __( 'Provide request to receive AI response.', 'mind' ) );
            exit;
        }

        $connected_model = $this->get_connected_model();

        if ( ! $connected_model ) {
            $this->send_stream_error( 'no_model_connected', __( 'Select an AI model and provide API key in the plugin settings.', 'mind' ) );
            exit;
        }

        $messages = $this->prepare_messages( $request, $selected_blocks, $page_blocks, $page_context );

        switch ( $connected_model['name'] ) {
            case 'gpt-4o':
                $this->request_open_ai( $connected_model, $messages );
                break;
            case 'deepseek-chat':
                $this->request_deepseek( $connected_model, $messages );
                break;
            default:
                $this->request_anthropic( $connected_model, $messages );
                break;
        }

        exit;
    }

    /**
     * Prepare messages for request.
     *
     * @param string $user_query user query.
     * @param string $selected_blocks selected blocks context.
     * @param string $page_blocks page blocks context.
     * @param string $page_context page context.
     */
    public function prepare_messages( $user_query, $selected_blocks, $page_blocks, $page_context ) {
        $user_query = '<user_query>' . $user_query . '</user_query>';

        if ( $selected_blocks ) {
            $user_query .= "\n";
            $user_query .= '<selected_blocks_context>' . $selected_blocks . '</selected_blocks_context>';
        }
        if ( $page_blocks ) {
            $user_query .= "\n";
            $user_query .= '<page_blocks_context>' . $page_blocks . '</page_blocks_context>';
        }
        if ( $page_context ) {
            $user_query .= "\n";
            $user_query .= '<page_context>' . $page_context . '</page_context>';
        }

        return [
            [
                'role'    => 'system',
                'content' => Mind_Prompts::get_system_prompt(),
            ],
            [
                'role'    => 'user',
                'content' => $user_query,
            ],
        ];
    }

    /**
     * Convert OpenAI messages format to Anthropic format.
     *
     * @param array $openai_messages Array of messages in OpenAI format.
     * @return array Messages in Anthropic format
     */
    public function convert_to_anthropic_messages( $openai_messages ) {
        $system   = [];
        $messages = [];

        foreach ( $openai_messages as $message ) {
            if ( 'system' === $message['role'] ) {
                $allow_cache = strlen( $message['content'] ) > 2100;

                // Convert system message.
                $system[] = array_merge(
                    array(
                        'type' => 'text',
                        'text' => $message['content'],
                    ),
                    $allow_cache ? array(
                        'cache_control' => [ 'type' => 'ephemeral' ],
                    ) : array()
                );
            } else {
                // Convert user/assistant messages.
                $messages[] = [
                    'role'    => 'assistant' === $message['role'] ? 'assistant' : 'user',
                    'content' => $message['content'],
                ];
            }
        }

        return array(
            'system'   => $system,
            'messages' => $messages,
        );
    }

    /**
     * Request Anthropic API.
     *
     * @param array $model model.
     * @param array $messages messages.
     */
    public function request_anthropic( $model, $messages ) {
        $anthropic_messages = $this->convert_to_anthropic_messages( $messages );
        $anthropic_version  = '2023-06-01';
        $model_name         = $model['name'];

        if ( 'claude-3-5-haiku' === $model['name'] ) {
            $model_name = 'claude-3-5-haiku-20241022';
        } else {
            $model_name = 'claude-3-7-sonnet-20250219';
        }

        $body = [
            'model'      => $model_name,
            'max_tokens' => 8192,
            'system'     => $anthropic_messages['system'],
            'messages'   => $anthropic_messages['messages'],
            'stream'     => true,
        ];

        /* phpcs:disable WordPress.WP.AlternativeFunctions.curl_curl_init, WordPress.WP.AlternativeFunctions.curl_curl_setopt, WordPress.WP.AlternativeFunctions.curl_curl_exec, WordPress.WP.AlternativeFunctions.curl_curl_errno, WordPress.WP.AlternativeFunctions.curl_curl_error, WordPress.WP.AlternativeFunctions.curl_curl_close */

        $ch = curl_init( 'https://api.anthropic.com/v1/messages' );
        curl_setopt( $ch, CURLOPT_POST, 1 );
        curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
        curl_setopt(
            $ch,
            CURLOPT_HTTPHEADER,
            [
                'Content-Type: application/json',
                'x-api-key: ' . $model['key'],
                'anthropic-version: ' . $anthropic_version,
            ]
        );
        curl_setopt( $ch, CURLOPT_POSTFIELDS, wp_json_encode( $body ) );
        curl_setopt(
            $ch,
            CURLOPT_WRITEFUNCTION,
            function ( $curl, $data ) {
                // Response with error message.
                if ( $data && strpos( $data, '{"type":"error","error":{' ) !== false ) {
                    $error_data = json_decode( $data, true );

                    if ( isset( $error_data['error']['message'] ) ) {
                        $this->send_stream_error( 'anthropic_error', $error_data['error']['message'] );
                    }

                    return strlen( $data );
                }

                $this->process_anthropic_stream_chunk( $data );

                return strlen( $data );
            }
        );

        curl_exec( $ch );

        if ( curl_errno( $ch ) ) {
            $this->send_stream_error( 'curl_error', curl_error( $ch ) );
        }

        curl_close( $ch );
    }

    /**
     * Request OpenAI API (for gpt-4o only).
     *
     * @param array $model model.
     * @param array $messages messages.
     */
    public function request_open_ai( $model, $messages ) {
        $body = [
            'model'       => 'gpt-4o',
            'stream'      => true,
            'top_p'       => 0.9,
            'temperature' => 0.7,
            'messages'    => $messages,
        ];

        /* phpcs:disable WordPress.WP.AlternativeFunctions.curl_curl_init, WordPress.WP.AlternativeFunctions.curl_curl_setopt, WordPress.WP.AlternativeFunctions.curl_curl_exec, WordPress.WP.AlternativeFunctions.curl_curl_errno, WordPress.WP.AlternativeFunctions.curl_curl_error, WordPress.WP.AlternativeFunctions.curl_curl_close */

        $ch = curl_init( 'https://api.openai.com/v1/chat/completions' );
        curl_setopt( $ch, CURLOPT_POST, 1 );
        curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
        curl_setopt(
            $ch,
            CURLOPT_HTTPHEADER,
            [
                'Content-Type: application/json',
                'Authorization: Bearer ' . $model['key'],
            ]
        );
        curl_setopt( $ch, CURLOPT_POSTFIELDS, wp_json_encode( $body ) );
        curl_setopt(
            $ch,
            CURLOPT_WRITEFUNCTION,
            function ( $curl, $data ) {
                // Response with error message.
                if ( $data && strpos( $data, "{\n    \"error\": {\n        \"message\":" ) !== false ) {
                    $error_data = json_decode( $data, true );

                    if ( isset( $error_data['error']['message'] ) ) {
                        $this->send_stream_error( 'openai_error', $error_data['error']['message'] );
                    }

                    return strlen( $data );
                }

                $this->process_openai_stream_chunk( $data );

                return strlen( $data );
            }
        );

        curl_exec( $ch );

        if ( curl_errno( $ch ) ) {
            $this->send_stream_error( 'curl_error', curl_error( $ch ) );
        }

        curl_close( $ch );
    }

    /**
     * Request DeepSeek API.
     *
     * @param array $model model.
     * @param array $messages messages.
     */
    public function request_deepseek( $model, $messages ) {
        $body = [
            'model'       => 'deepseek-chat',
            'stream'      => true,
            'messages'    => $messages,
        ];

        /* phpcs:disable WordPress.WP.AlternativeFunctions.curl_curl_init, WordPress.WP.AlternativeFunctions.curl_curl_setopt, WordPress.WP.AlternativeFunctions.curl_curl_exec, WordPress.WP.AlternativeFunctions.curl_curl_errno, WordPress.WP.AlternativeFunctions.curl_curl_error, WordPress.WP.AlternativeFunctions.curl_curl_close */

        $ch = curl_init( 'https://api.deepseek.com/v1/chat/completions' );
        curl_setopt( $ch, CURLOPT_POST, 1 );
        curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
        curl_setopt(
            $ch,
            CURLOPT_HTTPHEADER,
            [
                'Content-Type: application/json',
                'Authorization: Bearer ' . $model['key'],
            ]
        );
        curl_setopt( $ch, CURLOPT_POSTFIELDS, wp_json_encode( $body ) );
        curl_setopt(
            $ch,
            CURLOPT_WRITEFUNCTION,
            function ( $curl, $data ) {
                if ( $data && strpos( $data, "{\"error\":" ) !== false ) {
                    $error_data = json_decode( $data, true );
                    if ( isset( $error_data['error']['message'] ) ) {
                        $this->send_stream_error( 'deepseek_error', $error_data['error']['message'] );
                    }
                    return strlen( $data );
                }
                $this->process_deepseek_stream_chunk( $data );
                return strlen( $data );
            }
        );

        curl_exec( $ch );
        if ( curl_errno( $ch ) ) {
            $this->send_stream_error( 'curl_error', curl_error( $ch ) );
        }
        curl_close( $ch );
    }

    /**
     * Process streaming chunk from OpenAI.
     *
     * @param string $chunk - chunk of data.
     */
    private function process_openai_stream_chunk( $chunk ) {
        $lines = explode( "\n", $chunk );

        foreach ( $lines as $line ) {
            if ( strlen( trim( $line ) ) === 0 ) {
                continue;
            }

            if ( strpos( $line, 'data: ' ) === 0 ) {
                $json_data = trim( substr( $line, 6 ) );

                if ( '[DONE]' === $json_data ) {
                    if ( ! empty( $this->buffer ) ) {
                        $this->send_buffered_chunk();
                    }
                    $this->send_stream_chunk( [ 'done' => true ] );
                    return;
                }

                try {
                    $data = json_decode( $json_data, true );

                    if ( isset( $data['choices'][0]['delta']['content'] ) ) {
                        $content = $data['choices'][0]['delta']['content'];

                        // Send immediately for JSON markers.
                        if ( strpos( $content, '```json' ) !== false ||
                            strpos( $content, '```' ) !== false ) {
                            if ( ! empty( $this->buffer ) ) {
                                $this->send_buffered_chunk();
                            }
                            $this->send_stream_chunk( [ 'content' => $content ] );
                            $this->last_send_time = microtime( true );
                            continue;
                        }

                        $this->buffer        .= $content;
                        $current_time         = microtime( true );
                        $time_since_last_send = $current_time - $this->last_send_time;

                        if ( strlen( $this->buffer ) >= self::BUFFER_THRESHOLD ||
                            $time_since_last_send >= self::MIN_SEND_INTERVAL ||
                            strpos( $this->buffer, "\n" ) !== false ) {
                            $this->send_buffered_chunk();
                        }
                    }
                } catch ( Exception $e ) {
                    $this->send_stream_error( 'json_error', $e->getMessage() );
                }
            }
        }
    }

    /**
     * Process streaming chunk from DeepSeek.
     *
     * @param string $chunk - chunk of data.
     */
    private function process_deepseek_stream_chunk( $chunk ) {
        $lines = explode( "\n", $chunk );

        foreach ( $lines as $line ) {
            if ( strlen( trim( $line ) ) === 0 ) {
                continue;
            }

            if ( strpos( $line, 'data: ' ) === 0 ) {
                $json_data = trim( substr( $line, 6 ) );

                if ( '[DONE]' === $json_data ) {
                    if ( ! empty( $this->buffer ) ) {
                        $this->send_buffered_chunk();
                    }
                    $this->send_stream_chunk( [ 'done' => true ] );
                    return;
                }

                try {
                    $data = json_decode( $json_data, true );

                    if ( isset( $data['choices'][0]['delta']['content'] ) ) {
                        $content = $data['choices'][0]['delta']['content'];

                        // Send immediately for JSON markers.
                        if ( strpos( $content, '```json' ) !== false ||
                            strpos( $content, '```' ) !== false ) {
                            if ( ! empty( $this->buffer ) ) {
                                $this->send_buffered_chunk();
                            }
                            $this->send_stream_chunk( [ 'content' => $content ] );
                            $this->last_send_time = microtime( true );
                            continue;
                        }

                        $this->buffer        .= $content;
                        $current_time         = microtime( true );
                        $time_since_last_send = $current_time - $this->last_send_time;

                        if ( strlen( $this->buffer ) >= self::BUFFER_THRESHOLD ||
                            $time_since_last_send >= self::MIN_SEND_INTERVAL ||
                            strpos( $this->buffer, "\n" ) !== false ) {
                            $this->send_buffered_chunk();
                        }
                    }
                } catch ( Exception $e ) {
                    $this->send_stream_error( 'json_error', $e->getMessage() );
                }
            }
        }
    }

    /**
     * Process streaming chunk from Anthropic.
     *
     * @param string $chunk - chunk of data.
     */
    private function process_anthropic_stream_chunk( $chunk ) {
        $lines = explode( "\n", $chunk );

        foreach ( $lines as $line ) {
            if ( strlen( trim( $line ) ) === 0 ) {
                continue;
            }

            // Remove "data: " prefix if exists.
            if ( strpos( $line, 'data: ' ) === 0 ) {
                $json_data = trim( substr( $line, 6 ) );
            } else {
                $json_data = trim( $line );
            }

            // Skip empty events.
            if ( '' === $json_data ) {
                continue;
            }

            try {
                $data = json_decode( $json_data, true );

                if ( isset( $data['type'] ) ) {
                    if ( 'content_block_delta' === $data['type'] && isset( $data['delta']['text'] ) ) {
                        $content = $data['delta']['text'];

                        // Send immediately for JSON markers.
                        if ( strpos( $content, '```json' ) !== false ||
                            strpos( $content, '```' ) !== false ) {
                            if ( ! empty( $this->buffer ) ) {
                                $this->send_buffered_chunk();
                            }
                            $this->send_stream_chunk( [ 'content' => $content ] );
                            $this->last_send_time = microtime( true );
                        } else {
                            $this->buffer .= $content;
                            $current_time  = microtime( true );
                            $time_since_last_send = $current_time - $this->last_send_time;

                            if ( strlen( $this->buffer ) >= self::BUFFER_THRESHOLD ||
                                $time_since_last_send >= self::MIN_SEND_INTERVAL ||
                                strpos( $this->buffer, "\n" ) !== false ) {
                                $this->send_buffered_chunk();
                            }
                        }
                    } elseif ( 'message_stop' === $data['type'] ) {
                        if ( ! empty( $this->buffer ) ) {
                            $this->send_buffered_chunk();
                        }
                        $this->send_stream_chunk( [ 'done' => true ] );
                        return;
                    }
                }
            } catch ( Exception $e ) {
                $this->send_stream_error( 'json_error', $e->getMessage() );
            }
        }
    }

    /**
     * Send buffered chunk.
     */
    private function send_buffered_chunk() {
        if ( empty( $this->buffer ) ) {
            return;
        }

        $this->send_stream_chunk(
            [
                'content' => $this->buffer,
            ]
        );

        $this->buffer         = '';
        $this->last_send_time = microtime( true );
    }

    /**
     * Send stream chunk.
     *
     * @param array $data - data to send.
     */
    private function send_stream_chunk( $data ) {
        echo 'data: ' . wp_json_encode( $data ) . "\n\n";

        if ( ob_get_level() > 0 ) {
            ob_flush();
        }

        flush();
    }

    /**
     * Send stream error.
     *
     * @param string $code - error code.
     * @param string $message - error message.
     */
    private function send_stream_error( $code, $message ) {
        $this->send_stream_chunk(
            [
                'error'   => true,
                'code'    => $code,
                'message' => $message,
            ]
        );
    }
}
