<?php
namespace App\Repositories;

use App\Enums\ModelEventEnum;
use App\Exceptions\InsufficientPermissionException;
use App\Exceptions\NexusException;
use App\Http\Resources\ExamUserResource;
use App\Http\Resources\TorrentResource;
use App\Http\Resources\UserResource;
use App\Models\ExamUser;
use App\Models\Invite;
use App\Models\LoginLog;
use App\Models\Message;
use App\Models\OauthProvider;
use App\Models\Setting;
use App\Models\SiteLog;
use App\Models\Snatch;
use App\Models\Torrent;
use App\Models\User;
use App\Models\UserBanLog;
use App\Models\UserMeta;
use App\Models\UserModifyLog;
use App\Models\UsernameChangeLog;
use App\Utils\ApiQueryBuilder;
use Carbon\Carbon;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Str;
use Nexus\Database\NexusDB;

class UserRepository extends BaseRepository
{
    private static array $allowIncludes = ['inviter', 'valid_medals'];
    private static array $allowIncludeFields = ['seeding_leeching_data'];
    private static array $allowIncludeCounts = [];
    public function getList(array $params)
    {
        $query = User::query();
        if (!empty($params['id'])) {
            $query->where('id', $params['id']);
        }
        if (!empty($params['username'])) {
            $query->where('username', 'like',"%{$params['username']}%");
        }
        if (!empty($params['email'])) {
            $query->where('email', 'like',"%{$params['email']}%");
        }
        if (isset($params['class']) && $params['class'] !== '') {
            $query->where('class', $params['class']);
        }
        list($sortField, $sortType) = $this->getSortFieldAndType($params);
        $query->orderBy($sortField, $sortType);
        return $query->paginate();
    }

    public function getBase($id)
    {
        $user = User::query()->findOrFail($id, ['id', 'username', 'email', 'avatar']);
        return $user;
    }

    public function getDetail($id, Authenticatable $currentUser)
    {
        //query this info default
        $query = User::query()->with([]);
        $apiQueryBuilder = ApiQueryBuilder::for(UserResource::NAME, $query)
            ->allowIncludes(self::$allowIncludes)
            ->allowIncludeCounts(self::$allowIncludeCounts)
            ->allowIncludeFields(self::$allowIncludeFields)
        ;
        $query = $apiQueryBuilder->build();
        $user = $query->findOrFail($id);
        Gate::authorize('view', $user);
        $userList =  $this->appendIncludeFields($apiQueryBuilder, $currentUser, [$user]);
        return $userList[0];
    }

    private function appendIncludeFields(ApiQueryBuilder $apiQueryBuilder, Authenticatable $currentUser, $userList)
    {
        $idArr = [];
        foreach ($userList as $user) {
            $idArr[] = $user->id;
        }
        if ($hasFieldSeedingData = $apiQueryBuilder->hasIncludeField('seeding_leeching_data')) {
            $seedingData = $this->listUserSeedingLeechingData($idArr);
        }
        foreach ($userList as $user) {
            $id = $user->id;
            if ($hasFieldSeedingData && isset($seedingData[$id])) {
                $user->seeding_leeching_data = $seedingData[$id];
            }
        }
        return $userList;
    }

