<?php

function get_global_sp_state()
{
	static $global_promotion_state;
	$cacheKey = \App\Models\Setting::TORRENT_GLOBAL_STATE_CACHE_KEY;
	if (is_null($global_promotion_state)) {
        $row = \Nexus\Database\NexusDB::remember($cacheKey, 600, function () use ($cacheKey) {
            return \Nexus\Database\NexusDB::getOne('torrents_state', 1);
        });
        if (is_array($row) && isset($row['deadline']) && $row['deadline'] < date('Y-m-d H:i:s')) {
            //expired
            $global_promotion_state = \App\Models\Torrent::PROMOTION_NORMAL;
        } elseif (is_array($row) && isset($row['begin']) && $row['begin'] > date('Y-m-d H:i:s')) {
            //Not begin
            $global_promotion_state = \App\Models\Torrent::PROMOTION_NORMAL;
        } elseif (is_array($row)) {
            $global_promotion_state = $row["global_sp_state"];
        } else {
            $global_promotion_state = $row;
        }
	}
	return $global_promotion_state;
}

// IP Validation
function validip($ip)
{
	if (!ip2long($ip)) //IPv6
		return true;
	if (!empty($ip) && $ip == long2ip(ip2long($ip)))
	{
		// reserved IANA IPv4 addresses
		// http://www.iana.org/assignments/ipv4-address-space
		$reserved_ips = array (
		array('192.0.2.0','192.0.2.255'),
		array('192.168.0.0','192.168.255.255'),
		array('255.255.255.0','255.255.255.255')
		);

		foreach ($reserved_ips as $r)
		{
			$min = ip2long($r[0]);
			$max = ip2long($r[1]);
			if ((ip2long($ip) >= $min) && (ip2long($ip) <= $max)) return false;
		}
		return true;
	}
	else return false;
}

function getip($real = true) {
	if (isset($_SERVER)) {
		if (isset($_SERVER['HTTP_X_FORWARDED_FOR']) && validip($_SERVER['HTTP_X_FORWARDED_FOR'])) {
			$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
		} elseif (isset($_SERVER['HTTP_CLIENT_IP']) && validip($_SERVER['HTTP_CLIENT_IP'])) {
			$ip = $_SERVER['HTTP_CLIENT_IP'];
		} else {
			$ip = $_SERVER['REMOTE_ADDR'] ?? '';
		}
	} else {
		if (getenv('HTTP_X_FORWARDED_FOR') && validip(getenv('HTTP_X_FORWARDED_FOR'))) {
			$ip = getenv('HTTP_X_FORWARDED_FOR');
		} elseif (getenv('HTTP_CLIENT_IP') && validip(getenv('HTTP_CLIENT_IP'))) {
			$ip = getenv('HTTP_CLIENT_IP');
		} else {
			$ip = getenv('REMOTE_ADDR') ?? '';
		}
	}
    $ip = trim(trim($ip), ",");
    if ($real && str_contains($ip, ",")) {
        return strstr($ip, ",", true);
    }
	return $ip;
}

function sql_query($query)
{
	$begin = microtime(true);
	global $query_name;
	$result = mysql_query($query);
	$end = microtime(true);
	$query_name[] = [
		'query' => $query,
		'time' => sprintf('%.2f ms', ($end - $begin) * 1000),
	];
	return $result;
}

function sqlesc($value) {
	if (is_null($value)) {
		return 'null';
	}
	$value = "'" . mysql_real_escape_string($value) . "'";
	return $value;
}

function hash_pad($hash) {
    return str_pad($hash, 20);
}

function hash_where($name, $hash) {
//	$shhash = preg_replace('/ *$/s', "", $hash);
//	return "($name = " . sqlesc($hash) . " OR $name = " . sqlesc($shhash) . ")";
//	return sprintf("$name in (%s, %s)", sqlesc($hash), sqlesc($shhash));
    return "$name = " . sqlesc($hash);
}

//no need any more...
/*
function strip_magic_quotes($arr)
{
	foreach ($arr as $k => $v)
	{
		if (is_array($v))
		{
			$arr[$k] = strip_magic_quotes($v);
		} else {
			$arr[$k] = stripslashes($v);
		}
	}
	return $arr;
}

if (function_exists('get_magic_quotes_gpc') && get_magic_quotes_gpc())
{
	if (!empty($_GET)) {
		$_GET = strip_magic_quotes($_GET);
	}
	if (!empty($_POST)) {
		$_POST = strip_magic_quotes($_POST);
	}
	if (!empty($_COOKIE)) {
		$_COOKIE = strip_magic_quotes($_COOKIE);
	}
}
*/

function get_langfolder_list()
{
	//do not access db for speed up, or for flexibility
//	return array("en", "chs", "cht", "ko", "ja");
    return \App\Models\Language::listAvailable();
}

function printLine($line, $exist = false)
{
	echo "[" . date('Y-m-d H:i:s') . "] $line<br />";
	if ($exist) {
		exit(0);
	}
}

function nexus_dd($vars)
{
    echo '<pre>';
    array_map(function ($var) {
        var_dump($var);
    }, func_get_args());
    echo '</pre>';
    exit(0);
}

/**
 * write log, use in both pure nexus and inside laravel
 *
 * @param $log
 * @param string $level
 */
function do_log($log, $level = 'info', $echo = false)
{
    static $env, $setLogLevel;
    if (is_null($setLogLevel)) {
        $setLogLevel = nexus_env('LOG_LEVEL', 'debug');
    }
    if (is_null($env)) {
        $env = nexus_env('APP_ENV', 'production');
    }
    $logLevels = ['debug', 'info', 'notice', 'warning', 'error', 'critical', 'alert', 'emergency'];
    $setLogLevelKey = array_search($setLogLevel, $logLevels);
    $currentLogLevelKey = array_search($level, $logLevels);
    if ($currentLogLevelKey === false) {
        $level = 'error';
        $log = "[ERROR_LOG_LEVEL] $log";
        $currentLogLevelKey = array_search($level, $logLevels);
    }
    if ($currentLogLevelKey < $setLogLevelKey) {
        return;
    }

    $logFile = getLogFile();
	if (($fd = fopen($logFile, 'a')) === false) {
	    $log .= "--------Can not open $logFile";
        $fd = fopen(sys_get_temp_dir() . '/nexus.log', 'a');
	}
	$uid = 0;
    if (IN_NEXUS) {
        global $CURUSER;
        $user = $CURUSER;
        $uid = $user['id'] ?? 0;
        $passkey = $user['passkey'] ?? $_REQUEST['passkey'] ?? $_REQUEST['authkey'] ?? '';
    } else {
        try {
            $user = \Illuminate\Support\Facades\Auth::user();
            $uid = $user->id ?? 0;
            $passkey = $user->passkey ?? request('passkey', request('authkey', ''));
        } catch (\Throwable $exception) {
            $passkey = "!IN_NEXUS:" . $exception->getMessage();
        }
    }
    $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
    $content = sprintf(
        "[%s] [%s] [%s] [%s] [%s] [%s] %s.%s %s:%s %s%s%s %s%s",
        getDtMillis(true),
        nexus() ? nexus()->getRequestId() : 'NO_REQUEST_ID',
        nexus() ? nexus()->getLogSequence() : 0,
        sprintf('%.3f', microtime(true) - (nexus() ? nexus()->getStartTimestamp() : 0)),
        $uid,
        $passkey,
        $env, strtoupper($level),
        $backtrace[0]['file'] ?? '',
        $backtrace[0]['line'] ?? '',
        $backtrace[1]['class'] ?? '',
        $backtrace[1]['type'] ?? '',
        $backtrace[1]['function'] ?? '',
        $log,
        PHP_EOL
    );
    fwrite($fd, $content);
    fclose($fd);
    if (is_bool($echo) && $echo) {
        echo $content . PHP_EOL;
    }
    if (nexus()) {
        nexus()->incrementLogSequence();
    }
}

