<?php

namespace apexl\Io\modules\user\controllers;

use apexl\hashing\Hash;
use apexl\Io\includes\Controller;
use apexl\Io\includes\Hook;
use apexl\Io\includes\Routes;
use apexl\Io\includes\System;
use apexl\Io\modules\email\services\templateService;
use apexl\Io\modules\user\entities\refreshTokenEntity;
use apexl\Io\modules\user\entities\sessionEntity;
use apexl\Io\modules\user\entities\userEntity;
use apexl\Io\modules\user\services\currentUser;

use apexl\Io\modules\user\services\userTools;
use apexl\Io\services\Logger;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;

class userController extends Controller
{
    protected $database;
    /** @var userEntity */
    protected userEntity $currentUser;
    protected userTools $userTools;

    public function __construct(currentUser $currentUser, userTools $userTools)
    {
        parent::__construct();
        $this->currentUser = $currentUser::getCurrentUser();
        $this->userTools = $userTools;
    }

    /**
     * @param Request $request
     * @param Response $response
     * @method GET
     * @return Response
     * @throws \Exception
     */
    public function login(Request $request, Response $response): Response
    {
        $body = $request->getParsedBody();
        //basic validation
        if (empty($body) || !isset($body->email) || !isset($body->password)) {
            $this->output::addMessage('user.login.validation', 'error', "Please provide an email address and a password.");
            $this->output::addResponse($request, [], false); //added so we can hook into this elsewhere.
            return System::asJson($response, [], 400);
        }

        //first, grab the user data.
        $user = userEntity::getUserByEmail($body->email);

        $errorText = 'The email address and password don\'t match';
        //if we have user data, grab the salt and password and check the provided one matches.
        //we also need the user to be active, so check that at the same time.
        if(isset($user->email)){
            if ($user->active == 1) {
            $hash = new Hash();
            $match = $hash->matchHash($body->password, $user->password, $user->salt);
            //if we match, complete the login process

            $match = Hook::processHook('userLogin', $match, $user, $request);
            } else {
                $match = FALSE;
                $errorText = "You can only login using a validated account";
            }
        } else {
            $match = FALSE;
            $match = Hook::processHook('userLoginNoMatchingUser', $match, $body->email, $body->password, $request); // This hook adds the possibility of third-party credential check and adding local user
            if ($match) {
                $user = userEntity::getUserByEmail($body->email);
            }
        }

        if($match && isset($user->email) && $user->active == 1){
            list($authToken, $refreshToken) = $this->userTools->startLoggedInSession($user);
            //allow others to act on this response. Hook is keyed by route name.
            if (isset($body->redirectToRoute)) {
                $body->redirectToRoute = base64_decode($body->redirectToRoute);
                $url = strpos($body->redirectToRoute, "/") !== false ? $body->redirectToRoute : Routes::getRoutePattern($body->redirectToRoute);
                $this->output->addMetadata('user.login.validation.redirect', 'redirect', $url);
            }
            $this->output::addResponse($request, ['authToken' => $authToken, 'refreshToken' => $refreshToken]);
            return System::asJson($response);
        }
        //if we end up here, then the login attempt failed, so return a 400
        $this->output::addMessage('user.login.validation', 'error', $errorText);
        $this->output::addResponse($request, [], false); // added so we can hook into this elsewhere.
        return System::asJson($response, [], 400);
    }

    public function loginAsUser(Request $request, Response $response){
        $body = $request->getParsedBody();

        //basic validation
        if(empty($body) || !isset($body->userId)){
            $this->output::addMessage('user.loginAsUser.validation', 'error', "Invalid User.");
            $this->output::addResponse($request, [], FALSE); //added so we can hook into this elsewhere.
            return System::asJson($response, [], 400);
        }

        //first, grab the user data.
        $user = userEntity::load($body->userId);
        if(!isset($user->id)){
            $this->output::addMessage('user.loginAsUser.validation', 'error', "Invalid User.");
            $this->output::addResponse($request, [], FALSE); //added so we can hook into this elsewhere.
            return System::asJson($response, [], 400);
        }

        list($authToken, $refreshToken) = $this->userTools->startLoggedInSession($user);
        //allow others to act on this response. Hook is keyed by route name.
        if (isset($body->redirectToRoute)) {
            $body->redirectToRoute = base64_decode($body->redirectToRoute);
            $url = strpos($body->redirectToRoute, "/") !== false ? $body->redirectToRoute : Routes::getRoutePattern($body->redirectToRoute);
            $this->output->addMetadata('user.loginAsUser.validation.redirect', 'redirect', $url);
        }
        $this->output::addMetadata('user.loginAsUser.post.redirect', 'redirect', '/');
        $this->output::addResponse($request,['authToken' => $authToken, 'refreshToken' => $refreshToken]);
        return System::asJson($response);

    }

