<?php

namespace app\admin\controller\user;

use Throwable;
use app\common\controller\Backend;
use app\common\service\GameHotDataCoordinator;
use app\common\service\GameHotDataRedis;
use support\think\Db;
use support\Response;
use Webman\Http\Request as WebmanRequest;

/**
 * 用户管理
 */
class User extends Backend
{
    /**
     * 这些接口只要求登录，不单独校验权限节点
     */
    protected array $noNeedPermission = ['adminScopeTree'];

    /**
     * User模型对象
     * @var object|null
     * @phpstan-var \app\common\model\User|null
     */
    protected ?object $model = null;

    /**
     * 渠道管理员仅可管理自己名下（admin_id=当前管理员）的用户
     */
    protected bool|string|int $dataLimit = true;

    protected array|string $preExcludeFields = ['id', 'uuid', 'create_time', 'update_time', 'invite_code', 'coin', 'total_deposit_coin', 'total_withdraw_coin', 'bet_flow_coin'];

    protected array $withJoinTable = ['channel', 'admin'];

    protected string|array $quickSearchField = ['id', 'username', 'phone'];

    protected bool $modelSceneValidate = true;

    protected function initController(WebmanRequest $request): ?Response
    {
        $this->model = new \app\common\model\User();
        return null;
    }

    /**
     * 添加（重写：password 使用 Admin 同款加密；uuid 为 10 位唯一对外标识）
     * @throws Throwable
     */
    protected function _add(): Response
    {
        if ($this->request && $this->request->method() === 'POST') {
            $data = $this->request->post();
            if (!$data) {
                return $this->error(__('Parameter %s can not be empty', ['']));
            }

            $data = $this->applyInputFilter($data);
            $inviteResolved = $this->applyInviteCodeToUserChannel($data);
            if ($inviteResolved !== null) {
                return $inviteResolved;
            }
            $data = $this->excludeFields($data);

            $password = $data['password'] ?? null;
            if (!is_string($password) || trim($password) === '') {
                return $this->error(__('Parameter %s can not be empty', ['password']));
            }
            $data['password'] = hash_password($password);

            $username = $data['username'] ?? '';
            $channelId = $data['channel_id'] ?? null;
            if (!is_string($username) || trim($username) === '' || $channelId === null || $channelId === '') {
                return $this->error(__('Parameter %s can not be empty', ['username/channel_id']));
            }
            $data['uuid'] = \app\common\model\User::generateUniquePublicCode10();

            if ($this->dataLimit && $this->dataLimitFieldAutoFill) {
                $data[$this->dataLimitField] = $this->auth->id;
            }

            $result = false;
            $this->model->startTrans();
            try {
                if ($this->modelValidate) {
                    $validate = str_replace("\\model\\", "\\validate\\", get_class($this->model));
                    if (class_exists($validate)) {
                        $validate = new $validate();
                        if ($this->modelSceneValidate) {
                            $validate->scene('add');
                        }
                        $validate->check($data);
                    }
                }
                $result = $this->model->save($data);
                $this->model->commit();
            } catch (Throwable $e) {
                $this->model->rollback();
                return $this->error($e->getMessage());
            }
            if ($result !== false) {
                return $this->success(__('Added successfully'));
            }
            return $this->error(__('No rows were added'));
        }

        return $this->error(__('Parameter error'));
    }