function getDtMillis($withTimeZone = false): string {
    $dt = DateTime::createFromFormat('U.u', sprintf('%.6f', microtime(true)));
    $dt->setTimezone(new DateTimeZone(nexus_env('TIMEZONE', 'UTC')));
    $format = $withTimeZone ? 'Y-m-d\TH:i:s.vP' : 'Y-m-d H:i:s.v';
    return $dt->format($format);
}

function getDtMicro($withTimeZone = false): string {
    $dt = DateTime::createFromFormat('U.u', sprintf('%.6f', microtime(true)));
    $dt->setTimezone(new DateTimeZone(nexus_env('TIMEZONE', 'UTC')));
    $format = $withTimeZone ? 'Y-m-d\TH:i:s.uP' : 'Y-m-d H:i:s.u';
    return $dt->format($format);
}

function getLogFile($append = '')
{
    static $logFiles = [];
    if (isset($logFiles[$append])) {
        return $logFiles[$append];
    }
    $std = ["php://stdout", "php://stderr"];
    $logFileFromDotEnv = nexus_env('LOG_FILE');
    if ($logFileFromDotEnv && in_array($logFileFromDotEnv, $std)) {
        return $logFiles[$append] = $logFileFromDotEnv;
    }
    $path = getenv('NEXUS_LOG_DIR', true);
    if (in_array($path, $std)) {
        return $logFiles[$append] = $path;
    }
    $fromEnv = true;
    if ($path === false) {
        $fromEnv = false;
        $path = sys_get_temp_dir();
    }
    $logFile = rtrim($path, '/') . '/nexus.log';
    if (!$fromEnv && $logFileFromDotEnv) {
        $logFile = $logFileFromDotEnv;
    }
    $lastDotPos = strrpos($logFile, '.');
    if ($lastDotPos !== false) {
        $prefix = substr($logFile, 0, $lastDotPos);
        $suffix = substr($logFile, $lastDotPos);
    } else {
        $prefix = $logFile;
        $suffix = '';
    }
    $name = $prefix;
    if ($append) {
        $name .= "-$append";
    }
    if (isRunningInConsole()) {
        $scriptUserInfo = posix_getpwuid(posix_getuid());
        $name .= sprintf("-cli-%s", $scriptUserInfo['name']);
    }
    $name .= "-" . date('Y-m-d');
    return $logFiles[$append] = $name . $suffix;

}

function nexus_config($key, $default = null)
{
    if (!IN_NEXUS) {
        return config($key, $default);
    }
    static $configs;
    if (is_null($configs)) {
        //get all configuration from config file
//		$files = glob(ROOT_PATH . 'config/*.php');
        $files = [
            ROOT_PATH . 'config/nexus.php',
            ROOT_PATH . 'config/emoji.php',
        ];
        foreach ($files as $file) {
            $basename = basename($file);
            if ($basename == 'allconfig.php') {
                //exclude the NexusPHP default config file
                continue;
            }
            $values = require $file;
            $configPrefix = strstr($basename, '.php', true);
            $configs[$configPrefix] = $values;
        }
    }
    return arr_get($configs, $key, $default);
}


/**
 * get setting for given name and prefix
 *
 * @date 2021/1/11
 * @param null $name
 * @param null $default
 * @return mixed
 */
function get_setting($name = null, $default = null): mixed
{
	static $settings;
	if (is_null($settings)) {
        $settings = \Nexus\Database\NexusDB::remember("nexus_settings_in_nexus", 600, function () {
            //get all settings from database
            return \App\Models\Setting::getFromDb();
        });
	}
	if (is_null($name)) {
	    return $settings;
    }
    return arr_get($settings, $name, $default);
}

function get_setting_from_db($name = null, $default = null)
{
    static $final;
    if (is_null($final)) {
        $final = \App\Models\Setting::getFromDb();
    }
    if (is_null($name)) {
        return $final;
    }
    return arr_get($final, $name, $default);
}


function nexus_env($key = null, $default = null)
{
    static $env;
    if (is_null($env)) {
        $envFile = dirname(__DIR__) . '/.env';
        $env = readEnvFile($envFile);
    }
    if (is_null($key)) {
        return $env;
    }
    return $env[$key] ?? $default;
}

function readEnvFile($envFile)
{
    if (!file_exists($envFile)) {
        if (php_sapi_name() == 'cli') {
            return [];
        }
        throw new \RuntimeException("env file : $envFile is not exists in the root path.");
    }
    $env = [];
    $fp = fopen($envFile, 'r');
    if ($fp === false) {
        throw new \RuntimeException(".env file: $envFile is not readable.");
    }
    while (($line = fgets($fp)) !== false) {
        $line = trim($line);
        if (empty($line)) {
            continue;
        }
        $pos = strpos($line, '=');
        if ($pos <= 0) {
            continue;
        }
        if (mb_substr($line, 0, 1, 'utf-8') == '#') {
            continue;
        }
        $lineKey = normalize_env(mb_substr($line, 0, $pos, 'utf-8'));
        $lineValue = normalize_env(mb_substr($line, $pos + 1, null, 'utf-8'));
        $env[$lineKey] = $lineValue;
    }
    return $env;
}

function normalize_env($value)
{
	$value = trim($value);
	$toStrip = ['\'', '"'];
	if (in_array(mb_substr($value, 0, 1, 'utf-8'), $toStrip)) {
		$value = mb_substr($value, 1, null, 'utf-8');
	}
	if (in_array(mb_substr($value, -1, null,'utf-8'), $toStrip)) {
		$value = mb_substr($value, 0, -1, 'utf-8');
	}
	switch (strtolower($value)) {
		case 'true':
			return true;
		case 'false':
			return false;
		case 'null':
			return null;
		default:
			return $value;
	}
}

/**
 * Get an item from an array using "dot" notation.
 *
 * reference to Laravel
 *
 * @date 2021/1/14
 * @param $array
 * @param $key
 * @param null $default
 * @return mixed|null
 */
function arr_get($array, $key, $default = null)
{
	if (strpos($key, '.') === false) {
		return $array[$key] ?? $default;
	}
	foreach (explode('.', $key) as $segment) {
		if (isset($array[$segment])) {
			$array = $array[$segment];
		} else {
			return $default;
		}
	}
	return $array;
}

/**
 * From Laravel
 *
 * Set an array item to a given value using "dot" notation.
 *
 * If no key is given to the method, the entire array will be replaced.
 *
 * @param  array  $array
 * @param  string|null  $key
 * @param  mixed  $value
 * @return array
 */
function arr_set(&$array, $key, $value)
{
    if (is_null($key)) {
        return $array = $value;
    }

    $keys = explode('.', $key);

    foreach ($keys as $i => $key) {
        if (count($keys) === 1) {
            break;
        }

        unset($keys[$i]);

        // If the key doesn't exist at this depth, we will just create an empty array
        // to hold the next value, allowing us to create the arrays to hold final
        // values at the correct depth. Then we'll keep digging into the array.
        if (! isset($array[$key]) || ! is_array($array[$key])) {
            $array[$key] = [];
        }

        $array = &$array[$key];
    }

    $array[array_shift($keys)] = $value;

    return $array;
}

