<?php
namespace Misuzu\Auth;

use InvalidArgumentException;
use RuntimeException;
use Index\Data\DbStatementCache;
use Index\Data\IDbConnection;
use Index\Net\IPAddress;
use Index\Serialisation\Base32;
use Misuzu\ClientInfo;
use Misuzu\Pagination;
use Misuzu\Users\UserInfo;

class RecoveryTokens {
    private DbStatementCache $cache;

    public function __construct(IDbConnection $dbConn) {
        $this->cache = new DbStatementCache($dbConn);
    }

    private static function generateCode(): string {
        // results in a 12 char code
        return Base32::encode(random_bytes(7));
    }

    public function getToken(
        UserInfo|string|null $userInfo = null,
        IPAddress|string|null $remoteAddr = null,
        ?string $verifyCode = null,
        ?bool $isUnused = null
    ): RecoveryTokenInfo {
        if($userInfo instanceof UserInfo)
            $userInfo = $userInfo->getId();
        if($remoteAddr instanceof IPAddress)
            $remoteAddr = (string)$remoteAddr;

        $hasUserInfo = $userInfo !== null;
        $hasRemoteAddr = $remoteAddr !== null;
        $hasVerifyCode = $verifyCode !== null;
        $hasIsUnused = $isUnused !== null;

        if(!$hasUserInfo && !$hasRemoteAddr && !$hasVerifyCode)
            throw new InvalidArgumentException('At least one argument must be correctly specified.');
        if($hasIsUnused && $hasVerifyCode)
            throw new InvalidArgumentException('You may not specify $isUnused and $verifyCode at the same time.');

        $args = 0;
        $query = 'SELECT user_id, INET6_NTOA(reset_ip), UNIX_TIMESTAMP(reset_requested), verification_code FROM msz_users_password_resets';
        if($hasUserInfo) {
            ++$args;
            $query .= ' WHERE user_id = ?';
        }
        if($hasRemoteAddr)
            $query .= sprintf(' %s reset_ip = INET6_ATON(?)', ++$args > 1 ? 'AND' : 'WHERE');
        if($hasIsUnused)
            $query .= sprintf(' %s verification_code %s NULL', ++$args > 1 ? 'AND' : 'WHERE', $isUnused ? 'IS NOT' : 'IS');
        elseif($hasVerifyCode)
            $query .= sprintf(' %s verification_code = ?', ++$args > 1 ? 'AND' : 'WHERE');

        $args = 0;
        $stmt = $this->cache->get($query);
        if($hasUserInfo)
            $stmt->addParameter(++$args, $userInfo);
        if($hasRemoteAddr)
            $stmt->addParameter(++$args, $remoteAddr);
        if($hasVerifyCode)
            $stmt->addParameter(++$args, $verifyCode);
        $stmt->execute();
        $result = $stmt->getResult();

        if(!$result->next())
            throw new RuntimeException('Recovery token not found.');

        return new RecoveryTokenInfo($result);
    }

    public function createToken(
        UserInfo|string $userInfo,
        IPAddress|string $remoteAddr
    ): RecoveryTokenInfo {
        if($userInfo instanceof UserInfo)
            $userInfo = $userInfo->getId();
        if($remoteAddr instanceof IPAddress)
            $remoteAddr = (string)$remoteAddr;
        $verifyCode = self::generateCode();

        $stmt = $this->cache->get('INSERT INTO msz_users_password_resets (user_id, reset_ip, verification_code) VALUES (?, INET6_ATON(?), ?)');
        $stmt->addParameter(1, $userInfo);
        $stmt->addParameter(2, $remoteAddr);
        $stmt->addParameter(3, $verifyCode);
        $stmt->execute();

        return $this->getToken(verifyCode: $verifyCode);
    }

    public function invalidateToken(RecoveryTokenInfo $tokenInfo): void {
        if(!$tokenInfo->hasCode())
            return;

        $stmt = $this->cache->get('UPDATE msz_users_password_resets SET verification_code = NULL WHERE verification_code = ? AND user_id = ?');
        $stmt->addParameter(1, $tokenInfo->getCode());
        $stmt->addParameter(2, $tokenInfo->getUserId());
        $stmt->execute();
    }
}
