<?php
namespace boru\ai\Runtime;

use boru\ai\Conversation\Conversation;
use boru\ai\Conversation\ConversationReference;
use boru\ai\Conversation\ConversationReplayService;
use boru\ai\Conversation\Turn;
use boru\ai\Persistence\Db\DbPersistenceProvider;
use boru\ai\Schema\Request;
use boru\ai\Stream\StreamEvent;

class Runtime
{
    /** @var RuntimeContext */
    protected $context;

    /** @var int */
    protected $maxToolIterations = 10;

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

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

    public function maxToolIterations($value = null)
    {
        if ($value !== null) {
            $this->maxToolIterations = (int)$value;
            return $this;
        }
        return $this->maxToolIterations;
    }

    public function execute(Request $request, ConversationReference $reference = null, $eventCallback = null)
    {
        $conversation = $this->loadConversation($reference, $request);
        $this->context()->persistenceProvider()->saveConversation($conversation);
        $this->dispatchTurnEvent(StreamEvent::TURN_STARTED, $conversation, null, $eventCallback);

        $turn = $this->startTurn($request, $conversation);
        $conversation->addTurn($turn);
        $this->context()->persistenceProvider()->saveTurn($conversation, $turn);

        $toolExecutor = $this->context()->toolExecutor();
        if ($toolExecutor instanceof ToolExecutor) {
            $toolExecutor->beforeExecuteHook($this->createBeforeToolExecutionHook($eventCallback, $conversation, $turn));
            $toolExecutor->afterExecuteHook($this->createAfterToolExecutionHook($eventCallback, $conversation, $turn));
        }

        try {
            $provider = $this->resolveProvider($request);
            $workingRequest = $request->copy();
            $workingRequest->metadata('conversation', $conversation);
            $response = $provider->execute($workingRequest, $eventCallback);
            $turn->response($response);
            $turn->status('provider_completed');
            $this->context()->persistenceProvider()->saveTurn($conversation, $turn);

            $iteration = 0;
            while ($response && $response->hasToolCalls()) {
                $iteration++;
                if ($iteration > $this->maxToolIterations()) {
                    throw new \RuntimeException('Maximum tool loop iterations exceeded');
                }

                foreach ($response->toolCalls() as $toolCall) {
                    $turn->addToolCall($toolCall);
                }
                $this->context()->persistenceProvider()->saveTurn($conversation, $turn);

                $toolResults = $this->context()->toolExecutor()->executeToolCalls($workingRequest, $response);
                if (empty($toolResults)) {
                    break;
                }

                foreach ($toolResults as $toolResult) {
                    $turn->addToolResult($toolResult);
                    $workingRequest->addToolResult($toolResult);
                }

                foreach ($response->toolCalls() as $toolCall) {
                    $toolCall->status('completed');
                }

                $turn->status('continuing_after_tools');
                $this->context()->persistenceProvider()->saveTurn($conversation, $turn);

                $response = $provider->continueResponse($workingRequest, $response, $toolResults, $eventCallback);
                $turn->response($response);
                $this->context()->persistenceProvider()->saveTurn($conversation, $turn);
            }

            $turn->status('completed');
            $turn->completedAt(date('c'));

            $this->context()->persistenceProvider()->saveConversation($conversation);
            $this->context()->persistenceProvider()->saveTurn($conversation, $turn);
            $this->dispatchTurnEvent(StreamEvent::TURN_COMPLETED, $conversation, $turn, $eventCallback);

            return $response;
        } catch (\Exception $e) {
            $turn->status('failed');
            $turn->completedAt(date('c'));
            $turn->metadata('exception_message', $e->getMessage());
            $turn->metadata('exception_class', get_class($e));
            $turn->metadata('exception_code', $e->getCode());
            $this->context()->persistenceProvider()->saveConversation($conversation);
            $this->context()->persistenceProvider()->saveTurn($conversation, $turn);
            $this->dispatchTurnEvent(StreamEvent::TURN_FAILED, $conversation, $turn, $eventCallback, array(
                'error' => array(
                    'message' => $e->getMessage(),
                    'class' => get_class($e),
                    'code' => $e->getCode(),
                ),
            ));
            throw $e;
        }
    }

    public function replayService()
    {
        return new ConversationReplayService();
    }

    public function buildReplayMessages(Conversation $conversation, Request $currentRequest = null)
    {
        return $this->replayService()->buildReplayMessages($conversation, $currentRequest);
    }

    public function buildReplayRequest(Request $request, Conversation $conversation)
    {
        return $this->replayService()->applyReplay($request, $conversation);
    }

