<?php
namespace Misuzu\SharpChat;

use RuntimeException;
use Index\Colour\Colour;
use Index\Routing\IRouter;
use Index\Routing\IRouteHandler;
use Index\Routing\Route;
use Misuzu\RoutingContext;
use Misuzu\Auth\AuthContext;
use Misuzu\Auth\AuthInfo;
use Misuzu\Auth\Sessions;
use Misuzu\Config\IConfig;
use Misuzu\Emoticons\Emotes;
use Misuzu\Perms\Permissions;
use Misuzu\URLs\URLRegistry;
use Misuzu\Users\Bans;
use Misuzu\Users\UsersContext;

final class SharpChatRoutes implements IRouteHandler {
    private string $hashKey;

    public function __construct(
        private IConfig $config,
        private URLRegistry $urls,
        private UsersContext $usersCtx,
        private AuthContext $authCtx,
        private Emotes $emotes,
        private Permissions $perms,
        private AuthInfo $authInfo
    ) {
        $this->hashKey = $this->config->getString('hashKey', 'woomy');
    }

    public function registerRoutes(IRouter $router): void {
        RoutingContext::registerSimpleErrorPages($router, '/_sockchat');
        Route::handleAttributes($router, $this);
    }

    #[Route('GET', '/_sockchat/emotes')]
    public function getEmotes($response, $request): array {
        $response->setHeader('Access-Control-Allow-Origin', '*');
        $response->setHeader('Access-Control-Allow-Methods', 'GET');

        $emotes = $this->emotes->getEmotes(orderBy: 'order');
        $out = [];

        foreach($emotes as $emoteInfo) {
            $strings = [];

            foreach($this->emotes->getEmoteStrings($emoteInfo) as $stringInfo)
                $strings[] = sprintf(':%s:', $stringInfo->getString());

            $out[] = [
                'Text' => $strings,
                'Image' => $emoteInfo->getUrl(),
                'Hierarchy' => $emoteInfo->getMinRank(),
            ];
        }

        return $out;
    }

    #[Route('GET', '/_sockchat/login')]
    public function getLogin($response, $request) {
        if(!$this->authInfo->isLoggedIn()) {
            $response->redirect($this->urls->format('auth-login'));
            return;
        }

        $response->redirect($this->config->getString(
            ($request->hasParam('legacy') ? 'chatPath.legacy' : 'chatPath.normal'),
            '/'
        ));
    }

    #[Route('OPTIONS', '/_sockchat/token')]
    #[Route('GET', '/_sockchat/token')]
    public function getToken($response, $request) {
        $host = $request->hasHeader('Host') ? $request->getHeaderFirstLine('Host') : '';
        $origin = $request->hasHeader('Origin') ? $request->getHeaderFirstLine('Origin') : '';
        $originHost = strtolower(parse_url($origin, PHP_URL_HOST) ?? '');

        if(!empty($originHost) && $originHost !== $host) {
            $whitelist = $this->config->getArray('origins', []);

            if(!in_array($originHost, $whitelist))
                return 403;

            $originProto = strtolower(parse_url($origin, PHP_URL_SCHEME));
            $origin = $originProto . '://' . $originHost;

            $response->setHeader('Access-Control-Allow-Origin', $origin);
            $response->setHeader('Access-Control-Allow-Methods', 'OPTIONS, GET');
            $response->setHeader('Access-Control-Allow-Credentials', 'true');
            $response->setHeader('Vary', 'Origin');
        }

        if($request->getMethod() === 'OPTIONS')
            return 204;

        $tokenInfo = $this->authInfo->getTokenInfo();

        if(!$tokenInfo->hasSessionToken())
            return ['ok' => false, 'err' => 'token'];

        try {
            $sessionInfo = $this->authCtx->getSessions()->getSession(sessionToken: $tokenInfo->getSessionToken());
        } catch(RuntimeException $ex) {
            return ['ok' => false, 'err' => 'session'];
        }

        if($sessionInfo->hasExpired())
            return ['ok' => false, 'err' => 'expired'];
        if($sessionInfo->getUserId() !== $tokenInfo->getUserId())
            return ['ok' => false, 'err' => 'user'];

        $userInfo = $this->usersCtx->getUsers()->getUser($sessionInfo->getUserId(), 'id');
        $userId = $tokenInfo->hasImpersonatedUserId() && $userInfo->isSuperUser()
            ? $tokenInfo->getImpersonatedUserId()
            : $userInfo->getId();

        $tokenPacker = $this->authCtx->createAuthTokenPacker();

        return [
            'ok' => true,
            'usr' => (int)$userId,
            'tkn' => $tokenPacker->pack($tokenInfo),
        ];
    }

