<?php
namespace boru\boruai;

use boru\boruai\Cli\BoruAICLI;
use boru\boruai\Models\ChatSession;
use boru\boruai\Models\Document;
use boru\boruai\Models\File;
use boru\boruai\Models\Queue;
use boru\boruai\Models\ToolDefinition;
use boru\boruai\OCR\VtigerDocument;
use boru\boruai\Tools\OCR;
use boru\boruai\Vtiger\Vtiger;
use boru\dhprocess\Task;
use boru\dhprocess\TaskQueue;
use boru\dhprocess\Threader;
use boru\dhutils\dhGlobal;
use boru\output\Output;
use boru\qdrant\Qdrant;
use boru\dhdb\dhDB;
use boru\query\Factory;
use boru\borumcp\Authenticators\JWTAuthenticator;
use boru\borumcp\Core\JWTMinter;

class BoruAI {
    private static $isInit = false;
    private static $debugMode = false;
    private static $debugHttp = false;
    private static $debugTools = false;

    private static $jwtAuthenticator = null;
    private static $jwtSettings = [
        "algo" => "HS256",
        "secret" => "boruAI-secret-here",
        "iss" => "boruAI-issuer",
        "aud" => "boruAI-audience",
        "kid" => null,
        "ttl" => 300
    ];

    /** @var \boru\queue\Queue[] */
    private static $boruQueues = [];

    private static $defaultModel = "gpt-4.1-mini";

    public static function init($fileOrarray=null,$save=false) {
        if($fileOrarray === null) {
            if(!defined("BORUAI_ROOT")) {
                define("BORUAI_ROOT",false);
            }
            if(defined("BORUAI_ROOT") && BORUAI_ROOT !== false) {
                $fileOrarray = BORUAI_ROOT."/config.json";
            } else {
                $fileOrarray = "config.json";
            }
        }
        if(is_array($fileOrarray)) {
            Config::loadManualConfig($fileOrarray);
        } else {
            Config::init($fileOrarray,$save);
        }
        if(!static::isInit()) {
            static::registerEntities();
        }
        static::$isInit=true;
    }
    public static function isInit() {
        return self::$isInit;
    }
    public static function verifyInit() {
        if(!static::isInit()) {
            throw new \Exception("OpenAI is not initialized. Please call OpenAI::init(\$api_key,\$api_version) first.");
        }
    }

    public static function outputDir($dir=null) {
        if($dir) {
            return Config::set("outputdir",$dir);
        }
        return Config::get("outputdir", __DIR__.'/../output/');
    }

    public static function bootstrapFile($file=null) {
        if($file) {
            return Config::set("threaderOptions.bootstrapFile",$file);
        }
        $rootDir = static::projectRoot();
        $bootstrap = $rootDir."/vendor/autoload.php";
        return Config::get("threaderOptions.bootstrapFile", $bootstrap);
    }
    public static function defaultModel($name,$default=null) {
        if($name) {
            $model = Config::get("defaultModels.$name",$default);
            if(!$model || !is_string($model)) {
                $model = Config::get("defaultModels.default",static::$defaultModel);
            }
            return $model;
        }
        return Config::get("defaultModels.default",$default);
    }
    public static function config($key,$default=null) {
        return Config::get($key,$default);
    }
    public static function setConfig($key,$value) {
        return Config::set($key,$value);
    }
    public static function saveConfig() {
        return Config::save();
    }
    public static function table($tableName) {
        return Config::get("tables.$tableName",false);
    }
    public static function socketConfig($key=null,$default=null) {
        if($key === null) {
            return Config::get("socket",$default);
        }
        return Config::get("socket.$key",$default);
    }
    public static function isCLI() {
        return php_sapi_name() == "cli";
    }

