<?php


namespace App\Model\Facades\Writers;


use App\Model\Entities\Security\User;
use App\Model\Exceptions\BattleNet\AlreadyConnectedBattleNetAccountException;
use App\Model\Exceptions\Steam\AlreadyConnectedSteamAccountException;
use App\Model\Exceptions\Steam\AuthenticationFailedException;
use App\Model\Exceptions\Steam\AuthenticationRequiredException;
use App\Model\Exceptions\Steam\EmptyResponseException;
use App\Model\Exceptions\Steam\ServerResponseException;
use App\Model\Exceptions\User\AlreadyAttachedCredentials;
use App\Model\Facades\Readers\PermissionReader;
use App\Model\Facades\Readers\SiteReader;
use App\Model\Facades\Readers\UserAccountReader;
use App\Model\Services\BattleNetApiService;
use App\Model\Services\BattleNetService;
use App\Model\Services\SiteService;
use App\Model\Services\SteamApiService;
use App\Model\Services\SteamService;
use App\Model\Services\UserCredentialsService;
use App\Model\Services\UserService;
use DateTime;
use Throwable;

class UserAccountWriter extends BaseWriter
{
    /** @var  BattleNetApiService */
    protected $battleNetApiService;
    /** @var  BattleNetService */
    protected $battleNetService;
    /** @var  SteamApiService */
    protected $steamApiService;
    /** @var  SteamService */
    protected $steamService;
    /** @var  UserAccountReader */
    protected $userAccountReader;
    /** @var  UserCredentialsService */
    protected $userCredentialsService;
    /** @var  UserService */
    protected $userService;
    /** @var  SiteService */
    protected $siteService;
    /** @var  PermissionReader */
    protected $permissionReader;

    public function inject(UserAccountReader $userAccountReader,
                           UserService $userService, UserCredentialsService $userCredentialsService,
                           SteamApiService $steamApiService, SteamService $steamService,
                           BattleNetApiService $battleNetApiService, BattleNetService $battleNetService,
                           SiteService $siteService, PermissionReader $permissionReader)
    {
        $this->userAccountReader = $userAccountReader;

        $this->userService = $userService;
        $this->userCredentialsService = $userCredentialsService;

        $this->steamApiService = $steamApiService;
        $this->steamService = $steamService;

        $this->battleNetApiService = $battleNetApiService;
        $this->battleNetService = $battleNetService;

        $this->siteService = $siteService;
        $this->permissionReader = $permissionReader;
    }

    protected function registerUser($nickName) : User
    {
        $user = $this->userService->createUser($nickName);

        $portalSite = $this->siteService->findPortalSite();

        // webmaster id registered before creation of portal site
        if ($portalSite) {
            $defaultRole = $this->permissionReader->findSiteDefaultRole($portalSite);
            $user->addRole($defaultRole);
        }

        return $user;
    }

    /**
     * @param $nickName
     * @param $email
     * @param $password
     * @return User|null
     * @throws Throwable
     */
    public function registerUserWithCredentials($nickName, $email, $password)
    {
        try {
            $this->beginTransaction();
            $userEntity = $this->registerUser($nickName);
            $this->userCredentialsService->createCredentials($userEntity, $email, $password);
            $this->commit();
        } catch (Throwable $e) {
            $this->rollback();
            throw  $e;
        }

        return $userEntity;
    }

    public function attachCredentials(User $user, $email, $password)
    {
        if ($this->userAccountReader->hasGuildGateAccount($user)) {
            throw new AlreadyAttachedCredentials();
        }

        try {
            $this->beginTransaction();
            $this->userCredentialsService->createCredentials($user, $email, $password);
            $this->commit();
        } catch (Throwable $e) {
            $this->rollback();
            throw  $e;
        }
    }

