<?php
/**
 * @package     MefisAssetBridge
 * @subpackage  Joomla_Administrator_Component
 *
 * @copyright   Copyright (C) 2025 Goran Usljebrka. All rights reserved.
 * @license     Proprietary — No resale or incorporation into other projects without explicit written permission. see LICENSE.txt
 */

namespace Mefi\Component\MefisAssetBridge\Administrator\Traits\CoreBridge;

defined('_JEXEC') or die;

use Exception;
use Joomla\CMS\Factory;
use Joomla\CMS\Uri\Uri;
use Joomla\CMS\Language\Text;
use phpseclib3\Crypt\Common\PublicKey;
use MefisAssetBridgeCore\Auth\KeyStore;
use MefisAssetBridgeCore\Traits\Encryptor;
use Joomla\CMS\Application\ApplicationHelper;
use MefisAssetBridgeCore\Auth\AuthenticatedUser;
use MefisAssetBridgeCore\Api\Response\ApiResponse;
use MefisAssetBridgeCore\DTO\AuthenticatedUserDto;
use Mefi\Component\MefisAssetBridge\Administrator\Helper\CoreBridgeHelper;
use Joomla\CMS\Log\Log;
/**
 * Core Authentication Bridge Trait
 *
 * @since  1.0.0
 */
trait AuthTrait
{
    use Encryptor;

    protected ?KeyStore $KeyStore = null;

    /**
     * Check if application is authenticated with the server and if not, try to register it
     * It will create necessary keys and delete old ones if needed
     *
     * Expect Exception to be thrown if something goes wrong
     *
     * @return bool True if application is registered
     */
    public function registerApplicationIfNeeded() {
        // Check if core is loaded
        if (! CoreBridgeHelper::isCoreLoaded()) {
            throw new \RuntimeException(Text::_('COM_MEFISASSETBRIDGE_ERROR_CORE_NOT_LOADED'));
            return false;
        }

        $altPath = JPATH_ADMINISTRATOR . DIRECTORY_SEPARATOR . 'components'.DIRECTORY_SEPARATOR.'com_mefisassetbridge'.DIRECTORY_SEPARATOR.'keys' . DIRECTORY_SEPARATOR;

        $this->KeyStore = CoreBridgeHelper::getKeyStore();
        $apiFactory = $this->getApiFactory();
        $this->KeyStore->setRemoteServerUrl($apiFactory->getBaseUrl());

        // load uuid (if exist) and see if keys exist for it
        $uuid = $this->getWebsiteId();

        if(!empty($uuid)) {

            $this->KeyStore->setKeyPrefix($uuid);

            try {
                $this->KeyStore->loadKeys();
            } catch (\Exception $th) {}

            // we have 3 keys, all good
            if($this->KeyStore->areKeysValid() === 3) {
                return true;
            } else {
                // something is wrong, delete all keys might be the best
                $this->KeyStore->deleteKeys();
            }
        }

        // reset key prefix
        $this->KeyStore->resetKeyPrefix();

        //no uuid so create keys and do handshake
        $this->KeyStore->createKeys(true);

        // do handshake
        $this->coreHandshake($this->KeyStore->getPublicKeyString());

        // should be ok now
        if($this->KeyStore->areKeysValid() == 3) {
            // rename keys to use new uuid prefix
            $this->KeyStore->finalizeHandshakeRenameKeys($this->getWebsiteId());
            return true;
        }

        return false;
    }

    public function getWebsiteId()
    {
        // Check if core is loaded
        if (! CoreBridgeHelper::isCoreLoaded()) {
            return null;
        }

        // Get website ID from the core
        $apiFactory = $this->getApiFactory();
        $websiteId  = $apiFactory->getWebsiteId();

        return $websiteId;
    }

    /**
     * Check do we have necessary api keys, try to register if not
     */
    public function isAppRegistered() : bool
    {
        try {
            return $this->registerApplicationIfNeeded();
        } catch (\Exception $e) {
            return false;
        }
    }

