<?php

declare(strict_types=1);

namespace app\admin\library;

use Throwable;
use ba\Random;
use support\think\Db;
use app\admin\model\Admin;
use app\common\facade\Token;
use app\admin\model\AdminGroup;

/**
 * 管理员权限类（Webman 迁移版）
 * @property int    $id         管理员ID
 * @property string $username   管理员用户名
 * @property string $nickname   管理员昵称
 * @property string $email      管理员邮箱
 * @property string $mobile     管理员手机号
 */
class Auth extends \ba\Auth
{
    public const LOGIN_RESPONSE_CODE = 303;
    public const NEED_LOGIN = 'need login';
    public const LOGGED_IN = 'logged in';
    public const TOKEN_TYPE = 'admin';

    protected bool $loginEd = false;
    protected string $error = '';
    protected ?Admin $model = null;
    protected string $token = '';
    protected string $refreshToken = '';
    protected int $keepTime = 86400;
    protected int $refreshTokenKeepTime = 2592000;
    protected array $allowFields = ['id', 'username', 'nickname', 'avatar', 'last_login_time'];

    public function __construct(array $config = [])
    {
        parent::__construct($config);
        $this->setKeepTime((int) config('buildadmin.admin_token_keep_time', 86400 * 3));
    }

    public function __get($name): mixed
    {
        return $this->model?->$name;
    }

    /**
     * 初始化（Webman：从 request 获取或创建新实例）
     */
    public static function instance(array $options = []): Auth
    {
        $request = function_exists('request') ? request() : null;
        if ($request !== null && isset($request->adminAuth) && $request->adminAuth instanceof Auth) {
            return $request->adminAuth;
        }
        $auth = new static($options);
        if ($request !== null) {
            $request->adminAuth = $auth;
        }
        return $auth;
    }

    public function init(string $token): bool
    {
        $tokenData = Token::get($token);
        if ($tokenData) {
            Token::tokenExpirationCheck($tokenData);

            $userId = (int) $tokenData['user_id'];
            if ($tokenData['type'] === self::TOKEN_TYPE && $userId > 0) {
                $this->model = Admin::where('id', $userId)->find();
                if (!$this->model) {
                    $this->setError('Account not exist');
                    return false;
                }
                if ($this->model['status'] !== 'enable') {
                    $this->setError('Account disabled');
                    return false;
                }
                $this->token = $token;
                $this->loginSuccessful();
                return true;
            }
        }
        $this->setError('Token login failed');
        $this->reset();
        return false;
    }

    public function login(string $username, string $password, bool $keep = false): bool
    {
        $this->model = Admin::where('username', $username)->find();
        if (!$this->model) {
            $this->setError('Username is incorrect');
            return false;
        }
        if ($this->model->status === 'disable') {
            $this->setError('Account disabled');
            return false;
        }

        $adminLoginRetry = config('buildadmin.admin_login_retry');
        if ($adminLoginRetry) {
            $lastLoginTime = $this->model->getData('last_login_time');
            if ($lastLoginTime) {
                if ($this->model->login_failure > 0 && time() - $lastLoginTime >= 86400) {
                    $this->model->login_failure = 0;
                    $this->model->save();
                    $this->model = Admin::where('username', $username)->find();
                }

                if ($this->model->login_failure >= $adminLoginRetry) {
                    $this->setError('Please try again after 1 day');
                    return false;
                }
            }
        }

        if (!verify_password($password, $this->model->password, ['salt' => $this->model->salt ?? ''])) {
            $this->loginFailed();
            $this->setError('Password is incorrect');
            return false;
        }

        if (config('buildadmin.admin_sso')) {
            Token::clear(self::TOKEN_TYPE, $this->model->id);
            Token::clear(self::TOKEN_TYPE . '-refresh', $this->model->id);
        }

        if ($keep) {
            $this->setRefreshToken($this->refreshTokenKeepTime);
        }
        $this->loginSuccessful();
        return true;
    }

    public function setRefreshToken(int $keepTime = 0): void
    {
        $this->refreshToken = Random::uuid();
        Token::set($this->refreshToken, self::TOKEN_TYPE . '-refresh', $this->model->id, $keepTime);
    }

    public function loginSuccessful(): bool
    {
        if (!$this->model) {
            return false;
        }
        $this->model->startTrans();
        try {
            $this->model->login_failure = 0;
            $this->model->last_login_time = time();
            $this->model->last_login_ip = function_exists('request') && request() ? request()->getRealIp() : '';
            $this->model->save();
            $this->loginEd = true;

            if (!$this->token) {
                $this->token = Random::uuid();
                Token::set($this->token, self::TOKEN_TYPE, $this->model->id, $this->keepTime);
            }
            $this->model->commit();
        } catch (Throwable $e) {
            $this->model->rollback();
            $this->setError($e->getMessage());
            return false;
        }
        return true;
    }

