<?php
namespace boru\ai;

use boru\ai\Asset\Asset;
use boru\ai\Asset\AssetBindingPolicy;
use boru\ai\Asset\AssetProviderRegistry;
use boru\ai\Asset\AssetResolver;
use boru\ai\Asset\AssetStoreInterface;
use boru\ai\Asset\Attachment;
use boru\ai\Asset\MimeTypeResolver;
use boru\ai\Compat\LegacyTool\LegacyToolAdapter;
use boru\ai\Config\Config;
use boru\ai\Config\ConfigResolver;
use boru\ai\Conversation\Conversation;
use boru\ai\Conversation\ConversationForkService;
use boru\ai\Conversation\ConversationNote;
use boru\ai\Conversation\ConversationReference;
use boru\ai\Conversation\Tool\AppendConversationNoteTool;
use boru\ai\Conversation\Tool\ReplaceConversationNotesTool;
use boru\ai\Conversation\Tool\UpdateConversationSummaryTool;
use boru\ai\Conversation\Tool\UpdateConversationTitleTool;
use boru\ai\Embedding\EmbeddingRequest;
use boru\ai\Embedding\EmbeddingService;
use boru\ai\Embedding\EmbeddingStoreInterface;
use boru\ai\Mcp\McpClientFactoryInterface;
use boru\ai\Mcp\McpServerRegistry;
use boru\ai\Mcp\McpToolDiscoveryInterface;
use boru\ai\Mcp\McpToolRegistrar;
use boru\ai\Mcp\McpToolInvokerInterface;
use boru\ai\Persistence\NullPersistenceProvider;
use boru\ai\Persistence\PersistenceProviderInterface;
use boru\ai\Provider\ProviderRegistry;
use boru\ai\Runtime\AbstractTool;
use boru\ai\Runtime\Runtime;
use boru\ai\Runtime\RuntimeContext;
use boru\ai\Runtime\ToolContext;
use boru\ai\Runtime\ToolDefinition;
use boru\ai\Runtime\ToolExecutor;
use boru\ai\Runtime\ToolExecutorInterface;
use boru\ai\Runtime\ToolInterface;
use boru\ai\Runtime\ToolPromptAdditionInterface;
use boru\ai\Runtime\ToolRegistry;
use boru\ai\Schema\Request;
use boru\ai\Stream\StreamEvent;
use boru\ai\Stream\StreamEventDispatcher;
use boru\ai\Stream\StreamEventListenerInterface;

class AI
{
    /** @var RuntimeContext */
    protected static $runtimeContext;

    /** @var Config|null */
    protected static $config;

    /** @var Request */
    protected $request;

    /** @var callable|null */
    protected $eventCallback;

    /** @var StreamEventDispatcher|null */
    protected $eventDispatcher;

    /** @var string|null */
    protected $providerName;

    /** @var ConversationReference|null */
    protected $conversationReference;

    public function __construct(Request $request = null)
    {
        $this->request = $request ? $request : new Request();
    }

    public static function runtimeContext(RuntimeContext $runtimeContext = null)
    {
        if ($runtimeContext !== null) {
            static::$runtimeContext = $runtimeContext;
        }
        if (!static::$runtimeContext) {
            static::$runtimeContext = new RuntimeContext();
        }
        return static::$runtimeContext;
    }

    public static function config(Config $config = null)
    {
        if ($config !== null) {
            static::$config = $config;
        }
        return static::$config;
    }

    public static function registry(ProviderRegistry $registry = null)
    {
        $context = static::runtimeContext();
        if ($registry !== null) {
            $context->providerRegistry($registry);
        }
        return $context->providerRegistry();
    }

    public static function persistence(PersistenceProviderInterface $provider = null)
    {
        $context = static::runtimeContext();
        if ($provider !== null) {
            $context->persistenceProvider($provider);
        }
        return $context->persistenceProvider();
    }

    public static function assetStore(AssetStoreInterface $assetStore = null)
    {
        $context = static::runtimeContext();
        if ($assetStore !== null) {
            $context->assetStore($assetStore);
        }
        return $context->assetStore();
    }