    /**
     * create user
     *
     * @param array $params must: username, email, password, password_confirmation. optional: id, class, provider_id
     * @return User
     */
    public function store(array $params)
    {
        $password = $params['password'];
        if ($password != $params['password_confirmation']) {
            throw new \InvalidArgumentException("password confirmation != password");
        }
        $username = $params['username'];
        if (!validusername($username)) {
            throw new \InvalidArgumentException("Invalid username: $username");
        }
        $email = htmlspecialchars(trim($params['email']));
        $email = safe_email($email);
        if (!check_email($email)) {
            throw new \InvalidArgumentException("Invalid email: $email");
        }
        if (User::query()->where('email', $email)->exists()) {
            throw new \InvalidArgumentException("The email address: $email is already in use");
        }
        if (User::query()->where('username', $username)->exists()) {
            throw new \InvalidArgumentException("The username: $username is already in use");
        }
        if (mb_strlen($password) < 6 || mb_strlen($password) > 40) {
            throw new \InvalidArgumentException("Invalid password: $password, it should be more than 6 character and less than 40 character");
        }
        if (!empty($params['class'])) {
            $class =intval($params['class']);
            if (!IN_NEXUS) {
                $authUser = Auth::user();
                if ($authUser && $class >= $authUser->class) {
                    throw new InsufficientPermissionException('No permission');
                }
            }
        } else {
            $class = User::CLASS_USER;
        }

        if (!isset(User::$classes[$class])) {
            throw new \InvalidArgumentException("Invalid user class: $class");
        }
        $setting = Setting::get('main');
        $secret = mksecret();
        $passhash = hash('sha256', $secret . hash('sha256', $password));
        $data = [
            'username' => $username,
            'email' => $email,
            'secret' => $secret,
            'auth_key' => mksecret(),
            'editsecret' => '',
            'passhash' => $passhash,
            'stylesheet' => $setting['defstylesheet'],
            'added' => now()->toDateTimeString(),
            'status' => User::STATUS_CONFIRMED,
            'class' => $class,
            'passkey' => md5($username.date("Y-m-d H:i:s").$passhash)
        ];
        $user = new User($data);
        if (!empty($params['id'])) {
            if (User::query()->where('id', $params['id'])->exists()) {
                throw new \InvalidArgumentException("uid: {$params['id']} already exists.");
            }
            do_log("[CREATE_USER], specific id: " . $params['id']);
            $user->id = $params['id'];
        }
        if (!empty($params['provider_id'])) {
            if (!OauthProvider::query()->find($params['provider_id'])) {
                throw new \InvalidArgumentException("provider_id: {$params['provider_id']} not exists.");
            }
            do_log("[CREATE_USER], specific provider_id: " . $params['provider_id']);
            $user->provider_id = $params['provider_id'];
        }
        $user->save();
        fire_event("user_created", $user);
        return $user;
    }

    public function resetPassword($id, $password, $passwordConfirmation)
    {
        if ($password != $passwordConfirmation) {
            throw new \InvalidArgumentException("password confirmation != password");
        }
        $user = User::query()->findOrFail($id, ['id', 'username', 'class']);
        $operator = get_user_id();
        if ($operator) {
            $this->checkPermission($operator, $user);
        }
        $secret = mksecret();
        $passhash = hash('sha256', $secret . hash('sha256', $password));
        $update = [
            'secret' => $secret,
            'passhash' => $passhash,
            'auth_key' => mksecret(),
        ];
        $user->update($update);
        return true;
    }

    /**
     * @deprecated  use User::listClass() instead !
     *
     * @return array
     */
    public function listClass()
    {
        return User::listClass();
    }

    public function disableUser(User $operator, $uid, $reason = '')
    {
        $targetUser = User::query()->findOrFail($uid, ['id', 'enabled', 'username', 'class']);
        if ($targetUser->enabled == User::ENABLED_NO) {
            throw new NexusException('Already disabled !');
        }
        if (empty($reason)) {
            $reason = nexus_trans("user.disable_by_admin");
        }
        $this->checkPermission($operator, $targetUser);
        $banLog = [
            'uid' => $uid,
            'username' => $targetUser->username,
            'reason' => $reason,
            'operator' => $operator->id,
        ];
        $modCommentText = sprintf("%s - Disable by %s, reason: %s.", now()->format('Y-m-d'), $operator->username, $reason);
        NexusDB::transaction(function () use ($targetUser, $banLog, $modCommentText) {
            $targetUser->updateWithModComment(['enabled' => User::ENABLED_NO], $modCommentText);
            UserBanLog::query()->create($banLog);
        });
        do_log("user: $uid, $modCommentText");
        $this->clearCache($targetUser);
        fire_event("user_disabled", $targetUser);
        return true;
    }