    public function isAuthenticated(): bool
    {
        // Check if core is loaded
        if (! CoreBridgeHelper::isCoreLoaded()) {
            return false;
        }

        // Get user data from session
        $userDto = $this->getDataFromSession();

        // No user data in session
        if ($userDto === null) {
            return false;
        }

        // Create a Core AuthenticatedUser instance which handles validation internally
        try {
            $authenticatedUser = new AuthenticatedUser($userDto);
            // The AuthenticatedUser class has its own isAuthenticated method that checks token validity
            if ($authenticatedUser->isAuthenticated()) {
                return true;
            }

            // If token is expired but refresh token is valid, try refreshing
            if ($authenticatedUser->canRefresh()) {
                try {
                    // Get refresh token from the DTO
                    $refreshToken = $userDto->getToken()->getRefreshToken();

                    // Try to refresh the token @AuthenticatedUserDto
                    $authenticatedUserNew = $this->coreRefreshToken($refreshToken);
                    // make sure to copy master key to new object (it is not stored in database)
                    $authenticatedUserNew->setMasterKey($userDto->getMasterKey());
                    // Update session with refreshed token
                    $this->setDataToSession($authenticatedUserNew);

                    return true;
                } catch (\Exception $e) {

                    return false;
                }
            }
        } catch (\Exception $e) {
            return false;
        }

        return false;
    }

    /**
     * Get authenticated user data from session
     *
     * @return AuthenticatedUserDto|null User data or null if not found
     * @since  1.0.0
     */
    public function getDataFromSession(): ?AuthenticatedUserDto
    {
        // Check if core is loaded
        if (! CoreBridgeHelper::isCoreLoaded()) {
            return null;
        }
        $app = Factory::getApplication();
        // Get session data
        $userData = $app->getSession()->get('mefisAssetBridge.auth', null);

        // No data in session
        if (empty($userData)) {
            return null;
        }

        // Create DTO from array data
        try {
            // If the userData is already a DTO (happens during testing/development)
            if ($userData instanceof AuthenticatedUserDto) {
                return $userData;
            }

            // Convert array data to DTO object
            return AuthenticatedUserDto::fromArray($userData);
        } catch (\Exception $e) {
            // If there's an error creating the DTO, return null
            return null;
        }
    }

    /**
     * Get information about the current device/browser
     *
     * @return  string
     *
     * @since  1.0.0
     */
    public function getDeviceInfo($userAgent = '')
    {
        if (empty($userAgent)) {
            $userAgent = $_SERVER['HTTP_USER_AGENT'] ?? '';
        }
        $os      = 'Unknown';
        $browser = 'Unknown';

        // Detect OS
        if (preg_match('/windows nt 10/i', $userAgent)) {
            $os = 'Windows 10';
        } elseif (preg_match('/windows nt 11/i', $userAgent)) {
            $os = 'Windows 11';
        } elseif (preg_match('/mac os x/i', $userAgent)) {
            $os = 'macOS';
        } elseif (preg_match('/linux/i', $userAgent)) {
            $os = 'Linux';
        }

        // Detect Browser
        if (preg_match('/chrome/i', $userAgent)) {
            $browser = 'Chrome';
        } elseif (preg_match('/safari/i', $userAgent)) {
            $browser = 'Safari';
        } elseif (preg_match('/firefox/i', $userAgent)) {
            $browser = 'Firefox';
        } elseif (preg_match('/edge/i', $userAgent)) {
            $browser = 'Edge';
        }

        return $os . ', ' . $browser;
    }

    /**
     * Store authenticated user in session
     *
     * @param  AuthenticatedUserDto  $userDto  User DTO to store
     *
     * @return void
     * @since  1.0.0
     */
    public function setDataToSession(AuthenticatedUserDto $userDto): void
    {
        // Check if core is loaded
        if (! CoreBridgeHelper::isCoreLoaded()) {
            return;
        }

        // Convert the DTO to an array for session storage
        $userData = $userDto->toArray();

        $app = Factory::getApplication();

        // Store in session
        $app->getSession()->set('mefisAssetBridge.auth', $userData);

        // Set authenticated flag for quick checking
        $app->getSession()->set('mefisAssetBridge.authenticated', true);

    }