    #[Route('POST', '/_sockchat/bump')]
    public function postBump($response, $request) {
        if(!$request->hasHeader('X-SharpChat-Signature'))
            return 400;

        if($request->isFormContent()) {
            $content = $request->getContent();

            $bumpList = $content->getParam('u', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY);
            if(!is_array($bumpList))
                return 400;

            $userTime = (int)$content->getParam('t', FILTER_SANITIZE_NUMBER_INT);
            $signature = "bump#{$userTime}";

            foreach($bumpList as $userId => $ipAddr)
                $signature .= "#{$userId}:{$ipAddr}";
        } else return 400;

        $userHash = (string)$request->getHeaderFirstLine('X-SharpChat-Signature');
        $realHash = hash_hmac('sha256', $signature, $this->hashKey);
        if(!hash_equals($realHash, $userHash))
            return 403;
        if($userTime < time() - 60)
            return 403;

        foreach($bumpList as $userId => $ipAddr)
            $this->usersCtx->getUsers()->recordUserActivity($userId, remoteAddr: $ipAddr);
    }

    #[Route('POST', '/_sockchat/verify')]
    public function postVerify($response, $request) {
        if(!$request->hasHeader('X-SharpChat-Signature'))
            return 400;

        if($request->isFormContent()) {
            $content = $request->getContent();
            $authMethod = (string)$content->getParam('method');
            $authToken = (string)$content->getParam('token');
            $ipAddress = (string)$content->getParam('ipaddr');
        } else
            return ['success' => false, 'reason' => 'request'];

        $userHash = (string)$request->getHeaderFirstLine('X-SharpChat-Signature');
        if(strlen($userHash) !== 64)
            return ['success' => false, 'reason' => 'length'];

        if(empty($authMethod) || empty($authToken) || empty($ipAddress))
            return ['success' => false, 'reason' => 'data'];

        $signature = "verify#{$authMethod}#{$authToken}#{$ipAddress}";
        $realHash = hash_hmac('sha256', $signature, $this->hashKey);
        if(!hash_equals($realHash, $userHash))
            return ['success' => false, 'reason' => 'hash'];

        if($authMethod === 'SESS' || $authMethod === 'Misuzu') {
            $tokenPacker = $this->authCtx->createAuthTokenPacker();
            $tokenInfo = $tokenPacker->unpack($authToken);
            if($tokenInfo->isEmpty()) {
                // don't support using the raw session key for Misuzu format
                if($authMethod !== 'SESS')
                    return ['success' => false, 'reason' => 'format'];

                $sessionToken = $authToken;
            } else
                $sessionToken = $tokenInfo->getSessionToken();

            try {
                $sessionInfo = $this->authCtx->getSessions()->getSession(sessionToken: $sessionToken);
            } catch(RuntimeException $ex) {
                return ['success' => false, 'reason' => 'token'];
            }

            if($sessionInfo->hasExpired()) {
                $this->authCtx->getSessions()->deleteSessions(sessionInfos: $sessionInfo);
                return ['success' => false, 'reason' => 'expired'];
            }

            $this->authCtx->getSessions()->recordSessionActivity(sessionInfo: $sessionInfo, remoteAddr: $ipAddress);

            $userInfo = $this->usersCtx->getUsers()->getUser($sessionInfo->getUserId(), 'id');
            if($tokenInfo->hasImpersonatedUserId() && $userInfo->isSuperUser()) {
                $userInfoReal = $userInfo;

                try {
                    $userInfo = $this->usersCtx->getUsers()->getUser($tokenInfo->getImpersonatedUserId(), 'id');
                } catch(RuntimeException $ex) {
                    $userInfo = $userInfoReal;
                }
            }
        } else {
            return ['success' => false, 'reason' => 'unsupported'];
        }

        $this->usersCtx->getUsers()->recordUserActivity($userInfo, remoteAddr: $ipAddress);
        $userColour = $this->usersCtx->getUsers()->getUserColour($userInfo);
        $userRank = $this->usersCtx->getUsers()->getUserRank($userInfo);
        $chatPerms = $this->perms->getPermissions('chat', $userInfo);

        return [
            'success' => true,
            'user_id' => (int)$userInfo->getId(),
            'username' => $userInfo->getName(),
            'colour_raw' => Colour::toMisuzu($userColour),
            'rank' => $userRank,
            'hierarchy' => $userRank,
            'perms' => $chatPerms->getCalculated(),
        ];
    }

    #[Route('GET', '/_sockchat/bans/list')]
    public function getBanList($response, $request) {
        if(!$request->hasHeader('X-SharpChat-Signature'))
            return 400;

        $userHash = (string)$request->getHeaderFirstLine('X-SharpChat-Signature');
        $userTime = (int)$request->getParam('x', FILTER_SANITIZE_NUMBER_INT);

        $realHash = hash_hmac('sha256', "list#{$userTime}", $this->hashKey);
        if(!hash_equals($realHash, $userHash) || $userTime < time() - 60)
            return 403;

        $list = [];
        $bans = $this->usersCtx->getBans()->getBans(activeOnly: true);

        foreach($bans as $banInfo) {
            $userId = $banInfo->getUserId();
            $userInfo = $this->usersCtx->getUserInfo($userId);
            $userColour = $this->usersCtx->getUserColour($userInfo);
            $isPerma = $banInfo->isPermanent();

            $list[] = [
                'is_ban' => true,
                'user_id' => $userId,
                'user_name' => $userInfo->getName(),
                'user_colour' => Colour::toMisuzu($userColour),
                'ip_addr' => '::',
                'is_perma' => $isPerma,
                'expires' => date('c', $isPerma ? 0x7FFFFFFF : $banInfo->getExpiresTime())
            ];
        }

        return $list;
    }

