<?php

/*
 * Copyright © 2020 Miguel Nogueira
 *
 *   This file is part of Raspberry Staff Manager.
 *
 *     Raspberry Staff Manager is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 *
 *     Raspberry Staff Manager is distributed in the hope that it will be useful,
 *     but WITHOUT ANY WARRANTY; without even the implied warranty of
 *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *     GNU General Public License for more details.
 *
 *     You should have received a copy of the GNU General Public License
 *     along with Raspberry Staff Manager.  If not, see <https://www.gnu.org/licenses/>.
 */

namespace App\Http\Controllers;

use App\Ban;
use App\Facades\Options;
use App\Http\Requests\Add2FASecretRequest;
use App\Http\Requests\AddDobRequest;
use App\Http\Requests\BanUserRequest;
use App\Http\Requests\ChangeEmailRequest;
use App\Http\Requests\ChangePasswordRequest;
use App\Http\Requests\DeleteUserRequest;
use App\Http\Requests\FlushSessionsRequest;
use App\Http\Requests\Remove2FASecretRequest;
use App\Http\Requests\Reset2FASecretRequest;
use App\Http\Requests\SearchPlayerRequest;
use App\Http\Requests\SetNewPasswordRequest;
use App\Http\Requests\UpdateUserRequest;
use App\Notifications\ChangedPassword;
use App\Notifications\EmailChanged;
use App\Notifications\PasswordAdminResetNotification;
use App\Notifications\TwoFactorResetNotification;
use App\Services\AccountSuspensionService;
use App\Services\DiscordService;
use App\Traits\DisablesFeatures;
use App\Traits\HandlesAccountDeletion;
use App\User;
use Google2FA;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Http\Client\RequestException;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Routing\Redirector;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Log;
use Spatie\Permission\Models\Role;

class UserController extends Controller
{
    use HandlesAccountDeletion, DisablesFeatures;

    /**
     * Shows list of users
     *
     * @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View
     *
     * @throws \Illuminate\Auth\Access\AuthorizationException
     */
    public function showUsers()
    {
        $this->authorize('viewPlayers', User::class);

        return view('dashboard.administration.users')
            ->with([
                'users' => User::with('roles')->paginate('6'),
                'numUsers' => count(User::all()),
                'bannedUserCount' => Ban::count(),
            ]);
    }

    /**
     * Searches for a player with the given search query.
     *
     * @deprecated Until Algolia implementation
     *
     * @param  SearchPlayerRequest  $request
     * @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
     *
     * @throws \Illuminate\Auth\Access\AuthorizationException
     */
    public function showPlayersLike(SearchPlayerRequest $request)
    {
        $this->authorize('viewPlayers', User::class);

        $searchTerm = $request->searchTerm;
        $matchingUsers = User::query()
            ->where('name', 'LIKE', "%{$searchTerm}%")
            ->orWhere('email', 'LIKE', "%{$searchTerm}%")
            ->paginate(6);

        if (! $matchingUsers->isEmpty()) {
            $request->session()->flash('success', __('There were :usersCount user(s) matching your search.', ['usersCount' => $matchingUsers->count()]));

            return view('dashboard.administration.users')
            ->with([
                'users' => $matchingUsers,
                'numUsers' => count(User::all()),
                'bannedUserCount' => Ban::count(),
            ]);
        } else {
            $request->session()->flash('error', __('Your search term did not return any results.'));

            return redirect(route('registeredPlayerList'));
        }
    }

    /**
     * Shows the user account's settings page
     *
     * @param  Request  $request
     * @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View
     *
     * @throws \PragmaRX\Google2FA\Exceptions\IncompatibleWithGoogleAuthenticatorException
     * @throws \PragmaRX\Google2FA\Exceptions\InvalidCharactersException
     * @throws \PragmaRX\Google2FA\Exceptions\SecretKeyTooShortException
     */
    public function showAccount(Request $request)
    {
        $QRCode = null;

        if (! $request->user()->has2FA()) {
            if ($request->session()->has('twofaAttemptFailed')) {
                $twoFactorSecret = $request->session()->get('current2FA');
            } else {
                $twoFactorSecret = Google2FA::generateSecretKey(32, '');
                $request->session()->put('current2FA', $twoFactorSecret);
            }

            $QRCode = Google2FA::getQRCodeInline(
                config('app.name'),
                $request->user()->email,
                $twoFactorSecret
            );
        }

        return view('dashboard.user.profile.useraccount')
            ->with('ip', request()->ip())
            ->with('twofaQRCode', $QRCode);
    }

