<?php
/**
 * Ollama Cloud Chat Model.
 *
 * Extends OpenAI Chat Model for Ollama Cloud-specific customizations.
 * Forces non-streaming completions whenever tools are attached, because
 * Ollama's /v1 endpoint does not reliably stream tool_call deltas.
 *
 * @see       https://github.com/ollama/ollama/issues/9084
 *
 * @package   Agenistrator
 * @since     1.0.1
 * @author    Alex Ruban <alex@maximum.guru>
 * @author    Maximum Software Inc <contact@maximum.software>
 * @license   GPL-3.0-only
 * @link      https://agenistrator.com/
 *
 * SPDX-FileCopyrightText: 2026 Alex Ruban <alex@maximum.guru>
 * SPDX-FileCopyrightText: 2026 Maximum Software Inc <contact@maximum.software>
 * SPDX-License-Identifier: GPL-3.0-only
 */

namespace Agenistrator;

// Direct access prevention
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * LLM_Chat_Ollama_Cloud class.
 */
class LLM_Chat_Ollama_Cloud extends LLM_Chat_OpenAI {
	/**
	 * Create a new context object specific to Ollama Cloud
	 *
	 * @param      string $system_message Optional custom system message
	 * @return     Context_Ollama_Cloud Ollama Cloud-specific context object
	 * @since     1.0.1
	 */
	public function create_context( $system_message = null ) {
		return new Context_Ollama_Cloud( $system_message );
	}

	/**
	 * Validate that the context is the correct type for this provider
	 *
	 * @param      Context $context Context object to validate
	 * @throws     \Exception If context is not the correct type
	 * @since     1.0.1
	 */
	protected function validate_context( $context ) {
		if ( ! ( $context instanceof Context_Ollama_Cloud ) ) {
			// phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception messages are not HTML output
			throw new \Exception(
				String_Utils::replace_tags(
					__( 'Invalid context type: expected {expected}, got {actual}', 'agenistrator' ),
					array(
						'expected' => 'Context_Ollama_Cloud',
						'actual'   => get_class( $context ),
					)
				)
			);
			// phpcs:enable WordPress.Security.EscapeOutput.ExceptionNotEscaped
		}
	}

	/**
	 * Generate a streaming chat completion
	 *
	 * When the context carries tools, force a synchronous completion so that
	 * tool_calls are parsed reliably. Ollama's OpenAI-compatible /v1 endpoint
	 * garbles streamed tool_call deltas (they arrive as plain text fragments
	 * inside `content` instead of structured `tool_calls` chunks), so the
	 * synchronous result is re-emitted through the streaming-callback contract
	 * used by the agent loop. Streaming tool_calls are only reliable on the
	 * native /api/chat endpoint, which this provider does not use.
	 *
	 * @see        https://github.com/ollama/ollama/issues/9084 Upstream tracking issue for /v1 tool+stream breakage
	 * @see        https://docs.ollama.com/api/openai-compatibility OpenAI compatibility surface (no streamed tool_calls)
	 * @see        https://www.ollama.ai/blog/streaming-tool Native /api/chat streaming tool calls (not /v1)
	 *
	 * @param      Context $context Context object containing all request information
	 * @param      callable $stream_callback Callback function for streaming. Should return bool: true to continue, false to stop.
	 * @return     bool Success flag (true if streaming completed successfully)
	 * @since     1.0.1
	 */
	public function generate_streaming_completion( $context, $stream_callback = null ) {
		if ( empty( $context->get_available_tools() ) ) {
			return parent::generate_streaming_completion( $context, $stream_callback );
		}

		$this->validate_context( $context );

		try {
			$response = $this->generate_sync_completion( $context );
		} catch ( \Throwable $e ) {
			call_user_func(
				$stream_callback,
				array(
					'type'    => 'error',
					'message' => $e->getMessage(),
				)
			);
			return false;
		}

		// Emit accumulated content as a single content event - the synchronous
		// path has no token-by-token stream by design.
		if ( ! empty( $response['content'] ) ) {
			call_user_func(
				$stream_callback,
				array(
					'type'    => 'content',
					'content' => $response['content'],
				)
			);
		}

		// Send done event before tool calls, mirroring LLM_Chat_OpenAI's order.
		call_user_func( $stream_callback, array( 'type' => 'done' ) );

		if ( ! empty( $response['tool_calls'] ) ) {
			foreach ( $response['tool_calls'] as $tool_call ) {
				if ( call_user_func(
					$stream_callback,
					array(
						'type'      => 'tool_call_complete',
						'tool_call' => $tool_call,
					)
				) === false ) {
					break;
				}
			}
		}

		return true;
	}
}
