<?php

namespace xmvc;

/**
 * 登录处理类
 */
abstract class Login
{
    private string $realm = PROJECT . '-' . SITE;
    private string $loginKey = '';
    private string $infoKey = '';
    private bool $logined = false;
    private string $logoutController = '';
    private string $logoutFunction = '';

    /**
     * 登录处理类 - 构造
     *
     * @param string $loginKey 用于存储在 SESSION 中的是否登录标识的键, 一般为 SITE_USER_LOGIN_KEY
     * @param string $infoKey 用于存储在 SESSION 中的用户基本信息, 一般为 SITE_USER_INFO_KEY
     */
    public function __construct(string $loginKey, string $infoKey)
    {
        Session::start();
        $this->loginKey = $loginKey;
        $this->infoKey = $infoKey;
        if (PROJECT == '' || SITE == '') {
            error('PROJECT 项目名常量与 SITE 站点名常量不能为空', null, [[__FILE__, __LINE__]]);
        }

        if ($this->loginKey == '' || $this->infoKey == '') {
            error('请指定用于存储登录的键', null, [[__FILE__, __LINE__]]);
        }

        $parts = $this->logout();
        $parts = explode('-', $parts);
        try {
            list($this->logoutController, $this->logoutFunction) = $parts;
        } catch (\Throwable $th) {
            error('logout() 返回的 控制器-方法名 格式不正确, 注意: 区分类名与方法名的大小写', null, [[__FILE__, __LINE__]]);
        }
    }

    /**
     * 权限检测
     */
    abstract public function verify(): bool;

    /**
     * 取用户名的密码
     * @param string $username 用户名
     * @param string|false 返回已被 digestPass() 哈希过的密码过来, 返回 false 即用户不存在
     */
    abstract public function getPassword(string $username): string|false;

    /**
     * 退出方法
     * - 注意: 区分类名与方法名的大小写
     * @param string 返回 控制器::方法名, 例 xmvc/core/Cmd::logout
     */
    abstract public function logout(): string;

    /**
     * 登录成功事件 - 写日志 或者 其他处理
     * @param string $username 用户名
     */
    abstract public function onSuccess(string $username);

    /**
     * 登录失败事件 - 写日志 或者 其他处理
     * @param string $username 用户名
     */
    abstract public function onFail(string $username);

    /**
     * 是否登录成功
     */
    public function isLogined(): bool
    {
        return $this->logined && Session::get($this->loginKey);
    }

    /**
     * 浏览器验证模式的 密码 Hash 值
     */
    public function passwordForWWWAuth(string $username, string $password): string
    {
        //dd($this->realm, $username, $password, md5($username . ':' . $this->realm . ':' . $password));
        return md5($username . ':' . $this->realm . ':' . $password);
    }

    /**
     * 登录成功
     */
    public function success(string $username)
    {
        $sessionData = [
            'username' => $username,
            'token' => bin2hex(random_bytes(16)),
            'loginTime' => time(),
        ];
        Session::set($this->infoKey, $sessionData);
        Session::set($this->loginKey, true);
        $this->logined = true;
        $this->onSuccess($username);
    }

    /**
     * 登录失败
     */
    public function fail(string $username)
    {
        $this->clearLogin();
        $this->onFail($username);
    }

    /**
     * 清除登录信息
     */
    private function clearLogin()
    {
        Session::remove($this->infoKey);
        Session::remove($this->loginKey);
    }