    /**
     * 编辑（重写：password 使用 Admin 同款加密；uuid 创建后不因改名改渠道自动变更）
     * @throws Throwable
     */
    protected function _edit(): Response
    {
        $pk = $this->model->getPk();
        $id = $this->request ? ($this->request->post($pk) ?? $this->request->get($pk)) : null;
        $row = $this->model->find($id);
        if (!$row) {
            return $this->error(__('Record not found'));
        }

        $dataLimitAdminIds = $this->getDataLimitAdminIds();
        if ($dataLimitAdminIds && !in_array($row[$this->dataLimitField], $dataLimitAdminIds)) {
            return $this->error(__('You have no permission'));
        }

        if ($this->request && $this->request->method() === 'POST') {
            $data = $this->request->post();
            if (!$data) {
                return $this->error(__('Parameter %s can not be empty', ['']));
            }

            $data = $this->applyInputFilter($data);
            $data = $this->excludeFields($data);

            if (array_key_exists('password', $data)) {
                $password = $data['password'];
                if (!is_string($password) || trim($password) === '') {
                    unset($data['password']);
                } else {
                    $data['password'] = hash_password($password);
                }
            }

            $result = false;
            $this->model->startTrans();
            try {
                if ($this->modelValidate) {
                    $validate = str_replace("\\model\\", "\\validate\\", get_class($this->model));
                    if (class_exists($validate)) {
                        $validate = new $validate();
                        if ($this->modelSceneValidate) {
                            $validate->scene('edit');
                        }
                        $data[$pk] = $row[$pk];
                        $validate->check($data);
                    }
                }
                $result = $row->save($data);
                $this->model->commit();
            } catch (Throwable $e) {
                $this->model->rollback();
                return $this->error($e->getMessage());
            }
            if ($result !== false) {
                return $this->success(__('Update successful'));
            }
            return $this->error(__('No rows updated'));
        }

        // GET: 返回编辑数据时，剔除敏感字段
        unset($row['password'], $row['salt'], $row['token'], $row['refresh_token']);
        return $this->success('', [
            'row' => $row
        ]);
    }

    /**
     * 查看
     * @throws Throwable
     */
    protected function _index(): Response
    {
        // 如果是 select 则转发到 select 方法，若未重写该方法，其实还是继续执行 index
        if ($this->request && $this->request->get('select')) {
            return $this->select($this->request);
        }

        /**
         * 1. withJoin 不可使用 alias 方法设置表别名，别名将自动使用关联模型名称（小写下划线命名规则）
         * 2. 以下的别名设置了主表别名，同时便于拼接查询参数等
         * 3. paginate 数据集可使用链式操作 each(function($item, $key) {}) 遍历处理
         */
        list($where, $alias, $limit, $order) = $this->queryBuilder();
        $res = $this->model
            ->withJoin($this->withJoinTable, $this->withJoinType)
            ->with($this->withJoinTable)
            ->visible(['channel' => ['name'], 'admin' => ['username']])
            ->alias($alias)
            ->where($where)
            ->order($order)
            ->paginate($limit);

        return $this->success('', [
            'list'   => $res->items(),
            'total'  => $res->total(),
            'remark' => get_route_remark(),
        ]);
    }

