<?php
/**
 * WhatsJet
 *
 * This file is part of the WhatsJet software package developed and licensed by livelyworks.
 *
 * You must have a valid license to use this software.
 *
 * © 2025 livelyworks. All rights reserved.
 * Redistribution or resale of this file, in whole or in part, is prohibited without prior written permission from the author.
 *
 * For support or inquiries, contact: contact@livelyworks.net
 *
 * @package     WhatsJet
 * @author      livelyworks <contact@livelyworks.net>
 * @copyright   Copyright (c) 2025, livelyworks
 * @website     https://livelyworks.net
 */


namespace App\Yantrana\Services\YesTokenAuth;

use App\Yantrana\Services\YesTokenAuth\TokenRegistry\Repositories\TokenRegistryRepository;
use Exception;
use Firebase\JWT\JWT as FirebaseJwt;
use Firebase\JWT\Key as FirebaseKey;
use Route;
use YesSecurity;

/**
 * This YesTokenAuth class.
 *---------------------------------------------------------------- */
class YesTokenAuth
{
    /**
     * $key - encryption key
     *-----------------------------------------------------------------------*/
    private $useTokenRegistry = true;

    /**
     * $key - encryption key
     *-----------------------------------------------------------------------*/
    private $key = null;

    /**
     * $token - token key
     *-----------------------------------------------------------------------*/
    private $token = null;

    /**
     * $token - token key
     *-----------------------------------------------------------------------*/
    private $refreshedToken = false;

    /**
     * $ipAddress - record ip address
     *-----------------------------------------------------------------------*/
    private $ipAddress = false;

    /**
     * $userAgent - record ip address
     *-----------------------------------------------------------------------*/
    private $userAgent = true;

    /**
     * $expirationPeriod - default 5 days
     *-----------------------------------------------------------------------*/
    private $expirationPeriod; // = 60 * 60 * 24 * 5;

    /**
     * $refreshTokenAfter - refresh token auto after
     *-----------------------------------------------------------------------*/
    private $refreshTokenAfter; // = 60 * 60;// * 24 * 4;

    /**
     * Token Registry Repository.
     *
     *-----------------------------------------*/
    protected $tokenRegistryRepository;

    /**
     * Route via URl.
     *
     *-----------------------------------------*/
    private $routeViaURL;

    /**
     * Route via Input
     *
     *-----------------------------------------*/
    private $routeViaInput;

    /**
     * Api Request from any of mobile or web app
     *
     *-----------------------------------------*/
    private $apiRequestFrom;

    /**
     * __construct
     *
     * @param  TokenRegistryRepository  $tokenRegistryRepository     - Token Registry Repository
     *-----------------------------------------------------------------------*/
    public function __construct()
    {
        if (! $this->key) {
            $this->key = config('app.key');
        }

        // FirebaseJwt::$leeway = 60;

        $this->userAgent = ! config('app.debug');

        //set api request on every request
        $this->apiRequestFrom = request()->header('api-request-signature');

        $this->initialize();

        if ($this->useTokenRegistry == true) {
            $this->tokenRegistryRepository = new TokenRegistryRepository();
        }
    }

    /**
     * initialization
     *-----------------------------------------------------------------------*/
    public function initialize()
    {
        $yesTokenConfig = config('yes-token-auth', []);
        $this->useTokenRegistry = array_get($yesTokenConfig, 'token_registry.enabled', false);
        $this->key = array_get($yesTokenConfig, 'encryption_key', config('app.key'));
        $this->userAgent = array_get($yesTokenConfig, 'verify_user_agent', true);
        $this->ipAddress = array_get($yesTokenConfig, 'verify_ip_address', true);
        $this->routeViaURL = array_get($yesTokenConfig, 'routes_via_url', []);
        $this->routeViaInput = array_get($yesTokenConfig, 'routes_via_input', []);
        $this->expirationPeriod = array_get($yesTokenConfig, 'expiration', (60 * 60 * 5)); // 5 hours
        $this->refreshTokenAfter = array_get($yesTokenConfig, 'refresh_after', (60 * 30));  // 30 mins

        //set token time when requested from MOBILE APP
        if (isset($this->apiRequestFrom) && $this->apiRequestFrom == 'mobile-app-request') {
            $this->refreshTokenAfter = array_get($yesTokenConfig, 'refresh_after_for_mobile_app', (24 * 60 * 60 * 7));  // 7 days
            $this->expirationPeriod = array_get($yesTokenConfig, 'expiration_for_mobile_app', (24 * 60 * 60 * 10)); // 10 days
        }
    }