    public function clearMefisCookie()
    {
        $app = Factory::getApplication();
        $input = $app->getInput();

        $cookieLifeTime = time() + 30 * 86400;
        $cookieDomain   = $app->get('cookie_domain', '');
        $cookiePath     = $app->get('cookie_path', '/');
        $isHttpsForced  = $app->isHttpsForced();

        $input->cookie->set(
            ApplicationHelper::getHash('mefisassetmanager.auth'),
            '',
            [
                'expires'  => 0,
                'path'     => $cookiePath,
                'domain'   => $cookieDomain,
                'secure'   => $isHttpsForced,
                'httponly' => true,
            ]
        );
    }


    public function setMefisCookie(string $data)
    {
        // Check if core is loaded
        if (! CoreBridgeHelper::isCoreLoaded()) {
            return;
        }

        $app = Factory::getApplication();
        $input = $app->getInput();
        $ciphertext = $this->KeyStore->encrypt($data);

        $cookieLifeTime = time() + 30 * 86400;
        $cookieDomain   = $app->get('cookie_domain', '');
        $cookiePath     = $app->get('cookie_path', '/');
        $isHttpsForced  = $app->isHttpsForced();

        $input->cookie->set(
            ApplicationHelper::getHash('mefisassetmanager.auth'),
            $ciphertext,
            [
                'expires'  => $cookieLifeTime,
                'path'     => $cookiePath,
                'domain'   => $cookieDomain,
                'secure'   => $isHttpsForced,
                'httponly' => true,
            ]
        );
    }

    public function getMefisCookie(): ?array
    {
        // Check if core is loaded
        if (! CoreBridgeHelper::isCoreLoaded()) {
            return null;
        }

        $app = Factory::getApplication();
        $input = $app->getInput();
        $ciphertext = $input->cookie->getString(ApplicationHelper::getHash('mefisassetmanager.auth'), false);

        if(empty($ciphertext)) {
            return null;
        }

        try {
            $decrypted = $this->KeyStore->decrypt($ciphertext);

            if ($decrypted === null) {
                $this->clearMefisCookie();
                return null;
            }

            // Split on the LAST dot — JWT tokens contain dots.
            $lastDot = strrpos($decrypted, '.');
            if ($lastDot === false) {
                $this->clearMefisCookie();
                return null;
            }
            $refresh = substr($decrypted, 0, $lastDot);
            $master  = substr($decrypted, $lastDot + 1);
            if ($refresh === '' || $master === '' || strlen($master) < 4 || strlen($master) > 16) {
                $this->clearMefisCookie();
                return null;
            }
            return ['refresh_token' => $refresh, 'master_key' => $master];

        } catch (Exception $e) {
            LOG::add('Invalid cookie - can`t authenticate!', LOG::ERROR, 'mefis-asset-bridge');
            // uncomment to see error in log
            // LOG::add('Failed to decrypt refresh token from cookie: ' . $e->getMessage(), LOG::ERROR, 'mefis-asset-bridge');
            $this->clearMefisCookie();
            return null;
        }
    }

    /**
     * Perform login via the core API
     *
     * @param  string  $email        User email
     * @param  string  $password     User password
     *
     * @return object  Response object with user data and tokens
     *
     * @throws \RuntimeException On API error
     * @since  1.0.0
     */
    protected function coreLogin(string $email, string $password): AuthenticatedUserDto
    {
        // Get API factory - this calls the base trait method
        $apiFactory = $this->getApiFactory();

        // Create Login endpoint
        $loginEndpoint = $apiFactory->createLoginEndpoint();

        // Prepare parameters
        $params = [
            'email'    => $email,
            'password' => $password,
        ];

        // Send request
        $response = $loginEndpoint->sendRequest($params);

        // failed login, check if we suspect this is because website was copied over
        if (! $response->isSuccess()) {

            // we've received message to delete website identificator because we suspect it has been cloned or copied
            if (stripos($response->getMessage(), 'Unauthorized App') !== false) {
                $this->registerApplicationIfNeeded();

                $loginEndpoint = $apiFactory->createLoginEndpoint();
                $response1     = $loginEndpoint->sendRequest($params);

                // if it's ok now, then this is done
                if ($response1->isSuccess()) {

                    // replace initial response with the new one
                    $response = $response1;
                } else {
                    // failed again so not done
                    throw new \Exception($response1->getMessage() ?: Text::_('COM_MEFISASSETBRIDGE_AUTH_FAILED'));
                }
            } else {
                // not related to uuid, show initial error
                throw new \Exception($response->getMessage() ?: Text::_('COM_MEFISASSETBRIDGE_AUTH_FAILED'));
            }
        }

        $data = $response->getData();

        // check is of AuthenticatedUserDto type as well
        if (! isset($data['user']) || ! $data['user'] instanceof AuthenticatedUserDto) {
            throw new \Exception(Text::_('COM_MEFISASSETBRIDGE_AUTH_INVALID_RESPONSE'));
        }

        return $data['user'];

    }