    public function enableUser(User $operator, $uid, $reason = '')
    {
        $targetUser = User::query()->findOrFail($uid, ['id', 'enabled', 'username', 'class']);
        if ($targetUser->enabled == User::ENABLED_YES) {
            throw new NexusException('Already enabled !');
        }
        $this->checkPermission($operator, $targetUser);
        $update = [
            'enabled' => User::ENABLED_YES
        ];
        if ($targetUser->class == User::CLASS_PEASANT) {
            // warn users until 30 days
            $until = now()->addDays(30)->toDateTimeString();
            $update['leechwarn'] = 'yes';
            $update['leechwarnuntil'] = $until;
        } else {
            $update['leechwarn'] = 'no';
            $update['leechwarnuntil'] = null;
        }
        $modCommentText = sprintf("%s - Enable by %s, reason: %s", now()->format('Y-m-d'), $operator->username, $reason);
        $targetUser->updateWithModComment($update, $modCommentText);
        do_log("user: $uid, $modCommentText, update: " . nexus_json_encode($update));
        $this->clearCache($targetUser);
        fire_event("user_enabled", $targetUser);
        $this->setEnableLatelyCache($targetUser->id);
        return true;
    }

    private function setEnableLatelyCache(int $userId): void
    {
        NexusDB::cache_put(User::getUserEnableLatelyCacheKey($userId), now()->toDateTimeString(), 86400);
    }

    public function getInviteInfo($id)
    {
        $user = User::query()->findOrFail($id, ['id']);
        return $user->invitee_code()->with('inviter_user')->first();
    }

    public function getModComment($id)
    {
        $user = User::query()->findOrFail($id, ['modcomment']);
        return $user->modcomment;
    }

    public function incrementDecrement(User $operator, $uid, $action, $field, $value, $reason = ''): bool
    {
        $fieldMap = [
            'uploaded' => 'uploaded',
            'downloaded' => 'downloaded',
            'seedbonus' => 'seedbonus',
            'invites' => 'invites',
            'attendance_card' => 'attendance_card',
        ];
        if (!isset($fieldMap[$field])) {
            throw new \InvalidArgumentException("Invalid field: $field, only support: " . implode(', ', array_keys($fieldMap)));
        }
        $sourceField = $fieldMap[$field];
        $targetUser = User::query()->findOrFail($uid, User::$commonFields);
        $this->checkPermission($operator, $targetUser);
        $old = $targetUser->{$sourceField};
        $valueAtomic = $value;
        $formatSize = false;
        if (in_array($field, ['uploaded', 'downloaded'])) {
            //Frontend unit: GB
            $valueAtomic = $value * 1024 * 1024 * 1024;
            $formatSize = true;
        }
        if ($action == 'Increment') {
            $new = $old + abs($valueAtomic);
        } elseif ($action == 'Decrement') {
            $new = $old - abs($valueAtomic);
        } else {
            throw new \InvalidArgumentException("Invalid action: $action.");
        }
        if ($new < 0) {
            throw new NexusException("New value($new) lte 0");
        }
        //for administrator, use english
        $modCommentText = nexus_trans('message.field_value_change_message_body', [
            'field' => nexus_trans("user.labels.$sourceField", [], 'en'),
            'operator' => $operator->username,
            'old' => $formatSize ? mksize($old) : $old,
            'new' => $formatSize ?  mksize($new) : $new,
            'reason' => $reason,
        ], 'en');
        do_log("user: $uid, $modCommentText", 'alert');
        $update = [
            $sourceField => $new,
//            'modcomment' => NexusDB::raw("if(modcomment = '', '$modCommentText', concat_ws('\n', '$modCommentText', modcomment))"),
        ];
        $locale = $targetUser->locale;
        $fieldLabel = nexus_trans("user.labels.$sourceField", [], $locale);
        $msg = nexus_trans('message.field_value_change_message_body', [
            'field' => $fieldLabel,
            'operator' => $operator->username,
            'old' => $formatSize ? mksize($old) : $old,
            'new' => $formatSize ?  mksize($new) : $new,
            'reason' => $reason,
        ], $locale);
        $message = [
            'sender' => 0,
            'receiver' => $targetUser->id,
            'subject' => nexus_trans("message.field_value_change_message_subject", ['field' =>  $fieldLabel], $locale),
            'msg' => $msg,
            'added' => Carbon::now(),
        ];
        NexusDB::transaction(function () use ($uid, $sourceField, $old, $new, $update, $message, $modCommentText) {
            $affectedRows = User::query()
                ->where('id', $uid)
                ->where($sourceField, $old)
                ->update($update)
            ;
            if ($affectedRows != 1) {
                throw new \RuntimeException("Change fail, affected rows != 1($affectedRows)");
            }
            Message::query()->insert($message);
            UserModifyLog::query()->insert([
                'user_id' => $uid,
                'content' => $modCommentText,
                'created_at' => Carbon::now(),
                'updated_at' => Carbon::now(),
            ]);
        });
        $this->clearCache($targetUser);
        return true;
    }