    /**
     * Show account management screen
     *
     * @param  AccountSuspensionService  $suspensionService
     * @param  Request  $request
     * @param  User  $user
     * @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View
     *
     * @throws \Illuminate\Auth\Access\AuthorizationException
     */
    public function showAcocuntManagement(AccountSuspensionService $suspensionService, Request $request, User $user)
    {
        $this->authorize('adminEdit', $user);

        $systemRoles = Role::all()->pluck('name')->all();
        $userRoles = $user->roles->pluck('name')->all();

        $roleList = [];

        foreach ($systemRoles as $role) {
            if (in_array($role, $userRoles)) {
                $roleList[$role] = true;
            } else {
                $roleList[$role] = false;
            }
        }

        return view('dashboard.user.manage')
            ->with([
                'user' => $user,
                'roles' => $roleList,
                'isVerified' => $user->isVerified(),
                'isLocked' => $suspensionService->isLocked($user),
                'isSuspended' => $suspensionService->isSuspended($user),
                'hasDiscord' => $user->hasDiscordConnection(),
                'hasPassword' => $user->hasPassword(),
                'requireLicense' => Options::getOption('requireGameLicense'),
                'suspensionReason' => $suspensionService->getSuspensionReason($user),
                'suspensionDuration' => $suspensionService->getSuspensionDuration($user),
                'has2FA' => $user->has2FA(),
                'applications' => $user->applications()->get(),
            ]);
    }

    /**
     * Log out other sessions for the current user
     *
     * @param  FlushSessionsRequest  $request
     * @return \Illuminate\Http\RedirectResponse
     *
     * @throws \Illuminate\Auth\AuthenticationException
     */
    public function flushSessions(FlushSessionsRequest $request)
    {
        // TODO: Move all log calls to a listener, which binds to an event fired by each significant event, such as this one
        // This will allow for other actions to be performed on certain events (like login failed event)

        Auth::logoutOtherDevices($request->currentPasswordFlush);
        Log::notice('User '.Auth::user()->name.' has logged out other devices in their account',
            [
                'originIPAddress' => $request->ip(),
                'userID' => Auth::user()->id,
                'timestamp' => now(),
            ]);

        $request->session()->flash('success', __('Successfully logged out other devices. Remember to change your password if you think you\'ve been compromised.'));

        return redirect()->back();
    }

    /**
     * Change the current user's password
     *
     * @param  ChangePasswordRequest  $request
     * @return \Illuminate\Http\RedirectResponse|void
     */
    public function changePassword(ChangePasswordRequest $request)
    {
        if (config('demo.is_enabled')) {
            return redirect()
                ->back()
                ->with('error', __('This feature is disabled'));
        }
        $user = User::find(Auth::user()->id);

        if (! is_null($user)) {
            $user->password = Hash::make($request->newPassword);
            $user->password_last_updated = now();

            $user->save();

            Log::info('User '.$user->name.' has changed their password', [
                'originIPAddress' => $request->ip(),
                'userID' => $user->id,
                'timestamp' => now(),
            ]);
            $user->notify(new ChangedPassword());

            Auth::logout();

            return redirect()->back();
        }
    }

    /**
     * Sets a new password for the user.
     *
     * @param  SetNewPasswordRequest  $request
     * @return Application|RedirectResponse|Redirector
     */
    public function setPassword(SetNewPasswordRequest $request)
    {
        if (! Auth::user()->hasPassword()) {
            Auth::user()->password = Hash::make($request->newpass);
            Auth::user()->save();

            Auth::logout();
            $request->session()->invalidate();
            $request->session()->regenerateToken();

            return redirect(route('login'));
        }

        return redirect()
            ->back()
            ->with('error', __('Your account already has a password.'));
    }

    /**
     * Sets a user's password and removes their discord information from storage
     *
     * @param  User  $user
     * @param  SetNewPasswordRequest  $request
     * @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
     */
    public function unlinkDiscordAccount(Request $request, DiscordService $discordService)
    {
        if ($request->user()->hasPassword()) {
            try {
                $discordService->revokeAccountTokens(Auth::user());
                Log::warning('Revoking social account tokens, user initiated', [
                    'user' => Auth::user()->email,
                ]);
            } catch (RequestException $requestException) {
                if ($requestException->getCode() == 401) {
                    return redirect(route('discordRedirect'));
                }

                Log::error('Error while trying to revoke Discord credentials', [$requestException->getMessage()]);

                return redirect()
                    ->back()
                    ->with('error', __('An unknown error ocurred. Please try again later.'));
            }

            $request->session()->flash('success', __('Discord account unlinked successfully. Link it again by re-authorizing the app with the same account in the login screen, or through your account settings.'));

            return redirect()->back();
        }

        return redirect()
            ->back()
            ->with('error', __('Please set a password for your account first before trying to unlink Discord.'));
    }