    public static function assetProviders(AssetProviderRegistry $assetProviderRegistry = null)
    {
        $context = static::runtimeContext();
        if ($assetProviderRegistry !== null) {
            $context->assetProviderRegistry($assetProviderRegistry);
        }
        return $context->assetProviderRegistry();
    }

    public static function embeddingStore(EmbeddingStoreInterface $embeddingStore = null)
    {
        $context = static::runtimeContext();
        if ($embeddingStore !== null) {
            $context->embeddingStore($embeddingStore);
        }
        return $context->embeddingStore();
    }

    public static function toolRegistry(ToolRegistry $toolRegistry = null)
    {
        $context = static::runtimeContext();
        if ($toolRegistry !== null) {
            $context->toolRegistry($toolRegistry);
        }
        return $context->toolRegistry();
    }

    public static function toolContext(ToolContext $toolContext = null)
    {
        $context = static::runtimeContext();
        if ($toolContext !== null) {
            $context->toolContext($toolContext);
        }
        return $context->toolContext();
    }

    public static function toolExecutor(ToolExecutorInterface $toolExecutor = null)
    {
        $context = static::runtimeContext();
        if ($toolExecutor !== null) {
            $context->toolExecutor($toolExecutor);
        }
        return $context->toolExecutor();
    }

    public static function mcpServers(McpServerRegistry $mcpServerRegistry = null)
    {
        $context = static::runtimeContext();
        if ($mcpServerRegistry !== null) {
            $context->mcpServerRegistry($mcpServerRegistry);
        }
        return $context->mcpServerRegistry();
    }

    public static function mcpClientFactory(McpClientFactoryInterface $mcpClientFactory = null)
    {
        $context = static::runtimeContext();
        if ($mcpClientFactory !== null) {
            $context->mcpClientFactory($mcpClientFactory);
        }
        return $context->mcpClientFactory();
    }

    public static function mcpToolInvoker(McpToolInvokerInterface $mcpToolInvoker = null)
    {
        $context = static::runtimeContext();
        if ($mcpToolInvoker !== null) {
            $context->mcpToolInvoker($mcpToolInvoker);
        }
        return $context->mcpToolInvoker();
    }

    public static function mcpToolDiscovery(McpToolDiscoveryInterface $mcpToolDiscovery = null)
    {
        $context = static::runtimeContext();
        if ($mcpToolDiscovery !== null) {
            $context->mcpToolDiscovery($mcpToolDiscovery);
        }
        return $context->mcpToolDiscovery();
    }

    public static function assetFromFile($path, $kind = 'file')
    {
        if (!is_string($path) || trim($path) === '') {
            throw new \InvalidArgumentException('Asset file path is required');
        }
        if (!file_exists($path)) {
            throw new \InvalidArgumentException('Asset file not found: ' . $path);
        }

        $realPath = realpath($path);
        if ($realPath !== false) {
            $path = $realPath;
        }

        $size = filesize($path);
        if ($size === false) {
            throw new \RuntimeException('Unable to determine asset file size: ' . $path);
        }

        $sha256 = hash_file('sha256', $path);
        if (!is_string($sha256) || $sha256 === '') {
            throw new \RuntimeException('Unable to calculate asset sha256: ' . $path);
        }

        $store = static::assetStore();
        $existing = $store->findAssetBySha256($sha256);
        if ($existing instanceof Asset) {
            if (!$existing->path()) {
                $existing->path($path);
            }
            if (!$existing->filename()) {
                $existing->filename(basename($path));
            }
            if (!$existing->mimeType()) {
                $existing->mimeType(static::detectMimeType($path));
            }
            if (!$existing->size()) {
                $existing->size($size);
            }
            return $existing;
        }

        $asset = new Asset();
        $asset->kind($kind);
        $asset->path($path);
        $asset->filename(basename($path));
        $asset->mimeType(static::detectMimeType($path));
        $asset->size($size);
        $asset->sha256($sha256);

        return $store->saveAsset($asset);
    }

    protected static function detectMimeType($path)
    {
        return MimeTypeResolver::detect($path);
    }

    /**
     * Get an AI instance for a specific provider. This is a shortcut for creating a new instance and setting the provider.
     * @param mixed $name
     * @return AI
     */
    public static function provider($name)
    {
        $instance = new static();
        $instance->providerName($name);
        return $instance;
    }