    /**
     * 后台钱包加减点（不允许在用户编辑表单直接改余额）
     */
    public function walletAdjust(WebmanRequest $request): Response
    {
        $response = $this->initializeBackend($request);
        if ($response !== null) {
            return $response;
        }
        if ($request->method() !== 'POST') {
            return $this->error(__('Parameter error'));
        }

        $userIdRaw = $request->post('user_id');
        $userId = filter_var($userIdRaw, FILTER_VALIDATE_INT);
        if ($userId === false || $userId <= 0) {
            return $this->error(__('Parameter error'));
        }

        $opRaw = $request->post('op');
        $op = is_string($opRaw) ? trim($opRaw) : '';
        if (!in_array($op, ['credit', 'deduct'], true)) {
            return $this->error(__('Invalid operation type'));
        }

        $amountRaw = $request->post('amount');
        $amountText = is_string($amountRaw) || is_numeric($amountRaw) ? trim(strval($amountRaw)) : '';
        if ($amountText === '' || !is_numeric($amountText)) {
            return $this->error(__('Invalid amount format'));
        }
        if (bccomp($amountText, '0', 2) <= 0) {
            return $this->error(__('Amount must be greater than 0'));
        }

        $remarkRaw = $request->post('remark');
        $remark = is_string($remarkRaw) ? trim($remarkRaw) : '';
        $adminName = is_string($this->auth->username ?? null) ? $this->auth->username : ('#' . strval($this->auth->id));
        $amountForRemark = self::formatAmountForDisplay($amountText);
        if ($remark === '') {
            $actionText = $op === 'credit' ? '加点' : '扣点';
            $remark = '后台管理员（' . $adminName . '）' . $actionText . $amountForRemark . '（值）';
        }

        $lock = GameHotDataRedis::userAdminMutationLockTry($userId);
        if (!$lock['acquired']) {
            return $this->error(__('This user is being operated by another admin (wallet/concurrent save); please try again later'));
        }

        try {
            $user = $this->model->where('id', $userId)->find();
            if (!$user) {
                return $this->error(__('Record not found'));
            }
            $dataLimitAdminIds = $this->getDataLimitAdminIds();
            if ($dataLimitAdminIds && !in_array($user[$this->dataLimitField], $dataLimitAdminIds)) {
                return $this->error(__('You have no permission'));
            }

            $channelIdRaw = $user['channel_id'] ?? null;
            $channelId = filter_var($channelIdRaw, FILTER_VALIDATE_INT);
            if ($channelId === false) {
                $channelId = null;
            }

            $before = strval($user['coin'] ?? '0');
            $delta = self::normalizeAmountScale($amountText, 2);
            if ($op === 'credit') {
                $after = bcadd($before, $delta, 2);
                $bizType = 'admin_credit';
                $direction = 1;
            } else {
                if (bccomp($before, $delta, 2) < 0) {
                    return $this->error(__('Insufficient balance; deduction failed'));
                }
                $after = bcsub($before, $delta, 2);
                $bizType = 'admin_deduct';
                $direction = 2;
            }

            $now = time();
            $idem = 'admin_adjust_' . $userId . '_' . $this->auth->id . '_' . $now . '_' . random_int(1000, 9999);
            $operatorId = filter_var($this->auth->id, FILTER_VALIDATE_INT);
            if ($operatorId === false) {
                $operatorId = 0;
            }

            Db::startTrans();
            try {
                $affected = Db::name('user')->where('id', $userId)->where('coin', $before)->update([
                    'coin' => $after,
                    'update_time' => $now,
                ]);
                if ($affected !== 1) {
                    Db::rollback();
                    return $this->error(__('Save failed: user balance changed by another request; please refresh and retry'));
                }

                Db::name('user_wallet_record')->insert([
                    'user_id' => $userId,
                    'channel_id' => $channelId,
                    'biz_type' => $bizType,
                    'direction' => $direction,
                    'amount' => $delta,
                    'balance_before' => $before,
                    'balance_after' => $after,
                    'ref_type' => 'admin_user_wallet_adjust',
                    'ref_id' => null,
                    'idempotency_key' => $idem,
                    'operator_admin_id' => $operatorId,
                    'remark' => substr($remark, 0, 500),
                    'create_time' => $now,
                ]);
                Db::commit();
            } catch (Throwable $e) {
                Db::rollback();
                return $this->error($e->getMessage());
            }

            GameHotDataCoordinator::afterUserCommitted($userId);

            return $this->success(__('Wallet adjusted successfully'), [
                'user_id' => $userId,
                'coin_before' => self::formatAmountForDisplay($before),
                'coin_after' => self::formatAmountForDisplay($after),
                'amount' => self::formatAmountForDisplay($delta),
                'op' => $op,
            ]);
        } finally {
            GameHotDataRedis::userAdminMutationLockRelease($userId, $lock['token'], $lock['redis_lock']);
        }
    }

    private static function normalizeAmountScale(string $amount, int $scale): string
    {
        $raw = trim(str_replace(',', '.', $amount));
        if ($raw === '') {
            return '0';
        }
        $negative = false;
        if (str_starts_with($raw, '-')) {
            $negative = true;
            $raw = ltrim(substr($raw, 1));
        }
        if (!str_contains($raw, '.')) {
            $v = ltrim($raw, '0');
            $v = $v === '' ? '0' : $v;
            return $negative ? ('-' . $v) : $v;
        }
        [$intPart, $fracPart] = explode('.', $raw, 2);
        $intPart = ltrim($intPart, '0');
        $intPart = $intPart === '' ? '0' : $intPart;
        $fracPart = preg_replace('/\D+/', '', $fracPart) ?? '';
        if (strlen($fracPart) > $scale) {
            $fracPart = substr($fracPart, 0, $scale);
        } else {
            $fracPart = str_pad($fracPart, $scale, '0');
        }
        $v = $intPart . '.' . $fracPart;
        return $negative ? ('-' . $v) : $v;
    }

