<?php

namespace apexl\Io\modules\user\services\twoFactorAuth;

use apexl\Config\Singleton;
use apexl\encryption\Encrypt;
use apexl\Io\modules\user\entities\twoFactorMethodEntity;
use apexl\Io\modules\user\entities\twoFactorRecoveryCodeEntity;
use apexl\Io\modules\user\services\currentUser;
use apexl\Io\services\Logger;
use Psr\Http\Message\ServerRequestInterface as Request;

class TwoFactorAuthService
{

    private $typeSlug; // Identifier to use in config, e.g. totp, sms etc.
    private $encryptionService;

    public function __construct()
    {
        $this->config = Singleton::getInstance();
        $this->encryptionService = new Encrypt();
        $this->currentUser = currentUser::getCurrentUser();
    }

    public function isEnabled(): bool
    {
        return ($this->config->app->twoFactorAuth->enabled ?? false)
            && count($this->config->app->twoFactorAuth->methods ?? []) > 0;
    }

    public function getAppName()
    {
        return ($this->config->app->twoFactorAuth->appName ?? null);
    }


    public function store2FaMethod($type, $userId, $secret)
    {
        list($secretIv, $secretEncrypted) = $this->encryptionService->encrypt($secret, $this->config->app->encryption->key);

        $twoFaMethod = new twoFactorMethodEntity();
        $twoFaMethod->user_id = $userId;
        $twoFaMethod->secret_iv = $secretIv;
        $twoFaMethod->secret = $secretEncrypted;
        $twoFaMethod->type = $type;
        $twoFaMethod->is_active = 0;
        $twoFaMethod->store();

        return $twoFaMethod;
    }

    public function retrievePending2FaMethod($type, $userId)
    {
        $twoFaMethod = new twoFactorMethodEntity();
        $twoFaMethod->getMostRecentPendingOfTypeForUser($type, $userId);

        return $twoFaMethod;
    }

    public function retrieveActive2FaMethod($userId)
    {
        $twoFaMethod = new twoFactorMethodEntity();
        $twoFaMethod->getMostRecentActiveForUser($userId);

        return $twoFaMethod;
    }

    public function getDecryptedSecret(twoFactorMethodEntity $twoFaMethod)
    {
        return $this->encryptionService->decrypt($twoFaMethod->secret, $twoFaMethod->secret_iv, $this->config->app->encryption->key);
    }

    public function getUser2FaMethod()
    {
        $currentUser = currentUser::getCurrentUser();
        $twoFaMethod = $this->retrieveActive2FaMethod($currentUser->id);
        if (isset($twoFaMethod->type) && $twoFaMethod->type) {
            return $this->getMethodService($twoFaMethod->type);
        }

        return null;
    }

    public function enable2Fa()
    {
        $this->log('enable', ['user_id' => $this->currentUser->id, 'user_two_factor_enabled' => $this->currentUser->two_factor_enabled]);
        if ($this->currentUser->id > 0 && $this->currentUser->two_factor_enabled == 0) {
            $codes = $this->generateRecoveryCodes($this->currentUser->id);
            $this->currentUser->two_factor_enabled = 1;
            $this->currentUser->store();
            return $codes;
        }

        return [];
    }

    private function generateRecoveryCodes($userId): array
    {

        $plainTextCodes = [];
        $numberOfCodes = 8;

        $this->log('generate_recovery_codes', ['user_id' => $this->currentUser->id, 'user_two_factor_enabled' => $this->currentUser->two_factor_enabled]);

        if ($this->currentUser->id > 0 && $this->currentUser->two_factor_enabled == 0) {
            for ($i = 0; $i < $numberOfCodes; $i++) {
                // 2. Generate a secure, 8-character string (e.g., "A1B2-C3D4")
                // random_bytes(4) creates 4 bytes, bin2hex makes it an 8-character string
                $rawCode = strtoupper(bin2hex(random_bytes(6)));

                // Add a dash in the middle for readability
                $formattedCode = substr($rawCode, 0, 4) . '-' . substr($rawCode, 4, 4) . '-' . substr($rawCode, 8, 4);

                $plainTextCodes[] = $formattedCode;

                // 3. Hash the code for database storage
                $hashedCode = password_hash($formattedCode, PASSWORD_DEFAULT);

                // 4. Create and populate the mapped entity
                $recoveryEntity = new twoFactorRecoveryCodeEntity();
                $recoveryEntity->user_id = $this->currentUser->id;
                $recoveryEntity->code_hash = $hashedCode;
                $recoveryEntity->store();
            }
        }

        // 6. Return the plain text array so Vue can display it
        return $plainTextCodes;

    }

    // Function allows loading of method service using methodName, e.g. 'totp'
    public function getMethodService($methodName)
    {
        $directory = __DIR__ . '/methods';
        $baseNamespace = __NAMESPACE__ . '\\methods\\';

        foreach (glob("$directory/*.php") as $file) {

            $className = pathinfo($file, PATHINFO_FILENAME);
            $fullClass = $baseNamespace . $className;

            if (class_exists($fullClass)) {
                if (is_callable([$fullClass, 'getMethodName'])) {

                    if ($fullClass::getMethodName() === $methodName) {
                        return new $fullClass();
                    }
                }
            }
        }
    }

    public function validateRecoveryCode(Request $request, $userId, $recoveryCode)
    {
        $recoveryEntity = new twoFactorRecoveryCodeEntity();
        $unusedCodes = $recoveryEntity->getAllForUser($userId);

        // Loop and check the cryptographic hashes
        $matchedEntity = null;
        foreach ($unusedCodes as $codeEntity) {
            if (password_verify($recoveryCode, $codeEntity->code_hash)) {
                $matchedEntity = new twoFactorRecoveryCodeEntity();
                $matchedEntity->load($codeEntity->id);
                break;
            }
        }

        // Handle Mismatch / Invalid Code
        if (!$matchedEntity) {
            $this->log('validate_recovery_code_result', ['result' => false, 'providedCode' => $recoveryCode]);
            return false;
        }

        // SUCCESS: Burn the code immediately so it can never be used again
        $matchedEntity->used_at = date('Y-m-d H:i:s');
        $matchedEntity->store();

        currentUser::addSessionData($request, ['twoFaPassed' => true]);

        $this->log('validate_recovery_code_result', ['result' => true, 'providedCode' => $recoveryCode]);

        return true;
    }

    public function log($type, $data=[])
    {
        if (is_object($data)) $data = (array)$data;
        $user = currentUser::getCurrentUser();
        $data['user_id'] = $user->id ?? null;
        $data['user_email'] = $user->email ?? null;
        Logger::log('2fa_'.$type, $data);
    }

}