    public function providerName($name = null)
    {
        if ($name !== null) {
            $this->providerName = (string)$name;
            $this->request->provider($this->providerName);
            return $this;
        }
        return $this->providerName;
    }

    public function conversationReference(ConversationReference $reference = null)
    {
        if ($reference !== null) {
            $this->conversationReference = $reference;
            return $this;
        }
        return $this->conversationReference;
    }

    public function reference($externalReference, $userId = null)
    {
        $reference = $this->conversationReference ? $this->conversationReference : new ConversationReference();
        $reference->externalReference($externalReference);
        if ($userId !== null) {
            $reference->userId($userId);
        }
        $this->conversationReference($reference);
        return $this;
    }

    public function loadConversation(ConversationReference $reference = null)
    {
        $reference = $reference ? $reference : $this->conversationReference();
        if (!$reference instanceof ConversationReference) {
            return false;
        }
        return static::persistence()->loadConversation($reference);
    }

    public static function forkConversation($sourceReference, $targetReference, array $options = array())
    {
        return (new ConversationForkService())->fork(
            static::persistence(),
            static::conversationReferenceFromValue($sourceReference),
            static::conversationReferenceFromValue($targetReference),
            $options
        );
    }

    public static function cloneConversation($sourceReference, $targetReference, array $options = array())
    {
        return (new ConversationForkService())->cloneConversation(
            static::persistence(),
            static::conversationReferenceFromValue($sourceReference),
            static::conversationReferenceFromValue($targetReference),
            $options
        );
    }

    public function forkTo($targetReference, array $options = array())
    {
        $sourceReference = $this->conversationReference();
        if (!$sourceReference instanceof ConversationReference) {
            throw new \RuntimeException('Conversation reference is required to fork from this AI instance');
        }

        return static::forkConversation($sourceReference, $targetReference, $options);
    }

    public function cloneTo($targetReference, array $options = array())
    {
        $sourceReference = $this->conversationReference();
        if (!$sourceReference instanceof ConversationReference) {
            throw new \RuntimeException('Conversation reference is required to clone from this AI instance');
        }

        return static::cloneConversation($sourceReference, $targetReference, $options);
    }

    protected static function conversationReferenceFromValue($value)
    {
        if ($value instanceof ConversationReference) {
            return $value;
        }

        $reference = new ConversationReference();
        $reference->externalReference((string)$value);
        return $reference;
    }

    public function conversationMemory(ConversationReference $reference = null)
    {
        $conversation = $this->loadConversation($reference);
        if (!$conversation instanceof Conversation) {
            return false;
        }
        return $conversation->memory();
    }

    public function updateConversationTitle($title, ConversationReference $reference = null)
    {
        $conversation = $this->loadOrCreateConversationForMemory($reference);
        $conversation->memory()->title($title);
        static::persistence()->saveConversation($conversation);
        return $conversation->memory();
    }

    public function updateConversationSummary($summary, ConversationReference $reference = null)
    {
        $conversation = $this->loadOrCreateConversationForMemory($reference);
        $conversation->memory()->summary($summary);
        static::persistence()->saveConversation($conversation);
        return $conversation->memory();
    }

    public function addConversationNote($text, $kind = 'note', ConversationReference $reference = null, array $metadata = array())
    {
        $conversation = $this->loadOrCreateConversationForMemory($reference);
        $note = new ConversationNote();
        $note->kind($kind);
        $note->text($text);
        $note->metadata($metadata);
        $conversation->memory()->addNote($note);
        static::persistence()->saveConversation($conversation);
        return $note;
    }

    public function replaceConversationNotes(array $notes, ConversationReference $reference = null)
    {
        $conversation = $this->loadOrCreateConversationForMemory($reference);
        $conversation->memory()->clearNotes();
        foreach ($notes as $note) {
            if ($note instanceof ConversationNote) {
                $conversation->memory()->addNote($note);
                continue;
            }

            if (is_array($note) && isset($note['text'])) {
                $memoryNote = new ConversationNote();
                $memoryNote->id(isset($note['id']) ? $note['id'] : null);
                $memoryNote->kind(isset($note['kind']) ? $note['kind'] : 'note');
                $memoryNote->text($note['text']);
                if (isset($note['confidence'])) {
                    $memoryNote->confidence($note['confidence']);
                }
                if (isset($note['source_turn_number'])) {
                    $memoryNote->sourceTurnNumber($note['source_turn_number']);
                }
                if (isset($note['metadata']) && is_array($note['metadata'])) {
                    $memoryNote->metadata($note['metadata']);
                }
                $conversation->memory()->addNote($memoryNote);
            }
        }
        static::persistence()->saveConversation($conversation);
        return $conversation->memory();
    }