    /**
     * @param Request $request
     * @param Response $response
     * @return Response
     * @throws \Exception
     */
    public function refresh(Request $request, Response $response): Response
    {
        $body = $request->getParsedBody();
        if (isset($body->refreshToken)) {
            $refreshTokenEntity = new refreshTokenEntity();
            $refreshTokenEntity->loadByToken(hash('sha256', $body->refreshToken));
            $claims = currentUser::getClaimsFromJWT($request);
            if (isset($refreshTokenEntity->user_id) && isset($claims->userId) && $refreshTokenEntity->user_id == $claims->userId) { // Refresh token exists and matches the current user?
                if ($refreshTokenEntity->expiry > time()) { // Refresh token still valid?
                    $user = userEntity::load($refreshTokenEntity->user_id);
                    if (($authToken = currentUser::createJWT($user, $claims->sessionId, $this->config->app->jwt->secret_key, $this->config->app->jwt->algorithm, ($this->config->app->jwt->lifetime ?? 3600))) !== '' && ($authToken = currentUser::createJWT($user, $claims->sessionId, $this->config->app->jwt->secret_key, $this->config->app->jwt->algorithm, ($this->config->app->jwt->lifetime ?? 3600))) !== '0') {
                        $this->output::addResponse($request, ['authToken' => $authToken]);
                        return System::asJson($response);
                    }
                }
            }
        }

        return System::asJson($response, [], 403);
    }

    /**
     * @param Request $request
     * @param Response $response
     * @return Response
     * @throws \Exception
     * @method POST
     * Automatic install method. Runs the core IO installation process, requires Database user and pass as a minimum to run.
     */
    public function logout(Request $request, Response $response)
    {
        list($token, $error) = currentUser::authenticateJWT($request, $this->config->app->jwt->secret_key, $this->config->app->jwt->algorithm);
        if ($token) {
            $session = sessionEntity::load($token->sessionId, true);
            if (isset($session->sessionId)) {
                $session->active = 0;
                $session->ended = time();
                $session->store();
            }

            $this->output::addResponse($request); //added so we can hook into this elsewhere.
            return System::asJson($response);
        }

        $this->output::addMessage('user.logout.validation', 'error', 'The user is not logged in');
        $this->output::addResponse($request, [], false); //added so we can hook into this elsewhere.
        return System::asJson($response, [], 403);
    }

    /**
     * @param Request $request
     * @param Response $response
     * @param $args
     * @return Response
     * @throws \Exception
     */
    public function register(Request $request, Response $response, $args): Response
    {
        $body = $request->getParsedBody();
        $user = new userEntity();
        if ($id = $args['id'] ?? false) {
            //Permissions check
            if ($this->currentUser->id !== $id && !$this->currentUser->isAllowed('UpdateUsers')) {
                $this->output::addMessage($this->path->getRouteName($request).'.validation', 'error', 'The UpdateUsers permission is required to do this.');
                $this->output::addResponse($request, [], false);
                //added so we can hook into this elsewhere.
                return System::asJson($response, [], 403);
            }
            if ($this->currentUser->id === $id && (!$this->currentUser->isAllowed('UpdateUsers') && !$this->currentUser->isAllowed('UpdateSelf'))) {
                $this->output::addMessage($this->path->getRouteName($request).'.validation', 'error', 'The UpdateSelf permission is required to do this.');
                $this->output::addResponse($request, [], false);
                //added so we can hook into this elsewhere.
                return System::asJson($response, [], 403);
            }
            $user->load($id);
            //check we have all the data we need, make sure password is not required as this is an update.
            $user->setRequiredData(['email', 'first_name', 'last_name']);
        } else {
            if (!$this->currentUser->isAllowed('CanRegister') && !$this->currentUser->isAllowed('CreateUsers')) {
                $this->output::addMessage($this->path->getRouteName($request).'.validation', 'error', 'The CreateUsers permission is required to do this.');
                $this->output::addResponse($request, [], false); //added so we can hook into this elsewhere.
                return System::asJson($response, [], 403);
            }
            //does this user already exist?
            $user->getUserByEmail($body->email);
            if ($user->email !== '' && $user->email !== '0') {
                $this->output::addMessage($this->path->getRouteName($request).'.validation', 'error', 'A user with that email address already exists.');
                $this->output::addResponse($request, [], false); //added so we can hook into this elsewhere.
                return System::asJson($response, [], 400);
            }
        }

        //check if passwords match, if we're providing them
        if ($body->password ?? false) {
            if ($body->password != $body->confirm_password) {
                $this->output::addMessage($this->path->getRouteName($request).'.validation', 'error', 'Passwords don\'t match');
                $this->output::addResponse($request, [], false); //added so we can hook into this elsewhere.
                return System::asJson($response, [], 400);
            }
            $passwordHash = '';
            $passwordSalt = '';
            if (isset($body->password) && !empty($body->password)) {
                $hash = new Hash();
                $hashData = $hash->hashString($body->password);
                $passwordHash = $hashData->hash;
                $passwordSalt = $hashData->salt;
                unset($body->password);
            }
            $user->password = $passwordHash;
            $user->salt = $passwordSalt;
        }
        //unset password
        if (isset($body->password)) {
            unset($body->password);
        }

        if (isset($body->confirm_password)) {
            //unset confirm_password so we dont try to store it.
            unset($body->confirm_password);
        }

        if (isset($body->active) && $this->currentUser->isAllowed('ChangeUserActiveState')) {
            $user->active = (int)$body->active;
        }

        //we need to unset all roles if we're setting any at all, so check we can do this on first pass.
        if ($this->currentUser->isAllowed('CanSetUserRole')) {
            $user->removeRoles();
        }

        foreach ($body as $name => $value) {
            if ($this->currentUser->isAllowed('CanSetUserRole')) {
                if (strpos($name, 'role_') !== false) {
                    //Check we have permission to set user roles.
                    if ((int)$value !== 0) { //Ignore any FALSE/0/blank string values
                        $user->addRole(str_replace('role_', '', $name));
                    }
                } else {
                    $user->$name = $value;
                }
            } elseif (strpos($name, 'role_')) {
                //dont allow users without permission to set roles. Just ignore them and move on.
                continue;
            } else {
                $user->$name = $value;
            }
        }


        //Hook user pre save - allow other modules to act on the user save process, before storage.
        $user = Hook::processHook('userPreSave', $user, $request);

        if ($user->isValid()) {
            $user->store();
            //Hook user post save - allow other modules to act on the user save process, after storage.
            $user = Hook::processHook('userPostSave', $user, $request);
            $this->output::addMetadata($this->path->getRouteName($request).'.complete', 'entityId', $user->id);
            $this->output::addMessage($this->path->getRouteName($request).'.complete', 'success', $user->getNiceName().' '. ($id ? 'updated' : 'created'));
            $this->output::addResponse($request, $user->getData()); //added so we can hook into this elsewhere.
            return System::asJson($response);
        }
        $this->output::addMessage($this->path->getRouteName($request).'.validation', 'error', 'Not all of the data provided is valid');
        $this->output::addResponse($request, ['missingFields' => $user->getMissingData()], false); //added so we can hook into this elsewhere.
        return System::asJson($response, [], 400);
    }

