<?php
namespace boru\boruai\Openai;

use boru\boruai\Openai\Api\Endpoints\AssistantsAPI;
use boru\boruai\Openai\Api\Endpoints\ThreadsAPI;
use boru\boruai\Openai\Api\Responses\AssistantResponse;
use boru\boruai\Openai\OpenAI;
use boru\boruai\Openai\Models\BaseModel;
use boru\boruai\Tiktoken\EncoderProvider;
use Exception;

class OpenAIAssistant extends BaseModel {
    /** @var string */
    private $id;
    /** @var string */
    private $object = "assistant";
    /** @var int */
    private $createdAt;
    /** @var string|null */
    private $name;
    /** @var string|null */
    private $description;
    /** @var string */
    private $model;
    /** @var string|null */
    private $instructions;
    /** @var array */
    private $tools;
    /** @var array */
    private $fileIds;
    /** @var array */
    private $metadata;
    /** @var string */
    private $tag;

    private $encoder;

    private $customPrompts = false;

    /** @var array */
    private $prompts = [];

    private static $tableName;
    public static function tableName($tableName=null) {
        if($tableName) {
            static::$tableName = $tableName;
        }
        if(static::$tableName === null) {
            static::$tableName = OpenAI::table("assistants","boru_openai_assistants");
        }
        return static::$tableName;
    }

    /** @var OpenAIThread */
    private $thread;

    public function __clone() {
        $this->thread = null;
    }

    public function id($id=null) {
        if ($id !== null && $id != "new") {
            $this->id = $id;
        }
        return $this->id;
    }
    public function object($object=null) {
        if ($object !== null) {
            $this->object = $object;
        } elseif($object === false) {
            $this->object = null;
        }
        return $this->object;
    }
    public function createdAt($created=null) {
        if ($created !== null) {
            $this->createdAt = $created;
        } elseif($created === false) {
            $this->createdAt = null;
        }
        return $this->createdAt;
    }
    public function name($name=null) {
        if ($name !== null) {
            $this->name = $name;
        } elseif($name === false) {
            $this->name = null;
        }
        return $this->name;
    }
    public function description($description=null) {
        if ($description !== null) {
            $this->description = $description;
        } elseif($description === false) {
            $this->description = null;
        }
        return $this->description;
    }
    public function model($model=null) {
        if ($model !== null) {
            $this->model = $model;
        } elseif($model === false) {
            $this->model = null;
        }
        return $this->model;
    }
    public function instructions($instructions=null) {
        if ($instructions !== null) {
            $this->instructions = $instructions;
        } elseif($instructions === false) {
            $this->instructions = null;
        }
        return $this->instructions;
    }
    public function tools($tools=null) {
        if ($tools !== null) {
            $this->tools = $tools;
        } elseif($tools === false) {
            $this->tools = null;
        }
        return $this->tools;
    }
    public function fileIds($fileIds=null) {
        if ($fileIds !== null) {
            $this->fileIds = $fileIds;
        } elseif($fileIds === false) {
            $this->fileIds = null;
        }
        return $this->fileIds;
    }
    public function metadata($metadata=null) {
        if ($metadata !== null) {
            $this->metadata = $metadata;
        } elseif($metadata === false) {
            $this->metadata = null;
        }
        return $this->metadata;
    }
    public function tag($tag=null) {
        if ($tag !== null) {
            $this->tag = $tag;
        } elseif($tag === false) {
            $this->tag = null;
        }
        return $this->tag;
    }
    public function encoder($encoder=null) {
        if ($encoder !== null) {
            $this->encoder = $encoder;
        } elseif($encoder === false) {
            $this->encoder = null;
        }
        if(!$this->encoder) {
            $provider = new EncoderProvider();
            $this->encoder = $provider->getForModel($this->model());
        }
        return $this->encoder;
    }
    public function encode($text) {
        return $this->encoder()->encode($text);
    }
    public function decode($text) {
        return $this->encoder()->decode($text);
    }
    public function isCustomPrompts($customPrompts=null) {
        if ($customPrompts !== null) {
            $this->customPrompts = $customPrompts ? true : false;
        }
        return $this->customPrompts;
    }
    /**
     * Return the prompt body as a string, or as an array of Prompt objects. Default is true (string).
     * @param $name string The name of the prompt to return
     * @return string|array[]|false
     */
    public function getPrompt($name) {
        if(isset($this->prompts[$name])) {
            return $this->prompts[$name];
        }
        return false;
    }
    public function setPrompt($name,$prompt) {
        $this->prompts[$name] = $prompt;
    }
    public function prompt($name,$prompt=null) {
        if($prompt) {
            $this->setPrompt($name,$prompt);
        }
        return $this->getPrompt($name);
    }
    public function prompts() {
        return $this->prompts;
    }
    public function addTool($tool) {
        //dedupe
        if(is_array($this->tools) && count($this->tools) > 0) {       
            foreach($this->tools as $key => $value) {
                if(isset($value["type"]) && $value["type"] == $tool) {
                    return $this;
                } elseif($value == $tool) {
                    return $this;
                }
            }
        }
        if(is_array($tool)) {
            $this->tools[] = $tool;
        } else {
            $this->tools[] = ["type" => $tool];
        }
        
    }
    public function hasTool($tool) {
        if(is_array($this->tools) && count($this->tools) > 0) {       
            foreach($this->tools as $key => $value) {
                if(isset($value["type"]) && $value["type"] == $tool) {
                    return true;
                } elseif($value == $tool) {
                    return true;
                }
            }
        }
        return false;
    }
    public function removeTool($tool) {
        if(is_array($this->tools) && count($this->tools) > 0) {       
            foreach($this->tools as $key => $value) {
                if(isset($value["type"]) && $value["type"] == $tool) {
                    unset($this->tools[$key]);
                } elseif($value == $tool) {
                    unset($this->tools[$key]);
                }
            }
        }
    }