    public function request(Request $request = null)
    {
        if ($request !== null) {
            $this->request = $request;
            return $this;
        }
        return $this->request;
    }

    public function model($model)
    {
        $this->request->model($model);
        return $this;
    }

    public function option($key, $value = null)
    {
        if (func_num_args() > 1) {
            $this->request->option($key, $value);
            return $this;
        }
        return $this->request->option($key);
    }

    public function options($options = null)
    {
        if ($options !== null) {
            $this->request->options($options);
            return $this;
        }
        return $this->request->options();
    }

    public function reasoning($effort = null, array $settings = array())
    {
        if (func_num_args() === 0) {
            return $this->request->option('reasoning');
        }

        $settings['effort'] = (string)$effort;
        $this->request->option('reasoning', $settings);
        return $this;
    }

    public function instructions($instructions)
    {
        $this->request->instructions($instructions);
        return $this;
    }

    public function addMessage($message, $role = 'user')
    {
        $this->request->addMessage($message, $role);
        return $this;
    }

    public function addTool($tool)
    {
        if ($tool instanceof ToolDefinition || $tool instanceof ToolInterface) {
            static::toolRegistry()->register($tool);

            if ($tool instanceof ToolDefinition) {
                $this->request->addTool($tool->toArray());
            } elseif ($tool instanceof AbstractTool) {
                $this->request->addTool($tool->toToolDefinition()->toArray());
            } else {
                $this->request->addTool(array(
                    'name' => $tool->name(),
                    'description' => $tool->description(),
                    'schema' => $tool->schema(),
                    'metadata' => array(
                        'class' => get_class($tool),
                    ),
                ));
            }
            return $this;
        }

        $this->request->addTool($tool);
        return $this;
    }

    public function addLegacyTool($legacyClass)
    {
        return $this->addTool(LegacyToolAdapter::fromClass($legacyClass));
    }

    public function addMcpTool($serverName, array $tool)
    {
        $definition = $this->mcpToolRegistrar()->register($serverName, $tool);
        $this->request->addTool($definition->toArray());

        return $this;
    }

    public function addMcpTools($serverName, array $tools)
    {
        $definitions = $this->mcpToolRegistrar()->registerMany($serverName, $tools);
        foreach ($definitions as $definition) {
            $this->request->addTool($definition->toArray());
        }

        return $this;
    }

    public function discoverMcpTools($serverName)
    {
        $serverConfig = static::mcpServers()->get($serverName);
        return static::mcpToolDiscovery()->discover($serverConfig);
    }

    public function addDiscoveredMcpTools($serverName)
    {
        return $this->addMcpTools($serverName, $this->discoverMcpTools($serverName));
    }

    public function tryAddDiscoveredMcpTools($serverName, $errorHandler = null)
    {
        if ($errorHandler !== null && !is_callable($errorHandler)) {
            throw new \InvalidArgumentException('MCP discovery error handler must be callable');
        }

        try {
            return $this->addDiscoveredMcpTools($serverName);
        } catch (\Exception $e) {
            $error = array(
                'server' => $serverName,
                'message' => $e->getMessage(),
                'class' => get_class($e),
                'code' => $e->getCode(),
            );
            $this->recordMcpDiscoveryError($error);

            if (is_callable($errorHandler)) {
                call_user_func($errorHandler, $error, $e, $this);
            }

            return $this;
        }
    }

    public function defineTool($name, $description, array $schema, $callback)
    {
        $definition = new ToolDefinition();
        $definition->name($name)
            ->description($description)
            ->schema($schema)
            ->callback($callback);

        return $this->addTool($definition);
    }

    public function setToolContextValue($key, $value)
    {
        $toolContext = static::toolContext();
        $toolContext->set($key, $value);

        $toolExecutor = static::toolExecutor();
        if ($toolExecutor instanceof ToolExecutor) {
            $toolExecutor->contextValues($toolContext->all());
        }

        return $this;
    }

