<?php
namespace boru\boruai\Models;

class ToolDefinition {
    private $name;
    private $properties = [];
    private $description;
    private $strict = true;
    /** @var ToolParameters */
    private $parameters;

    private $rawJson = null;

    public function __construct($input=[],$rawJson=false) {
        $this->parameters = new ToolParameters();
        if($rawJson) {
            if(is_array($input)) {
                $this->rawJson = json_encode($input,JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE);
            } else {
                $this->rawJson = $input;
            }
            return;
        }
        if(!is_array($input)) {
            return;
        }
        if(isset($input["name"])) {
            $this->name = $input["name"];
        }
        if(isset($input["description"])) {
            $this->description = $input["description"];
        }
        if(isset($input["strict"])) {
            $this->strict = $input["strict"];
        }
        if(isset($input["properties"])) {
            $this->parameters = new ToolParameters($input["properties"]);
        } elseif(isset($input["parameters"]) && isset($input["parameters"]["properties"])) {
            $this->parameters = new ToolParameters($input["parameters"]["properties"]);
        }
    }

    /**
     * Set or get the tool name.. returns $this if setting, for fluent style definition
     * @param mixed $name 
     * @return $this|mixed 
     */
    public function name($name=null) {
        if($name !== null) {
            $this->name = $name;
            return $this;
        }
        return $this->name;
    }

    /**
     * Set or get the tool description.. returns $this if setting, for fluent style definition
     * @param mixed $description 
     * @return $this|mixed 
     */
    public function description($description=null) {
        if($description !== null) {
            $this->description = $description;
            return $this;
        }
        return $this->description;
    }

    /**
     * Set or get the strict mode.. returns $this if setting, for fluent style definition
     * @param mixed $strict 
     * @return $this|mixed 
     */
    public function strict($strict=null) {
        if($strict !== null) {
            $this->strict = $strict ? true : false;
            return $this;
        }
        return $this->strict;
    }

    /**
     * Set or get the tool parameters.. returns $this if setting, for fluent style definition
     * @param mixed $properties 
     * @return $this|mixed 
     */
    public function properties($properties=null) {
        if($properties !== null) {
            $this->properties = $properties;
            return $this;
        }
        return $this->properties;
    }

    /**
     * Set or get the tool parameters.. returns $this if setting, for fluent style definition
     * @param mixed $parameters 
     * @return $this|mixed 
     */
    public function parameters($parameters=null) {
        if($parameters !== null) {
            $this->parameters = $parameters;
            return $this;
        }
        return $this->parameters;
    }

    /**
     * Compile the tool definition for use in a response
     * @return array|false 
     */
    public function forResponse() {
        if($this->rawJson) {
            $array = json_decode($this->rawJson, true);
            if(!$array) {
                throw new \Exception("Invalid JSON in ToolDefinition");
            }
            $array["type"] = "function";
        } else {
            if(!$this->name) {
                return false;
            }
            $array = [];
            $array["type"] = "function";
            $array["name"] = $this->name;
            $array["description"] = $this->description;
            $array["parameters"] = ["type"=>"object","properties"=>[]];
            $array["parameters"] = $this->parameters->compile($this->strict);
            if($this->strict) {
                $array["strict"] = true;
            }
        }
        
        return $array;
    }

    /**
     * Compile to JSON or array
     * @param boolean $asJson 
     * @return string|array 
     */
    public function compile($asJson=false) {
        if($this->rawJson) {
            $array = json_decode($this->rawJson, true);
            if(!$array) {
                throw new \Exception("Invalid JSON in ToolDefinition");
            }
        } else {
         
            $array = [];
            $array["name"] = $this->name;
            $array["description"] = $this->description;
            $array["parameters"] = ["type"=>"object","properties"=>[]];
            $array["parameters"] = $this->parameters->compile($this->strict);
            if($this->strict) {
                $array["strict"] = true;
            }
        }
        
        
        if($asJson) {
            return json_encode($array);
        }
        return $array;
    }

    /**
     * Register the tool callback globally
     * @param callable|string $callback 
     * @return void 
     * @throws \Exception 
     */
    public function register($callback) {
        if(!is_callable($callback)) {
            if(is_string($callback) && class_exists($callback) && method_exists($callback,'handle')) {
                $class = $callback;
                $callback = function($args) use ($class) {
                    return $class::callback('handle',$args);
                };
            } else {
                throw new \Exception("Callback is not callable");
            }
        }
        Tool::register($this->name,$callback);
    }