    /**
     * set Expiration
     *-----------------------------------------------------------------------*/
    public function setExpiration(int $expirationTime)
    {
        if ($expirationTime <= 0) {
            throw new Exception('YesTokenAuth: Expiration time can not be lower than 1');
        }

        $time = time();
        $this->expirationPeriod = $expirationTime;
    }

    /**
     * Generate Token
     *
     * @param    $tokenItems
     * @return void
     *------------------------------------------------------------------------ */
    public function issueToken($tokenItems = [], $registryId = null)
    {
        $this->initialize();

        $time = time();
        $tokenData = array_merge([
            'typ' => 'JWT',
            'alg' => 'HS256',
            // The issuer of the token
            'iss' => config('app.name'),
            // The audience of the token
            'aud' => config('app.name'),
            // The subject of the token
            'sub' => 'auth token',
            // The time the JWT was issued. Can be used to determine the age of the JWT
            'iat' => $time,
            // Defines the time before which the JWT MUST NOT be accepted for processing
            'nbf' => $time,
            // This will probably be the registered claim most often used.
            // This will define the expiration in NumericDate value.
            // The expiration MUST be after the current date/time
            'exp' => $time + $this->expirationPeriod,
            'rta' => $time + $this->refreshTokenAfter, // Refresh token after this time
            // Unique identifier for the JWT. Can be used to prevent the JWT from being replayed. This is helpful for a one time use token
            'jti' => YesSecurity::generateUid(),
            // The audience of the token
            'uaid' => config('app.name'),
        ], $tokenItems);

        $tokenData['uai'] = $_SERVER['HTTP_USER_AGENT']; // USER AGENT info
        $tokenData['cip'] = request()->getClientIp();

        $token = FirebaseJwt::encode($tokenData, $this->key, 'HS256');

        config([
            'app.yestoken.jti' => $tokenData['jti'],
        ]);

        // make entry for db
        if ($this->useTokenRegistry == true) {
            $this->registryEntry($tokenData, $token, $registryId);
        }

        unset($time);

        return encrypt($token);
    }

    /**
     * Record data into the db
     *
     * @param    $tokenData
     * @param    $token
     * @return array
     *------------------------------------------------------------------------ */
    public function registryEntry(array $tokenData, $token, $registryId = null)
    {
        $schema = config('yes-token-auth.token_registry.schema', [
            'jti' => '_uid',
            'jwt_token' => 'jwt_token',
            'uaid' => 'user_authorities__id',
            'ip_address' => 'ip_address',
            'expiry_at' => 'expiry_at',
        ]);

        $tokenRegistryData = [];
        foreach ($schema as $schemaKey => $schemaValue) {
            if ($schemaKey !== 'jwt_token') {
                if ($schemaValue == 'ip_address') {
                    if (! isset($tokenData['ip_address'])) {
                        $tokenRegistryData['ip_address'] = request()->getClientIp();
                    }
                } elseif ($schemaValue == 'expiry_at') {
                    if (! isset($tokenData['expiry_at'])) {
                        $tokenRegistryData['expiry_at'] = $this->expirationPeriod;
                    }
                } else {
                    $tokenRegistryData[$schemaValue] = $tokenData[$schemaKey];
                }
            } else {
                $tokenRegistryData[$schemaValue] = $token;
            }
        }

        if ($registryId) {
            $tokenRegistryData['predecessor_token_id'] = $registryId;
            $this->tokenRegistryRepository->delete($registryId);
        }

        return $this->tokenRegistryRepository->storeTokenRegistry($tokenRegistryData);
    }

