<?php

namespace Limb_Chatbot\Includes\AI_Providers\Open_Router\Endpoints\Chat_Completion;

use Limb_Chatbot\Includes\Services\Helper;
use Limb_Chatbot\Includes\Services\Stream_Event_Service;

/**
 * Class Stream_Parser
 *
 * Parses streamed chunks received from OpenRouter's Chat Completion API.
 *
 * @since 1.0.14
 */
class Stream_Parser {

	/**
	 * Collected JSON strings that were parsed completely from stream.
	 *
	 * @var array
	 * @since 1.0.14
	 */
	public array $complete_jsons = [];

	/**
	 * Flag to indicate if the first data chunk has started.
	 *
	 * @var bool
	 * @since 1.0.14
	 */
	public bool $data_chunk_started = false;

	/**
	 * Buffer for incomplete data across multiple stream chunks.
	 *
	 * @var string
	 * @since 1.0.14
	 */
	public string $buffer = '';

	/**
	 * Parsed error information from the stream, if any.
	 *
	 * @var array|null
	 * @since 1.0.14
	 */
	public ?array $error = null;

	/**
	 * Parses an individual stream chunk.
	 *
	 * Handles SSE format:
	 * - Lines starting with ':' are comments (keep-alive)
	 * - Lines starting with 'data: ' contain JSON data
	 * - 'data: [DONE]' signals end of stream
	 *
	 * @param  string  $chunk  A single raw chunk received from the streaming API.
	 *
	 * @return void
	 * @since 1.0.14
	 */
	public function parser( $chunk ): void {
		// Append chunk to buffer
		$this->buffer .= $chunk;

		// Process complete lines from buffer
		while ( ( $line_end = strpos( $this->buffer, "\n" ) ) !== false ) {
			$line         = substr( $this->buffer, 0, $line_end );
			$this->buffer = substr( $this->buffer, $line_end + 1 );

			$this->process_line( trim( $line ) );
		}

		// Try to process remaining buffer if it looks like complete JSON
		// This handles cases where the last chunk doesn't end with newline
		$this->try_process_remaining_buffer();
	}

	/**
	 * Processes a single SSE line.
	 *
	 * @param string $line The line to process (already trimmed).
	 * @return void
	 * @since 1.0.14
	 */
	protected function process_line( string $line ): void {
		// Skip empty lines
		if ( empty( $line ) ) {
			return;
		}

		// Skip SSE comments (lines starting with ':')
		// These include ': OPENROUTER PROCESSING' keep-alive messages
		if ( str_starts_with( $line, ':' ) ) {
			return;
		}

		// Handle request ID logging
		if ( str_contains( $line, 'x-request-id:' ) ) {
			$request_id = trim( explode( ':', $line, 2 )[1] ?? '' );
			if ( ! empty( $request_id ) ) {
				Helper::log( 'x-request-id: ' . $request_id );
			}
			return;
		}

		// Skip event type lines
		if ( str_starts_with( $line, 'event:' ) ) {
			return;
		}

		// Handle [DONE] marker
		if ( $line === 'data: [DONE]' ) {
			return;
		}

		// Extract data from 'data: ' prefix
		if ( str_starts_with( $line, 'data: ' ) ) {
			$json_str = substr( $line, 6 );
			$this->try_parse_json( $json_str );
			return;
		}

		// Handle raw JSON error responses (no 'data:' prefix)
		// OpenRouter may return errors as raw JSON when the request fails before streaming starts
		if ( str_starts_with( $line, '{' ) ) {
			$this->try_parse_json( $line );
		}
	}

	/**
	 * Try to process any remaining data in buffer.
	 *
	 * @return void
	 * @since 1.0.14
	 */
	protected function try_process_remaining_buffer(): void {
		$trimmed = trim( $this->buffer );

		if ( empty( $trimmed ) ) {
			return;
		}

		// If buffer starts with 'data: ', extract and try to parse
		if ( str_starts_with( $trimmed, 'data: ' ) ) {
			$json_str = substr( $trimmed, 6 );
			if ( $this->try_parse_json( $json_str ) ) {
				$this->buffer = '';
			}
			return;
		}

		// Handle raw JSON error responses (no 'data:' prefix)
		if ( str_starts_with( $trimmed, '{' ) ) {
			if ( $this->try_parse_json( $trimmed ) ) {
				$this->buffer = '';
			}
		}
	}

	/**
	 * Attempts to parse a JSON string.
	 *
	 * @param string $json_str JSON string to parse.
	 * @return bool True if successfully parsed, false otherwise.
	 * @since 1.0.14
	 */
	protected function try_parse_json( string $json_str ): bool {
		if ( empty( $json_str ) ) {
			return false;
		}

		$decoded = json_decode( $json_str );

		if ( json_last_error() !== JSON_ERROR_NONE ) {
			// Not valid JSON - might be incomplete
			return false;
		}

		// Check for error response
		if ( isset( $decoded->error ) ) {
			$this->error = (array) $decoded->error;
			return true;
		}

		$this->complete_jsons[] = $decoded;
		$this->print( $decoded );
		return true;
	}

	/**
	 * Emits a structured SSE delta event to the frontend.
	 *
	 * Handles different provider formats:
	 * - Standard: delta.content
	 * - Reasoning models (Grok, DeepSeek): delta.reasoning for thinking tokens
	 *
	 * @param object $parsed_data
	 * @return void
	 * @since 1.0.14
	 */
	public function print( $parsed_data ): void {
		$delta = $parsed_data->choices[0]->delta ?? null;

		if ( ! $delta ) {
			return;
		}

		// Get content - standard field for actual response
		$content = $delta->content ?? '';

		if ( $content === '' ) {
			return;
		}

		Stream_Event_Service::text( $content );
	}
}