    public function attachAsset(Asset $asset, $purpose = null, $kind = 'file')
    {
        $providerName = $this->request->provider() ? $this->request->provider() : $this->providerName();
        if (!$providerName) {
            throw new \RuntimeException('Cannot attach asset before resolving a provider');
        }

        $assetProviders = static::assetProviders();
        if (!$assetProviders->has($providerName)) {
            throw new \RuntimeException('No asset provider registered for provider: ' . $providerName);
        }

        $assetProvider = $assetProviders->get($providerName);
        $policy = AssetBindingPolicy::fromConfig(static::config(), $providerName);
        $resolver = new AssetResolver(static::assetStore(), $policy);
        $binding = $resolver->resolveBinding($assetProvider, $asset, $purpose);

        $attachment = new Attachment();
        $attachment->assetId($asset->id());
        $attachment->kind($kind);

        $this->request->addAttachment($attachment);
        $this->request->addAssetBinding($binding);

        return $this;
    }

    public function embed($input = null, array $options = array())
    {
        $request = $input instanceof EmbeddingRequest ? $input : $this->buildEmbeddingRequest($input, $options);
        return $this->embeddingService()->embed($request);
    }

    public function embeddings(array $inputs, array $options = array())
    {
        return $this->embed($this->buildEmbeddingRequest($inputs, $options, true));
    }

    public function eventDispatcher(StreamEventDispatcher $dispatcher = null)
    {
        if ($dispatcher !== null) {
            $this->eventDispatcher = $dispatcher;
            return $this;
        }
        return $this->eventDispatcher;
    }

    public function addEventListener(StreamEventListenerInterface $listener)
    {
        $this->ensureEventDispatcher()->addListener($listener);
        return $this;
    }

    public function onEvent($callback)
    {
        if (!is_callable($callback)) {
            throw new \InvalidArgumentException('Event callback must be callable');
        }
        $this->eventCallback = $callback;
        return $this;
    }

    public function onStreamDelta($callback)
    {
        return $this->onStreamEvent(StreamEvent::TEXT_DELTA, function (StreamEvent $event) use ($callback) {
            call_user_func($callback, $event->delta(), $event);
        });
    }

    public function onStreamComplete($callback)
    {
        return $this->onStreamEvent(StreamEvent::TEXT_COMPLETED, function (StreamEvent $event) use ($callback) {
            call_user_func($callback, $event->text(), $event);
        });
    }

    public function onToolStart($callback)
    {
        return $this->onStreamEvent(StreamEvent::TOOL_EXECUTION_STARTED, function (StreamEvent $event) use ($callback) {
            call_user_func(
                $callback,
                $event->toolCallId(),
                $event->toolName(),
                $event->data('arguments'),
                $event
            );
        });
    }

    public function onToolComplete($callback)
    {
        return $this->onStreamEvent(StreamEvent::TOOL_EXECUTION_COMPLETED, function (StreamEvent $event) use ($callback) {
            call_user_func(
                $callback,
                $event->toolCallId(),
                $event->toolName(),
                $event->data('output'),
                $event
            );
        });
    }

    public function onToolFailed($callback)
    {
        return $this->onStreamEvent(StreamEvent::TOOL_EXECUTION_FAILED, function (StreamEvent $event) use ($callback) {
            call_user_func(
                $callback,
                $event->toolCallId(),
                $event->toolName(),
                $event->data('output'),
                $event
            );
        });
    }

    public function onTurnComplete($callback)
    {
        return $this->onStreamEvent(StreamEvent::TURN_COMPLETED, function (StreamEvent $event) use ($callback) {
            call_user_func(
                $callback,
                $event->data('conversation_id'),
                $event->data('turn_number'),
                $event
            );
        });
    }

    public function onTurnFailed($callback)
    {
        return $this->onStreamEvent(StreamEvent::TURN_FAILED, function (StreamEvent $event) use ($callback) {
            call_user_func(
                $callback,
                $event->data('conversation_id'),
                $event->data('turn_number'),
                $event->data('error'),
                $event
            );
        });
    }