    public function removeLeechWarn($operator, $uid): bool
    {
        $operator = $this->getUser($operator);
        $user = User::query()->findOrFail($uid, User::$commonFields);
        $this->checkPermission($operator, $user);
        $this->clearCache($user);
        $user->leechwarn = 'no';
        $user->leechwarnuntil = null;
        return $user->save();
    }

    public function removeTwoStepAuthentication($operator, $uid): bool
    {
        if (!$operator->canAccessAdmin()) {
            throw new \RuntimeException("No permission.");
        }
        $user = User::query()->findOrFail($uid, User::$commonFields);
        $this->checkPermission($operator, $user);
        $this->clearCache($user);
        $user->two_step_secret = '';
        return $user->save();
    }


    public function updateDownloadPrivileges($operator, $user, $status, $disableReasonKey = null)
    {
        if (!in_array($status, ['yes', 'no'])) {
            throw new \InvalidArgumentException("Invalid status: $status");
        }
        $targetUser = $this->getUser($user);
        $operator = $this->getUser($operator);
        $operatorUsername = 'System';
        if ($operator) {
            $operatorUsername = $operator->username;
            $this->checkPermission($operator, $targetUser);
        }
        $message = [
            'added' => now(),
            'receiver' => $targetUser->id,
        ];
        if ($status == 'no') {
            $update = ['downloadpos' => 'no'];
            $modComment = date('Y-m-d') . " - Download disable by " . $operatorUsername;
            $msgTransPrefix = "message.download_disable";
            if ($disableReasonKey !== null) {
                $msgTransPrefix .= "_$disableReasonKey";
            }
            $message['subject'] = nexus_trans("$msgTransPrefix.subject", [], $targetUser->locale);
            $message['msg'] = nexus_trans("$msgTransPrefix.body", ['operator' => $operatorUsername], $targetUser->locale);
        } else {
            $update = ['downloadpos' => 'yes'];
            $modComment = date('Y-m-d') . " - Download enable by " . $operatorUsername;
            $message['subject'] = nexus_trans('message.download_enable.subject', [], $targetUser->locale);
            $message['msg'] = nexus_trans('message.download_enable.body', ['operator' => $operatorUsername], $targetUser->locale);
        }
        $result = NexusDB::transaction(function () use ($targetUser, $update, $modComment, $message) {
            Message::add($message);
            return $targetUser->updateWithModComment($update, $modComment);
        });
        $this->clearCache($targetUser);
        return $result;
    }


    private function checkPermission($operator, User $user, $minAuthClass = 'authority.prfmanage')
    {
        $operator = $this->getUser($operator);
        if ($operator->id == $user->id) {
            return;
        }
        $classRequire = Setting::get($minAuthClass);
        if ($operator->class < $classRequire || $operator->class <= $user->class) {
            throw new InsufficientPermissionException();
        }
    }

    private function clearCache(User $user)
    {
        clear_user_cache($user->id, $user->passkey);
    }

