<?php

/*
 * MfaController.php
 * Copyright (c) 2024 james@firefly-iii.org.
 *
 * This file is part of Firefly III (https://github.com/firefly-iii).
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see https://www.gnu.org/licenses/.
 */

declare(strict_types=1);

namespace FireflyIII\Http\Controllers\Profile;

use Carbon\Carbon;
use FireflyIII\Events\Security\User\UserHasDisabledMFA;
use FireflyIII\Events\Security\User\UserHasEnabledMFA;
use FireflyIII\Events\Security\User\UserHasGeneratedNewBackupCodes;
use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Http\Middleware\IsDemoUser;
use FireflyIII\Http\Requests\ExistingTokenFormRequest;
use FireflyIII\Http\Requests\TokenFormRequest;
use FireflyIII\Repositories\User\UserRepositoryInterface;
use FireflyIII\Support\Facades\Preferences;
use FireflyIII\User;
use Illuminate\Contracts\View\Factory;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cookie;
use Illuminate\Support\Facades\Log;
use Illuminate\View\View;
use PragmaRX\Google2FALaravel\Facade as Google2FA;
use PragmaRX\Recovery\Recovery;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;

/**
 * Class MfaController
 *
 * Enable MFA Flow:
 *
 * Page 1 (GET): Show QR code and the manual code. Secret keeps rotating.
 *  POST: store secret, store response, validate password.
 * ---
 * Page 3 (GET): Confirm 2FA status and show recovery codes.
 *        Same page as page 1, but when secret is present.
 */
final class MfaController extends Controller
{
    protected bool $internalAuth;

    /**
     * ProfileController constructor.
     */
    public function __construct()
    {
        parent::__construct();

        $this->middleware(static function ($request, $next) {
            app('view')->share('title', (string) trans('firefly.profile'));
            app('view')->share('mainTitleIcon', 'fa-user');

            return $next($request);
        });
        $authGuard          = config('firefly.authentication_guard');
        $this->internalAuth = 'web' === $authGuard;
        Log::debug(sprintf('ProfileController::__construct(). Authentication guard is "%s"', $authGuard));

        $this->middleware(IsDemoUser::class)->except(['index']);
    }

    public function backupCodes(Request $request): Factory|RedirectResponse|View
    {
        if (!$this->internalAuth) {
            $request->session()->flash('error', trans('firefly.external_user_mgt_disabled'));

            return redirect(route('profile.index'));
        }
        $enabledMFA = null !== auth()->user()->mfa_secret;
        if (false === $enabledMFA) {
            request()->session()->flash('info', trans('firefly.mfa_not_enabled'));

            return redirect(route('profile.index'));
        }

        return view('profile.mfa.backup-codes-intro');
    }

    public function backupCodesPost(ExistingTokenFormRequest $request): RedirectResponse|View
    {
        if (!$this->internalAuth) {
            $request->session()->flash('error', trans('firefly.external_user_mgt_disabled'));

            return redirect(route('profile.index'));
        }
        $enabledMFA    = null !== auth()->user()->mfa_secret;
        if (false === $enabledMFA) {
            request()->session()->flash('info', trans('firefly.mfa_not_enabled'));

            return redirect(route('profile.index'));
        }
        // generate recovery codes:
        $recovery      = app(Recovery::class);
        $recoveryCodes = $recovery->lowercase()->setCount(8)->setBlocks(2)->setChars(6)->toArray(); // Generate 8 codes // Every code must have 2 blocks // Each block must have 6 chars
        $codes         = implode("\r\n", $recoveryCodes);

        Preferences::set('mfa_recovery', $recoveryCodes);
        Preferences::mark();

        // send user notification.
        $user          = auth()->user();
        Log::channel('audit')->info(sprintf('User "%s" has generated new backup codes.', $user->email));
        event(new UserHasGeneratedNewBackupCodes($user));

        return view('profile.mfa.backup-codes-post')->with(['codes' => $codes]);
    }

    public function disableMFA(Request $request): Factory|RedirectResponse|View
    {
        if (!$this->internalAuth) {
            request()->session()->flash('error', trans('firefly.external_user_mgt_disabled'));

            return redirect(route('profile.index'));
        }
        $enabledMFA   = null !== auth()->user()->mfa_secret;
        if (false === $enabledMFA) {
            request()->session()->flash('info', trans('firefly.mfa_already_disabled'));

            return redirect(route('profile.index'));
        }
        $subTitle     = (string) trans('firefly.mfa_index_title');
        $subTitleIcon = 'fa-calculator';

        return view('profile.mfa.disable-mfa')->with(['subTitle' => $subTitle, 'subTitleIcon' => $subTitleIcon, 'enabledMFA' => $enabledMFA]);
    }

