<?php
namespace Misuzu\Auth;

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

class Sessions {
    private IDbConnection $dbConn;
    private DbStatementCache $cache;

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

    public static function generateToken(): string {
        return XString::random(64);
    }

    public function countSessions(
        UserInfo|string|null $userInfo = null
    ): int {
        if($userInfo instanceof UserInfo)
            $userInfo = $userInfo->getId();

        $hasUserInfo = $userInfo !== null;

        //$args = 0;
        $query = 'SELECT COUNT(*) FROM msz_sessions';
        if($hasUserInfo) {
            //++$args;
            $query .= ' WHERE user_id = ?';
        }

        $args = 0;
        $stmt = $this->cache->get($query);
        if($hasUserInfo)
            $stmt->addParameter(++$args, $userInfo);
        $stmt->execute();

        $result = $stmt->getResult();
        $count = 0;

        if($result->next())
            $count = $result->getInteger(0);

        return $count;
    }

    public function getSessions(
        UserInfo|string|null $userInfo = null,
        ?Pagination $pagination = null
    ): array {
        if($userInfo instanceof UserInfo)
            $userInfo = $userInfo->getId();

        $hasUserInfo = $userInfo !== null;
        $hasPagination = $pagination !== null;

        //$args = 0;
        $query = 'SELECT session_id, user_id, session_key, INET6_NTOA(session_ip), INET6_NTOA(session_ip_last), session_user_agent, session_client_info, session_country, UNIX_TIMESTAMP(session_expires), session_expires_bump, UNIX_TIMESTAMP(session_created), UNIX_TIMESTAMP(session_active) FROM msz_sessions';
        if($hasUserInfo) {
            //++$args;
            $query .= ' WHERE user_id = ?';
        }
        $query .= ' ORDER BY session_active DESC';
        if($hasPagination)
            $query .= ' LIMIT ? OFFSET ?';

        $args = 0;
        $stmt = $this->cache->get($query);
        if($hasUserInfo)
            $stmt->addParameter(++$args, $userInfo);
        if($hasPagination) {
            $stmt->addParameter(++$args, $pagination->getRange());
            $stmt->addParameter(++$args, $pagination->getOffset());
        }
        $stmt->execute();

        $result = $stmt->getResult();
        $sessions = [];

        while($result->next())
            $sessions[] = new SessionInfo($result);

        return $sessions;
    }

    public function getSession(
        ?string $sessionId = null,
        ?string $sessionToken = null
    ): SessionInfo {
        if($sessionId === null && $sessionToken === null)
            throw new InvalidArgumentException('At least one argument must be specified.');
        if($sessionId !== null && $sessionToken !== null)
            throw new InvalidArgumentException('Only one argument may be specified.');

        $hasSessionId = $sessionId !== null;
        $hasSessionToken = $sessionToken !== null;
        $value = null;

        $query = 'SELECT session_id, user_id, session_key, INET6_NTOA(session_ip), INET6_NTOA(session_ip_last), session_user_agent, session_client_info, session_country, UNIX_TIMESTAMP(session_expires), session_expires_bump, UNIX_TIMESTAMP(session_created), UNIX_TIMESTAMP(session_active) FROM msz_sessions';
        if($hasSessionId) {
            $query .= ' WHERE session_id = ?';
            $value = $sessionId;
        } elseif($hasSessionToken) {
            $query .= ' WHERE session_key = ?';
            $value = $sessionToken;
        }

        $stmt = $this->cache->get($query);
        $stmt->addParameter(1, $value);
        $stmt->execute();

        $result = $stmt->getResult();
        if(!$result->next())
            throw new RuntimeException('Session not found.');

        return new SessionInfo($result);
    }

    public function createSession(
        UserInfo|string $userInfo,
        IPAddress|string $remoteAddr,
        string $countryCode,
        string $userAgentString,
        ?ClientInfo $clientInfo = null
    ): SessionInfo {
        if($userInfo instanceof UserInfo)
            $userInfo = $userInfo->getId();
        if($remoteAddr instanceof IPAddress)
            $remoteAddr = (string)$remoteAddr;

        $sessionToken = self::generateToken();
        $clientInfo = json_encode($clientInfo ?? ClientInfo::parse($userAgentString));

        $stmt = $this->cache->get('INSERT INTO msz_sessions (user_id, session_key, session_ip, session_user_agent, session_client_info, session_country, session_expires) VALUES (?, ?, INET6_ATON(?), ?, ?, ?, NOW() + INTERVAL 1 MONTH)');
        $stmt->addParameter(1, $userInfo);
        $stmt->addParameter(2, $sessionToken);
        $stmt->addParameter(3, $remoteAddr);
        $stmt->addParameter(4, $userAgentString);
        $stmt->addParameter(5, $clientInfo);
        $stmt->addParameter(6, $countryCode);
        $stmt->execute();

        return $this->getSession(sessionId: (string)$this->dbConn->getLastInsertId());
    }

