<?php
namespace boru\ai\Runtime;

use boru\ai\Schema\Request;
use boru\ai\Schema\Response;
use boru\ai\Schema\ToolResult;

class ToolExecutor implements ToolExecutorInterface
{
    /** @var ToolRegistry */
    protected $registry;

    /** @var array */
    protected $contextValues = array();

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

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

    public function __construct(ToolRegistry $registry = null, array $contextValues = array())
    {
        $this->registry = $registry ? $registry : new ToolRegistry();
        $this->contextValues = $contextValues;
    }

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

    public function contextValues(array $values = null)
    {
        if ($values !== null) {
            $this->contextValues = $values;
            return $this;
        }
        return $this->contextValues;
    }

    public function beforeExecuteHook($hook = null)
    {
        if ($hook !== null) {
            if (!is_callable($hook)) {
                throw new \InvalidArgumentException('beforeExecuteHook must be callable');
            }
            $this->beforeExecuteHook = $hook;
            return $this;
        }
        return $this->beforeExecuteHook;
    }

    public function afterExecuteHook($hook = null)
    {
        if ($hook !== null) {
            if (!is_callable($hook)) {
                throw new \InvalidArgumentException('afterExecuteHook must be callable');
            }
            $this->afterExecuteHook = $hook;
            return $this;
        }
        return $this->afterExecuteHook;
    }

    public function executeToolCalls(Request $request, Response $response)
    {
        $results = array();

        foreach ($response->toolCalls() as $toolCall) {
            $result = new ToolResult();
            $result->toolCallId($toolCall->id());
            $result->name($toolCall->name());

            if (is_callable($this->beforeExecuteHook)) {
                call_user_func($this->beforeExecuteHook, $toolCall, $request, $response);
            }

            if (!$this->registry()->has($toolCall->name())) {
                $result->success(false);
                $result->output('Unknown tool: ' . $toolCall->name());
                if (is_callable($this->afterExecuteHook)) {
                    call_user_func($this->afterExecuteHook, $toolCall, $result, $request, $response);
                }
                $results[] = $result;
                continue;
            }

            $definition = $this->registry()->get($toolCall->name());
            $context = new ToolExecutionContext($request, $response, $toolCall, $this->contextValues());

            try {
                $output = $definition->invoke($toolCall->arguments(), $context);
                $result->success(true);
                $result->output($output);
            } catch (\Exception $e) {
                $result->success(false);
                $result->output($e->getMessage());
                $result->metadata('exception_class', get_class($e));
                $result->metadata('exception_code', $e->getCode());
            }

            if (is_callable($this->afterExecuteHook)) {
                call_user_func($this->afterExecuteHook, $toolCall, $result, $request, $response);
            }

            $results[] = $result;
        }

        return $results;
    }
}