    protected function ensureEventDispatcher()
    {
        if (!$this->eventDispatcher) {
            $this->eventDispatcher = new StreamEventDispatcher();
        }

        return $this->eventDispatcher;
    }

    protected function embeddingService()
    {
        return new EmbeddingService(static::registry(), static::embeddingStore(), static::config());
    }

    protected function loadOrCreateConversationForMemory(ConversationReference $reference = null)
    {
        $reference = $reference ? $reference : $this->conversationReference();
        if (!$reference instanceof ConversationReference) {
            throw new \RuntimeException('Conversation reference is required for memory updates');
        }

        $conversation = static::persistence()->loadConversation($reference);
        if ($conversation instanceof Conversation) {
            return $conversation;
        }

        $conversation = new Conversation($reference);
        $providerName = $this->request->provider() ? $this->request->provider() : $this->providerName();
        if ($providerName) {
            $conversation->provider($providerName);
        }
        if ($this->request->model()) {
            $conversation->model($this->request->model());
        }

        return $conversation;
    }

    protected function mcpToolRegistrar()
    {
        return new McpToolRegistrar(static::toolRegistry(), static::mcpToolInvoker(), static::mcpServers());
    }

    protected function recordMcpDiscoveryError(array $error)
    {
        $errors = $this->request->metadata('mcp_discovery_errors');
        if (!is_array($errors)) {
            $errors = array();
        }

        $errors[] = $error;
        $this->request->metadata('mcp_discovery_errors', $errors);

        return $this;
    }

    protected function onStreamEvent($type, $callback)
    {
        if (!is_callable($callback)) {
            throw new \InvalidArgumentException('Event callback must be callable');
        }

        $this->ensureEventDispatcher()->addCallback(function ($event) use ($type, $callback) {
            if (!$event instanceof StreamEvent || $event->type() !== $type) {
                return;
            }

            call_user_func($callback, $event);
        });

        return $this;
    }

    protected function applyConfiguredConversationMemoryTools()
    {
        $providerName = $this->request->provider() ? $this->request->provider() : $this->providerName();
        if (!$providerName) {
            return $this;
        }

        $resolver = new ConfigResolver(static::config());
        $enabled = $resolver->resolveProviderSetting($providerName, 'conversation_memory.tools.enabled', false, 'runtime');
        if (!$enabled) {
            return $this;
        }

        if (!$this->shouldAutoExposeConversationMemoryTools($resolver, $providerName)) {
            return $this;
        }

        if ($resolver->resolveProviderSetting($providerName, 'conversation_memory.tools.title', true, 'runtime')) {
            $this->addConfiguredToolIfMissing(new UpdateConversationTitleTool());
        }

        if ($resolver->resolveProviderSetting($providerName, 'conversation_memory.tools.summary', true, 'runtime')) {
            $this->addConfiguredToolIfMissing(new UpdateConversationSummaryTool());
        }

        if ($resolver->resolveProviderSetting($providerName, 'conversation_memory.tools.append_note', true, 'runtime')) {
            $this->addConfiguredToolIfMissing(new AppendConversationNoteTool());
        }

        if ($resolver->resolveProviderSetting($providerName, 'conversation_memory.tools.replace_notes', false, 'runtime')) {
            $this->addConfiguredToolIfMissing(new ReplaceConversationNotesTool());
        }

        return $this;
    }

    protected function addConfiguredToolIfMissing($tool)
    {
        $name = null;
        if ($tool instanceof ToolDefinition) {
            $name = $tool->name();
        } elseif ($tool instanceof ToolInterface) {
            $name = $tool->name();
        }

        if (!$name || $this->requestHasTool($name)) {
            return $this;
        }

        return $this->addTool($tool);
    }

    protected function requestHasTool($name)
    {
        foreach ($this->request->tools() as $tool) {
            if (is_array($tool) && isset($tool['name']) && $tool['name'] === $name) {
                return true;
            }
            if ($tool instanceof ToolDefinition && $tool->name() === $name) {
                return true;
            }
            if ($tool instanceof ToolInterface && $tool->name() === $name) {
                return true;
            }
        }

        return false;
    }

