<?php
namespace boru\boruai;

use boru\boruai\Models\Services\ServiceInterface;
use boru\boruai\Models\Services\OpenAIResponseService;
use boru\boruai\Openai\Stream\SimpleStreamEventDispatcher;
use boru\boruai\Openai\Stream\Listener\OutputBufferListener;
use boru\boruai\Openai\Stream\Listener\FunctionCallListener;

class AICall {

    /** @var array<string,string> explicit model→service overrides */
    private static $modelToServiceMap = [];
    public static function addService($model, $serviceClass) {
        self::$modelToServiceMap[$model] = $serviceClass;
    }
    /**
     * Convenience factory: pick service by model.
     *
     * @param string $model
     * @return static
     * @throws \Exception
     */
    public static function create($model = 'gpt-5.1')
    {
        $service = self::createServiceForModel($model);
        $instance = new self($service);
        $instance->model($model);
        return $instance;
    }

    /**
     * Decide which service to use for a model.
     *
     * @param string $model
     * @return ServiceInterface
     * @throws \Exception
     */
    private static function createServiceForModel($model)
    {
        // 1) Explicit override wins
        if (isset(self::$modelToServiceMap[$model])) {
            $class = self::$modelToServiceMap[$model];
            $service = new $class();
            if (!$service instanceof ServiceInterface) {
                throw new \Exception("Service for model $model must implement ServiceInterface");
            }
            return $service;
        }

        // 2) Pattern-based fallback
        // gpt-* and o* (OpenAI "o" series) default to OpenAIResponseService
        $lower = strtolower($model);
        if (strpos($lower, 'gpt-') === 0 || strpos($lower, 'o') === 0) {
            $service = new OpenAIResponseService();
            $service->model($model);
            return $service;
        }

        // 3) Last resort: default OpenAIResponseService (still set model)
        $service = new OpenAIResponseService();
        $service->model($model);
        return $service;
    }

    /** @var ServiceInterface */
    private $service;

    // Streaming-related configuration callbacks:
    /** @var callable|null */
    private $onTextDelta = null;
    /** @var callable|null */
    private $onTextDone = null;
    /** @var callable|null */
    private $onFunctionStart = null;
    /** @var callable|null */
    private $onFunctionCompleted = null;
    /** @var callable|null */
    private $onFunctionFailed = null;

    /**
     * @param ServiceInterface|string|null $serviceOrModel
     *        - ServiceInterface: use as-is
     *        - string: interpreted as model name
     *        - null: default OpenAIResponseService
     */
    public function __construct($serviceOrModel = null)
    {
        if ($serviceOrModel instanceof ServiceInterface) {
            $this->service = $serviceOrModel;
        } elseif (is_string($serviceOrModel) && $serviceOrModel !== '') {
            $this->service = self::createServiceForModel($serviceOrModel);
        } else {
            $this->service = new OpenAIResponseService();
        }
    }

    public function service($service=null) {
        if($service) {
            $this->service = $service;
        }
        return $this->service;
    }

    public function call($options=[]) {
        $dispatcher = $this->buildDispatcher();
        if ($dispatcher) {   
            $this->service->setEventDispatcher($dispatcher);
        }
        return $this->service->call($options);
    }
    public function callback($callback = null) {
        return $this->service->callback($callback);
    }
    public function model($model=null) {
        return $this->service->model($model);
    }
    public function set($k,$value=null) {
        return $this->service->set($k,$value);
    }
    public function get($k=null,$default=null) {
        return $this->service->get($k,$default);
    }
    public function addTool($tool=[]) {
        return $this->service->addTool($tool);
    }
    public function addTools($tools=[]) {
        return $this->service->addTools($tools);
    }
    public function getTools() {
        return $this->service->getTools();
    }
    public function addMessage($message,$options=[]) {
        return $this->service->addMessage($message,$options);
    }
    public function addImage($image,$options=[]) {
        return $this->service->addImage($image,$options);
    }
    public function addFile($file,$options=[]) {
        return $this->service->addFile($file,$options);
    }

    public function addToolMcp($jsonStringOrArray) {
        $toolData = is_string($jsonStringOrArray) ? json_decode($jsonStringOrArray,true) : $jsonStringOrArray;
        if(is_array($toolData)) {
            $this->service->addTool(Models\Tool\McpTool::fromJson($toolData));
        }
    }