    private static function formatAmountForDisplay(string $amount): string
    {
        $normalized = self::normalizeAmountScale($amount, 2);
        $negative = false;
        if (str_starts_with($normalized, '-')) {
            $negative = true;
            $normalized = substr($normalized, 1);
        }
        $parts = explode('.', $normalized, 2);
        $intPart = $parts[0] ?? '0';
        $fracPart = $parts[1] ?? '0000';
        $displayFrac = substr($fracPart, 0, 2);
        $v = $intPart . '.' . str_pad($displayFrac, 2, '0');
        return $negative ? ('-' . $v) : $v;
    }

    /**
     * 角色组 → 管理员树（仅当前账号可管理的角色组及其下管理员；用于游戏用户归属）
     * 同一管理员若属于多个组，只挂在 id 最小的所属组下，避免树中重复 value
     */
    public function adminScopeTree(WebmanRequest $request): Response
    {
        $response = $this->initializeBackend($request);
        if ($response !== null) {
            return $response;
        }

        $currentAdminLeaf = $this->getCurrentAdminLeaf();
        if ($currentAdminLeaf === null) {
            return $this->error(__('Record not found'));
        }

        $groupIds = $this->getManageableAdminGroupIds();
        if ($groupIds === []) {
            return $this->success('', [
                'list' => [$currentAdminLeaf],
                'current_admin_id' => $currentAdminLeaf['value'],
            ]);
        }

        $groups = Db::name('admin_group')
            ->where('id', 'in', $groupIds)
            ->where('status', 1)
            ->field(['id', 'pid', 'name'])
            ->order('id', 'asc')
            ->select()
            ->toArray();

        $accessRows = Db::name('admin_group_access')->alias('aga')
            ->join('admin a', 'aga.uid = a.id')
            ->where('aga.group_id', 'in', $groupIds)
            ->field(['a.id', 'a.username', 'a.channel_id', 'a.invite_code', 'aga.group_id'])
            ->select()
            ->toArray();

        $adminPrimary = [];
        foreach ($accessRows as $row) {
            $uid = intval((string)$row['id']);
            $gid = intval((string)$row['group_id']);
            if (!isset($adminPrimary[$uid]) || $gid < $adminPrimary[$uid]['gid']) {
                $adminPrimary[$uid] = [
                    'gid'  => $gid,
                    'user' => $row,
                ];
            }
        }

        $adminsByGroup = [];
        foreach ($adminPrimary as $item) {
            $row = $item['user'];
            $gid = $item['gid'];
            $invite = $row['invite_code'] ?? '';
            $invite = is_string($invite) ? $invite : '';
            $channelId = $row['channel_id'] ?? null;
            $adminsByGroup[$gid][] = [
                'value'       => (string)$row['id'],
                'label'       => (string)$row['username'],
                'is_leaf'     => true,
                'channel_id'  => $channelId === null || $channelId === '' ? null : intval((string)$channelId),
                'invite_code' => $invite,
            ];
        }

        $groupMap = [];
        foreach ($groups as $g) {
            $groupMap[intval((string)$g['id'])] = $g;
        }

        $childGroupIdsByPid = [];
        foreach ($groups as $g) {
            $id = intval((string)$g['id']);
            $pid = intval((string)($g['pid'] ?? 0));
            $childGroupIdsByPid[$pid][] = $id;
        }

        $buildNode = null;
        $buildNode = function (int $groupId) use (&$buildNode, $groupMap, $childGroupIdsByPid, $adminsByGroup): array {
            if (!isset($groupMap[$groupId])) {
                return [];
            }
            $g = $groupMap[$groupId];
            $children = [];
            foreach ($childGroupIdsByPid[$groupId] ?? [] as $childId) {
                $children[] = $buildNode($childId);
            }
            foreach ($adminsByGroup[$groupId] ?? [] as $leaf) {
                $children[] = $leaf;
            }

            return [
                'value'    => 'group_' . $groupId,
                'label'    => (string)$g['name'],
                'disabled' => true,
                'children' => $children,
            ];
        };

        $groupIdSet = array_fill_keys(array_keys($groupMap), true);
        $roots = [];
        foreach ($groups as $g) {
            $id = intval((string)$g['id']);
            $pid = intval((string)($g['pid'] ?? 0));
            if ($pid === 0 || !isset($groupIdSet[$pid])) {
                $roots[] = $id;
            }
        }
        $roots = array_values(array_unique($roots));
        sort($roots);

        $tree = [];
        foreach ($roots as $rid) {
            $tree[] = $buildNode($rid);
        }
        if (!isset($adminPrimary[intval(strval($currentAdminLeaf['value']))])) {
            $tree[] = $currentAdminLeaf;
        }

        return $this->success('', [
            'list' => $tree,
            'current_admin_id' => $currentAdminLeaf['value'],
        ]);
    }