    public function save() {
        $parameters = [
            "model" => $this->model,
            "name" => $this->name,
            "description" => $this->description,
            "instructions" => $this->instructions,
            "tools" => $this->tools,
            "file_ids" => $this->fileIds,
            "metadata" => $this->metadata
        ];
        foreach($parameters as $key => $value) {
            if(!$value || $value == 'null' || $value == '[]' || (is_array($value) && count($value) == 0)) {
                unset($parameters[$key]);
            }
        }
        if($this->id) {
            $result = AssistantsAPI::update($this->id,$parameters);
            if($result->id()) {
                $this->id = $result->id();
                return $this;
            }
        } else {
            $result = AssistantsAPI::create($parameters);
            $this->id = $result->id();
            return $this;
        }
        return false;
        //return $this->request("patch","assistants/".$this->id,$parameters);
    }
    public function delete() {
        $result = AssistantsAPI::delete($this->id);
        return $result;
    }

    public function reload($useDb=false) {
        $copy = static::fromId($this->id,$useDb);
        $this->setFromData($copy->toArray());
        return $this;
    }

    /**
     * 
     * @return AssistantResponse|Run|string|false 
     * @param $concatenate string - the string to concatenate messages with, false to return an iteratable object AssestantResponse
     * @param $returnRun bool - if true, return the Run object instead of the result
     * @throws Exception 
     */
    public function run($concatenate=false,$returnRun=false,$keepThread=false) {
        if(!$this->thread) {
            throw new \Exception("Thread not found. Add a message first to create one.");
        }

        $beforeMessage = false;
        $afterMessage = false;
        if(!$this->isCustomPrompts()) {
            $this->prompts();
            if($this->prompt("before") && !empty($this->getPrompt("before"))) {
                $data = [
                    "role" => "user",
                    "content" => $this->getPrompt("before"),
                    "attachments" => [],
                    "metadata" => []
                ];
                $beforeMessage = new OpenAIMessage($data);
            }
            if($this->prompt("after") && !empty($this->getPrompt("after"))) {
                $data = [
                    "role" => "user",
                    "content" => $this->getPrompt("after"),
                    "attachments" => [],
                    "metadata" => []
                ];
                $afterMessage = new OpenAIMessage($data);
            }
        }
        if($beforeMessage || $afterMessage) {
            $messageArray = $this->thread->getMessages(false);
            if($beforeMessage) {
                array_unshift($messageArray,$beforeMessage);
            }
            if($afterMessage) {
                $messageArray[] = $afterMessage;
            }
            foreach($messageArray as $message) {
                $messages[] = $message->forThread();
            }
        } else {
            $messages = $this->thread->getMessages();
        }
        

        $parameters = [
            "assistant_id" => $this->id,
            "thread" => [
                "messages" => $messages
            ]
        ];
        //print_r($parameters);
        //exit();
        $run = ThreadsAPI::run($parameters);
        
        if($returnRun) {
            if(!$keepThread) {
                $this->thread = null;
            }
            return $run;
        }
        
        $response = new AssistantResponse($run->result());
        if(!$keepThread) {
            $this->thread = null;
        }
        if($concatenate === false) {
            return $response;
        }
        if($concatenate === true) {
            $concatenate = "\n";
        }
        $output = "";
        foreach($response as $message) {
            $output .= $message->value().$concatenate;
        }
        return $output;
    }