    /**
     * 登录 basic 认证
     */
    protected function basicVerify()
    {
        //dd($this->passwordForWWWAuth('admin', 'abc123...'));
        if (strcasecmp(MVC::$controller, $this->logoutController) == 0 && strcasecmp(MVC::$function, $this->logoutFunction) == 0) {
            $this->clearLogin();
            $msg = '<div class="box-alert"><h1>CORE SAFELY LOGOUT</h1><br /><p>' . lg('已安全退出') . '</p></div>';
            header($_SERVER['SERVER_PROTOCOL'] . ' 401 Unauthorized', true, 401);
            MVC::echo($msg);
            exit();
        }
        if (Session::get($this->loginKey) !== true) {
            if (isset($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW'])) {
                $username = $_SERVER['PHP_AUTH_USER'];
                $password = $_SERVER['PHP_AUTH_PW'];
                $storePassword = $this->getPassword($username);
                if (
                    $storePassword !== false
                    && hash_equals(
                        $this->passwordForWWWAuth($username, $password),
                        $this->passwordForWWWAuth($username, $storePassword)
                    )
                ) {
                    $this->success($username);
                } else {
                    $this->fail($username);
                }
            }
        }
        if (Session::get($this->loginKey) !== true) {
            $msg = '<div class="box-alert">
                    <h1>CORE ERROR</h1><br />
                    <p>Incorrect username or password<br />用户名或者密码错误</p>
                    </div>';
            header($_SERVER['SERVER_PROTOCOL'] . ' 401 Unauthorized', true, 401);
            header('WWW-Authenticate: Basic realm="' . $this->realm . '"');
            MVC::echo($msg);
            exit();
        } else {
            $this->logined = true;
        }
    }

    /**
     * 登录 digest 认证
     */
    protected function digestVerify()
    {
        if (strcasecmp(MVC::$controller, $this->logoutController) == 0 && strcasecmp(MVC::$function, $this->logoutFunction) == 0) {
            $this->clearLogin();
            $msg = '<div class="box-alert"><h1>CORE SAFELY LOGOUT</h1><br /><p>' . lg('已安全退出') . '</p></div>';
            header($_SERVER['SERVER_PROTOCOL'] . ' 401 Unauthorized', true, 401);
            MVC::echo($msg);
            exit();
        }
        if (Session::get($this->loginKey) !== true) {
            $data = null;
            if (isset($_SERVER['HTTP_AUTHORIZATION']) && substr($_SERVER['HTTP_AUTHORIZATION'], 0, 7) == 'Digest ') {
                $data = $this->httpDigestParse(substr($_SERVER['HTTP_AUTHORIZATION'], 7));
            } elseif (isset($_SERVER['PHP_AUTH_DIGEST'])) {
                $digestStr = $_SERVER['PHP_AUTH_DIGEST'];
                $digestStr = trim($digestStr);
                $digestParts = explode(',', $digestStr);
                $data = [];
                foreach ($digestParts as $part) {
                    $part = trim($part);
                    $pos = strpos($part, '=');
                    if ($pos !== false) {
                        $key = trim(substr($part, 0, $pos));
                        $value = trim(substr($part, $pos + 1));
                        $value = trim($value, '"');
                        $data[$key] = $value;
                    }
                }
            }
            if ($data) {
                $username = $data['username'];
                $storePassword = $this->getPassword($username);
                if ($storePassword !== false) {
                    $ha1 = $this->passwordForWWWAuth($username, $storePassword);
                    $ha2 = md5($_SERVER['REQUEST_METHOD'] . ':' . $data['uri']);
                    $hash = md5($ha1 . ':' . $data['nonce'] . ':' . $data['nc'] . ':'
                    . $data['cnonce'] . ':' . $data['qop'] . ':' . $ha2);
                    if (hash_equals($data['response'], $hash)) {
                        $this->success($username);
                    } else {
                        $this->fail($username);
                    }
                }
            }
        }
        if (Session::get($this->loginKey) !== true) {
            $msg = '<div class="box-alert">
                    <h1>CORE ERROR</h1><br />
                    <p>' . lg('用户名或者密码错误') . '</p>
                    </div>';
            header($_SERVER['SERVER_PROTOCOL'] . ' 401 Unauthorized', true, 401);
            header('WWW-Authenticate: Digest realm="' . $this->realm
                . '",qop="auth",nonce="' . uniqid(SITE)
                . '",opaque="' . md5($this->realm) . '"');
            MVC::echo($msg);
            exit();
        } else {
            $this->logined = true;
        }
    }

    protected function httpDigestParse($digestStr)
    {
        $needed_parts = [
            'nonce' => 1,
            'nc' => 1,
            'cnonce' => 1,
            'qop' => 1,
            'username' => 1,
            'uri' => 1,
            'response' => 1
        ];
        $data = [];
        $keys = implode('|', array_keys($needed_parts));
        preg_match_all('@(' . $keys . ')=(?:([\'"])([^\2]+?)\2|([^\s,]+))@', $digestStr, $matches, PREG_SET_ORDER);
        foreach ($matches as $m) {
            $data[$m[1]] = $m[3] ? $m[3] : $m[4];
            unset($needed_parts[$m[1]]);
        }
        return $needed_parts ? false : $data;
    }
}