    public function deleteSessions(
        SessionInfo|string|array|null $sessionInfos = null,
        string|array|null $sessionTokens = null,
        UserInfo|string|array|null $userInfos = null
    ): void {
        $hasSessionInfos = $sessionInfos !== null;
        $hasSessionTokens = $sessionTokens !== null;
        $hasUserInfos = $userInfos !== null;

        $args = 0;
        $query = 'DELETE FROM msz_sessions';

        if($hasSessionInfos) {
            if(!is_array($sessionInfos))
                $sessionInfos = [$sessionInfos];

            if(empty($sessionInfos))
                $hasSessionInfos = false;
            else {
                ++$args;
                $query .= sprintf(
                    ' WHERE session_id IN (%s)',
                    DbTools::prepareListString($sessionInfos)
                );
            }
        }

        if($hasSessionTokens) {
            if(!is_array($sessionTokens))
                $sessionTokens = [$sessionTokens];

            if(empty($sessionTokens))
                $hasSessionTokens = false;
            else
                $query .= sprintf(
                    ' %s session_key IN (%s)',
                    ++$args > 1 ? 'OR' : 'WHERE',
                    DbTools::prepareListString($sessionTokens)
                );
        }

        if($hasUserInfos) {
            if(!is_array($userInfos))
                $userInfos = [$userInfos];

            if(empty($userInfos))
                $hasUserInfos = false;
            else
                $query .= sprintf(
                    ' %s user_id IN (%s)',
                    ++$args > 1 ? 'OR' : 'WHERE',
                    DbTools::prepareListString($userInfos)
                );
        }

        if(!$hasSessionInfos && !$hasSessionTokens && !$hasUserInfos)
            throw new InvalidArgumentException('At least one argument must be specified.');

        $args = 0;
        $stmt = $this->cache->get($query);

        if($hasSessionInfos)
            foreach($sessionInfos as $sessionInfo) {
                if($sessionInfo instanceof SessionInfo)
                    $sessionInfo = $sessionInfo->getId();
                elseif(!is_string($sessionInfo))
                    throw new InvalidArgumentException('$sessionInfos must be strings or instances of SessionInfo.');

                $stmt->addParameter(++$args, $sessionInfo);
            }

        if($hasSessionTokens)
            foreach($sessionTokens as $sessionToken) {
                if(!is_string($sessionToken))
                    throw new InvalidArgumentException('$sessionTokens must be strings.');

                $stmt->addParameter(++$args, $sessionToken);
            }

        if($hasUserInfos)
            foreach($userInfos as $userInfo) {
                if($userInfo instanceof UserInfo)
                    $userInfo = $userInfo->getId();
                elseif(!is_string($userInfo))
                    throw new InvalidArgumentException('$userInfos must be strings or instances of UserInfo.');

                $stmt->addParameter(++$args, $userInfo);
            }

        $stmt->execute();
    }

    public function recordSessionActivity(
        SessionInfo|string|null $sessionInfo = null,
        ?string $sessionToken = null,
        IPAddress|string|null $remoteAddr = null
    ): void {
        if($sessionInfo === null && $sessionToken === null)
            throw new InvalidArgumentException('Either $sessionInfo or $sessionToken needs to be set.');
        if($sessionInfo !== null && $sessionToken !== null)
            throw new InvalidArgumentException('Only one of $sessionInfo and $sessionToken may be set at once.');
        if($sessionInfo instanceof SessionInfo)
            $sessionInfo = $sessionInfo->getId();
        if($remoteAddr instanceof IPAddress)
            $remoteAddr = (string)$remoteAddr;

        $hasSessionInfo = $sessionInfo !== null;
        $hasSessionToken = $sessionToken !== null;
        $value = null;

        $query = 'UPDATE msz_sessions SET session_ip_last = COALESCE(INET6_ATON(?), session_ip_last), session_active = NOW(), session_expires = IF(session_expires_bump, NOW() + INTERVAL 1 MONTH, session_expires)';
        if($hasSessionInfo) {
            $query .= ' WHERE session_id = ?';
            $value = $sessionInfo;
        } elseif($hasSessionToken) {
            $query .= ' WHERE session_key = ?';
            $value = $sessionToken;
        } else throw new RuntimeException('Failsafe to prevent all sessions from being updated at once somehow.');

        $stmt = $this->cache->get($query);
        $stmt->addParameter(1, $remoteAddr);
        $stmt->addParameter(2, $value);
        $stmt->execute();
    }

    public function purgeExpiredSessions(): void {
        $this->dbConn->execute('DELETE FROM msz_sessions WHERE session_expires <= NOW()');
    }
}