    #[Route('GET', '/_sockchat/bans/check')]
    public function getBanCheck($response, $request) {
        if(!$request->hasHeader('X-SharpChat-Signature'))
            return 400;

        $userHash = (string)$request->getHeaderFirstLine('X-SharpChat-Signature');
        $userTime = (int)$request->getParam('x', FILTER_SANITIZE_NUMBER_INT);
        $ipAddress = (string)$request->getParam('a');
        $userId = (string)$request->getParam('u');
        $userIdIsName = (int)$request->getParam('n', FILTER_SANITIZE_NUMBER_INT);

        $realHash = hash_hmac('sha256', "check#{$userTime}#{$userId}#{$ipAddress}#{$userIdIsName}", $this->hashKey);
        if(!hash_equals($realHash, $userHash) || $userTime < time() - 60)
            return 403;

        if($userIdIsName)
            try {
                $userInfo = $this->usersCtx->getUsers()->getUser($userId, 'name');
                $userId = (string)$userInfo->getId();
            } catch(RuntimeException $ex) {
                $userId = '';
            }

        $banInfo = $this->usersCtx->tryGetActiveBan($userId);
        if($banInfo === null)
            return ['is_ban' => false];

        $isPerma = $banInfo->isPermanent();

        return [
            'is_ban' => true,
            'user_id' => $banInfo->getUserId(),
            'ip_addr' => '::',
            'is_perma' => $isPerma,
            'expires' => date('c', $isPerma ? 0x7FFFFFFF : $banInfo->getExpiresTime()),
        ];
    }

    #[Route('POST', '/_sockchat/bans/create')]
    public function postBanCreate($response, $request): int {
        if(!$request->hasHeader('X-SharpChat-Signature') || !$request->isFormContent())
            return 400;

        $userHash = (string)$request->getHeaderFirstLine('X-SharpChat-Signature');
        $content = $request->getContent();
        $userTime = (int)$content->getParam('t', FILTER_SANITIZE_NUMBER_INT);
        $userId = (string)$content->getParam('ui', FILTER_SANITIZE_NUMBER_INT);
        $userAddr = (string)$content->getParam('ua');
        $modId = (string)$content->getParam('mi', FILTER_SANITIZE_NUMBER_INT);
        $modAddr = (string)$content->getParam('ma');
        $duration = (int)$content->getParam('d', FILTER_SANITIZE_NUMBER_INT);
        $isPermanent = (int)$content->getParam('p', FILTER_SANITIZE_NUMBER_INT);
        $reason = (string)$content->getParam('r');

        $signature = implode('#', [
            'create', $userTime, $userId, $userAddr,
            $modId, $modAddr, $duration, $isPermanent, $reason,
        ]);

        $realHash = hash_hmac('sha256', $signature, $this->hashKey);
        if(!hash_equals($realHash, $userHash) || $userTime < time() - 60)
            return 403;

        if(empty($reason))
            $reason = 'Banned through chat.';

        // maybe also adds the last couple lines of the chat log to the private reason
        $comment = sprintf('User IP address: %s, Moderator IP address: %s', $userAddr, $modAddr);

        if($isPermanent)
            $expires = null;
        else {
            $now = time();
            $expires = $now + $duration;
            if($expires < $now)
                return 400;
        }

        // IPs cannot be banned on their own
        // substituting with the unused Railgun account for now.
        if(empty($userId))
            $userId = 69;
        if(empty($modId))
            $modId = 69;

        try {
            $modInfo = $this->usersCtx->getUsers()->getUser($modId, 'id');
        } catch(RuntimeException $ex) {
            return 404;
        }

        try {
            $userInfo = $this->usersCtx->getUsers()->getUser($userId, 'id');
        } catch(RuntimeException $ex) {
            return 404;
        }

        try {
            $this->usersCtx->getBans()->createBan(
                $userInfo,
                $expires,
                $reason,
                $comment,
                modInfo: $modInfo
            );
        } catch(RuntimeException $ex) {
            return 500;
        }

        return 201;
    }

    #[Route('DELETE', '/_sockchat/bans/revoke')]
    public function deleteBanRevoke($response, $request): int {
        if(!$request->hasHeader('X-SharpChat-Signature'))
            return 400;

        $userHash = (string)$request->getHeaderFirstLine('X-SharpChat-Signature');
        $userTime = (int)$request->getParam('x', FILTER_SANITIZE_NUMBER_INT);
        $type = (string)$request->getParam('t');
        $subject = (string)$request->getParam('s');

        $realHash = hash_hmac('sha256', "revoke#{$userTime}#{$type}#{$subject}", $this->hashKey);
        if(!hash_equals($realHash, $userHash) || $userTime < time() - 60)
            return 403;

        if($type !== 'user')
            return 404;

        $banInfo = $this->usersCtx->tryGetActiveBan($subject);
        if($banInfo === null)
            return 404;

        $this->usersCtx->getBans()->deleteBans($banInfo);

        return 204;
    }
}
