<?php

namespace Limb_Chatbot\Includes\AI_Providers\Claude\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 Claude's Messages API.
 *
 * Handles reconstruction of incomplete JSON lines, identification of request metadata,
 * and manages stream state including streamed content output.
 *
 * Claude streaming uses Server-Sent Events (SSE) with the following event types:
 * - message_start: Initial message metadata
 * - content_block_start: Start of content block
 * - content_block_delta: Text chunk (type: text_delta) or tool input (type: input_json_delta)
 * - content_block_stop: End of content block
 * - message_delta: Stop reason and usage
 * - message_stop: Stream complete
 *
 * @package Limb_Chatbot\Includes\AI_Providers\Claude\Endpoints\Chat_Completion
 * @since 1.0.9
 */
class Stream_Parser {

	/**
	 * Collected JSON objects that were parsed completely from stream.
	 *
	 * @var array
	 * @since 1.0.9
	 */
	public array $complete_jsons = array();

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

	/**
	 * Buffer for incomplete (half) JSON lines across multiple stream chunks.
	 *
	 * @var string
	 * @since 1.0.9
	 */
	public string $half_lines = '';

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

	/**
	 * Current event type being processed.
	 *
	 * @var string|null
	 * @since 1.0.9
	 */
	public ?string $current_event = null;

	/**
	 * Parses an individual stream chunk.
	 *
	 * Handles:
	 * - SSE event type detection
	 * - JSON line collection (including partial JSON across chunks)
	 * - Extraction of request-id for debugging
	 * - Stream content printing
	 *
	 * @param  string  $chunk  A single raw chunk received from the streaming API.
	 *
	 * @return void
	 * @since 1.0.9
	 */
	public function parser( $chunk ): void {

		$lines = explode( "\n", $chunk );

		foreach ( $lines as $line ) {
			if ( empty( trim( $line ) ) ) {
				continue;
			}

			$line = trim( $line );

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

			// Handle SSE event type
			if ( str_starts_with( $line, 'event: ' ) ) {
				$this->current_event = substr( $line, 7 );
				continue;
			}

			// Handle data lines
			if ( str_starts_with( $line, 'data: ' ) ) {
				$line = substr( $line, 6 );
			}

			// Skip empty data or [DONE] marker
			if ( empty( trim( $line ) ) || $line === '[DONE]' ) {
				continue;
			}

			// Append to buffer (handles both raw JSON errors and SSE data)
			$this->half_lines .= $line;

			// Try to decode buffer if it looks like JSON
			if ( Helper::is_probable_json( $this->half_lines ) ) {
				$decoded = json_decode( $this->half_lines );

				if ( $decoded !== null ) {
					// Check for error response (Claude error format)
					if ( isset( $decoded->type ) && $decoded->type === 'error' ) {
						$this->error = array(
							'type'    => $decoded->error->type ?? 'unknown',
							'message' => $decoded->error->message ?? 'Unknown error',
						);
						$this->half_lines = '';
						return;
					}

					// Check for error property directly
					if ( isset( $decoded->error ) ) {
						$this->error = (array) $decoded->error;
						$this->half_lines = '';
						return;
					}

					$this->complete_jsons[] = $decoded;
					$this->print( $decoded );
					$this->half_lines = '';
				}
			}
		}
	}

	/**
	 * Prints streamed content to output.
	 *
	 * For Claude, we print text_delta content as it arrives.
	 *
	 * @param  object  $decoded  The decoded JSON object from the stream.
	 *
	 * @return void
	 * @since 1.0.9
	 */
	public function print( $decoded ): void {
		// Check if this is a content_block_delta with text
		if ( isset( $decoded->type ) && $decoded->type === 'content_block_delta' ) {
			$delta = $decoded->delta ?? null;
			if ( $delta && isset( $delta->type ) && $delta->type === 'text_delta' ) {
				$text = $delta->text ?? '';
				if ( ! empty( $text ) ) {
					if ( ! $this->data_chunk_started ) {
						$this->data_chunk_started = true;
					}
					// Send the chunk via the central service (consistent with OpenAI/DeepSeek)
					Stream_Event_Service::text( $text );
				}
			}
		}
	}

	/**
	 * Gets the accumulated text content from all stream chunks.
	 *
	 * @return string The accumulated text content.
	 * @since 1.0.9
	 */
	public function get_text_content(): string {
		$content = '';

		foreach ( $this->complete_jsons as $chunk ) {
			if ( isset( $chunk->type ) && $chunk->type === 'content_block_delta' ) {
				$delta = $chunk->delta ?? null;
				if ( $delta && isset( $delta->type ) && $delta->type === 'text_delta' ) {
					$content .= $delta->text ?? '';
				}
			}
		}

		return $content;
	}

	/**
	 * Gets the usage information from the stream.
	 *
	 * @return array|null Usage array with input_tokens and output_tokens.
	 * @since 1.0.9
	 */
	public function get_usage(): ?array {
		foreach ( $this->complete_jsons as $chunk ) {
			if ( isset( $chunk->type ) && $chunk->type === 'message_delta' && isset( $chunk->usage ) ) {
				return array(
					'input_tokens'  => $chunk->usage->input_tokens ?? 0,
					'output_tokens' => $chunk->usage->output_tokens ?? 0,
				);
			}
			if ( isset( $chunk->type ) && $chunk->type === 'message_start' && isset( $chunk->message->usage ) ) {
				return array(
					'input_tokens'  => $chunk->message->usage->input_tokens ?? 0,
					'output_tokens' => $chunk->message->usage->output_tokens ?? 0,
				);
			}
		}

		return null;
	}
}