    /**
     * Register a new user via core API
     *
     * @param  string  $name      User name
     * @param  string  $email     User email
     * @param  string  $password  User password
     *
     * @return ApiResponse  API response object
     *
     * @since  1.0.0
     */
    protected function coreRegister(string $name, string $email, string $password, string $passwordConfirm): ApiResponse
    {
        // Get API factory
        $apiFactory = $this->getApiFactory();

        // Create Register endpoint
        $registerEndpoint = $apiFactory->createRegisterEndpoint();

        // Prepare parameters
        $params = [
            'name'             => $name,
            'email'            => $email,
            'password'         => $password,
            'password_confirm' => $passwordConfirm,
        ];

        // Send request
        return $registerEndpoint->sendRequest($params);
    }

    protected function coreReset(string $email): ApiResponse
    {
        // Get API factory
        $apiFactory = $this->getApiFactory();

        // Create Register endpoint
        $resetEndpoint = $apiFactory->createResetEndpoint();

        // Prepare parameters
        $params = [
            'email' => $email,
        ];

        // Send request
        return $resetEndpoint->sendRequest($params);
    }

    /**
     * Refresh an authentication token
     *
     * @param  string  $refreshToken  The refresh token
     *
     * @return object  Refreshed token data
     *
     * @throws \RuntimeException On API error
     * @since  1.0.0
     */
    protected function coreRefreshToken(string $refreshToken): AuthenticatedUserDto
    {
        // Get API factory
        $apiFactory = $this->getApiFactory();
        // Create RefreshToken endpoint
        $refreshEndpoint = $apiFactory->createRefreshTokenEndpoint();

        // Prepare parameters
        $params = [
            'refresh_token' => $refreshToken
        ];

        // Send request
        $response = $refreshEndpoint->sendRequest($params);

        if (! $response->isSuccess()) {
            throw new \RuntimeException($response->getMessage() ?: Text::_('COM_MEFISASSETBRIDGE_AUTH_REFRESH_FAILED'));
        }

        $data = $response->getData();

        // check is of AuthenticatedUserDto type as well
        if (! isset($data['user']) || ! $data['user'] instanceof AuthenticatedUserDto) {
            throw new \Exception(Text::_('COM_MEFISASSETBRIDGE_AUTH_INVALID_RESPONSE'));
        }

        return $data['user'];

    }

    /**
     * Get list of active API keys for the current user
     * @return array List of active API keys
     */
    protected function coreGetActiveApiKeys(): array
    {
        // Get API factory
        $apiFactory = $this->getApiFactory();

        // Create ActiveTokens endpoint
        $endpoint = $apiFactory->createGetActiveApiKeysEndpoint();

        // Get user data from session
        $userDto = $this->getDataFromSession();
        if (! $userDto) {
            return [];
        }
        $apiFactory->setUser(new AuthenticatedUser($userDto));

        // Get refresh token from the DTO
        $refreshToken = $userDto->getToken()->getRefreshToken();

        // Prepare parameters
        $params = [
            'refresh_token' => $refreshToken,
        ];

        // Send request
        $response = $endpoint->sendRequest($params);

        if (! $response->isSuccess()) {
            throw new \RuntimeException($response->getMessage() ?: Text::_('COM_MEFISASSETBRIDGE_GET_ACTIVE_API_KEYS_FAILED'));
        }

        $responseData = $response->getData();
        $userDto->getUser()->setActiveTokens( count($responseData) );
        $this->setDataToSession($userDto);

        return $response->getData();
    }