    public function loginFailed(): bool
    {
        if (!$this->model) {
            return false;
        }
        $this->model->startTrans();
        try {
            $this->model->login_failure++;
            $this->model->last_login_time = time();
            $this->model->last_login_ip = function_exists('request') && request() ? request()->getRealIp() : '';
            $this->model->save();
            $this->model->commit();
        } catch (Throwable $e) {
            $this->model->rollback();
        }
        return $this->reset();
    }

    public function logout(): bool
    {
        if (!$this->loginEd) {
            $this->setError('You are not logged in');
            return false;
        }
        return $this->reset();
    }

    public function isLogin(): bool
    {
        return $this->loginEd;
    }

    public function getAdmin(): Admin
    {
        return $this->model;
    }

    public function getToken(): string
    {
        return $this->token;
    }

    public function getRefreshToken(): string
    {
        return $this->refreshToken;
    }

    public function getInfo(): array
    {
        if (!$this->model) {
            return [];
        }
        $info = $this->model->toArray();
        $info = array_intersect_key($info, array_flip($this->getAllowFields()));
        $info['token'] = $this->getToken();
        $info['refresh_token'] = $this->getRefreshToken();
        return $info;
    }

    public function getAllowFields(): array
    {
        return $this->allowFields;
    }

    public function setAllowFields($fields): void
    {
        $this->allowFields = $fields;
    }

    public function setKeepTime(int $keepTime = 0): void
    {
        $this->keepTime = $keepTime;
    }

    public function check(string $name, int $uid = 0, string $relation = 'or', string $mode = 'url'): bool
    {
        return parent::check($name, $uid ?: $this->id, $relation, $mode);
    }

    public function getGroups(int $uid = 0): array
    {
        return parent::getGroups($uid ?: $this->id);
    }

    public function getRuleList(int $uid = 0): array
    {
        return parent::getRuleList($uid ?: $this->id);
    }

    public function getRuleIds(int $uid = 0): array
    {
        return parent::getRuleIds($uid ?: $this->id);
    }

    public function getMenus(int $uid = 0): array
    {
        return parent::getMenus($uid ?: $this->id);
    }

    public function isSuperAdmin(): bool
    {
        return in_array('*', $this->getRuleIds());
    }

    public function getAdminChildGroups(): array
    {
        $groupIds = Db::name('admin_group_access')
            ->where('uid', $this->id)
            ->select();
        $children = [];
        foreach ($groupIds as $group) {
            $this->getGroupChildGroups($group['group_id'], $children);
        }
        return array_unique($children);
    }

    public function getGroupChildGroups(int $groupId, array &$children): void
    {
        $childrenTemp = AdminGroup::where('pid', $groupId)
            ->where('status', 1)
            ->select();
        foreach ($childrenTemp as $item) {
            $children[] = $item['id'];
            $this->getGroupChildGroups($item['id'], $children);
        }
    }

    public function getGroupAdmins(array $groups): array
    {
        return Db::name('admin_group_access')
            ->where('group_id', 'in', $groups)
            ->column('uid');
    }

    public function getAllAuthGroups(string $dataLimit, array $groupQueryWhere = [['status', '=', 1]]): array
    {
        $rules = $this->getRuleIds();
        $allAuthGroups = [];
        $groups = AdminGroup::where($groupQueryWhere)->select();
        foreach ($groups as $group) {
            if ($group['rules'] === '*') {
                continue;
            }
            $groupRules = explode(',', $group['rules']);
            $all = true;
            foreach ($groupRules as $groupRule) {
                if (!in_array($groupRule, $rules)) {
                    $all = false;
                    break;
                }
            }
            if ($all) {
                if ($dataLimit === 'allAuth' || ($dataLimit === 'allAuthAndOthers' && array_diff($rules, $groupRules))) {
                    $allAuthGroups[] = $group['id'];
                }
            }
        }
        return $allAuthGroups;
    }

    public function setError($error): Auth
    {
        $this->error = $error;
        return $this;
    }

    public function getError(): string
    {
        return $this->error ? __($this->error) : '';
    }

    protected function reset(bool $deleteToken = true): bool
    {
        if ($deleteToken && $this->token) {
            Token::delete($this->token);
        }

        $this->token = '';
        $this->loginEd = false;
        $this->model = null;
        $this->refreshToken = '';
        $this->setError('');
        $this->setKeepTime((int) config('buildadmin.admin_token_keep_time', 86400 * 3));
        return true;
    }
}