    /**
     * Add a property to the tool parameters
     * @param string $name 
     * @param string $type 
     * @param string|null $description 
     * @param boolean $required 
     * @param array $enum 
     * @return ToolProperty
     */
    public function addProperty($name,$type,$description=null,$required=true,$enum=[]) {
        //$this->properties[$name] = static::makeProperty($name,$type,$description,$required,$enum);
        $this->parameters->addProperty(static::makeProperty($name,$type,$description,$required,$enum));
        return $this;
    }

    /**
     * Add a string property
     * @param string $name 
     * @param string $description 
     * @param boolean $required 
     * @return ToolProperty
     */
    public function addString($name,$description,$values=[],$required=true) {
        return $this->addProperty($name,"string",$description,$required,$values);
    }

    /**
     * Add an integer property
     * @param string $name 
     * @param string $description 
     * @param boolean $required 
     * @return ToolProperty
     */
    public function addInteger($name,$description,$values=[],$required=true) {
        return $this->addProperty($name,"integer",$description,$required,$values);
    }

    /**
     * Add a float property
     * @param string $name 
     * @param string $description 
     * @param boolean $required 
     * @return ToolProperty
     */
    public function addFloat($name,$description,$required=true) {
        return $this->addProperty($name,"number",$description,$required);
    }

    /**
     * Add a boolean property
     * @param string $name 
     * @param string $description 
     * @param boolean $required 
     * @return ToolProperty
     */
    public function addBool($name,$description,$required=true) {
        return $this->addProperty($name,"integer",$description,$required);
    }

    /**
     * Add an anyOf property, which is a list of properties that can be used
     * @param string $name 
     * @param array $properties 
     * @return ToolProperty
     */
    public function addAnyOf($name,$properties=[]) {
        return $this->addProperty($name,"anyOf",null,true,$properties);
    }

    /**
     * Add an object property
     * @param string $name 
     * @param string $description 
     * @param boolean $required 
     * @return ToolProperty
     */
    public function addObject($name,$description,$required=true,$properties=[]) {
        return $this->addProperty($name,"object",$description,$required,$properties);
    }

    public static function makeProperty($name,$type,$description=null,$required=true,$enum=[]) {
        if($type == "anyOf" || $type == "object") {
            $input = [
                "name" => $name,
                "type" => $type,
                "properties" => $enum,
                "required" => $required
            ];
            return static::createProperty($input);
        }
        if($type == "float") {
            $type = "number";
        }
        $array = [
            "name" => $name,
            "type" => $type,
            "description" => $description,
            "required" => $required
        ];
        if(count($enum) > 0) {
            $array["enum"] = $enum;
        }
        return static::createProperty($array);
    }

    public static function createProperty($input=[]) {
        return new ToolProperty($input);
    }

    public static function fromJson($json) {
        $input = json_decode($json,true);
        
        if(!$input) {
            throw new \Exception("Invalid JSON");
        }
        if(!isset($input["name"])) {
            throw new \Exception("Missing name");
        }
        if(!isset($input["parameters"])) {
            throw new \Exception("Missing parameters");
        }
        if(!isset($input["parameters"]["properties"])) {
            throw new \Exception("Missing properties");
        }
        $params = $input["parameters"]["properties"];
        unset($input["parameters"]["properties"]);
        $input["properties"] = $params;
        return new ToolDefinition($input);
    }

    public static function merge($array1=[],$array2=[],$array3=[]) {
        $newArray = [];
        if(!is_array($array1) || empty($array1)) {
            $array1 = [];
        }
        if(!is_array($array2) || empty($array2)) {
            $array2 = [];
        }
        if(!is_array($array3) || empty($array3)) {
            $array3 = [];
        }
        $newArray = [];
        foreach($array1 as $tool) {
            $toolName = static::getSafeToolName($tool);
            if(!$toolName) {
                continue;
            }
            $newArray[$toolName] = $tool;
        }
        foreach($array2 as $tool) {
            $toolName = static::getSafeToolName($tool);
            if(!$toolName) {
                continue;
            }
            if(isset($newArray[$toolName])) {
                $newArray[$toolName] = $tool;
            } else {
                $newArray[$toolName] = $tool;
            }
        }
        foreach($array3 as $tool) {
            $toolName = static::getSafeToolName($tool);
            if(!$toolName) {
                continue;
            }
            if(isset($newArray[$toolName])) {
                $newArray[$toolName] = $tool;
            } else {
                $newArray[$toolName] = $tool;
            }
        }
        return array_values($newArray);
    }
    private static function getSafeToolName($tool) {
        if($tool instanceof ToolDefinition) {
            return $tool->name();
        }
        if(is_array($tool) && isset($tool["name"])) {
            return $tool["name"];
        }
        if(is_array($tool) && isset($tool["type"])) {
            return "_openai_".$tool["type"];
        }
        return null;
    }
}