<?php

namespace App\Controllers\Shield;

use CodeIgniter\HTTP\IncomingRequest;
use CodeIgniter\HTTP\RedirectResponse;
use CodeIgniter\I18n\Time;
use CodeIgniter\Shield\Authentication\Actions\ActionInterface;
use CodeIgniter\Shield\Authentication\Authenticators\Session;
use CodeIgniter\Shield\Entities\User;
use CodeIgniter\Shield\Entities\UserIdentity;
use CodeIgniter\Shield\Exceptions\RuntimeException;
use CodeIgniter\Shield\Models\UserIdentityModel;
use CodeIgniter\Shield\Traits\Viewable;
use PragmaRX\Google2FA\Google2FA;
use BaconQrCode\Renderer\ImageRenderer;
use BaconQrCode\Renderer\Image\ImagickImageBackEnd;
use BaconQrCode\Renderer\RendererStyle\RendererStyle;
use BaconQrCode\Writer;

/**
 * Class Google2FA
 * 1. Shield akan menjalankan createIdentity setelah login;
 * 2. Pada createIdentity, jika 2FA blm aktif (tfa = 0), maka
 *    generate secret key dan simpan ke tabel user;
 * 3. Buat kode identity dengan generator berupa secret key user (stfa);
 * 4. Sesuai Shield Actioninterface, yg dijalankan selanjutnya adalah function show;
 * 5. Pada function show cek apakah tfa = 0. Jika 0 maka generate kode QR 
 *    sehingga user bisa memindai kode dengan aplikasi mobile authenticator dan
 *    mengarahkan user ke halaman pemindaian kode qr (action_email_2fa_qr);
 * 6. Setelah dipindai, user diarahkan ke function handle utk proses simpan status tfa menjadi 1;
 *    kemudian user diarahkan ke halaman input token 2FA (action_email_2fa_verify);
 * 7. Jika tfa = 1 (2FA sdh aktif), user tdk perlu memindai kode qr,
 *    langsung diarahkan ke halaman input token 2FA (action_email_2fa_verify);
 * 8. setelah input token, user diarahkan ke function verify,
 *    verifikasi token 2FA dan verifikasi codeidentity. Jika sesuai maka login berhasil.
 *
 * Class ini modifikasi dari class 2FA Email dan routingnya mengikuti Actioninterface shield.
 */
class Goog2FA implements ActionInterface
{
    use Viewable;
    protected $ga;

    private string $type = Session::ID_TYPE_EMAIL_2FA;

    public function __construct()
    {
        $this->ga = new Google2FA();
    }
    
    public function show()
    {
        $authenticator = auth('session')->getAuthenticator();

        $user = $authenticator->getPendingUser();
        if ($user === null) {
            throw new RuntimeException('Cannot get the pending login User.');
        }

        if ($user->tfa == '0') {
            //create QR
            $g2faUrl = $this->ga->getQRCodeUrl(
                'Satria3.0',
                $user->nip,
                $user->stfa,
            );

            $writer = new Writer(
                new ImageRenderer(
                    new RendererStyle(250),
                    new ImagickImageBackEnd()
                )
            );

            $qr_2fa = base64_encode($writer->writeString($g2faUrl));
            $user->qrcode = $qr_2fa;
            
            return $this->view(setting('Auth.views')['action_email_2fa_qr'], ['user' => $user]);
        } else {
            return $this->view(setting('Auth.views')['action_email_2fa_verify'], ['user' => $user]);
        }
    }
    
    /**
     * Generates the random number, saves it as a temp identity
     * with the user, and fires off an email to the user with the code,
     * then displays the form to accept the 6 digits
     *
     * @return RedirectResponse|string
     */
    public function handle(IncomingRequest $request)
    {
        /** @var Session $authenticator */
        $authenticator = auth('session')->getAuthenticator();

        $user = $authenticator->getPendingUser();
        if ($user === null) {
            throw new RuntimeException('Cannot get the pending login User.');
        }

        if ($user->tfa == '0') {
            //simpan tfa dan stfa
            $users = auth()->getProvider();
            $ybs  = $users->findById($user->id);

            $data = [
                'tfa' => 1,
            ];

            $ybs->fill($data);
            $users->save($ybs);
        }

        return $this->view(setting('Auth.views')['action_email_2fa_verify'], ['user' => $user]);
    }

    /**
     * Attempts to verify the code the user entered.
     *
     * @return RedirectResponse|string
     */
    public function verify(IncomingRequest $request)
    {
        /** @var Session $authenticator */
        $authenticator = auth('session')->getAuthenticator();

        $user = $authenticator->getPendingUser();
        if ($user === null) {
            throw new RuntimeException('Cannot get the pending login User.');
        }

        //Validasi 2FA
        $secret = $request->getPost('token_2fa');
        $window = 4; //kode 2 menit sebelum dan sesudah
        $identity = $this->getIdentity($user);

        //jika toke 2FA sesuai
        if ($this->ga->verifyKey($user->stfa, $secret, $window)) {

            // Token mismatch? Let them try again...
            if (! $authenticator->checkAction($identity, $user->stfa)) {
                session()->setFlashdata('error', lang('Auth.invalid2FAToken'));
                return $this->view(setting('Auth.views')['action_email_2fa_verify']);
            }

            return redirect()->to(config('Auth')->loginRedirect());
        } else {
            session()->setFlashdata('error', lang('Auth.invalid2FAToken'));
            return $this->view(setting('Auth.views')['action_email_2fa_verify']);
        }
    }

    /**
     * Creates an identity for the action of the user.
     *
     * @return string secret
     */
    public function createIdentity(User $user): string
    {
        /** @var UserIdentityModel $identityModel */
        $identityModel = model(UserIdentityModel::class);

        // Delete any previous identities for action
        $identityModel->deleteIdentitiesByType($user, $this->type);

        if ($user->tfa == '0') {
            //simpan tfa dan stfa
            $users = auth()->getProvider();
            $ybs  = $users->findById($user->id);

            //buat secretkey
            $stfa = $this->ga->generateSecretKey();

            $data = [
                'stfa' => $stfa,
            ];

            $ybs->fill($data);
            $users->save($ybs);
        } else {
            $stfa = $user->stfa;
        }

        $generator = static fn (): string => $stfa;

        return $identityModel->createCodeIdentity(
            $user,
            [
                'type'  => $this->type,
                'name'  => 'login',
                'extra' => lang('Auth.need2FA'),
            ],
            $generator
        );
    }

    /**
     * Returns an identity for the action of the user.
     */
    private function getIdentity(User $user): ?UserIdentity
    {
        
        $identityModel = model(UserIdentityModel::class);

        return $identityModel->getIdentityByType(
            $user,
            $this->type
        );
    }
    
    /**
     * Returns the string type of the action class.
     */
    public function getType(): string
    {
        return $this->type;
    }
}