function isHttps(): bool
{
    if (isRunningInConsole()) {
        $securityLogin = get_setting("security.securelogin");
        if ($securityLogin != "no") {
            return true;
        }
        return false;
    }
    return nexus()->getRequestSchema() == 'https';
}


function getSchemeAndHttpHost(bool $fromConfig = false): string
{
    if (isRunningInConsole() || $fromConfig) {
        $host = get_setting("basic.BASEURL");
    } else {
        $host = nexus()->getRequestHost();
    }
    $isHttps = isHttps();
    $protocol = $isHttps ? 'https' : 'http';
    return "$protocol://" . $host;
}

function getBaseUrl()
{
    $url = getSchemeAndHttpHost();
    if (!isRunningInConsole()) {
        $requestUri = $_SERVER['REQUEST_URI'];
        $pos = strpos($requestUri, '?');
        if ($pos !== false) {
            $url .= substr($requestUri, 0, $pos);
        } else {
            $url .= $requestUri;
        }
    }
    return trim($url, '/');
}


function nexus_json_encode($data)
{
    return json_encode($data, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
}

function api(...$args)
{
    do_log("api begin");
    if (func_num_args() < 3) {
        //参数少于3个时，默认为错误状态。
        $ret = -1;
        $msg = isset($args[0]) ? $args[0] : 'ERROR';
        $data = isset($args[1]) ? $args[1] : [];
    } else {
        $ret = $args[0];
        $msg = $args[1];
        $data = $args[2];
    }
    if ($data instanceof \Illuminate\Http\Resources\Json\JsonResource) {
        $data = $data->response()->getData(true);
    }
    do_log("api after prepare data");
//    dd($data);
    $time = (float)number_format(microtime(true) - nexus()->getStartTimestamp(), 3);
    $count = null;
    $resultKey = 'ret';
    $msgKey = 'msg';
    $format = $_REQUEST['__format'] ?? '';
    if (in_array($format, ['layui-table', 'data-table'])) {
        $resultKey = 'code';
        $count = $data['meta']['total'] ?? 0;
        if (isset($data['data'])) {
            $data = $data['data'];
        }
    }
    $results = [
        $resultKey => (int)$ret,
        $msgKey => (string)$msg,
        'data' => $data,
        'time' => $time,
        'rid' => nexus()->getRequestId(),
    ];
    if ($format == 'layui-table') {
        $results['count'] = $count;
    }
    if ($format == 'data-table') {
        $results['draw'] = intval($_REQUEST['draw'] ?? 1);
        $results['recordsTotal'] = $count;
        $results['recordsFiltered'] = $count;
    }
    if (!IN_NEXUS && config('app.debug')) {
        $results['queries'] = last_query(true);
    }
    do_log("api end");
    return $results;
}

function success(...$args)
{
    $ret = 0;
    $msg = 'OK';
    $data = [];
    $argumentCount = func_num_args();
    if ($argumentCount == 1) {
        $data = $args[0];
    } elseif ($argumentCount == 2) {
        $msg = $args[0];
        $data = $args[1];
    }
    do_log("success before api");
    return api($ret, $msg, $data);
}

function fail(...$args)
{
    $ret = -1;
    $msg = 'ERROR';
    $data = [];
    $argumentCount = func_num_args();
    if ($argumentCount == 1) {
        $data = $args[0];
    } elseif ($argumentCount == 2) {
        $msg = $args[0];
        $data = $args[1];
    }
    return api($ret, $msg, $data);
}

function last_query($all = false, $format = 'json')
{
    static $connection;
    if (is_null($connection)) {
        if (IN_NEXUS) {
            $connection = \Illuminate\Database\Capsule\Manager::connection(\Nexus\Database\NexusDB::ELOQUENT_CONNECTION_NAME);
        } else {
            $connection = \Illuminate\Support\Facades\DB::connection(config('database.default'));
        }
    }
    if ($all === 'COUNT') {
        return count($connection->getQueryLog());
    }
    $queries = $connection->getRawQueryLog();
    if ($all) {
        return $queries;
    }
    if (empty($queries)) {
        return '';
    }
    $last = last($queries);
    if ($format === 'json') {
        return nexus_json_encode($last);
    }
    return $last;
}

function format_datetime($datetime, $format = 'Y-m-d H:i')
{
    if (empty($datetime)) {
        return null;
    }
    try {
        $carbonTime = \Carbon\Carbon::parse($datetime);
        return $carbonTime->format($format);
    } catch (\Exception) {
        do_log("Invalid datetime: $datetime", 'error');
        return $datetime;
    }
}

function nexus_trans($key, $replace = [], $locale = null)
{
    return \Nexus\Nexus::trans($key, $replace, $locale);
}

function isRunningInConsole(): bool
{
    return !RUNNING_IN_OCTANE && php_sapi_name() == 'cli';
}

function isRunningOnWindows(): bool
{
    return !RUNNING_IN_OCTANE && strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
}

function command_exists($command): bool
{
    return !(trim(exec("command -v $command")) == '');
}

function get_tracker_schema_and_host($trackerUrlId, $combine = false): array|string
{
    /*
    global $https_announce_urls, $announce_urls;
    $httpsAnnounceUrls = array_filter($https_announce_urls);
    $log = "cookie: " . json_encode($_COOKIE) . ", https_announce_urls: " . json_encode($httpsAnnounceUrls);
    if (
        (isset($_COOKIE["c_secure_tracker_ssl"]) && $_COOKIE["c_secure_tracker_ssl"] == base64("yeah"))
        || !empty($httpsAnnounceUrls)
        || isHttps()
    ) {
        $log .= ", c_secure_tracker_ssl = base64('yeah'): " . base64("yeah") . ", or not empty https_announce_urls, or isHttps()";
        $tracker_ssl = true;
    }  else {
        $tracker_ssl = false;
    }
    $log .= ", tracker_ssl: $tracker_ssl";

    if ($tracker_ssl == true){
        $ssl_torrent = "https://";
        if ($https_announce_urls[0] != "") {
            $log .= ", https_announce_urls not empty, use it";
            $base_announce_url = $https_announce_urls[0];
        } else {
            $log .= ", https_announce_urls empty, use announce_urls[0]";
            $base_announce_url = $announce_urls[0];
        }
    } else {
        $ssl_torrent = "http://";
        $base_announce_url = $announce_urls[0];
    }
    do_log($log);
    if ($combine) {
        return $ssl_torrent . $base_announce_url;
    }
    */
    $log = "tracker_url_id: $trackerUrlId, combine: $combine";
    $url = \App\Models\TrackerUrl::getById($trackerUrlId);
    if (empty($url)) {
        $ssl_torrent = isHttps() ? 'https://' : 'http://';
        $base_announce_url = sprintf(
            "%s/%s",
            trim(\App\Models\Setting::getBaseUrl(), '/'), trim(DEFAULT_TRACKER_URI, '/')
        );
        $log .= ", ById no value";
    } else {
        $ssl_torrent = parse_url($url, PHP_URL_SCHEME) . "://" ;
        $base_announce_url = substr($url, strlen($ssl_torrent));
        $log .= ", ById has value";
    }
    do_log("$log, ssl_torrent: $ssl_torrent, base_announce_url: $base_announce_url");
    if ($combine) {
        return $ssl_torrent .  $base_announce_url;
    }
    return compact('ssl_torrent', 'base_announce_url');
}


function get_hr_ratio($uped, $downed)
{
    if ($downed > 0) {
        $ratio = $uped / $downed;
        $color = get_ratio_color($ratio);
        if ($ratio > 10000) $ratio = 'Inf.';
        else
            $ratio = number_format($ratio, 3);

        if ($color)
            $ratio = "<font color=\"" . $color . "\">" . $ratio . "</font>";
    } elseif ($uped > 0)
        $ratio = 'Inf.';
    else
        $ratio = "---";

    return $ratio;
}

function get_row_count($table, $suffix = "")
{
    $r = sql_query("SELECT COUNT(*) FROM $table $suffix") or sqlerr(__FILE__, __LINE__);
    $a = mysql_fetch_row($r);
    return $a[0];
}

function get_user_row($id)
{
    global $Cache, $CURUSER;
    static $userRows = [];
    static $curuserRowUpdated = false;
    static $neededColumns = array(
        'id', 'noad', 'class', 'enabled', 'privacy', 'avatar', 'signature', 'uploaded', 'downloaded', 'last_access', 'username', 'donor',
        'donoruntil', 'leechwarn', 'warned', 'title', 'downloadpos', 'parked', 'clientselect', 'showclienterror',
    );
    if (isset($userRows[$id])) return $userRows[$id];
    $cacheKey = 'user_'.$id.'_content';
    $row = \Nexus\Database\NexusDB::remember($cacheKey, 3600, function () use ($id, $neededColumns) {
        $user = \App\Models\User::query()->with([
            'wearing_medals' => function ($query) {
                $query->orderBy('user_medals.priority', 'desc')
                    ->orderBy('user_medals.id', 'desc')
                    ->limit(get_setting('system.maximum_number_of_medals_can_be_worn', 3));
            }
        ])->find($id, $neededColumns);
        if (!$user) {
            return null;
        }
        $arr = $user->toArray();
        //Rainbow ID
        $userRep = new \App\Repositories\UserRepository();
        $metas = $userRep->listMetas($id, \App\Models\UserMeta::META_KEY_PERSONALIZED_USERNAME);
        if ($metas->isNotEmpty()) {
            $arr['__is_rainbow'] = 1;
        } else {
            $arr['__is_rainbow'] = 0;
        }
        $arr['__is_donor'] = is_donor($arr);
        return apply_filter("user_row", $arr);
    });

//	if ($CURUSER && $id == $CURUSER['id']) {
//		$row = array();
//		foreach($neededColumns as $column) {
//			$row[$column] = $CURUSER[$column];
//		}
//		if (!$curuserRowUpdated) {
//			$Cache->cache_value('user_'.$CURUSER['id'].'_content', $row, 900);
//			$curuserRowUpdated = true;
//		}
//	} elseif (!$row = $Cache->get_value('user_'.$id.'_content')){
//		$res = sql_query("SELECT ".implode(',', $neededColumns)." FROM users WHERE id = ".sqlesc($id)) or sqlerr(__FILE__,__LINE__);
//		$row = mysql_fetch_array($res);
//		$Cache->cache_value('user_'.$id.'_content', $row, 900);
//	}

    if (!$row)
        return false;
    else return $userRows[$id] = $row;
}

function get_user_class()
{
    if (IN_NEXUS) {
        global $CURUSER;
        return $CURUSER["class"] ?? '';
    }
    return auth()->user()->class;
}

function get_user_id()
{
    if (IN_NEXUS) {
        global $CURUSER;
        return $CURUSER["id"] ?? 0;
    }
    return auth()->user()->id ?? 0;
}

function get_user_passkey()
{
    if (IN_NEXUS) {
        global $CURUSER;
        return $CURUSER["passkey"] ?? "";
    }
    return auth()->user()->passkey ?? "";
}

function get_pure_username()
{
    if (IN_NEXUS) {
        global $CURUSER;
        return $CURUSER["username"] ?? "";
    }
    return auth()->user()->username ?? "";
}

function nexus()
{
    return \Nexus\Nexus::instance();
}

function site_info()
{
    $setting = \App\Models\Setting::get('basic');
    $siteInfo = [
        'site_name' => $setting['SITENAME'],
        'base_url' => getSchemeAndHttpHost(),
    ];
    return $siteInfo;
}

function isIPV4 ($ip)
{
    return filter_var($ip,FILTER_VALIDATE_IP, FILTER_FLAG_IPV4);
}

function isIPV6 ($ip)
{
    return filter_var($ip,FILTER_VALIDATE_IP, FILTER_FLAG_IPV6);
}

function add_filter($name, $function, $priority = 10, $argc = 1)
{
    global $hook;
    $hook->addFilter($name, $function, $priority, $argc);
}

function apply_filter($name, ...$args)
{
    global $hook;
//    do_log("[APPLY_FILTER]: $name");
    return $hook->applyFilter(...func_get_args());
}

function add_action($name, $function, $priority = 10, $argc = 1)
{
    global $hook;
    $hook->addAction($name, $function, $priority, $argc);
}

function do_action($name, ...$args)
{
    global $hook;
//    do_log("[DO_ACTION]: $name");
    return $hook->doAction(...func_get_args());
}

function isIPSeedBoxFromASN($ip, $exceptionWhenYes = false): bool
{
    $redis = \Nexus\Database\NexusDB::redis();
    $key = "nexus_asn";
    $notFoundCacheValue = "__NOT_FOUND__";
   try {
       static $reader;
       $database = nexus_env('GEOIP2_ASN_DATABASE');
       if (!file_exists($database) || !is_readable($database)) {
           do_log("GEOIP2_ASN_DATABASE: $database not exists or not readable", "debug");
           return false;
       }
       if (is_null($reader)) {
           $reader = new \GeoIp2\Database\Reader($database);
       }
       $asnObj = $reader->asn($ip);
       $asn = $asnObj->autonomousSystemNumber;
       if ($asn <= 0) {
           return false;
       }
       $cacheResult = $redis->hGet($key, $asn);
       if ($cacheResult !== false) {
           if ($cacheResult === $notFoundCacheValue) {
               return false;
           } else {
               return true;
           }
       }
       $row = \Nexus\Database\NexusDB::getOne("seed_box_records", "asn = $asn", "id");
       if (!empty($row)) {
           $redis->hSet($key, $asn, $row['id']);
       } else {
           $redis->hSet($key, $asn, $notFoundCacheValue);
       }
   } catch (\Throwable $throwable) {
       do_log("ip: $ip, " . $throwable->getMessage());
       $redis->hSet($key, $asn, $notFoundCacheValue);
   }
   $result = !empty($row);
   if ($result && $exceptionWhenYes) {
       throw new \App\Exceptions\SeedBoxYesException($row['id']);
   }
   return $result;
}

function isIPSeedBox($ip, $uid): bool
{
    return \App\Repositories\SeedBoxRepository::isSeedBoxFromUserRecords($uid, $ip)['result'];

    /*
    $key = "nexus_is_ip_seed_box:ip:$ip:uid:$uid";
    $cacheData = \Nexus\Database\NexusDB::cache_get($key);
    if (in_array($cacheData, [0, 1, '0', '1'], true) && !$withoutCache) {
        do_log("$key, get result from cache: $cacheData(" . gettype($cacheData) . ")");
        return (bool)$cacheData;
    }
    //check from asn
    $res = isIPSeedBoxFromASN($ip, $exceptionWhenYes);
    if (!empty($res)) {
        \Nexus\Database\NexusDB::cache_put($key, 1, 300);
        do_log("$key, get result from asn, true");
        return true;
    }

    $ipObject = \PhpIP\IP::create($ip);
    $ipNumeric = $ipObject->numeric();
    $ipVersion = $ipObject->getVersion();
    //check allow list first, not consider specific user
    $checkSeedBoxAllowedSql = sprintf(
        'select id from seed_box_records where `ip_begin_numeric` <= "%s" and `ip_end_numeric` >= "%s" and `version` = %s and `status` = %s and `is_allowed` = 1 and asn = 0 limit 1',
        $ipNumeric, $ipNumeric, $ipVersion, \App\Models\SeedBoxRecord::STATUS_ALLOWED
    );
    $res = \Nexus\Database\NexusDB::select($checkSeedBoxAllowedSql);
    if (!empty($res)) {
        \Nexus\Database\NexusDB::cache_put($key, 0, 300);
        do_log("$key, get result from database, is_allowed = 1, false");
        return false;
    }
    $checkSeedBoxAdminSql = sprintf(
        'select id from seed_box_records where `ip_begin_numeric` <= "%s" and `ip_end_numeric` >= "%s" and `type` = %s and `version` = %s and `status` = %s and `is_allowed` = 0 and asn = 0 limit 1',
        $ipNumeric, $ipNumeric, \App\Models\SeedBoxRecord::TYPE_ADMIN, $ipVersion, \App\Models\SeedBoxRecord::STATUS_ALLOWED
    );
    $res = \Nexus\Database\NexusDB::select($checkSeedBoxAdminSql);
    if (!empty($res)) {
        \Nexus\Database\NexusDB::cache_put($key, 1, 300);
        do_log("$key, get result from admin, true");
        if ($exceptionWhenYes) {
            throw new \App\Exceptions\SeedBoxYesException($res[0]['id']);
        }
        return true;
    }
    if ($uid !== null) {
        $checkSeedBoxUserSql = sprintf(
            'select id from seed_box_records where `ip_begin_numeric` <= "%s" and `ip_end_numeric` >= "%s" and `uid` = %s and `type` = %s and `version` = %s and `status` = %s and `is_allowed` = 0 and asn = 0  limit 1',
            $ipNumeric, $ipNumeric, $uid, \App\Models\SeedBoxRecord::TYPE_USER, $ipVersion, \App\Models\SeedBoxRecord::STATUS_ALLOWED
        );
        $res = \Nexus\Database\NexusDB::select($checkSeedBoxUserSql);
        if (!empty($res)) {
            \Nexus\Database\NexusDB::cache_put($key, 1, 300);
            do_log("$key, get result from user, true");
            if ($exceptionWhenYes) {
                throw new \App\Exceptions\SeedBoxYesException($res[0]['id']);
            }
            return true;
        }
    }
    \Nexus\Database\NexusDB::cache_put($key, 0, 300);
    do_log("$key, no result, false");
    return false;
    */
}

function getDataTraffic(array $torrent, array $queries, array $user, $peer, $snatch, $promotionInfo)
{
    if (!isset($user['__is_donor'])) {
        throw new \InvalidArgumentException("user no '__is_donor' field");
    }
    $log = sprintf(
        "torrent: %s, owner: %s, user: %s, peerUploaded: %s, peerDownloaded: %s, queriesUploaded: %s, queriesDownloaded: %s",
        $torrent['id'], $torrent['owner'], $user['id'], $peer['uploaded'] ?? '', $peer['downloaded'] ?? '', $queries['uploaded'], $queries['downloaded']
    );
    if (!empty($peer)) {
        $realUploaded = max(bcsub($queries['uploaded'], $peer['uploaded']), 0);
        $realDownloaded = max(bcsub($queries['downloaded'], $peer['downloaded']), 0);
        $log .= ", [PEER_EXISTS], realUploaded: $realUploaded, realDownloaded: $realDownloaded, [SP_STATE]";
        $spStateGlobal = get_global_sp_state();
        $spStateNormal = \App\Models\Torrent::PROMOTION_NORMAL;
        if (!empty($promotionInfo) && isset($promotionInfo['__ignore_global_sp_state'])) {
            $log .= ', use promotionInfo';
            $spStateReal = $promotionInfo['sp_state'];
        } elseif ($spStateGlobal != $spStateNormal) {
            $log .= ", use global";
            $spStateReal = $spStateGlobal;
        } else {
            $log .= ", use torrent individual";
            $spStateReal = $torrent['sp_state'];
        }
        if (!isset(\App\Models\Torrent::$promotionTypes[$spStateReal])) {
            $log .= ", spStateReal = $spStateReal, invalid, reset to: $spStateNormal";
            $spStateReal = $spStateNormal;
        }
        $uploaderRatio = get_setting('torrent.uploaderdouble');
        $log .= ", uploaderRatio: $uploaderRatio";
        if ($torrent['owner'] == $user['id'] && $uploaderRatio != 1) {
            //uploader, use the bigger one
            $upRatio = max($uploaderRatio, \App\Models\Torrent::$promotionTypes[$spStateReal]['up_multiplier']);
            $log .= ", [IS_UPLOADER] && uploaderRatio != 1, upRatio: $upRatio";
        } else {
            $upRatio = \App\Models\Torrent::$promotionTypes[$spStateReal]['up_multiplier'];
            $log .= ", [IS_NOT_UPLOADER] || uploaderRatio == 1, upRatio: $upRatio";
        }
        /**
         * VIP do not calculate downloaded
         * @since 1.7.13
         */
        if ($user['class'] == \App\Models\User::CLASS_VIP) {
            $downRatio = 0;
            $log .= ", [IS_VIP], downRatio: $downRatio";
        } else {
            $downRatio = \App\Models\Torrent::$promotionTypes[$spStateReal]['down_multiplier'];
            $log .= ", [IS_NOT_VIP], downRatio: $downRatio";
        }
    } else {
        $realUploaded = $queries['uploaded'];
        $realDownloaded = $queries['downloaded'];
        /**
         * If peer not exits, user increment = 0;
         */
        $upRatio = 0;
        $downRatio = 0;
        $log .= ", [PEER_NOT_EXISTS], realUploaded: $realUploaded, realDownloaded: $realDownloaded, upRatio: $upRatio, downRatio: $downRatio";
    }
    $uploadedIncrementForUser = $realUploaded * $upRatio;
    $downloadedIncrementForUser = $realDownloaded * $downRatio;
    $log .= ", uploadedIncrementForUser: $uploadedIncrementForUser, downloadedIncrementForUser: $downloadedIncrementForUser";

    /**
     * check seed box rule
     */
    $isSeedBoxRuleEnabled = get_setting('seed_box.enabled') == 'yes';
    $log .= ", isSeedBoxRuleEnabled: $isSeedBoxRuleEnabled, user class: {$user['class']}, __is_donor: {$user['__is_donor']}";
    if ($isSeedBoxRuleEnabled && $torrent['owner'] != $user['id'] && !($user['class'] >= \App\Models\User::CLASS_VIP || $user['__is_donor'])) {
        $isIPSeedBox = isIPSeedBox($queries['ip'], $user['id']);
        $log .= ", isIPSeedBox: $isIPSeedBox";
        if ($isIPSeedBox) {
            $isSeedBoxNoPromotion = get_setting('seed_box.no_promotion') == 'yes';
            $log .= ", isSeedBoxNoPromotion: $isSeedBoxNoPromotion";
            if ($isSeedBoxNoPromotion) {
                $uploadedIncrementForUser = $realUploaded;
                $downloadedIncrementForUser = $realDownloaded;
                $log .= ", isIPSeedBox && isSeedBoxNoPromotion, increment for user = real";
            }
            $maxUploadedTimes = get_setting('seed_box.max_uploaded');
            $maxUploadedDurationSeconds = get_setting('seed_box.max_uploaded_duration', 0) * 3600;
            $torrentTTL = time() - strtotime($torrent['added']);
            $timeRangeValid = ($maxUploadedDurationSeconds == 0) || ($torrentTTL < $maxUploadedDurationSeconds);
            $log .= ", maxUploadedTimes: $maxUploadedTimes, maxUploadedDurationSeconds: $maxUploadedDurationSeconds, timeRangeValid: $timeRangeValid";
            if ($maxUploadedTimes > 0 && $timeRangeValid) {
                $log .= ", [LIMIT_UPLOADED]";
                if (!empty($snatch) && isset($torrent['size']) && $snatch['uploaded'] >= $torrent['size'] * $maxUploadedTimes) {
                    $log .= ", snatchUploaded({$snatch['uploaded']}) >= torrentSize({$torrent['size']}) * times($maxUploadedTimes), uploadedIncrementForUser = 0";
                    $uploadedIncrementForUser = 0;
                } else {
                    $log .= ", snatchUploaded({$snatch['uploaded']}) < torrentSize({$torrent['size']}) * times($maxUploadedTimes), uploadedIncrementForUser do not change to 0";
                }
            } else {
                $log .= ", [NOT_LIMIT_UPLOADED]";
            }
        }
    }

    $result = [
        'uploaded_increment' => $realUploaded,
        'uploaded_increment_for_user' => $uploadedIncrementForUser,
        'downloaded_increment' => $realDownloaded,
        'downloaded_increment_for_user' => $downloadedIncrementForUser,
    ];
    do_log("$log, result: " . json_encode($result), 'info');
    return $result;
}

function clear_user_cache($uid, $passkey = '')
{
    do_log("clear_user_cache, uid: $uid, passkey: $passkey");
    \Nexus\Database\NexusDB::cache_del("user_{$uid}_content");
    \Nexus\Database\NexusDB::cache_del("user_{$uid}_roles");
    \Nexus\Database\NexusDB::cache_del("announce_user_passkey_$uid");//announce.php
    \Nexus\Database\NexusDB::cache_del(\App\Models\Setting::DIRECT_PERMISSION_CACHE_KEY_PREFIX . $uid);
    \Nexus\Database\NexusDB::cache_del("user_role_ids:$uid");
    \Nexus\Database\NexusDB::cache_del("direct_permissions:$uid");
    if ($passkey) {
        \Nexus\Database\NexusDB::cache_del('user_passkey_'.$passkey.'_content');//announce.php
    }
    $userInfo = \App\Models\User::query()->find($uid, \App\Models\User::$commonFields);
    if ($userInfo) {
        fire_event("user_updated", $userInfo);
    }
}

function clear_setting_cache()
{
    do_log("clear_setting_cache");
    \Nexus\Database\NexusDB::cache_del('nexus_settings_in_laravel');
    \Nexus\Database\NexusDB::cache_del('nexus_settings_in_nexus');
    \Nexus\Database\NexusDB::cache_del('setting_protected_forum');
    $channel = nexus_env("CHANNEL_NAME_SETTING");
    if (!empty($channel)) {
        \Nexus\Database\NexusDB::redis()->publish($channel, "update");
    }
}

/**
 * @see functions.php::get_category_row(), genrelist()
 */
function clear_category_cache()
{
    do_log("clear_category_cache");
    \Nexus\Database\NexusDB::cache_del('category_content');
    $searchBoxList = \App\Models\SearchBox::query()->get(['id']);
    foreach ($searchBoxList as $item) {
        \Nexus\Database\NexusDB::cache_del("category_list_mode_{$item->id}");
    }

}

/**
 * @see functions.php::searchbox_item_list()
 */
function clear_taxonomy_cache($table)
{
    do_log("clear_taxonomy_cache: $table");
    $list = \App\Models\SearchBox::query()->get(['id']);
    foreach ($list as $item) {
        \Nexus\Database\NexusDB::cache_del("{$table}_list_mode_{$item->id}");
    }
    \Nexus\Database\NexusDB::cache_del("{$table}_list_mode_0");
}

function clear_staff_message_cache()
{
    do_log("clear_staff_message_cache");
    \App\Repositories\MessageRepository::updateStaffMessageCountCache(false);
}

/**
 * @see functions.php::get_searchbox_value()
 */
function clear_search_box_cache()
{
    do_log("clear_search_box_cache");
    \Nexus\Database\NexusDB::cache_del("search_box_content");
}

/**
 * @see functions.php::get_category_icon_row()
 */
function clear_icon_cache()
{
    do_log("clear_icon_cache");
    \Nexus\Database\NexusDB::cache_del("category_icon_content");
}

function clear_inbox_count_cache($uid)
{
    do_log("clear_inbox_count_cache");
    foreach (\Illuminate\Support\Arr::wrap($uid) as $id) {
        \Nexus\Database\NexusDB::cache_del('user_'.$id.'_inbox_count');
        \Nexus\Database\NexusDB::cache_del('user_'.$id.'_unread_message_count');
    }
}

function clear_agent_allow_deny_cache()
{
    do_log("clear_agent_allow_deny_cache");
    $allowCacheKey = nexus_env("CACHE_KEY_AGENT_ALLOW", "all_agent_allows");
    $denyCacheKey = nexus_env("CACHE_KEY_AGENT_DENY", "all_agent_denies");
    foreach (["", ":php", ":go"] as $suffix) {
        \Nexus\Database\NexusDB::cache_del($allowCacheKey . $suffix);
        \Nexus\Database\NexusDB::cache_del($denyCacheKey . $suffix);
    }
}

/**
 * @see announce.php
 * @param $infoHash
 * @return void
 */
function clear_torrent_cache($infoHash)
{
    do_log("clear_torrent_cache");
    \Nexus\Database\NexusDB::cache_del('torrent_hash_'.$infoHash.'_content');
    \Nexus\Database\NexusDB::cache_del("torrent_not_exists:$infoHash");
}

function user_can($permission, $fail = false, $uid = 0): bool
{
    $log = "permission: $permission, fail: $fail, user: $uid";
    static $userCanCached = [];
    static $sequence = 0;
    if ($uid == 0) {
        $uid = get_user_id();
        $log .= ", set current uid: $uid";
    }
    if ($uid <= 0) {
        if ($fail) {
            goto FAIL;
        }
        do_log("$log, unauthenticated, false");
        return false;
    }
    if (!$fail && isset($userCanCached[$permission][$uid])) {
        return $userCanCached[$permission][$uid];
    }
    $userInfo = get_user_row($uid);
    $class = $userInfo['class'];
    $log .= ", userClass: $class";
    if ($class == \App\Models\User::CLASS_STAFF_LEADER) {
        do_log("$log, CLASS_STAFF_LEADER, true");
        $userCanCached[$permission][$uid] = true;
        return true;
    }
    $userAllPermissions = \App\Repositories\ToolRepository::listUserAllPermissions($uid);
    $result = isset($userAllPermissions[$permission]);
    if ($sequence == 0) {
        $sequence++;
        $log .= ", userAllPermissions: " . json_encode($userAllPermissions);
    }
    $log .= ", result: $result";
    if (!$fail || $result) {
        do_log($log);
        $userCanCached[$permission][$uid] = $result;
        return $result;
    }
    FAIL:
    do_log("$log, [FAIL]");
    if (IN_NEXUS && !IN_TRACKER) {
        global $lang_functions;
        $requireClass = get_setting("authority.$permission");
        if (isset(\App\Models\User::$classes[$requireClass])) {
            stderr($lang_functions['std_sorry'],$lang_functions['std_permission_denied_only'].get_user_class_name($requireClass,false,true,true).sprintf($lang_functions['std_or_above_can_view'], \App\Models\Setting::getSiteName()),false);
        } else {
            stderr($lang_functions['std_error'], $lang_functions['std_permission_denied']);
        }
    }
    throw new \App\Exceptions\InsufficientPermissionException();
}

function assert_has_permission(bool $permissionCheckResult): void
{
    if (!$permissionCheckResult) {
        throw new \App\Exceptions\InsufficientPermissionException();
    }
}



function is_donor(array $userInfo): bool
{
    return $userInfo['donor'] == 'yes' && ($userInfo['donoruntil'] === null || $userInfo['donoruntil'] == '0000-00-00 00:00:00' || $userInfo['donoruntil'] >= date('Y-m-d H:i:s'));
}

/**
 * @deprecated
 * @param $authkey
 * @return false|int|mixed|string|null
 * @throws \App\Exceptions\NexusException
 * @see download.php
 */
function get_passkey_by_authkey($authkey)
{
    return \Nexus\Database\NexusDB::remember("authkey2passkey:$authkey", 3600*24, function () use ($authkey) {
        $arr = explode('|', $authkey);
        if (count($arr) != 3) {
            throw new \InvalidArgumentException("Invalid authkey: $authkey, format error");
        }
        $uid = $arr[1];
        $torrentRep = new \App\Repositories\TorrentRepository();
        $decrypted = $torrentRep->checkTrackerReportAuthKey($authkey);
        if (empty($decrypted)) {
            throw new \InvalidArgumentException("Invalid authkey: $authkey");
        }
        $userInfo = \Nexus\Database\NexusDB::remember("announce_user_passkey_$uid", 3600, function () use ($uid) {
            return \App\Models\User::query()->where('id', $uid)->first(['id', 'passkey']);
        });
        return $userInfo->passkey;
    });
}

function executeCommand($command, $format = 'string', $artisan = false, $exception = true): string|array
{
    $append = " 2>&1";
    if (!str_ends_with($command, $append)) {
        $command .= $append;
    }
    if ($artisan) {
        $phpPath = nexus_env('PHP_PATH') ?: 'php';
        $webRoot = rtrim(ROOT_PATH, '/');
        $command = "$phpPath $webRoot/artisan $command";
    }
    do_log("command: $command");
    $result = exec($command, $output, $result_code);
    $outputString = implode("\n", $output);
    $log = sprintf('result_code: %s, result: %s, output: %s', $result_code, $result, $outputString);
    if ($result_code != 0) {
        do_log($log, "error");
        if ($exception) {
            throw new \RuntimeException($outputString);
        }
    } else {
        do_log($log);
    }
    return $format == 'string' ? $outputString : $output;
}

function has_role_work_seeding($uid)
{
    $result = apply_filter('user_has_role_work_seeding', false, $uid);
    do_log("uid: $uid, result: $result");
    return $result;
}

function filter_src($src)
{
    $path = parse_url($src, PHP_URL_PATH);
    if (empty($path)) {
        return $src;
    }
    $host = parse_url($src, PHP_URL_HOST);
    $currentHost = parse_url(getSchemeAndHttpHost(), PHP_URL_HOST);
    if (!empty($host) && $host != $currentHost) {
        return $src;
    }
    if (isset($_SERVER['DOCUMENT_ROOT'])) {
        $guessScriptFilename = sprintf("%s/%s", $_SERVER['DOCUMENT_ROOT'], trim($path, '/'));
        if (!file_exists($guessScriptFilename)) {
            return $src;
        }
    }
    //only allow these
    $imgExtensions = implode("|", \App\Models\Attachment::IMG_EXTENSIONS);
    $allowSuffixPattern = "/\.($imgExtensions)/i";
    if (preg_match($allowSuffixPattern, $path)) {
        return $src;
    }
    $allowScriptPattern = "/(forums|details|offers)\.php/i";
    if (preg_match($allowScriptPattern, $path)) {
        return $src;
    }
    //log danger, deny directly
    $dangerScriptsPattern = "/(logout|login|ajax|announce|scrape|adduser|modtask|docleanup|freeleech|take.*)\.php/i";
    if (preg_match($dangerScriptsPattern, $path)) {
        $msg = sprintf( "[DANGER_URL]: $src [%s]", nexus()->getRequestId());
        do_log($msg, "alert");
        write_log($msg, "mod");
    }
    do_log("[NOT_ALLOW_SRC]: $src with path: $path");
    return "";
}

//here must retrieve the real time info, no cache!!!
function get_snatch_info($torrentId, $userId)
{
    return mysql_fetch_assoc(sql_query(sprintf('select * from snatched where torrentid = %s and userid = %s order by id desc limit 1', $torrentId, $userId)));
}

/**
 * 完整的 Laravel 事件, 在 php 端有监听者的需要触发. 同样会执行 publish_model_event()
 */
function fire_event(string $name, \Illuminate\Database\Eloquent\Model $model, ?\Illuminate\Database\Eloquent\Model $oldModel = null): void
{
    if (!isset(\App\Enums\ModelEventEnum::$eventMaps[$name])) {
        throw new \InvalidArgumentException("Event $name is not a valid event enumeration");
    }
    if (IN_NEXUS) {
        $prefix = "fire_event:";
        $idKey = $prefix . \Illuminate\Support\Str::random();
        $idKeyOld = "";
        \Nexus\Database\NexusDB::cache_put($idKey, serialize($model->toArray()), 3600*24*30);
        if ($oldModel) {
            $idKeyOld = $prefix . \Illuminate\Support\Str::random();
            \Nexus\Database\NexusDB::cache_put($idKeyOld, serialize($oldModel->toArray()), 3600*24*30);
        }
//        executeCommand("event:fire --name=$name --idKey=$idKey --idKeyOld=$idKeyOld", "string", true, false);
        \Nexus\Nexus::dispatchQueueJob(new \App\Jobs\FireEvent($name, $idKey, $idKeyOld));
        do_log("success fire_event in nexus, name: $name, idKey: $idKey, idKeyOld: $idKeyOld");
    } else {
        $eventClass = \App\Enums\ModelEventEnum::$eventMaps[$name]['event'];
        if (str_ends_with($name, '_deleted')) {
            //if deleted from database, can not pass model instance, use array
            $params = [$model->toArray()];
            if ($oldModel) {
                $params[] = $oldModel->toArray();
            }
        } else {
            $params = [$model];
            if ($oldModel) {
                $params[] = $oldModel;
            }
        }
        call_user_func_array([$eventClass, "dispatch"], $params);
        publish_model_event($name, $model->id, $model->toJson());
        do_log("success fire_event in laravel, name: $name, id: $model->id, oldId: " . ($oldModel ? $oldModel->id : ""));
    }
}

/**
 * 仅仅是往 redis 发布事件, php 端无监听者仅在其他平台有需要的触发这个即可, 较轻量
 */
function publish_model_event(string $event, int $id, string $json = ""): void
{
    $channel = nexus_env("CHANNEL_NAME_MODEL_EVENT");
    if (!empty($channel)) {
        \Nexus\Database\NexusDB::redis()->publish($channel, json_encode(["event" => $event, "id" => $id, "json" => $json]));
    } else {
        do_log("event: $event, id: $id, channel: $channel, channel is empty!", "error");
    }
}

function convertNamespaceToSnake(string $str): string
{
    return str_replace(["\\", "::"], ["_", "."], $str);
}

function get_user_locale(int $uid): string
{
    $sql = "select language.site_lang_folder from users inner join language on users.lang = language.id where users.id = $uid limit 1";
    $result = \Nexus\Database\NexusDB::select($sql);
    if (empty($result) || empty($result[0]['site_lang_folder'])) {
        return "en";
    }
    return \App\Http\Middleware\Locale::$languageMaps[$result[0]['site_lang_folder']] ?? $result[0]['site_lang_folder'];
}

function send_admin_success_notification(string $msg = ""): void {
    \Filament\Notifications\Notification::make()->success()->title($msg ?: "Success!")->send();
}

function send_admin_fail_notification(string $msg = ""): void {
    \Filament\Notifications\Notification::make()->danger()->title($msg ?: "Fail!")->send();
}

function ability(\App\Enums\Permission\RoutePermissionEnum $permission): string {
    return sprintf("ability:%s", $permission->value);
}

function get_challenge_key(string $challenge): string {
    return "challenge:".$challenge;
}

function get_user_from_cookie(array $cookie, $isArray = true): array|\App\Models\User|null {
    $log = "cookie: " . json_encode($cookie);
    $result = get_user_id_and_signature_from_cookie($cookie);
    if (empty($result)) {
        return null;
    }
    $id = $result['user_id'];
    $tokenJson = $result['token_json'];
    $signature = $result['signature'];
    $log .= ", uid = $id";
    if ($isArray) {
        $res = sql_query("SELECT * FROM users WHERE users.id = ".sqlesc($id)." AND users.enabled='yes' AND users.status = 'confirmed' LIMIT 1");
        $row = mysql_fetch_array($res);
        if (!$row) {
            do_log("$log, user not exists");
            return null;
        }
        $authKey = $row["auth_key"];
        unset($row['auth_key'], $row['passhash']);
    } else {
        $row = \App\Models\User::query()->find($id);
        if (!$row) {
            do_log("$log, user not exists");
            return null;
        }
        $row->checkIsNormal();
        $authKey = $row->auth_key;
    }
    $expectedSignature = hash_hmac('sha256', $tokenJson, $authKey);
    if (!hash_equals($expectedSignature, $signature)) {
        do_log("$log, !hash_equals, expectedSignature: $expectedSignature, actualSignature: $signature");
        return null;
    }
    return $row;
}

function get_user_id_and_signature_from_cookie(array $cookie): array|null
{
    $log = "cookie: " . json_encode($cookie);
    if (empty($cookie["c_secure_pass"])) {
        do_log("$log, param not enough");
        return null;
    }
    $base64Decoded = base64_decode($cookie["c_secure_pass"]);
    if (empty($base64Decoded)) {
        do_log("$log, invalid c_secure_pass");
        return null;
    }
    $log .= ", base64 decoded: " . $base64Decoded;
    $tokenJsonAndSignature = explode(".", $base64Decoded);
    if (count($tokenJsonAndSignature) != 2) {
        do_log("$log, invalid c_secure_pass base64_decoded");
        return null;
    }
    $tokenJson = $tokenJsonAndSignature[0];
    $signature = $tokenJsonAndSignature[1];
    if (empty($tokenJson) || empty($signature)) {
        do_log("$log, no tokenJson or signature");
        return null;
    }
    $tokenData = json_decode($tokenJson, true);
    if (!isset($tokenData['user_id'])) {
        do_log("$log, no user_id");
        return null;
    }
    if (!isset($tokenData['expires']) || $tokenData['expires'] < time()) {
        do_log("$log, signature expired");
        return null;
    }
    return [
        "user_id" => $tokenData['user_id'],
        'token_json' => $tokenJson,
        'signature' => $signature,
    ];
}

function render_password_hash_js(string $formId, string $passwordOriginalClass, string $passwordHashedName, bool $passwordRequired, string $passwordConfirmClass = "password_confirmation", string $usernameName = "username"): void {
    $tipTooShort = nexus_trans('signup.password_too_short');
    $tipTooLong = nexus_trans('signup.password_too_long');
    $tipEqualUsername = nexus_trans('signup.password_equals_username');
    $tipNotMatch = nexus_trans('signup.passwords_unmatched');
    $passwordValidateJS = "";
    if ($passwordRequired) {
        $passwordValidateJS = <<<JS
if (password.length < 6) {
    layer.alert("$tipTooShort")
    return
}
if (password.length > 40) {
    layer.alert("$tipTooLong")
    return
}
JS;
    }
    $formVar = "jqForm" . md5($formId);
    $js = <<<JS
var $formVar = jQuery("#{$formId}");
$formVar.on("click", "input[type=button]", function() {
    let jqUsername = $formVar.find("[name={$usernameName}]")
    let jqPassword = $formVar.find(".{$passwordOriginalClass}")
    let jqPasswordConfirm = $formVar.find(".{$passwordConfirmClass}")
    let password = jqPassword.val()
    $passwordValidateJS
    if (jqUsername.length > 0 && jqUsername.val() === password) {
        layer.alert("$tipEqualUsername")
        return
    }
    if (jqPasswordConfirm.length > 0 && password !== jqPasswordConfirm.val()) {
        layer.alert("$tipNotMatch")
        return
    }
    if (password !== "") {
        const passwordHashed = sha256(password)
        $formVar.find("input[name={$passwordHashedName}]").val(passwordHashed)
        $formVar.submit()
    } else {
        $formVar.submit()
    }
})
JS;
    \Nexus\Nexus::js("js/crypto-js.js", 'footer', true);
    \Nexus\Nexus::js($js, 'footer', false);
}

function render_password_challenge_js(string $formId, string $usernameName, string $passwordOriginalClass): void {
    $formVar = "jqForm" . md5($formId);
    $js = <<<JS
var $formVar = jQuery("#{$formId}");
$formVar.on("click", "input[type=button]", function() {
    let useChallengeResponseAuthentication = $formVar.find("input[name=response]").length > 0
    if (!useChallengeResponseAuthentication) {
        return $formVar.submit()
    }
    let jqUsername = $formVar.find("[name={$usernameName}]")
    let jqPassword = $formVar.find(".{$passwordOriginalClass}")
    let username = jqUsername.val()
    let password = jqPassword.val()
    login(username, password, $formVar)
})
async function login(username, password, jqForm) {
    try {
        jQuery('body').loading({stoppable: false});
        const challengeResponse = await fetch('/api/challenge', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({ username: username })
        });
        jQuery('body').loading('stop');

        const challengeData = await challengeResponse.json();
        if (challengeData.ret !== 0) {
            layer.alert(challengeData.msg)
            return
        }

        const clientHashedPassword = sha256(password);

        const serverSideHash = sha256(challengeData.data.secret + clientHashedPassword);

        const clientResponse = hmacSha256(challengeData.data.challenge, serverSideHash);
        jqForm.find("input[name=response]").val(clientResponse)
        jqForm.submit()
    } catch (error) {
        console.error(error);
        layer.alert(error.toString())
    }
}
JS;
    \Nexus\Nexus::js("vendor/jquery-loading/jquery.loading.min.js", 'footer', true);
    \Nexus\Nexus::js("js/crypto-js.js", 'footer', true);
    \Nexus\Nexus::js($js, 'footer', false);
}

function nexus_escape($data): array|string
{
    if (is_array($data)) {
        return array_map('nexus_escape', $data);
    }
    return htmlspecialchars($data, ENT_QUOTES, 'UTF-8');
}