    public function listMetas($uid, $metaKeys = [], $valid = true)
    {
        $query = UserMeta::query()->where('uid', $uid);
        if (!empty($metaKeys)) {
            $query->whereIn('meta_key', Arr::wrap($metaKeys));
        }
        if ($valid) {
            $query->where('status', 0)->where(function (Builder $query) {
                $query->whereNull('deadline')->orWhere('deadline', '>=', now());
            });
        }
        return $query->get()->groupBy('meta_key');
    }

    public function consumeBenefit($uid, array $params): bool
    {
        $metaKey = $params['meta_key'];
        $records = $this->listMetas($uid, $metaKey);
        if (!$records->has($metaKey)) {
            throw new \RuntimeException("User do not has this metaKey: $metaKey");
        }
        /** @var UserMeta $meta */
        $meta = $records->get($metaKey)->first();
        $user = User::query()->findOrFail($uid, User::$commonFields);
        if ($metaKey == UserMeta::META_KEY_CHANGE_USERNAME) {
            $changeLog = $user->usernameChangeLogs()->orderBy('id', 'desc')->first();
            if ($changeLog) {
                $miniDays = Setting::get('system.change_username_min_interval_in_days', 365);
                if (abs($changeLog->created_at->diffInDays()) <= $miniDays) {
                    $msg = nexus_trans('user.change_username_lte_min_interval', ['last_change_time' => $changeLog->created_at, 'interval' => $miniDays]);
                    throw new \RuntimeException($msg);
                }
            }
            NexusDB::transaction(function () use ($user, $meta, $params) {
                $this->changeUsername(
                    $user, UsernameChangeLog::CHANGE_TYPE_USER, $user, $params['username'],
                    Setting::get('system.change_username_card_allow_characters_outside_the_alphabets') == 'yes'
                );
                $meta->delete();
                clear_user_cache($user->id, $user->passkey);
            });
            return true;
        }

        throw new \InvalidArgumentException("Invalid meta_key: $metaKey");
    }

    private function changeUsername($operator, $changeType, $targetUser, $newUsername, $allowOutsideAlphabets = false): bool
    {
        $operator = $this->getUser($operator);
        $targetUser = $this->getUser($targetUser);
        $this->checkPermission($operator, $targetUser);
        if ($targetUser->username == $newUsername) {
            throw new \RuntimeException("New username can not be the same with current username !");
        }
        $strWidth = mb_strwidth($newUsername);
        if ($strWidth < 4 || $strWidth > 20) {
            throw new \InvalidArgumentException("Invalid username, maybe too long or too short");
        }
        if (!$allowOutsideAlphabets && !validusername($newUsername)) {
            throw new \InvalidArgumentException("Invalid username, only support alphabets");
        }
        if (User::query()->where('username', $newUsername)->where('id', '!=', $targetUser->id)->exists()) {
            throw new \RuntimeException("Username: $newUsername already exists !");
        }
        $changeLog = [
            'uid' => $targetUser->id,
            'operator' => $operator->username,
            'change_type' => $changeType,
            'username_old' => $targetUser->username,
            'username_new' => $newUsername
        ];
        NexusDB::transaction(function () use ($operator, $changeType,$targetUser, $changeLog) {
            $targetUser->usernameChangeLogs()->create($changeLog);
            $targetUser->username = $changeLog['username_new'];
            $targetUser->save();
        });
        $this->clearCache($targetUser);
        return true;
    }