    public static function debug($debug=null) {
        if($debug !== null) {
            self::$debugMode = $debug ? true : false;
        }
        return self::$debugMode ? true : false;
    }
    public static function debugHttp($debug=null) {
        if($debug !== null) {
            self::$debugHttp = $debug ? true : false;
        }
        return self::$debugHttp ? true : false;
    }
    public static function debugTools($debug=null) {
        if($debug !== null) {
            self::$debugTools = $debug ? true : false;
        }
        return self::$debugTools ? true : false;
    }

    public static function printDebug(...$messages) {
        if(static::debug() || Config::get("debug",false)) {
            array_unshift($messages,"[DEBUG]");
            Output::outLine(...$messages);
        }
    }
    public static function printHttpDebug(...$messages) {
        if(static::debugHttp()) {
            array_unshift($messages,"[HTTP]");
            Output::outLine(...$messages);
        }
    }
    public static function printToolDebug(...$messages) {
        if(static::debugTools()) {
            array_unshift($messages,"[TOOL]");
            Output::outLine(...$messages);
        }
    }

    /**
     * @param string $path
     * @param array $paramaters
     * @return dhDB
     * @throws \Exception
     */
    public static function db($dbconfig=null) {
        if($dbconfig) {
            return dhGlobal::db($dbconfig);
        }
        if(!dhGlobal::db() && Config::get("dbconfig.use",false)) {
            return dhGlobal::db(Config::get("dbconfig"));
        }
        return dhGlobal::db();
    }

    public static function redis($redisconfig=null) {
        $predisClassname = "\\Predis\\Client"; // to avoid VSCode error
        if(!class_exists($predisClassname)) {
            throw new \Exception("Predis not found. Please install predis/predis");
        }
        if($redisconfig) {
            $redis = new $predisClassname($redisconfig);
            dhGlobal::set("redis",$redis);
            return $redis;
        }
        if(($redis= dhGlobal::get("redis",false)) !== false) {
            return $redis;
        }
        if(Config::get("redis")) {
            $config = Config::get("redis");
            if(is_array($config) && isset($config["passwrod"]) && !empty($config["password"])) {
                if(is_array($config["password"])) {
                    $arr = $config["password"];
                    unset($config["password"]);
                    $config["username"] = $arr[0];
                    $config["password"] = $arr[1];
                }
            }
            $redis = new $predisClassname($config);
            dhGlobal::set("redis",$redis);
            return $redis;
        }
    }

    /**
     * @param array $qdrantconfig
     * @return \boru\qdrant\Qdrant
     */
    public static function qdrant($qdrantconfig=null) {
        if($qdrantconfig) {
            $qdrant = new \boru\qdrant\Qdrant($qdrantconfig);
            dhGlobal::set("qdrant",$qdrant);
            return $qdrant;
        }
        if(($qdrant = dhGlobal::get("qdrant",false)) !== false) {
            return $qdrant;
        }
        if(Config::get("qdrant")) {
            $config = Config::get("qdrant");
            $qdrant = new \boru\qdrant\Qdrant($config);
            dhGlobal::set("qdrant",$qdrant);
            return $qdrant;
        }
    }

    /**
     * 
     * @param mixed $toolName 
     * @return ToolDefinition|mixed|object|false 
     */
    public static function loadTool($toolName) {
        if($toolName instanceof ToolDefinition) {
            return $toolName;
        }
        $className = "\\boru\\boruai\\Models\\Functions\\".$toolName;
        if(class_exists($className) && method_exists($className,"functionDefinitions")) {
            return $className::functionDefinitions();
        }
        if(class_exists($toolName) && method_exists($toolName,"functionDefinitions")) {
            return $toolName::functionDefinitions();
        }
        //fallback for typo classes..
        if(class_exists($className) && method_exists($className,"functionDefintions")) {
            return $className::functionDefintions();
        }
        if(class_exists($toolName) && method_exists($toolName,"functionDefintions")) {
            return $toolName::functionDefintions();
        }
        //else if class exists and is a tool
        if(class_exists($toolName) && (is_subclass_of($toolName,'\\boru\\boruai\\Models\\ToolDefinition') || is_a($toolName, '\\boru\\boruai\\Models\\ToolDefinition')) ) {
            return new $toolName();
        }
        return false;
    }

