<?php

namespace apexl\Io\modules\user\controllers;

use apexl\hashing\Hash;
use apexl\Io\exceptions\RecordNotFoundException;
use apexl\Io\includes\Controller;
use apexl\Io\includes\HookManager;
use apexl\Io\modules\user\entities\refreshTokenEntity;
use apexl\Io\modules\user\entities\userEntity;
use apexl\Io\modules\user\exceptions\ClientCredentialsValidationException;
use apexl\Io\modules\user\exceptions\RefreshTokenExpiredException;
use apexl\Io\modules\user\exceptions\RefreshTokenUserMismatchException;
use apexl\Io\modules\user\services\currentUser;
use apexl\Io\modules\user\services\RefreshTokenSessionIdFinder;
use apexl\Io\modules\user\services\userTools;
use Exception;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

final readonly class LoginController extends Controller
{
    /**
     * @throws Exception
     */
    public function __invoke(
        userTools $userTools,
        RefreshTokenSessionIdFinder $sessionIdFinder,
        HookManager $hookManager,
        ServerRequestInterface $request,
        ResponseInterface $response
    ): ResponseInterface {
        $body = (object) $request->getParsedBody();
        if (isset($body->grant_type) && $body->grant_type === 'refresh_token') {
            return $this->refresh(
                $request,
                $response,
                $sessionIdFinder,
            );
        }

        //basic validation
        if (empty($body) || !isset($body->username) || !isset($body->password)) {
            $this->output->addMessage(
                'user.login.validation',
                'error',
                "Please provide a username and a password."
            );
            $this->output->addResponse($request, [], false); //added so we can hook into this elsewhere.

            return $this->json($response, [], 400);
        }

        //first, grab the user data.
        $user = new userEntity();
        $user->getUserByEmail($body->username);
        //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 ($user->email !== null && $user->active == 1) {
            $hash = new Hash();
            $match = $hash->matchHash($body->password, $user->password, $user->salt);
            //if we match, complete the login process

            $match = $hookManager->processHook('userLogin', $match, $user, $request);
            if ($match) {
                $this->loginPostAuth($userTools, $user, $request);
                if ($redirect = ((object) $request->getParsedBody())->redirect ?? null) {
                    $this->output->addRedirect('user.login.redirect', $redirect);
                }
                return $this->json($response);
            }
        }
        //if we end up here, then the login attempt failed, so return a 400
        $this->output->addMessage('user.login.validation', 'error', 'The email address and password don\'t match');
        $this->output->addResponse($request, [], false); // added so we can hook into this elsewhere.

        return $this->json($response, [], 400);
    }

    public function refresh(
        ServerRequestInterface $request,
        ResponseInterface $response,
        RefreshTokenSessionIdFinder $sessionIdFinder,
    ): ResponseInterface {
        $body = (object) $request->getParsedBody();

        try {
            if (isset($body->refresh_token)) {
                $refreshTokenEntity = new refreshTokenEntity();
                $refreshTokenEntity->loadByToken(hash('sha256', (string) $body->refresh_token));
                if ($refreshTokenEntity->isExpired()) {
                    throw new RefreshTokenExpiredException('Token expired');
                }

                if ($sessionId = $sessionIdFinder->find($request, $refreshTokenEntity)) {
                    // Refresh token still valid?
                    $user = new userEntity();
                    $user->load($refreshTokenEntity->user_id);
                    $authToken = currentUser::createJWT(
                        $user,
                        $sessionId,
                        config('auth.jwt.secret_key'),
                        config('auth.jwt.algorithm'),
                        config('auth.jwt.lifetime')
                    );
                    $refreshTokenEntity->expiry = refreshTokenEntity::newExpiryFrom();
                    $refreshTokenEntity->store();

                    $this->output->addResponse($request, ['access_token' => $authToken]);

                    return $this->json($response);
                }
            }
        } catch (
        ClientCredentialsValidationException|
        RefreshTokenUserMismatchException|
        RefreshTokenExpiredException
        $exception
        ) {
            return $this->json(
                $response,
                ['error' => $exception->getMessage()],
                401,
            );
        } catch (RecordNotFoundException $exception) {
            return $this->json(
                $response,
                ['error' => $exception->getMessage()],
                404,
            );
        } catch (Exception $exception) {
            return $this->json(
                $response,
                ['error' => $exception->getMessage()],
                500,
            );
        }

        return $this->json($response, [], 403);
    }

    /**
     * Allows other processes to force a user login without requiring a password. (Login by URL, Switch User etc)
     * @throws Exception
     */
    protected function loginPostAuth(userTools $userTools, $user, $request): void
    {
        [$authToken, $refreshToken] = $userTools->startLoggedInSession($user);
        //allow others to act on this response. Hook is keyed by route name.
        $this->output->addResponse(
            $request,
            [
                'access_token' => $authToken,
                'refresh_token' => $refreshToken,
                'expires_in' => currentUser::getTokenExpiryFromNow($authToken),
            ]
        );
    }
}