    public function changeClass($operator, $targetUser, $newClass, $reason = '', array $extra = []): bool
    {
        user_can('user-change-class', true);
        $operator = $this->getUser($operator);
        $targetUser = $this->getUser($targetUser);
        if ($operator) {
            if ($operator->class <= $targetUser->class || $operator->class <= $newClass)
            throw new InsufficientPermissionException();
        }
        if ($targetUser->class == $newClass && $newClass != User::CLASS_VIP) {
            return  true;
        }
        $locale = $targetUser->locale;
        $subject = nexus_trans('user.edit_notifications.change_class.subject', [], $locale);
        $body = nexus_trans('user.edit_notifications.change_class.body', [
            'action' => nexus_trans( 'user.edit_notifications.change_class.' . ($newClass > $targetUser->class ? 'promote' : 'demote')),
            'new_class' => User::getClassText($newClass),
            'operator' => $operator->username ?? '',
            'reason' => $reason,
        ], $locale);
        $message = [
            'sender' => 0,
            'receiver' => $targetUser->id,
            'subject' => $subject,
            'msg' => $body,
            'added' => Carbon::now(),
        ];
        $userUpdates = [
            'class' => $newClass,
        ];
        if ($newClass == User::CLASS_VIP) {
            if (!empty($extra['vip_added']) && in_array($extra['vip_added'], ['yes', 'no'])) {
                $userUpdates['vip_added'] = $extra['vip_added'];
            } else {
                $userUpdates['vip_added'] = 'no';
            }
            if (!empty($extra['vip_until'])) {
                $until = Carbon::parse($extra['vip_until']);
                $userUpdates['vip_until'] = $until;
            } else {
                $userUpdates['vip_until'] = null;
            }
        } else {
            $userUpdates['vip_added'] = 'no';
            $userUpdates['vip_until'] = null;
        }
        do_log("userUpdates: " . json_encode($userUpdates));
        NexusDB::transaction(function () use ($targetUser, $userUpdates, $message) {
            $modComment = date('Y-m-d') . " - " . $message['msg'];
            if ($targetUser->class != $userUpdates['class']) {
                $targetUser->updateWithModComment($userUpdates, $modComment);
                Message::add($message);
            } else {
                $targetUser->update($userUpdates);
            }
        });
        $this->clearCache($targetUser);

        return true;
    }

    public function addMeta($user, array $metaData, array $keyExistsUpdates = [], $notify = true)
    {
        $user = $this->getUser($user);
        $locale = $user->locale;
        $metaKey = $metaData['meta_key'];
        $metaName = nexus_trans("label.user_meta.meta_keys.$metaKey", [], $locale);
        $allowMultiple = UserMeta::$metaKeys[$metaKey]['multiple'];
        $log = "user: {$user->id}, locale: $locale, metaKey: $metaKey, allowMultiple: $allowMultiple";
        $message = [
            'receiver' => $user->id,
            'added' => now(),
            'subject' => nexus_trans('user.grant_props_notification.subject', ['name' => $metaName], $locale),
        ];
        if (!empty($keyExistsUpdates['duration']) && $metaKey != UserMeta::META_KEY_CHANGE_USERNAME) {
            $durationText = $keyExistsUpdates['duration'] . " Days";
        } else {
            $durationText = nexus_trans('label.permanent', [], $locale);
        }
        $operatorId = get_user_id();
        $operatorInfo = get_user_row($operatorId);
        $message['msg'] = nexus_trans('user.grant_props_notification.body', ['name' => $metaName, 'operator' => $operatorInfo['username'], 'duration' => $durationText], $locale);
        if (!empty($metaData['duration'])) {
            $metaData['deadline'] = now()->addDays((int)$metaData['duration']);
        }
        if ($allowMultiple) {
            //Allow multiple, just insert
            $result = $user->metas()->create($metaData);
            $log .= ", allowMultiple, just insert";
        } else {
            $metaExists = $user->metas()->where('meta_key', $metaKey)->first();
            $log .= ", metaExists: " . ($metaExists->id ?? '');
            if (!$metaExists) {
                $result = $user->metas()->create($metaData);
                $log .= ", meta not exists, just create";
            } else {
                $log .= ", meta exists";
                $keyExistsUpdates['updated_at'] = now();
                if (!empty($keyExistsUpdates['duration'])) {
                    if ($metaExists->deadline === null) {
                        throw new \RuntimeException(nexus_trans('user.metas.already_valid_forever', ['meta_key_text' => $metaExists->metaKeyText]));
                    }
                    $log .= ", has duration: {$keyExistsUpdates['duration']}";
                    if ($metaExists->deadline && $metaExists->deadline->gte(now())) {
                        $log .= ", not expire";
                        $keyExistsUpdates['deadline'] = $metaExists->deadline->addDays((int)$keyExistsUpdates['duration']);
                    } else {
                        $log .= ", expired or not set";
                        $keyExistsUpdates['deadline'] = now()->addDays((int)$keyExistsUpdates['duration']);
                    }
                    unset($keyExistsUpdates['duration']);
                } else {
                    $keyExistsUpdates['deadline'] = null;
                }
                $log .= ", update: " . json_encode($keyExistsUpdates);
                $result = $metaExists->update($keyExistsUpdates);
            }
        }
        if ($result) {
            $this->clearCache($user);
            if ($notify) {
                Message::add($message);
            }
        }
        do_log($log);
        return $result;
    }