    /**
     * Delete 2FA routine.
     */
    public function disableMFAPost(ExistingTokenFormRequest $request): RedirectResponse
    {
        if (!$this->internalAuth) {
            $request->session()->flash('error', trans('firefly.external_user_mgt_disabled'));

            return redirect(route('profile.index'));
        }

        /** @var UserRepositoryInterface $repository */
        $repository = app(UserRepositoryInterface::class);

        /** @var User $user */
        $user       = auth()->user();

        Preferences::delete('temp-mfa-secret');
        Preferences::delete('temp-mfa-codes');
        $repository->setMFACode($user, null);
        Preferences::mark();

        session()->flash('success', (string) trans('firefly.pref_two_factor_auth_disabled'));
        session()->flash('info', (string) trans('firefly.pref_two_factor_auth_remove_it'));

        // also logout current 2FA tokens.
        $cookieName = config('google2fa.cookie_name', 'google2fa_token');
        Cookie::forget($cookieName);

        // send user notification.
        Log::channel('audit')->info(sprintf('User "%s" has disabled MFA', $user->email));
        event(new UserHasDisabledMFA($user));

        return redirect(route('profile.index'));
    }

    /**
     * Enable 2FA screen.
     */
    public function enableMFA(Request $request): RedirectResponse|View
    {
        if (!$this->internalAuth) {
            $request->session()->flash('error', trans('firefly.external_user_mgt_disabled'));

            return redirect(route('profile.index'));
        }

        /** @var User $user */
        $user       = auth()->user();
        $enabledMFA = null !== $user->mfa_secret;

        // If FF3 already has a secret, just set the two-factor auth enabled to 1,
        // and let the user continue with the existing secret.
        if ($enabledMFA) {
            session()->flash('info', (string) trans('firefly.2fa_already_enabled'));

            return redirect(route('profile.index'));
        }

        $domain     = $this->getDomain();
        $secret     = Google2FA::generateSecretKey();
        $image      = Google2FA::getQRCodeInline($domain, auth()->user()->email, $secret);

        Preferences::set('temp-mfa-secret', $secret);

        return view('profile.mfa.enable-mfa', ['image' => $image, 'secret' => $secret]);
    }

    /**
     * Submit 2FA for the first time.
     *
     * @throws ContainerExceptionInterface
     * @throws NotFoundExceptionInterface
     */
    public function enableMFAPost(TokenFormRequest $request): RedirectResponse
    {
        if (!$this->internalAuth) {
            $request->session()->flash('error', trans('firefly.external_user_mgt_disabled'));

            return redirect(route('profile.index'));
        }

        /** @var User $user */
        $user       = auth()->user();

        // verify password.
        $password   = $request->get('password');
        if (!auth()->validate(['email' => $user->email, 'password' => $password])) {
            session()->flash('error', 'Bad user pw, no MFA for you!');

            return redirect(route('profile.mfa.index'));
        }

        /** @var UserRepositoryInterface $repository */
        $repository = app(UserRepositoryInterface::class);
        $secret     = Preferences::get('temp-mfa-secret')?->data;
        if (is_array($secret)) {
            $secret = null;
        }
        $secret     = (string) $secret;

        $repository->setMFACode($user, $secret);

        Preferences::delete('temp-mfa-secret');

        session()->flash('success', (string) trans('firefly.saved_preferences'));
        Preferences::mark();

        // also save the code so replay attack is prevented.
        $mfaCode    = $request->get('code');
        $this->addToMFAHistory($mfaCode);

        // make sure MFA is logged out.
        if ('testing' !== config('app.env')) {
            Google2FA::logout();
        }

        // drop all info from session:
        session()->forget(['temp-mfa-secret', 'two-factor-secret', 'two-factor-codes']);

        // send user notification.
        Log::channel('audit')->info(sprintf('User "%s" has enabled MFA', $user->email));
        event(new UserHasEnabledMFA($user));

        return redirect(route('profile.mfa.backup-codes'));
    }

    public function index(): Factory|RedirectResponse|View
    {
        if (!$this->internalAuth) {
            request()->session()->flash('error', trans('firefly.external_user_mgt_disabled'));

            return redirect(route('profile.index'));
        }

        $subTitle     = (string) trans('firefly.mfa_index_title');
        $subTitleIcon = 'fa-calculator';
        $enabledMFA   = null !== auth()->user()->mfa_secret;

        return view('profile.mfa.index')->with(['subTitle' => $subTitle, 'subTitleIcon' => $subTitleIcon, 'enabledMFA' => $enabledMFA]);
    }

    /**
     * TODO duplicate code.
     *
     * @throws ContainerExceptionInterface
     * @throws NotFoundExceptionInterface
     */
    private function addToMFAHistory(string $mfaCode): void
    {
        /** @var array $mfaHistory */
        $mfaHistory   = Preferences::get('mfa_history', [])->data;
        $entry        = ['time' => Carbon::now()->getTimestamp(), 'code' => $mfaCode];
        $mfaHistory[] = $entry;

        Preferences::set('mfa_history', $mfaHistory);
        $this->filterMFAHistory();
    }

    /**
     * Remove old entries from the preferences array.
     */
    private function filterMFAHistory(): void
    {
        /** @var array $mfaHistory */
        $mfaHistory = Preferences::get('mfa_history', [])->data;
        $newHistory = [];
        $now        = Carbon::now()->getTimestamp();
        foreach ($mfaHistory as $entry) {
            $time = $entry['time'];
            $code = $entry['code'];
            if (($now - $time) <= 300) {
                $newHistory[] = ['time' => $time, 'code' => $code];
            }
        }
        Preferences::set('mfa_history', $newHistory);
    }
}