    /**
     * Delete API key from the server
     * This is limited by server depending of the user plan
     *
     * @param  string  $refreshToken  The refresh token
     * @param  string  $apiKeyId      The API key ID to delete
     *
     * Expect Exception to be thrown on error
     * @return bool True on success
     */
    protected function coreDeleteApiKey(string $refreshToken, string $apiKeyId): bool
    {
        // Get API factory
        $apiFactory = $this->getApiFactory();

        // Create Logout endpoint
        $endpoint = $apiFactory->createDeleteApiKeyEndpoint();

        // Get user data from session
        $userDto = $this->getDataFromSession();
        if (! $userDto) {
            throw new \RuntimeException(Text::_('COM_MEFISASSETBRIDGE_NOT_AUTHENTICATED'));
        }
        $apiFactory->setUser(new AuthenticatedUser($userDto));

        // Prepare parameters
        $params = [
            'refresh_token' => $refreshToken,
            'id'            => $apiKeyId,
        ];

        // Send request
        $response = $endpoint->sendRequest($params);

        if(! $response->isSuccess()) {

            $msg = $response->getMessage();
            $r = $response->getData();

            // Handle 429 rate limit for token revocations
            // Check for RATE_LIMIT_EXCEEDED with max_revokes field (indicates token revocation limit)
            if($response->getStatusCode() == 429 && isset($r['type']) && $r['type'] === 'RATE_LIMIT_EXCEEDED' && isset($r['details']['max_revokes'])) {
                // Format retry timing for user display
                if(isset($r['details']['retry_after'])) {
                    $retryAfter = $r['details']['retry_after'];
                    if($retryAfter > 86400) {
                        $after = round($retryAfter / 86400) . ' days';
                    } elseif($retryAfter > 3600) {
                        $after = round($retryAfter / 3600) . ' hours';
                    } elseif($retryAfter > 60) {
                        $after = round($retryAfter / 60) . ' minutes';
                    } else {
                        $after = $retryAfter . ' seconds';
                    }
                    $msg = Text::sprintf("COM_MEFISASSETBRIDGE_ERROR_TOO_MANY_REVOKES", $after);
                }
            }
            throw new \RuntimeException($msg ?: Text::_('COM_MEFISASSETBRIDGE_API_KEY_DELETE_FAILED'));
        }

        $userDto->getUser()->setActiveTokens( $userDto->getUser()->getActiveTokens() - 1 );
        $this->setDataToSession($userDto);

        return $response->isSuccess();
    }

    /**
     * Perform initial handshake with the server to register the application
     *
     * @param  string  $publicKey  The public key to send
     *
     * @return PublicKey  The server's public key
     *
     * @throws \Exception On error
     * @since  1.0.0
     */
    protected function coreHandshake(string $publicKey): PublicKey
    {
        // Get API factory - this calls the base trait method
        $apiFactory = $this->getApiFactory();

        // Create Handshake endpoint
        $endpoint = $apiFactory->createHandshakeEndpoint();

        // Prepare parameters
        $params = [
            'publicKey'    => base64_encode($publicKey)
        ];

        // Send request
        $response = $endpoint->sendRequest($params);

        if (!$response->isSuccess()) {
            throw new Exception($response->getMessage() ?: Text::_('COM_MEFISASSETBRIDGE_HANDSHAKE_FAILED'));
        }
        $data = $response->getData();
        if( empty($data['public_key']) || empty($data['uuid'])) {
            throw new Exception(Text::_('COM_MEFISASSETBRIDGE_HANDSHAKE_INVALID_RESPONSE'));
        }

        $serverKeyString = $data['public_key'];
        // validate response key
        $serverKey = $this->KeyStore->loadPublicKey($serverKeyString);
        // store remote key
        $this->KeyStore->saveRemotePublicKey($serverKey);

        // store uuid
        $apiFactory->saveUuid($data['uuid']);
        return $serverKey;
    }
}