    private function buildDispatcher() {
        $useStreaming = $this->onTextDelta || $this->onTextDone || $this->onFunctionStart || $this->onFunctionCompleted || $this->onFunctionFailed;

        if (!$useStreaming) {
            return null;
        }
        // Build dispatcher and listeners
        $dispatcher = new SimpleStreamEventDispatcher(false);

        // Text streaming listener
        if ($this->onTextDelta || $this->onTextDone) {
            $outputListener = new OutputBufferListener();

            if ($this->onTextDelta) {
                $cb = $this->onTextDelta;
                $outputListener->onDelta(function ($event, $delta, $bufferSoFar) use ($cb) {
                    // user callback can accept 1–3 params; extra params are ignored by PHP
                    call_user_func($cb, $delta, $bufferSoFar, $event);
                });
            }

            if ($this->onTextDone) {
                $cb = $this->onTextDone;
                $outputListener->onDone(function ($event, $fullText) use ($cb) {
                    call_user_func($cb, $fullText, $event);
                });
            }

            $dispatcher->addListener($outputListener);
        }

        // Function call listener
        if ($this->onFunctionStart || $this->onFunctionCompleted || $this->onFunctionFailed) {
            $fnListener = new FunctionCallListener();

            if ($this->onFunctionStart) {
                $cb = $this->onFunctionStart;
                $fnListener->onStarted('*', function ($event, $functionName) use ($cb) {
                    call_user_func($cb, $functionName, $event);
                });
            }

            if ($this->onFunctionCompleted) {
                $cb = $this->onFunctionCompleted;
                $fnListener->onCompleted('*', function ($event, $functionName) use ($cb) {
                    call_user_func($cb, $functionName, $event);
                });
            }

            if ($this->onFunctionFailed) {
                $cb = $this->onFunctionFailed;
                $fnListener->onFailed('*', function ($event, $functionName) use ($cb) {
                    call_user_func($cb, $functionName, $event);
                });
            }

            $dispatcher->addListener($fnListener);
        }
        return $dispatcher;
    }

    /**
     * Ensure the current service supports streaming before configuring callbacks.
     *
     * @throws \Exception
     */
    private function assertStreamable()
    {
        if (!method_exists($this->service, 'isStreamable') || !$this->service->isStreamable()) {
            throw new \Exception("The selected AI service does not support streaming configuration");
        }
    }

    /**
     * Configure streaming text delta callback.
     *
     * Callback signature: function ($delta, $bufferSoFar) or function ($delta, $bufferSoFar, $event)
     *
     * @param callable $callback
     * @return $this
     * @throws \Exception
     */
    public function onTextDelta($callback)
    {
        $this->assertStreamable();
        if (!is_callable($callback)) {
            throw new \Exception("onTextDelta callback must be callable");
        }
        $this->onTextDelta = $callback;
        return $this;
    }

    /**
     * Configure streaming text completion callback.
     *
     * Callback signature: function ($fullText) or function ($fullText, $event)
     *
     * @param callable $callback
     * @return $this
     * @throws \Exception
     */
    public function onTextDone($callback)
    {
        $this->assertStreamable();
        if (!is_callable($callback)) {
            throw new \Exception("onTextDone callback must be callable");
        }
        $this->onTextDone = $callback;
        return $this;
    }

    /**
     * function_call.started
     *
     * Callback signature: function ($functionName) or function ($functionName, $event)
     */
    public function onFunctionStart($callback)
    {
        $this->assertStreamable();
        if (!is_callable($callback)) {
            throw new \Exception("onFunctionStart callback must be callable");
        }
        $this->onFunctionStart = $callback;
        return $this;
    }

    /**
     * function_call.completed
     *
     * Callback signature: function ($functionName) or function ($functionName, $event)
     */
    public function onFunctionCompleted($callback)
    {
        $this->assertStreamable();
        if (!is_callable($callback)) {
            throw new \Exception("onFunctionCompleted callback must be callable");
        }
        $this->onFunctionCompleted = $callback;
        return $this;
    }

    /**
     * function_call.failed
     *
     * Callback signature: function ($functionName) or function ($functionName, $event)
     */
    public function onFunctionFailed($callback)
    {
        $this->assertStreamable();
        if (!is_callable($callback)) {
            throw new \Exception("onFunctionFailed callback must be callable");
        }
        $this->onFunctionFailed = $callback;
        return $this;
    }

}