    /**
     * Change the current user's email address
     *
     * @param  ChangeEmailRequest  $request
     * @return \Illuminate\Http\RedirectResponse
     */
    public function changeEmail(ChangeEmailRequest $request)
    {
        $this->disable();

        $user = User::find(Auth::user()->id);

        if (! is_null($user)) {
            $user->email = $request->newEmail;
            $user->save();

            Log::notice('User '.$user->name.' has just changed their contact email address', [
                'originIPAddress' => $request->ip(),
                'userID' => $user->id,
                'timestamp' => now(),
            ]);
            $user->notify(new EmailChanged());

            $request->session()->flash('success', __('Your email address has been changed!'));
        } else {
            $request->session()->flash('error', __('There has been an error whilst trying to update your account. Please contact administrators.'));
        }

        return redirect()->back();
    }

    /**
     * Removes the user's password and notifies them.
     *
     * @param  User  $user The user to remove the password for
     * @return \Illuminate\Http\RedirectResponse
     *
     * @throws \Illuminate\Auth\Access\AuthorizationException
     */
    public function forcePasswordReset(User $user)
    {
        $this->authorize('adminEdit', $user);

        if ($user->hasPassword()) {
            $user->notify(new PasswordAdminResetNotification());

            $user->password = null;
            $user->save();

            Log::alert('Removed account password', [
                'target' => $user,
                'actor' => Auth::user(),
            ]);

            return redirect()
                ->back()
                ->with('success', __('Account password removed.'));
        }

        return redirect()
            ->back()
            ->with('error', __('This user doesn\'t have a password to reset.'));
    }

    /**
     * Adds a user's date of birth if they don't have one.
     *
     * @param  AddDobRequest  $request
     * @return RedirectResponse
     */
    public function addDob(AddDobRequest $request)
    {
        Auth::user()->dob = $request->dob;
        Auth::user()->save();

        return redirect()
            ->back();
    }

    /**
     * Delete the given user's account
     *
     * @param  DeleteUserRequest  $request
     * @param  User  $user
     * @return \Illuminate\Http\RedirectResponse
     *
     * @throws \Illuminate\Auth\Access\AuthorizationException
     */
    public function delete(DeleteUserRequest $request, User $user)
    {
        $this->disable();

        $this->authorize('delete', $user);

        if ($request->confirmPrompt == 'DELETE ACCOUNT') {
            $user->delete();
            $request->session()->flash('success', __('User deleted successfully.'));
        } else {
            $request->session()->flash('error', __('Wrong confirmation text! Try again.'));
        }

        return redirect()->route('registeredPlayerList');
    }

    /**
     * Update a given user's details
     *
     * @param  UpdateUserRequest  $request
     * @param  User  $user
     * @return \Illuminate\Http\RedirectResponse
     *
     * @throws \Illuminate\Auth\Access\AuthorizationException
     */
    public function update(UpdateUserRequest $request, User $user)
    {
        $this->authorize('adminEdit', $user);
        $this->disable();

        // Mass update would not be possible here without extra code, making route model binding useless
        $user->email = $request->email;
        $user->name = $request->name;
        $user->uuid = $request->uuid;

        $existingRoles = Role::all()
            ->pluck('name')
            ->all();

        $roleDiff = array_diff($existingRoles, $request->roles);

        // Adds roles that were selected. Removes roles that aren't selected if the user has them.
        foreach ($roleDiff as $deselectedRole) {
            if ($user->hasRole($deselectedRole) && $deselectedRole !== 'user') {
                $user->removeRole($deselectedRole);
            }
        }

        foreach ($request->roles as $role) {
            if (! $user->hasRole($role)) {
                $user->assignRole($role);
            }
        }

        $user->save();
        $request->session()->flash('success', __('User updated successfully!'));

        return redirect()->back();
    }

