<?php

namespace apexl\Io\modules\user\controllers;

use apexl\hashing\Hash;
use apexl\Io\includes\Controller;
use apexl\Io\includes\HookManager;
use apexl\Io\modules\user\entities\userEntity;
use apexl\Io\modules\user\enums\permissions\Role;
use apexl\Io\modules\user\enums\permissions\User;
use apexl\Io\modules\user\services\CurrentUserFactory;
use apexl\Io\services\HttpPaths;
use apexl\Io\services\Output;
use Exception;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

final readonly class RegisterController extends Controller
{
    /**
     * @throws Exception
     */
    public function __invoke(
        HttpPaths $path,
        currentUserFactory $currentUserFactory,
        Output $output,
        HookManager $hookManager,
        ServerRequestInterface $request,
        ResponseInterface $response,
        ?int $id = null
    ): ResponseInterface {
        $currentUser = $currentUserFactory->get();
        $body = $request->getParsedBody();
        $user = new userEntity();
        if ($id !== null) {
            //Permissions check
            if ($currentUser->id !== $id && !$currentUser->isAllowed(User::UPDATE)) {
                $this->output->addMessage(
                    $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 $this->json($response, [], 403);
            }
            if ($currentUser->id === $id && (!$currentUser->isAllowed(User::UPDATE) &&
                    !$currentUser->isAllowed(User::UPDATE_SELF))) {
                $this->output->addMessage(
                    $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 $this->json($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 (!$currentUser->isAllowed(User::CAN_REGISTER) && !$currentUser->isAllowed(User::CREATE)) {
                $this->output->addMessage(
                    $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 $this->json($response, [], 403);
            }
            //does this user already exist?
            $userExisting = new userEntity();
            $userExisting->getUserByEmail($body['email']);
            if (isset($userExisting->id) && $userExisting->id) {
                $user = $userExisting;
            }

            if (!empty($user->email)) {
                $this->output->addMessage(
                    $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 $this->json($response, [], 400);
            }
        }

        //check if passwords match, if we're providing them
        if ($body['password'] ?? false) {
            if ($body['password'] != $body['confirm_password']) {
                $this->output->addMessage(
                    $path->getRouteName($request) . '.validation',
                    'error',
                    'Passwords don\'t match'
                );
                $this->output->addResponse($request, [], false); //added so we can hook into this elsewhere.

                return $this->json($response, [], 400);
            }
            $passwordHash = '';
            $passwordSalt = '';
            if (!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']) && $currentUser->isAllowed(User::CHANGE_ACTIVE_STATE)) {
            $user->active = $body['active'];
        }

        foreach (['first_name', 'last_name', 'email'] as $field) {
            if (isset($body[$field])) $user->$field = $body[$field];
        }

        $hasRolesInBody = count(array_filter(array_keys($body), fn($key) => str_starts_with($key, 'role_')));

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

        $user->roles = serialize([]);

        //Hook user pre save - allow other modules to act on the user save process, before storage.
        $user = $hookManager->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 = $hookManager->processHook('userPostSave', $user, $request);

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

            $this->output->addMetadata($path->getRouteName($request) . '.complete', 'entityId', $user->id);
            $this->output->addMessage(
                $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 $this->json($response);
        }
        $this->output->addMessage(
            $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 $this->json($response, [], 400);
    }
}