    public function forgotPassword(Request $request, Response $response)
    {
        $body = $request->getParsedBody();
        //first, grab the user data.
        $user = userEntity::getUserByEmail($body->email);

        //got a match? we need to generate a new random hash, then email it to the user.
        if(isset($user->email)){
            //we do this so the has can be passed back as a url param rather than a query string.
            $randomHash = str_replace('/', '@', (new Hash())->generateRandomHash());
            //write the hash to the database
            $user->newForgottenPasswordLink($randomHash);
            //now we send an email to the user.
            $this->forgottenPasswordEmail($user->email, $randomHash, $user->first_name);
            $this->output->addMetadata('user.forgot-password.redirect', 'redirect', Routes::getRoutePattern('user.display.forgot-password.check-email'));
        } else {
            $this->output::addMessage('user.forgot-password', 'error', 'User account does not exist');
        }
        $this->output::addResponse($request); //added so we can hook into this elsewhere.
        return System::asJson($response);
    }

    public function forgotPasswordWithLink(Request $request, Response $response, $args)
    {
        $hash = $args['hash'] ?? false;
        if ($hash) {
            $user = new userEntity();
            $match = $user->getForgottenPasswordLink($hash);
            if (!empty($match) && ((time() - $match['created']) <= 86400 && $match['used'] == 0)) {
                //log the user in and send them to their settings page to update the password. flag the hash as used.
                $user->markForgottenPasswordLinkUsed($hash);
                $user->getUserByEmail($match['email']);
                if ($user->id ?? false) {
                    //log the user in.
                    list($authToken, $refreshToken) = $this->userTools->startLoggedInSession($user);
                    $this->output::addResponse($request, ['authToken' => $authToken]); //added so we can hook into this elsewhere.
                    return System::asJson($response);
                }
            }
        }
        $this->output::addMessage($this->path->getRouteName($request).'.validation', 'error', "Invalid, unknown or expired hash");
        $this->output::addResponse($request, [], false); //added so we can hook into this elsewhere.
        return System::asJson($response, [], 400);
    }

    /**
     * @todo Replace mailer code with central mailer system
     * @param $email
     * @param $string
     * @param $first_name
     * @return void
     */
    protected function forgottenPasswordEmail($email, $string, $first_name){

        $from = System::getVariable('site_from_email') ?? $this->config->app->site->email_address ?? 'no-reply@localhost.com';
        $fromName = System::getVariable('site_from_name') ?? $this->config->app->site->name ?? 'localhost';
        $frontEndDomain = System::getVariable('site_from_domain') ?? $this->config->app->site->frontend_domain ?? 'localhost';

        //move to config.
        $link = rtrim($frontEndDomain, '/').'/'.Routes::getRoutePattern('user.forgot-password.hash', ['hash' => $string]);;

        //$this->mailer->SMTPDebug = 1;
        $this->mailer->setFrom($from, $fromName);
        $this->mailer->addAddress($email);     // Add a recipient

        $this->mailer->Subject = 'Password Reset Request';
        $this->mailer->Body = templateService::fetch("password_reset", [
            'from_name' => $fromName,
            'reset_link' => $link,
            'name' => $first_name
        ]);
        $this->mailer->IsHTML();
        $result = $this->mailer->send();

        Logger::log('password_reset_email', ['sent' => $result, 'email' => $email]);
    }
}
