<?php
namespace Misuzu\SharpChat;

use RuntimeException;
use Index\Colour\Colour;
use Index\Routing\IRouter;
use Index\Http\HttpFx;
use Misuzu\AuthToken;
use Misuzu\Config\IConfig;
use Misuzu\Emoticons\Emotes;

// Replace
use Misuzu\Users\User;
use Misuzu\Users\UserSession;
use Misuzu\Users\UserWarning;

final class SharpChatRoutes {
    private IConfig $config;
    private Emotes $emotes;
    private string $hashKey;

    public function __construct(IRouter $router, IConfig $config, Emotes $emotes) {
        $this->config = $config;
        $this->emotes = $emotes;
        $this->hashKey = $this->config->getString('hashKey', 'woomy');

        // Simplify default error pages
        if($router instanceof HttpFx)
            $router->use('/_sockchat', function() use($router) {
                $router->addErrorHandler(400, function($response) {
                    $response->setContent('HTTP 400');
                });
                $router->addErrorHandler(403, function($response) {
                    $response->setContent('HTTP 403');
                });
                $router->addErrorHandler(404, function($response) {
                    $response->setContent('HTTP 404');
                });
                $router->addErrorHandler(500, function($response) {
                    $response->setContent('HTTP 500');
                });
                $router->addErrorHandler(503, function($response) {
                    $response->setContent('HTTP 503');
                });
            });

        // Public endpoints
        $router->get('/_sockchat/emotes', [$this, 'getEmotes']);
        $router->get('/_sockchat/login', [$this, 'getLogin']);
        $router->options('/_sockchat/token', [$this, 'getToken']);
        $router->get('/_sockchat/token', [$this, 'getToken']);

        // Private endpoints
        $router->post('/_sockchat/bump', [$this, 'postBump']);
        $router->post('/_sockchat/verify', [$this, 'postVerify']);
        $router->get('/_sockchat/bans/list', [$this, 'getBanList']);
        $router->get('/_sockchat/bans/check', [$this, 'getBanCheck']);
        $router->post('/_sockchat/bans/create', [$this, 'postBanCreate']);
        $router->delete('/_sockchat/bans/revoke', [$this, 'deleteBanRevoke']);
    }

    public function getEmotes($response, $request): array {
        $response->setHeader('Access-Control-Allow-Origin', '*');
        $response->setHeader('Access-Control-Allow-Methods', 'GET');

        $emotes = $this->emotes->getAllEmotes(withStrings: true);
        $out = [];

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

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

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

        return $out;
    }

    public function getLogin($response, $request): void {
        $currentUser = User::getCurrent();
        $configKey = $request->hasParam('legacy') ? 'chatPath.legacy' : 'chatPath.normal';
        $chatPath = $this->config->getString($configKey, '/');

        $response->redirect(
            $currentUser === null
                ? url('auth-login')
                : $chatPath
        );
    }

    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;

        if(!UserSession::hasCurrent() || !AuthToken::hasCurrent())
            return ['ok' => false];

        $token = AuthToken::getCurrent();
        $session = UserSession::getCurrent();
        if($session->getToken() !== $token->getSessionToken())
            return ['ok' => false];

        $user = $session->getUser();
        $userId = $token->hasImpersonatedUserId() && $user->isSuper()
            ? $token->getImpersonatedUserId()
            : $user->getId();

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

    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)
            User::byId($userId)->bumpActivity($ipAddr);
    }

    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') {
            $authTokenInfo = AuthToken::unpack($authToken);
            if($authTokenInfo->isValid())
                $authToken = $authTokenInfo->getSessionToken();

            try {
                $sessionInfo = UserSession::byToken($authToken);
            } catch(RuntimeException $ex) {
                return ['success' => false, 'reason' => 'token'];
            }

            if($sessionInfo->hasExpired()) {
                $sessionInfo->delete();
                return ['success' => false, 'reason' => 'expired'];
            }

            $sessionInfo->bump($ipAddress);

            $userInfo = $sessionInfo->getUser();
            if($authTokenInfo->hasImpersonatedUserId() && $userInfo->isSuper()) {
                $userInfoReal = $userInfo;

                try {
                    $userInfo = User::byId($authTokenInfo->getImpersonatedUserId());
                } catch(RuntimeException $ex) {
                    $userInfo = $userInfoReal;
                }
            }
        } else {
            return ['success' => false, 'reason' => 'unsupported'];
        }

        if(empty($userInfo))
            return ['success' => false, 'reason' => 'user'];

        $userInfo->bumpActivity($ipAddress);

        return [
            'success' => true,
            'user_id' => $userInfo->getId(),
            'username' => $userInfo->getUsername(),
            'colour_raw' => Colour::toMisuzu($userInfo->getColour()),
            'rank' => $rank = $userInfo->getRank(),
            'hierarchy' => $rank,
            'perms' => SharpChatPerms::convert($userInfo),
        ];
    }

    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;

        $warnings = UserWarning::byActive();
        $bans = [];

        foreach($warnings as $warning) {
            if(!$warning->isBan() || $warning->hasExpired())
                continue;

            $isPerma = $warning->isPermanent();
            $userInfo = $warning->getUser();
            $bans[] = [
                'is_ban' => true,
                'user_id' => (string)$userInfo->getId(),
                'user_name' => $userInfo->getUsername(),
                'user_colour' => Colour::toMisuzu($userInfo->getColour()),
                'ip_addr' => $warning->getUserRemoteAddress(),
                'is_perma' => $isPerma,
                'expires' => date('c', $isPerma ? 0xFFFFFFFF : $warning->getExpirationTime()),
            ];
        }

        return $bans;
    }

    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(!empty($ipAddress))
            $warning = UserWarning::byRemoteAddressActive($ipAddress);

        if(empty($warning) && !empty($userId)) {
            if($userIdIsName)
                try {
                    $userInfo = User::byUsername($userId);
                    $userId = $userInfo->getId();
                } catch(RuntimeException $ex) {
                    $userId = 0;
                }

            $warning = UserWarning::byUserIdActive((int)$userId);
        }

        if($warning === null)
            return ['is_ban' => false];

        $isPerma = $warning->isPermanent();

        return [
            'is_ban' => true,
            'user_id' => (string)$warning->getUserId(),
            'ip_addr' => $warning->getUserRemoteAddress(),
            'is_perma' => $isPerma,
            'expires' => date('c', $isPerma ? 0xFFFFFFFF : $warning->getExpirationTime()),
        ];
    }

    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.';

        if($isPermanent)
            $duration = -1;
        elseif($duration < 1)
            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 = User::byId((int)$modId);
        } catch(RuntimeException $ex) {
            return 404;
        }

        try {
            $userInfo = User::byId((int)$userId);
        } catch(RuntimeException $ex) {
            return 404;
        }

        try {
            UserWarning::create(
                $userInfo,
                $modInfo,
                UserWarning::TYPE_BAHN,
                $duration,
                $reason,
                null,
                $userAddr,
                $modAddr
            );
        } catch(RuntimeException $ex) {
            return 500;
        }

        return 201;
    }

    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;

        $warning = null;
        switch($type) {
            case 'addr':
                $warning = UserWarning::byRemoteAddressActive($subject);
                break;

            case 'user':
                $warning = UserWarning::byUserIdActive((int)$subject);
                break;
        }

        if($warning === null)
            return 404;

        $warning->delete();

        return 204;
    }
}