    private static $threaderInit = false;
    public static function initTaskQueue($options=[]) {
        if(static::$threaderInit) {
            return;
        }
        $defaultOptions = Config::get("threaderOptions",[]);
        $options = array_merge($defaultOptions,$options);
        TaskQueue::init($options);
        static::$threaderInit = true;
        static::registerFunctions();
    }
    public static function run_dococr($docId) {
        $document = new Document($docId);
        $document->ocr(false);
    }
    public static function run_upload($fileId) {
        $file = new File($fileId);
        return $file->upload();
    }
    public static function run_ocr($fileId,$options=[]) {
        $file = new File($fileId);
        $ocr = new OCR($file,$options);
        $result = $ocr->run();
        if($result) {
            $file->setOcr($result,$ocr->tokens());
        }
    }
    public static function registerFunctions() {
        $uploadFunction = function($fileId) {
            return BoruAI::run_upload($fileId);
        };
        $ocrFunction = function($fileId,$options=[]) {
            return BoruAI::run_ocr($fileId,$options);
        };
        $docOcrFunction = function($fileId,$options=[]) {
            return BoruAI::run_dococr($fileId,$options);
        };
        Task::register("upload",$uploadFunction);
        Task::register("ocr",$ocrFunction);
        Task::register("dococr",$docOcrFunction);
    }
    public static function threader($items,$callBack=null,$options=[]) {
        static::initTaskQueue();
        $defaultOptions = Config::get("threaderOptions",[]);
        $options = array_merge($defaultOptions,$options);
        return Threader::execute($items,$callBack,$options);
    }

    private static function projectRoot() {
        if(isset($GLOBALS['_composer_bin_dir'])) {
            $rootDir = realpath($GLOBALS['_composer_bin_dir']."/../../");
        } else {
            $dir = __DIR__;
            $myParent = "/vendor/boru/boruai/bin";
            if(substr($dir,-strlen($myParent))==$myParent) {
                //included as a dependency
                $rootDir = realpath($dir."/../../../../");
            } else {
                //in the boruai repository
                $rootDir = realpath($dir."/../");
            }
        }
        return $rootDir;
    }

    public static function registerEntities() {
        Factory::register(ChatSession::entityDefinition());
        Factory::register(Queue::entityDefinition());
        Factory::register(File::entityDefinition());
        Factory::register(Document::entityDefinition());
    }

    public static function executeCommand($commands=[]) {
        if(!is_array($commands)) {
            $commands = explode(" ",$commands);
        }
        BoruAICLI::parse($commands);
    }

    public static function jwtSettings($settings = null) {
        if($settings && is_array($settings)) {
            static::$jwtSettings = array_merge(static::$jwtSettings, $settings);
        }
        return static::$jwtSettings;
    }
    public static function jwtAuthenticator() {
        if(!static::$jwtAuthenticator) {
            $jwtSettings = static::jwtSettings();
            $algo = isset($jwtSettings["algo"]) ? $jwtSettings["algo"] : "HS256";
            $secret = isset($jwtSettings["secret"]) ? $jwtSettings["secret"] : "boruAI-secret-here";
            $iss = isset($jwtSettings["iss"]) ? $jwtSettings["iss"] : "boruAI-issuer";
            $aud = isset($jwtSettings["aud"]) ? $jwtSettings["aud"] : "boruAI-audience";
            $kid = isset($jwtSettings["kid"]) ? $jwtSettings["kid"] : null;
            $ttl = isset($jwtSettings["ttl"]) ? $jwtSettings["ttl"] : 300;
            static::$jwtAuthenticator = new JWTAuthenticator($algo, $secret, $iss, $aud, $kid, $ttl);
        }
        return static::$jwtAuthenticator;
    }