    public function confirmUser($id): bool
    {
        $update = [
            'status' => User::STATUS_CONFIRMED,
            'editsecret' => '',
        ];
        User::query()
            ->whereIn('id', Arr::wrap($id))
            ->where('status', User::STATUS_PENDING)
            ->update($update);

        return true;
    }

    public function destroy(Collection|int $id, $reasonKey = 'user.destroy_by_admin')
    {
        if (!isRunningInConsole()) {
            user_can('user-delete', true);
        }
        if (is_int($id)) {
            $uidArr = Arr::wrap($id);
        } else {
            $uidArr = $id->pluck('id')->toArray();
        }
        $uidStr = implode(',', $uidArr);
        $users = User::query()->with('language')->whereIn('id', $uidArr)->get();
        if ($users->isEmpty()) {
            return true;
        }
        $tables = [
            'users' => 'id',
            'hit_and_runs' => 'uid',
            'claims' => 'uid',
            'exam_users' => 'uid',
            'exam_progress' => 'uid',
            'user_metas' => 'uid',
            'user_medals' => 'uid',
            'attendance' => 'uid',
            'attendance_logs' => 'uid',
            'login_logs' => 'uid',
            'oauth_access_tokens' => 'user_id',
            'oauth_auth_codes' => 'user_id',
            'seed_box_records' => 'uid',
            'user_modify_logs' => 'user_id',
            'messages' => 'receiver',
        ];
        foreach ($tables as $table => $key) {
            NexusDB::statement(sprintf("delete from `%s` where `%s` in (%s)", $table, $key, $uidStr));
        }
        do_log("[DESTROY_USER]: " . json_encode($uidArr), 'error');
        $userBanLogs = [];
        foreach ($users as $user) {
            $userBanLogs[] = [
                'uid' => $user->id,
                'username' => $user->username,
                'reason' => nexus_trans($reasonKey, [], $user->locale)
            ];
        }
        UserBanLog::query()->insert($userBanLogs);
        //delete by user, make sure torrent is deleted
        NexusDB::statement(sprintf('DELETE FROM snatched WHERE userid IN (%s) and not exists (select 1 from torrents where id = snatched.torrentid)', $uidStr));
        if (is_int($id)) {
            do_action("user_delete", $id);
            fire_event(ModelEventEnum::USER_DELETED, $users->first());
        }
        return true;
    }