    private function getCurrentAdminLeaf(): ?array
    {
        $adminIdRaw = $this->auth->id ?? null;
        if ($adminIdRaw === null || $adminIdRaw === '' || !is_numeric(strval($adminIdRaw))) {
            return null;
        }
        $adminId = intval(strval($adminIdRaw));
        if ($adminId <= 0) {
            return null;
        }
        $row = Db::name('admin')
            ->field(['id', 'username', 'channel_id', 'invite_code'])
            ->where('id', $adminId)
            ->find();
        if (!$row) {
            return null;
        }
        $invite = $row['invite_code'] ?? '';
        $invite = is_string($invite) ? $invite : '';
        $channelId = $row['channel_id'] ?? null;
        return [
            'value'       => strval($adminId),
            'label'       => strval($row['username'] ?? ('#' . strval($adminId))),
            'is_leaf'     => true,
            'channel_id'  => $channelId === null || $channelId === '' ? null : intval(strval($channelId)),
            'invite_code' => $invite,
        ];
    }

    /**
     * @return int[]
     */
    private function getManageableAdminGroupIds(): array
    {
        if ($this->auth->isSuperAdmin()) {
            return Db::name('admin_group')->where('status', 1)->column('id');
        }
        $own = Db::name('admin_group_access')->where('uid', $this->auth->id)->column('group_id');
        $own = array_map('intval', $own);
        $children = array_map('intval', $this->auth->getAdminChildGroups());
        return array_values(array_unique(array_merge($own, $children)));
    }

    /**
     * 请求中的 `invite_code`（子代理 admin.invite_code）解析为 `channel_id`，并写入 `register_invite_code`、`admin_id`。
     * 若同时提交 `channel_id` / `admin_id`，须与邀请码对应记录一致。
     */
    private function applyInviteCodeToUserChannel(array &$data): ?Response
    {
        if (!array_key_exists('invite_code', $data)) {
            return null;
        }
        $raw = $data['invite_code'];
        unset($data['invite_code']);
        $inviteCode = is_string($raw) ? trim($raw) : '';
        if ($inviteCode === '') {
            return null;
        }

        $row = Db::name('admin')->field(['id', 'channel_id'])->where('invite_code', $inviteCode)->find();
        if (!$row) {
            return $this->error(__('Invite code does not exist'));
        }
        $cid = $row['channel_id'] ?? null;
        if ($cid === null || $cid === '') {
            return $this->error(__('Invite code not bound to channel'));
        }
        $cidInt = intval(trim((string) $cid));
        if ($cidInt <= 0) {
            return $this->error(__('Invite code not bound to channel'));
        }

        if (isset($data['channel_id']) && $data['channel_id'] !== null && $data['channel_id'] !== '') {
            $existing = intval(trim((string) $data['channel_id']));
            if ($existing > 0 && $existing !== $cidInt) {
                return $this->error(__('Parameter error'));
            }
        }
        $data['channel_id'] = $cidInt;
        $data['register_invite_code'] = $inviteCode;

        $aidRaw = $row['id'] ?? null;
        if ($aidRaw === null || $aidRaw === '') {
            return $this->error(__('Parameter error'));
        }
        $aidInt = intval(trim((string) $aidRaw));
        if ($aidInt <= 0) {
            return $this->error(__('Parameter error'));
        }
        if (isset($data['admin_id']) && $data['admin_id'] !== null && $data['admin_id'] !== '') {
            $reqAid = intval(trim((string) $data['admin_id']));
            if ($reqAid > 0 && $reqAid !== $aidInt) {
                return $this->error(__('Parameter error'));
            }
        }
        $data['admin_id'] = $aidInt;

        return null;
    }

    /**
     * 若需重写查看、编辑、删除等方法，请复制 @see \app\admin\library\traits\Backend 中对应的方法至此进行重写
     */
}