    public function output($concatenate=false) {
        return $this->run($concatenate);
    }
    public function json($asArray=false) {
        $output = trim($this->output("\n"));
        if(substr($output,0,7)=="```json") {
            $output = ltrim(substr($output,7));
        }
        if(substr($output,-3)=="```") {
            $output = rtrim(substr($output,0,-3));
        }
        if($asArray) {
            return json_decode($output, true);
        }
        return $output;
    }
    public function addMessage($role,$content,$attachments=[],$metadata=[]) {
        if(!$this->thread) {
            $this->thread = new OpenAIThread();
        }
        $this->thread->addMessage($role,$content,$attachments,$metadata);
    }

    public function compress($excludeLastN=0) {
        $compress = $this->thread->compress($excludeLastN);
        return $compress;
    }
    

    /**
     * 
     * @param $name string
     * @param $useTableCache bool
     * @return OpenAIAssistant|false
     */
    public static function fromName($name,$useTableCache=true) {
        $assistant = false;
        $assistant = AssistantsAPI::byName($name);
        if($assistant) {
            return $assistant;
        }
        if(OpenAI::config("assistants.$name",false)) {
            $assistant = static::fromId(OpenAI::config("assistants.$name"));
            if($assistant) {
                return $assistant;
            }
        }
        return false;
    }

    private static $assistantCache = [];
    public static function fromOpenAI($assistantNameOrId) {
        if(isset(static::$assistantCache[$assistantNameOrId])) {
            $assistant = clone static::$assistantCache[$assistantNameOrId];
            return $assistant;
        }
        try {
            $assistant = AssistantsAPI::getAssistant($assistantNameOrId);
        } catch(Exception $e) {
            $assistant = false;
        }
        if($assistant) {
            static::$assistantCache[$assistantNameOrId] = $assistant;
            return $assistant;
        }
        $assistant = AssistantsAPI::byName($assistantNameOrId);
        if($assistant) {
            static::$assistantCache[$assistantNameOrId] = $assistant;
            return $assistant;
        }
        return $assistant;
    }

    /**
     * 
     * @param $id string
     * @param $useTableCache bool
     * @return OpenAIAssistant|false
     */
    public static function fromId($id) {
        $assistant = false;
        if(strlen($id) >= 50) {
            return false;
        }
        $assistant = AssistantsAPI::getAssistant($id);
        return $assistant ? $assistant : false;
    }

    public function toArray() {
        return [
            "id" => $this->id,
            "object" => $this->object,
            "created_at" => $this->createdAt,
            "name" => $this->name,
            "description" => $this->description,
            "model" => $this->model,
            "instructions" => $this->instructions,
            "tools" => $this->tools,
            "file_ids" => $this->fileIds,
            "metadata" => $this->metadata
        ];
    }

    public function toFile($fileName) {
        file_put_contents($fileName,json_encode($this->toArray(),JSON_PRETTY_PRINT));
    }
    public function readFile($fileName) {
        if(file_exists($fileName)) {
            $data = json_decode(file_get_contents($fileName),true);
            foreach($data as $key => $value) {
                $this->$key = $value;
            }
        }
    }
    public static function fromFile($fileName,$forceCreate=false) {
        $data = json_decode(file_get_contents($fileName),true);
        $assistant = self::fromArray($data,$forceCreate);
        return $assistant;
    }
    public static function fromArray($data,$forceCreate=false) {
        if(is_object($data)) {
            $data = $data->toArray();
        }
        if(!is_array($data)) {
            $data = json_decode($data,true);
        }
        if(!is_array($data)) {
            throw new \Exception("Input for Assistant::fromArray must be an array, json_string, or an object that can be converted to an array.");
        }
        foreach($data as $key => $value) {
            if($value === null) {
                unset($data[$key]);
            }
        }
        if(!$forceCreate && isset($data["id"])) {
            return new OpenAIAssistant($data);
        }
        if(isset($data["id"])) {
            unset($data["id"]);
        }
        if(isset($data["created_at"])) {
            unset($data["created_at"]);
        }
        $assistant = new OpenAIAssistant($data);
        $assistant->save();
        return $assistant;
    }
}