    /**
     * Generate and add a 2FA secret for the current user
     *
     * @param  Add2FASecretRequest  $request
     * @return \Illuminate\Http\RedirectResponse
     *
     * @throws \PragmaRX\Google2FA\Exceptions\IncompatibleWithGoogleAuthenticatorException
     * @throws \PragmaRX\Google2FA\Exceptions\InvalidCharactersException
     * @throws \PragmaRX\Google2FA\Exceptions\SecretKeyTooShortException
     */
    public function add2FASecret(Add2FASecretRequest $request)
    {
        if (config('demo.is_enabled')) {
            return redirect()
                ->back()
                ->with('error', __('This feature is disabled'));
        }

        $currentSecret = $request->session()->get('current2FA');
        $isValid = Google2FA::verifyKey($currentSecret, $request->otp);

        if ($isValid) {
            $request->user()->twofa_secret = $currentSecret;
            $request->user()->save();

            Log::warning('SECURITY: User activated two-factor authentication', [
                'initiator' => $request->user()->email,
                'ip' => $request->ip(),
            ]);

            Google2FA::login();

            Log::warning('SECURITY: Started two factor session automatically', [
                'initiator' => $request->user()->email,
                'ip' => $request->ip(),
            ]);

            $request->session()->forget('current2FA');

            if ($request->session()->has('twofaAttemptFailed')) {
                $request->session()->forget('twofaAttemptFailed');
            }

            $request->session()->flash('success', __('2FA succesfully enabled! You\'ll now be prompted for an OTP each time you log in.'));
        } else {
            $request->session()->flash('error', __('Incorrect code. Please reopen the 2FA settings panel and try again.'));
            $request->session()->put('twofaAttemptFailed', true);
        }

        return redirect()->back();
    }

    /**
     * Remove the current user's two factor secret key
     *
     * @param  Remove2FASecretRequest  $request
     * @return \Illuminate\Http\RedirectResponse
     */
    public function remove2FASecret(Remove2FASecretRequest $request)
    {
        Log::warning('SECURITY: Disabling two factor authentication (user initiated)', [
            'initiator' => $request->user()->email,
            'ip' => $request->ip(),
        ]);

        $request->user()->twofa_secret = null;
        $request->user()->save();

        $request->session()->flash('success', __('Two-factor authentication disabled.'));

        return redirect()->back();
    }

    /**
     * Remove the given user's two factor secret key
     *
     * @param  Reset2FASecretRequest  $request
     * @param  User  $user
     * @return \Illuminate\Http\RedirectResponse
     */
    public function reset2FASecret(Reset2FASecretRequest $request, User $user)
    {
        // note: could invalidate other sessions for increased security
        if ($user->has2FA()) {
            Log::warning('SECURITY: Disabling two factor authentication (admin initiated)', [
                'initiator' => $request->user()->email,
                'target' => $user->email,
                'ip' => $request->ip(),
            ]);

            $user->twofa_secret = null;
            $user->password = null;
            $user->save();

            $user->notify(new TwoFactorResetNotification());

            return redirect()
                ->back()
                ->with('success', __('Two factor removed & user notified.'));
        }

        return redirect()
            ->back()
            ->with('error', 'This user does not have two-factor authentication enabled.');
    }

    /**
     * Suspend the given user
     *
     * @param  AccountSuspensionService  $suspensionService
     * @param  BanUserRequest  $request
     * @param  User  $user
     * @return \Illuminate\Http\RedirectResponse
     *
     * @throws \Illuminate\Auth\Access\AuthorizationException
     */
    public function suspend(AccountSuspensionService $suspensionService, BanUserRequest $request, User $user)
    {
        $this->authorize('create', [Ban::class, $user]);
        $this->disable();

        if ($suspensionService->isSuspended($user)) {
            return redirect()
                ->back()
                ->with('error', __('Account already suspended.'));
        }

        if ($request->suspensionType = 'on') {
            $suspensionService->suspend($user, $request->reason, $request->duration);
        } else {
            $suspensionService->suspend($user, $request->reason);
        }

        return redirect()->back();
    }

    /**
     * Unsuspend the given user
     *
     * @param  AccountSuspensionService  $suspensionService
     * @param  Request  $request
     * @param  User  $user
     * @return \Illuminate\Http\RedirectResponse
     *
     * @throws \Illuminate\Auth\Access\AuthorizationException
     */
    public function unsuspend(AccountSuspensionService $suspensionService, Request $request, User $user)
    {
        $this->authorize('delete', $user->bans);
        $this->disable();

        if ($suspensionService->isSuspended($user)) {
            $suspensionService->unsuspend($user);
            $request->session()->flash('success', __('Account unsuspended successfully!'));
        } else {
            $request->session()->flash('error', __('This account isn\'t suspended!'));
        }

        return redirect()->back();
    }
}