    /**
     * 
     * @return JWTMinter 
     */
    public static function jwtMinter() {
        $authenticator = static::jwtAuthenticator();
        return $authenticator->getMinter();
    }
    public static function jwtMint($claimsExtra = [], $ttlOverride = null) {
        $minter = static::jwtMinter();
        return $minter->mint($claimsExtra, $ttlOverride);
    }

    public static function mcpServer($port=9000,$tools=[],$authOptions=[]) {
        if(!empty($authOptions)) {
            static::jwtSettings($authOptions);
        }
        if($tools instanceof \boru\borumcp\ToolRegistry) {
            $toolRegistry = $tools;
        } else {
            $toolRegistry = new \boru\borumcp\ToolRegistry();
            if(is_array($tools)) {
                foreach($tools as $tool) {
                    $toolRegistry->register($tool);
                }
            }
        }
        return new \boru\borumcp\Server("127.0.0.1",$port,$toolRegistry,static::jwtAuthenticator());
    }

    public static function mcpUrl($url=null) {
        if($url) {
            return Config::set("mcp_url",$url);
        }
        return Config::get("mcp_url","http://localhost:8000");
    }

    /**
     * 
     * @param mixed $queueName 
     * @param mixed $bootstrapFile 
     * @return \boru\queue\Queue 
     */
    public static function initBoruQueue($queueName,$bootstrapFile=null) {
        $db = static::db();
        
        $queue = new \boru\queue\Queue();
        $queue->withDbStorage($db, 'boru_ai_queue_items');
        $queue->setQueueName($queueName);
        if($bootstrapFile === null) {
            $bootstrapFile = static::bootstrapFile();
        }
        $queue->withBootstrap($bootstrapFile);
        self::registerTasksForQueue($queue);
        $queue->configureWorker();
        self::$boruQueues[$queueName] = $queue;
        return $queue;
    }

    /**
     * 
     * @param mixed $queueName 
     * @return \boru\queue\Queue 
     * @throws \Exception 
     */
    public static function getBoruQueue($queueName) {
        if(!isset(self::$boruQueues[$queueName])) {
            throw new \Exception("Boru Queue not initialized: ".$queueName);
        }
        return self::$boruQueues[$queueName] ? self::$boruQueues[$queueName] : null;
    }

    private static function registerTasksForQueue($queue) {
        VtigerDocument::registerTask($queue, 'vtiger_document_ocr');
    }

    public static function resetStuckQueueItems($queueName,$maxAge=3600) {
        $db = static::db();
        $queueTable = static::table("queue");
        $sql = "UPDATE ".$queueTable." SET `status`=0 WHERE `status`=1 AND `updated_at`<=DATE_SUB(NOW(),INTERVAL ? SECOND)";
        $db->run($sql, [$maxAge]);
    }

    public static function cleanResponses($limit=null,$maxId=null) {
        $deleted = 0;
        $db = static::db();
        $responsesTable = static::table("responses");
        $sql = "select id from ".$responsesTable;
        $params = [];
        if($maxId !== null) {
            $sql.=" WHERE `id`<=?";
            $params[] = $maxId;
        }
        if($limit !== null) {
            $sql.=" LIMIT ?";
            $params[] = (int)$limit;
        }
        if($maxId === null && $limit === null) {
            $sql.=" WHERE `created_at`<=DATE_SUB(NOW(),INTERVAL 7 DAY)";
        }
        $result = $db->run($sql, $params);
        $ids = [];
        while($row=$db->next($result)) {
            $deleted++;
            $ids[] = $row['id'];
            if(count($ids) >= 20) {
                $sql = "DELETE FROM ".$responsesTable." WHERE id IN (".implode(",",$ids).")";
                $db->run($sql);
                $ids = [];
            }
        }
        if(count($ids) >= 20) {
            $sql = "DELETE FROM ".$responsesTable." WHERE id IN (".implode(",",$ids).")";
            $db->run($sql);
            $ids = [];
        }
        return $deleted;
    }
}