    protected function shouldAutoExposeConversationMemoryTools(ConfigResolver $resolver, $providerName)
    {
        $requirePersistence = $resolver->resolveProviderSetting($providerName, 'conversation_memory.tools.require_persistence', true, 'runtime');
        $persistence = static::persistence();

        if ($requirePersistence && (!$persistence || $persistence instanceof NullPersistenceProvider)) {
            return false;
        }

        $onlyAfterTurn = (int)$resolver->resolveProviderSetting($providerName, 'conversation_memory.tools.only_after_turn', 2, 'runtime');
        if ($onlyAfterTurn <= 1) {
            return true;
        }

        return $this->currentConversationTurnNumber() >= $onlyAfterTurn;
    }

    protected function currentConversationTurnNumber()
    {
        $reference = $this->conversationReference();
        if (!$reference instanceof ConversationReference) {
            return 1;
        }

        $conversation = static::persistence()->loadConversation($reference);
        if (!$conversation instanceof Conversation) {
            return 1;
        }

        return count($conversation->turns()) + 1;
    }

    protected function applyToolPromptAdditions()
    {
        $additions = array();

        foreach ($this->request->tools() as $tool) {
            $addition = $this->toolPromptAddition($tool);
            if ($addition === '') {
                continue;
            }

            if (!in_array($addition, $additions, true)) {
                $additions[] = $addition;
            }
        }

        if (empty($additions)) {
            return $this;
        }

        $instructions = trim((string)$this->request->instructions());
        foreach ($additions as $addition) {
            if ($instructions !== '' && strpos($instructions, $addition) !== false) {
                continue;
            }

            if ($instructions === '') {
                $instructions = $addition;
            } else {
                $instructions .= "\n\n" . $addition;
            }
        }

        $this->request->instructions($instructions);

        return $this;
    }

    protected function toolPromptAddition($tool)
    {
        if ($tool instanceof ToolPromptAdditionInterface) {
            return trim((string)$tool->promptAddition());
        }

        if ($tool instanceof ToolDefinition) {
            return trim((string)$tool->metadata('prompt_addition'));
        }

        if (is_array($tool)) {
            if (isset($tool['metadata']) && is_array($tool['metadata']) && isset($tool['metadata']['prompt_addition'])) {
                return trim((string)$tool['metadata']['prompt_addition']);
            }

            if (isset($tool['name']) && static::toolRegistry()->has($tool['name'])) {
                return trim((string)static::toolRegistry()->get($tool['name'])->metadata('prompt_addition'));
            }
        }

        if ($tool instanceof ToolInterface && !($tool instanceof ToolPromptAdditionInterface) && static::toolRegistry()->has($tool->name())) {
            return trim((string)static::toolRegistry()->get($tool->name())->metadata('prompt_addition'));
        }

        return '';
    }

    protected function buildEmbeddingRequest($input = null, array $options = array(), $multiple = false)
    {
        $request = new EmbeddingRequest();

        $providerName = $this->request->provider() ? $this->request->provider() : $this->providerName();
        if ($providerName) {
            $request->provider($providerName);
        }

        if ($this->request->model()) {
            $request->model($this->request->model());
        }

        if ($input !== null) {
            if ($multiple) {
                foreach ($input as $value) {
                    $request->addInput($value);
                }
            } elseif (is_array($input)) {
                foreach ($input as $value) {
                    $request->addInput($value);
                }
            } else {
                $request->addInput($input);
            }
        }

        foreach ($options as $key => $value) {
            if ($key === 'provider') {
                $request->provider($value);
                continue;
            }

            if ($key === 'model') {
                $request->model($value);
                continue;
            }

            if ($key === 'metadata' && is_array($value)) {
                $request->metadata($value);
                continue;
            }

            $request->option($key, $value);
        }

        return $request;
    }

    public function run()
    {
        $this->applyConfiguredConversationMemoryTools();
        $this->applyToolPromptAdditions();

        $runtime = new Runtime(static::runtimeContext());

        $eventCallback = $this->eventCallback;
        if ($this->eventDispatcher) {
            if ($eventCallback) {
                $this->eventDispatcher->addCallback($eventCallback);
            }
            $eventCallback = function ($event) {
                $this->eventDispatcher->dispatch($event);
            };
        }

        return $runtime->execute($this->request, $this->conversationReference, $eventCallback);
    }
}
