<?php

namespace boru\boruai\Openai\Stream\Listener;

use boru\boruai\Openai\Stream\AbstractStreamEventListener;
use boru\boruai\Openai\Stream\StreamEvent;

class FunctionCallListener extends AbstractStreamEventListener
{
    /** @var array<string, callable[]> keyed by function_name or '*' */
    private $startedCallbacks = array();

    /** @var array<string, callable[]> keyed by function_name or '*' */
    private $completedCallbacks = array();

    /** @var array<string, callable[]> keyed by function_name or '*' */
    private $failedCallbacks = array();

    /**
     * Register a callback for FUNCTION_CALL_STARTED.
     *
     * Callback signature: function (StreamEvent $event, $functionName): void
     *
     * @param string|null $functionName Specific function name, or null / '*' for any.
     * @param callable    $callback
     * @return $this
     * @throws \Exception
     */
    public function onStarted($functionName, $callback)
    {
        if (!is_callable($callback)) {
            throw new \Exception("onStarted callback must be callable");
        }
        if ($functionName === null || $functionName === '') {
            $functionName = '*';
        }
        if (!isset($this->startedCallbacks[$functionName])) {
            $this->startedCallbacks[$functionName] = array();
        }
        $this->startedCallbacks[$functionName][] = $callback;
        return $this;
    }

    /**
     * Register a callback for FUNCTION_CALL_COMPLETED.
     *
     * Callback signature: function (StreamEvent $event, $functionName): void
     *
     * @param string|null $functionName Specific function name, or null / '*' for any.
     * @param callable    $callback
     * @return $this
     * @throws \Exception
     */
    public function onCompleted($functionName, $callback)
    {
        if (!is_callable($callback)) {
            throw new \Exception("onCompleted callback must be callable");
        }
        if ($functionName === null || $functionName === '') {
            $functionName = '*';
        }
        if (!isset($this->completedCallbacks[$functionName])) {
            $this->completedCallbacks[$functionName] = array();
        }
        $this->completedCallbacks[$functionName][] = $callback;
        return $this;
    }

    /**
     * Register a callback for FUNCTION_CALL_FAILED.
     *
     * Callback signature: function (StreamEvent $event, $functionName): void
     *
     * @param string|null $functionName Specific function name, or null / '*' for any.
     * @param callable    $callback
     * @return $this
     * @throws \Exception
     */
    public function onFailed($functionName, $callback)
    {
        if (!is_callable($callback)) {
            throw new \Exception("onFailed callback must be callable");
        }
        if ($functionName === null || $functionName === '') {
            $functionName = '*';
        }
        if (!isset($this->failedCallbacks[$functionName])) {
            $this->failedCallbacks[$functionName] = array();
        }
        $this->failedCallbacks[$functionName][] = $callback;
        return $this;
    }

    public function handle(StreamEvent $event)
    {
        $type = $event->type();

        if ($type === StreamEvent::FUNCTION_CALL_STARTED) {
            return $this->fireFunctionCallbacks(
                $event,
                $this->startedCallbacks
            );
        }

        if ($type === StreamEvent::FUNCTION_CALL_COMPLETED) {
            return $this->fireFunctionCallbacks(
                $event,
                $this->completedCallbacks
            );
        }

        if ($type === StreamEvent::FUNCTION_CALL_FAILED) {
            return $this->fireFunctionCallbacks(
                $event,
                $this->failedCallbacks
            );
        }

        return false;
    }

    /**
     * @param StreamEvent $event
     * @param array       $callbacksByName
     * @return bool true if any callback was invoked
     */
    private function fireFunctionCallbacks(StreamEvent $event, array $callbacksByName)
    {
        if (empty($callbacksByName)) {
            return false;
        }

        if($event->type() === StreamEvent::FUNCTION_CALL_COMPLETED) {
            $output = $this->extractFunctionOutput($event);
        }

        $functionName = $this->extractFunctionName($event);
        $functionArgs = $this->extractFunctionArgs($event);
        $called = false;

        // Exact match
        if (isset($callbacksByName[$functionName])) {
            foreach ($callbacksByName[$functionName] as $cb) {
                if (isset($output)) {
                    call_user_func($cb, $event, $functionName, $functionArgs, $output);
                } else {
                    call_user_func($cb, $event, $functionName, $functionArgs);
                }
                $called = true;
            }
        }

        // Wildcard '*'
        if (isset($callbacksByName['*'])) {
            foreach ($callbacksByName['*'] as $cb) {
                if (isset($output)) {
                    call_user_func($cb, $event, $functionName, $functionArgs, $output);
                } else {
                    call_user_func($cb, $event, $functionName, $functionArgs);
                }
                $called = true;
            }
        }

        return $called;
    }

    /**
     * Best-effort extraction of function_name from event payload.
     *
     * @param StreamEvent $event
     * @return string
     */
    private function extractFunctionName(StreamEvent $event)
    {
        $data = $event->get("name", "unknown");
        if(is_string($data)) {
            return $data;
        }

        return 'unknown';
    }   

    private function extractFunctionArgs(StreamEvent $event)
    {
        $data = $event->get("args",[]);
        if (is_array($data)) {
            return $data;
        }

        return [];
    }

    private function extractFunctionOutput(StreamEvent $event)
    {
        return $event->get("output", null);
    }
}