    /**
     * Verify Token
     *
     * @param    $encryptedToken
     * $request->bearerToken();
     * @return mixed
     *------------------------------------------------------------------------ */
    public function verifyToken($encryptedToken = null)
    {
        // mobile app pusher auth check
        $isBroadcastCheckUrl = request()->is('api/broadcasting/auth');
        if (! $encryptedToken) {
            $currentRoute = Route::currentRouteName();
            if ($isBroadcastCheckUrl or in_array($currentRoute, $this->routeViaURL)) {
                $encryptedToken = request()->get('auth_token');
                //check length of token if not greater than 36, then fetch from DB
                if ($this->tokenRegistryRepository and ! (strlen($encryptedToken) > 36)) {
                    $tokenRegistryData = $this->tokenRegistryRepository->fetch($encryptedToken);

                    if (! __isEmpty($tokenRegistryData)) {
                        $encryptedToken = encrypt($tokenRegistryData->jwt_token);
                    }

                    if (__isEmpty($encryptedToken)) {
                        return [
                            'error' => 'token not registred',
                        ];
                    }
                }
            } elseif (in_array($currentRoute, $this->routeViaInput)) {
                $encryptedToken = request()->input('yes_access_token');

                //check length of token if not greater than 36, then fetch from DB
                if (! (strlen($encryptedToken) > 36)) {
                    $tokenRegistryData = $this->tokenRegistryRepository->fetch($encryptedToken);

                    if (! __isEmpty($tokenRegistryData)) {
                        $encryptedToken = encrypt($tokenRegistryData->jwt_token);
                    }

                    if (__isEmpty($encryptedToken)) {
                        return [
                            'error' => 'token not registered',
                        ];
                    }
                }
            } else {
                $encryptedToken = request()->header('authorization');
            }
        }
        $encryptedToken = str_replace('Bearer ', '', $encryptedToken);
        $decryptedToken = null;

        try {
            $decryptedToken = decrypt($encryptedToken);

            $decoded = FirebaseJwt::decode($decryptedToken, new FirebaseKey($this->key, 'HS256'));
            $this->refreshedToken = false;
            $time = time();
            if(!$isBroadcastCheckUrl) { // if not broadcast check url from mobile app etc
                if ($this->userAgent and ($decoded->uai != $_SERVER['HTTP_USER_AGENT'])) {
                    return [
                        'error' => 'user agent mismatch',
                    ];
                }
            }

            if ($this->ipAddress) {
                if ($decoded->cip != request()->getClientIp()) {
                    return [
                        'error' => 'client ip mismatch',
                    ];
                }
            }

            // fetch required entry
            if ($this->useTokenRegistry == true) {
                $isRegistered = $this->tokenRegistryRepository->fetch($decoded->jti);

                if (__isEmpty($isRegistered)) {
                    return [
                        'error' => 'token not registered',
                    ];
                }

                if ($decryptedToken !== $isRegistered->{config('yes-token-auth.token_registry.schema.jwt_token', 'jwt_token')}) {
                    return [
                        'error' => 'token mismatched',
                    ];
                    // if($decoded->jti !== $isRegistered->predecessor_token_id) {
                    //     return [
                    //         'error' => 'token mismatched'
                    //     ];
                    // }
                }
            }

            //check if registry enabled
            if ($this->useTokenRegistry) {
                // cleanup registry by deleting token not in use or expired
                $this->tokenRegistryRepository->cleanRegistry();
            }

            if (($decoded->rta < $time) and ($decoded->exp > $time)) {
                $this->refreshedToken = $this->issueToken([
                    'aud' => $decoded->aud,
                    'jti' => YesSecurity::generateUid(),
                    'uaid' => $decoded->uaid,
                    'iat' => $time,
                    // Defines the time before which the JWT MUST NOT be accepted for processing
                    'nbf' => $time,
                    // This will probably be the registered claim most often used.
                    // This will define the expiration in NumericDate value.
                    // The expiration MUST be after the current date/time
                    'exp' => $time + $this->expirationPeriod,
                    'rta' => $time + $this->refreshTokenAfter, // Refresh token after this time
                ], $decoded->jti);

                $decoded->refreshed_token = $this->refreshedToken;
            }

            $decoded = (array) $decoded;
            $decoded['error'] = false;

            return $decoded;
        } catch (Exception $e) {
            //check if registry enabled
            if ($this->useTokenRegistry) {
                $this->revokeAccessByToken($decryptedToken);
            }

            return [
                'error' => $e->getMessage(),
            ];
        }

        return false;
    }

    /**
     * Verify Token
     *
     * @return mixed
     *------------------------------------------------------------------------ */
    public function getRefreshed()
    {
        return $this->refreshedToken;
    }

    /**
     * Remove token access from db
     *
     * @return mixed
     *------------------------------------------------------------------------ */
    public function revokeAccessByToken($token)
    {
        return $this->tokenRegistryRepository->deleteByToken($token);
    }
}