    /**
     * @param $backlink
     * @return User
     * @throws AlreadyConnectedSteamAccountException
     * @throws Throwable
     * @throws AuthenticationFailedException
     * @throws AuthenticationRequiredException
     * @throws EmptyResponseException
     * @throws ServerResponseException
     */
    public function registerThroughSteam($backlink)
    {
        $steamId = $this->steamApiService->retrieveSteamId($backlink);
        $steamAccount = $this->steamService->findSteamAccountBySteamId($steamId);

        if ($steamAccount !== null) {
            throw new AlreadyConnectedSteamAccountException();
        }

        $profile = $this->steamApiService->retrievePlayerProfile($steamId);

        try {
            $this->beginTransaction();
            $userEntity = $this->registerUser($profile->getNickname());
            $this->steamService->createSteamAccount($userEntity, $steamId);
            $this->commit();
        } catch (Throwable $e) {
            $this->rollback();
            throw  $e;
        }

        return $userEntity;
    }

    public function attachSteamAccount(User $user, $backlink)
    {
        if ($this->userAccountReader->hasSteamAccount($user)) {
            throw new AlreadyConnectedSteamAccountException();
        }

        $steamId = $this->steamApiService->retrieveSteamId($backlink);
        $steamAccount = $this->steamService->findSteamAccountBySteamId($steamId);

        if ($steamAccount !== null) {
            throw new AlreadyConnectedSteamAccountException();
        }

        try {
            $this->beginTransaction();
            $this->steamService->createSteamAccount($user, $steamId);
            $this->commit();
        } catch (Throwable $e) {
            $this->rollback();
            throw  $e;
        }
    }

    public function registerThroughBattleNet($backlink, $region = 'eu')
    {
        $code = $this->battleNetApiService->retrieveAuthorizationCode();
        $accessToken = $this->battleNetApiService->retrieveAccessToken($backlink, $region, $code);
        $data = $this->battleNetApiService->retrieveUserAccount($backlink, $region, $accessToken);

        $battleNetAccount = $this->battleNetService->findBattleNetAccountByBattleNetId($data->getProfileId());

        if ($battleNetAccount !== null) {
            throw new AlreadyConnectedBattleNetAccountException();
        }

        try {
            $this->beginTransaction();
            $userEntity = $this->registerUser($data->extractNickname());
            $this->battleNetService->createBattleNetAccount($userEntity, $data->getProfileId(), $data->getBattleTag(), $accessToken->getToken());
            $this->commit();
        } catch (Throwable $e) {
            $this->rollback();
            throw  $e;
        }

        return $userEntity;
    }

    public function attachBattleNetAccount($user, $backlink, $region = 'eu')
    {
        if ($this->userAccountReader->hasBattleNetAccount($user)) {
            throw new AlreadyConnectedBattleNetAccountException();
        }

        $code = $this->battleNetApiService->retrieveAuthorizationCode();
        $accessToken = $this->battleNetApiService->retrieveAccessToken($backlink, $region, $code);
        $data = $this->battleNetApiService->retrieveUserAccount($backlink, $region, $accessToken);

        $battleNetAccount = $this->battleNetService->findBattleNetAccountByBattleNetId($data->getProfileId());

        if ($battleNetAccount !== null) {
            throw new AlreadyConnectedBattleNetAccountException();
        }

        try {
            $this->beginTransaction();
            $this->battleNetService->createBattleNetAccount($user, $data->getProfileId(), $data->getBattleTag(), $accessToken->getToken());
            $this->commit();
        } catch (Throwable $e) {
            $this->rollback();
            throw  $e;
        }
    }

    /**
     * @param $user
     * @param $password
     * @throws Throwable
     */
    public function rehashPassword($user, $password)
    {
        $userEntity = $this->userReader->validateUser($user);
        $credentials = $this->userCredentialsService->getUserCredentials($userEntity);

        try {
            $this->beginTransaction();
            $this->userCredentialsService->rehashPassword($credentials, $password);
            $this->commit();
        } catch (Throwable $e) {
            $this->rollback();
            throw  $e;
        }
    }

    public function updateLastOnline($user)
    {
        $userEntity = $this->userReader->validateUser($user);

        try {
            $this->beginTransaction();
            $userEntity->setLastOnline(new DateTime());
            $this->commit();
        } catch (Throwable $e) {
            $this->rollback();
            throw $e;
        }
    }
}