    public function addTemporaryInvite(User|null $operator, int $uid, string $action, int $count, int|null $days, string|null $reason = '')
    {
        do_log("uid: $uid, action: $action, count: $count, days: $days, reason: $reason");
        $action = strtolower($action);
        if ($count <= 0 || ($action == 'increment' && $days <= 0)) {
            throw new \InvalidArgumentException("days or count lte 0");
        }
        $targetUser = User::query()->findOrFail($uid, User::$commonFields);
        if ($operator) {
            $this->checkPermission($operator, $targetUser);
        }
        $toolRep = new ToolRepository();
        $locale = $targetUser->locale;

        $changeType = nexus_trans("nexus.$action", [], $locale);
        $subject = nexus_trans('message.temporary_invite_change.subject', ['change_type' => $changeType], $locale);
        $body = nexus_trans('message.temporary_invite_change.body', [
            'change_type' => $changeType,
            'count' => $count,
            'operator' => $operator->username ?? '',
            'reason' => $reason,
        ], $locale);
        $message = [
            'sender' => 0,
            'receiver' => $targetUser->id,
            'subject' => $subject,
            'msg' => $body,
            'added' => Carbon::now(),
        ];
        $inviteData = [];
        if ($action == 'increment') {
            $hashArr = $toolRep->generateUniqueInviteHash([], $count, $count);
            foreach ($hashArr as $hash) {
                $inviteData[] = [
                    'inviter' => $uid,
                    'invitee' => '',
                    'hash' => $hash,
                    'valid' => 0,
                    'expired_at' => Carbon::now()->addDays((int)$days),
                    'created_at' => Carbon::now(),
                ];
            }
        }
        NexusDB::transaction(function () use ($uid, $message, $inviteData, $count, $operator) {
            if (!empty($inviteData)) {
                Invite::query()->insert($inviteData);
                do_log("[INSERT TEMPORARY INVITE] to $uid, count: $count");
            } else {
                Invite::query()->where('inviter', $uid)
                    ->where('invitee', '')
                    ->orderBy('expired_at', 'asc')
                    ->limit($count)
                    ->delete()
                ;
                do_log("[DELETE TEMPORARY INVITE] of $uid, count: $count");
            }
            if ($operator) {
                Message::add($message);
            }
        });
        return true;
    }

    public function getInviteBtnText(int $uid)
    {
        if (Setting::get('main.invitesystem') != 'yes') {
            throw new NexusException(nexus_trans('invite.send_deny_reasons.invite_system_closed'));
        }
        $permission = 'sendinvite';
        if (!user_can($permission, false, $uid)) {
            $requireClass = get_setting("authority.$permission");
            throw new NexusException(nexus_trans('invite.send_deny_reasons.no_permission', ['class' => User::getClassText($requireClass)]));
        }
        $userInfo = User::query()->findOrFail($uid, User::$commonFields);
        $temporaryInviteCount = $userInfo->temporary_invites()->count();
        if ($userInfo->invites + $temporaryInviteCount < 1) {
            throw new NexusException(nexus_trans('invite.send_deny_reasons.invite_not_enough'));
        }
        return nexus_trans('invite.send_allow_text');
    }

    public function saveLoginLog(int $uid, string $ip,  string $client = '', bool $notify = false)
    {
        $locationInfo = get_ip_location_from_geoip($ip);
        $loginLog = LoginLog::query()->create([
            'ip' => $ip,
            'uid' => $uid,
            'country' => $locationInfo['country_en'] ?? '',
            'city' => $locationInfo['city_en'] ?? '',
            'client' => $client,
        ]);
        if ($notify) {
            $command = sprintf("user:login_notify --this_id=%s", $loginLog->id);
            do_log("[LOGIN_NOTIFY], user: $uid, $command");
            executeCommand($command, "string", true, false);
        }
        return $loginLog;
    }

    /**
     * get user seeding/leeching count and size
     *
     * @see calculate_seed_bonus()
     * @param array $userIdArr
     * @return array
     */
    private function listUserSeedingLeechingData(array $userIdArr)
    {
        $minSize = get_setting('bonus.min_size', 0);
        $idStr = implode(",", $userIdArr);
        $sql = "select peers.userid, peers.seeder, torrents.size from torrents LEFT JOIN peers ON peers.torrent = torrents.id WHERE peers.userid in ($idStr) and torrents.size > $minSize group by peers.torrent, peers.peer_id, peers.userid, peers.seeder";
        $data = NexusDB::select($sql);
        $result = [];
        foreach ($data as $row) {
            if (!isset($result[$row['userid']])) {
                $result[$row['userid']] = [
                    'seeding_count' => 0,
                    'seeding_size' => 0,
                    'leeching_count' => 0,
                    'leeching_size' => 0,
                ];
            }
            if ($row['seeder'] == 'yes') {
                $result[$row['userid']]['seeding_count'] += 1;
                $result[$row['userid']]['seeding_size'] += $row['size'];
            } else {
                $result[$row['userid']]['leeching_count'] += 1;
                $result[$row['userid']]['leeching_size'] += $row['size'];
            }
        }
        return $result;
    }

}