    protected function dispatchTurnEvent($type, Conversation $conversation, Turn $turn = null, $eventCallback = null, array $extra = array())
    {
        if (!is_callable($eventCallback)) {
            return;
        }

        $data = array(
            'conversation_id' => $conversation->reference()->id(),
            'reference' => $conversation->reference()->externalReference(),
            'turn_number' => $turn ? $turn->number() : null,
            'status' => $turn ? $turn->status() : null,
        );
        foreach ($extra as $key => $value) {
            $data[$key] = $value;
        }

        call_user_func($eventCallback, new StreamEvent($type, $data));
    }

    protected function createBeforeToolExecutionHook($eventCallback, Conversation $conversation, Turn $turn)
    {
        return function ($toolCall) use ($eventCallback, $conversation, $turn) {
            $persistence = $this->context()->persistenceProvider();
            if ($persistence instanceof DbPersistenceProvider) {
                $persistence->saveToolCallStarted($conversation->reference()->id(), $turn->number(), $toolCall);
            }

            if (!is_callable($eventCallback)) {
                return;
            }

            $event = new StreamEvent(
                StreamEvent::TOOL_EXECUTION_STARTED,
                $this->buildToolLifecycleEventData($toolCall, null, 'started')
            );
            call_user_func($eventCallback, $event);
        };
    }

    protected function createAfterToolExecutionHook($eventCallback, Conversation $conversation, Turn $turn)
    {
        return function ($toolCall, $toolResult) use ($eventCallback, $conversation, $turn) {
            $persistence = $this->context()->persistenceProvider();
            if ($persistence instanceof DbPersistenceProvider) {
                if ($toolResult->success()) {
                    $persistence->saveToolCallCompleted($conversation->reference()->id(), $turn->number(), $toolCall, $toolResult);
                } else {
                    $persistence->saveToolCallFailed($conversation->reference()->id(), $turn->number(), $toolCall, $toolResult);
                }
            }

            if (!is_callable($eventCallback)) {
                return;
            }

            $status = $toolResult->success() ? 'completed' : 'failed';
            $type = $toolResult->success() ? StreamEvent::TOOL_EXECUTION_COMPLETED : StreamEvent::TOOL_EXECUTION_FAILED;
            $event = new StreamEvent(
                $type,
                $this->buildToolLifecycleEventData($toolCall, $toolResult, $status)
            );
            call_user_func($eventCallback, $event);
        };
    }

    protected function buildToolLifecycleEventData($toolCall, $toolResult = null, $status = null)
    {
        $data = array(
            'tool_call_id' => $toolCall ? $toolCall->id() : null,
            'tool_name' => $toolCall ? $toolCall->name() : null,
            'arguments' => $toolCall ? $toolCall->arguments() : null,
            'arguments_json' => $toolCall ? $toolCall->argumentsJson() : null,
            'output' => null,
            'metadata' => array(),
            'status' => $status,
            'success' => null,
        );

        if ($toolResult) {
            $data['output'] = $toolResult->output();
            $data['metadata'] = is_array($toolResult->metadata()) ? $toolResult->metadata() : array();
            $data['success'] = $toolResult->success() ? true : false;
        }

        return $data;
    }

    protected function loadConversation(ConversationReference $reference = null, Request $request = null)
    {
        if (!$reference) {
            $reference = new ConversationReference();
        }

        $conversation = $this->context()->persistenceProvider()->loadConversation($reference);
        if ($conversation instanceof Conversation) {
            if ($request && $request->provider()) {
                $conversation->provider($request->provider());
            }
            if ($request && $request->model()) {
                $conversation->model($request->model());
            }
            return $conversation;
        }

        $conversation = new Conversation($reference);
        if ($request) {
            $conversation->provider($request->provider());
            $conversation->model($request->model());
        }
        return $conversation;
    }

    protected function startTurn(Request $request, Conversation $conversation)
    {
        $turn = new Turn();
        $turn->number(count($conversation->turns()) + 1);
        $turn->status('started');
        $turn->request($request->copy());
        $turn->startedAt(date('c'));
        return $turn;
    }

    protected function resolveProvider(Request $request)
    {
        $registry = $this->context()->providerRegistry();

        if ($request->provider()) {
            return $registry->get($request->provider());
        }

        $provider = $registry->resolveByModel($request->model());
        if ($provider) {
            return $provider;
        }

        throw new \RuntimeException('Unable to resolve provider for runtime request');
    }
}
