<?php

/**
 * @package     VP Prime Framework
 *
 * @author      Abhishek Das <info@virtueplanet.com>
 * @link        https://www.virtueplanet.com
 * @copyright   Copyright (C) 2012-2025 Virtueplanet Services LLP. All Rights Reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */

namespace Virtueplanet\Plugin\System\Prime\Helper;

use Exception;
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Http\HttpFactory;
use Joomla\CMS\Updater\Updater;
use Joomla\CMS\Uri\Uri;
use Joomla\Database\DatabaseInterface;
use Joomla\Registry\Registry;

// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects

/**
 * Update Helper class of VP Prime Framework.
 */
class UpdateHelper
{
    public const EXCEPTION_CODE_ERROR = 0;

    public const EXCEPTION_CODE_WARNING = 1;

    public const ACCESS_CODE_INVALID = 0;

    public const ACCESS_CODE_OKAY = 1;

    public const ACCESS_CODE_UNSURE = 2;

    protected const REMOTE_HOST = 'https://validate.virtueplanet.com/index.php';

    /**
     * Available http transport object for communication
     *
     * @var TransportInterface|boolean  Interface sub-class or boolean false if no adapters are available
     */
    protected static $adapter = null;

    /**
     * The access code
     *
     * @var   integer
     */
    protected static $accessCode = 0;

    /**
     * The current date object
     *
     * @var   \Joomla\CMS\Date\Date
     */
    protected static $date = null;

    /**
     * Method to get the package information of a template.
     *
     * @param    integer   $templateStyleId   The template style ID.
     *
     * @return   \stdClass|boolean   The package object or boolean false if not found.
     *
     * @deprecated  Use TemplateHelper::getPackage($templateStyleId)
     */
    public static function getPackage($templateStyleId)
    {
        return TemplateHelper::getPackage($templateStyleId);
    }

    public static function getCurrentVersion($extensionId)
    {
        /** @var \Joomla\Component\Installer\Administrator\Extension\InstallerComponent $component **/
        $component = Factory::getApplication()->bootComponent('installer');
        $factory   = $component->getMVCFactory();

        /** @var \Joomla\Component\Installer\Administrator\Model\UpdateModel */
        $model     = $factory->createModel('Update', 'Administrator', ['ignore_requests' => true]);

        // Get the caching duration.
        $params       = ComponentHelper::getComponent('com_installer')->getParams();
        $cacheTimeout = (int) $params->get('cachetimeout', 6);
        $cacheTimeout = 3600 * $cacheTimeout;

        // Get the minimum stability.
        $minimumStability = (int) $params->get('minimum_stability', Updater::STABILITY_STABLE);

        // Set required states.
        $model->setState('list.start', 0);
        $model->setState('list.limit', 0);
        $model->setState('filter.extension_id', $extensionId);

        // Purge the table before checking again.
        // $model->purge();

        // Find update.
        $model->findUpdates($extensionId, $cacheTimeout, $minimumStability);

        $updates = $model->getItems();

        if (!empty($updates) && is_array($updates)) {
            foreach ($updates as $update) {
                if ($update->extension_id == $extensionId) {
                    return $update;
                }
            }
        }

        return false;
    }

    public static function validate($templateStyleId, $uname = null, $dlk = null)
    {
        $package      = TemplateHelper::getPackage($templateStyleId);
        $revalidation = false;

        if (empty($package)) {
            throw new Exception('Failed to store Download Key. Package information could not be retrieved.', self::EXCEPTION_CODE_ERROR);
        }

        if (empty($package->pid)) {
            throw new Exception('VirtuePlanet Product ID (id) is not found in the package information.', self::EXCEPTION_CODE_ERROR);
        }

        if ($uname === null && $dlk === null) {
            if (empty($package->did)) {
                throw new Exception('Stored Download Key could not retrieved.', self::EXCEPTION_CODE_ERROR);
            }

            $data         = self::decodeData($package->did, true);
            $uname        = $data['uname'];
            $dlk          = $data['dlk'];
            $revalidation = true;
        }

        $query = [
            'uname' => $uname,
            'dlk'   => $dlk,
            'pid'   => $package->pid,
            'host'  => base64_encode(self::getHost()),
            '_pkey' => md5('time:' . microtime(true) . '.rand:' . mt_rand())
        ];

        self::verifyAndSaveDlk($query, $templateStyleId, $revalidation);
    }

    public static function deleteDlk($templateStyleId)
    {
        self::storeDlk($templateStyleId, []);
    }

    protected static function verifyAndSaveDlk(array $query, $templateStyleId, $revalidation = false)
    {
        $uri     = Uri::getInstance(self::REMOTE_HOST);
        $data    = http_build_query($query, '', '&');
        $adapter = self::getHttpAdapter();

        if (empty($adapter)) {
            throw new Exception('Download key verification failed. No HTTP adapter is available. Please install and enable cUrl on your server.', self::EXCEPTION_CODE_ERROR);
        }

        try {
            $response = $adapter->request('POST', $uri, $data, [], 30, 'VirtuePlanet Verifier/4.0');
        } catch (Exception $e) {
            self::setAccessCode(self::ACCESS_CODE_UNSURE);
            self::storeDlk($templateStyleId, $query, self::ACCESS_CODE_UNSURE);

            throw new Exception($e->getMessage(), self::EXCEPTION_CODE_WARNING);
        }

        if (empty($response)) {
            throw new Exception('Download key verification failed. No response received from remote server.', self::EXCEPTION_CODE_ERROR);
        }

        $responseBody = $response->body;

        if (empty($responseBody)) {
            throw new Exception('Download key verification failed. No response received from remote server.', self::EXCEPTION_CODE_ERROR);
        }

        if ($response->code != 200) {
            throw new Exception('Download key verification failed. Invalid response received from remote server. Response code: ' . $response->code, self::EXCEPTION_CODE_ERROR);
        }

        if (!is_string($responseBody)) {
            throw new Exception('Download key verification failed. Invalid response received from remote server.', self::EXCEPTION_CODE_ERROR);
        }

        // Blocked by captcha.
        if (strpos($responseBody, '.well-known') !== false && strpos($responseBody, 'captcha') !== false) {
            self::setAccessCode(self::ACCESS_CODE_UNSURE);
            self::storeDlk($templateStyleId, $query, self::ACCESS_CODE_UNSURE);

            throw new Exception('Download key verification failed. Failed to contact remote server. This website may not receive live updates.', self::EXCEPTION_CODE_WARNING);
        }

        // Convert JSON string to array.
        $result = (object) json_decode($responseBody);

        // Invalid json data.
        if (empty($result)) {
            throw new Exception('Download key verification failed. Invalid response received from remote server.', self::EXCEPTION_CODE_ERROR);
        }

        // Make sure we have the correct response.
        if (empty($result->_pkey) || $query['_pkey'] != $result->_pkey) {
            throw new Exception('Download key verification failed. Invalid response received from remote server.', self::EXCEPTION_CODE_ERROR);
        }

        if (!$result->valid) {
            if ($result->message) {
                throw new Exception($result->message, self::EXCEPTION_CODE_ERROR);
            }

            throw new Exception('Download key validation failed. Please enter a valid membership information.', self::EXCEPTION_CODE_ERROR);
        }

        if ($result->valid && !$result->access) {
            // When key is valid, but has no access and is revalidation.
            if ($revalidation) {
                self::setAccessCode(self::ACCESS_CODE_INVALID);
                self::storeDlk($templateStyleId, $query, self::ACCESS_CODE_INVALID);
            }

            if ($result->message) {
                throw new Exception($result->message, self::EXCEPTION_CODE_WARNING);
            }

            throw new Exception('Your subscription may have expired or you may not have an active subscription for this template.', self::EXCEPTION_CODE_WARNING);
        }

        self::setAccessCode(self::ACCESS_CODE_OKAY);
        self::storeDlk($templateStyleId, $query, self::ACCESS_CODE_OKAY);
    }

    protected static function storeDlk($templateStyleId, $query, $accessCode = 0)
    {
        $package = TemplateHelper::getPackage($templateStyleId);

        if (empty($package)) {
            throw new Exception('Failed to store Download Key. Package information could not be retrieved.', self::EXCEPTION_CODE_ERROR);
        }

        $date = Factory::getDate();
        $data = [
            'host'         => isset($query['host']) ? $query['host'] : '',
            'dlk'          => isset($query['dlk']) ? $query['dlk'] : '',
            'uname'        => isset($query['uname']) ? $query['uname'] : '',
            'access'       => $accessCode,
            'last_checked' => $date->toSql()
        ];

        self::setDate($date);

        $packageParams = $package->params->toArray();

        $packageParams['did'] = !empty($data['dlk']) ? self::encodeData($data) : '';

        $params = new Registry();

        $params->loadArray($packageParams);

        $db    = Factory::getContainer()->get(DatabaseInterface::class);
        $query = $db->getQuery(true);

        $query->select('s.*')
            ->from($db->quoteName('#__update_sites') . ' AS ' . $db->quoteName('s'))
            ->join('INNER', $db->quoteName('#__update_sites_extensions') . ' AS ' . $db->quoteName('u') . ' ON ' . $db->quoteName('u.update_site_id') . ' = ' . $db->quoteName('s.update_site_id'))
            ->where($db->quoteName('u.extension_id') . ' = ' . $db->quote($package->extension_id));

        $db->setQuery($query);
        $updateSites = $db->loadObjectList();

        if (empty($updateSites) && !empty($data['dlk'])) {
            throw new Exception('Failed to store Download Key. Update site entries not found.', self::EXCEPTION_CODE_ERROR);
        }

        if (!empty($updateSites)) {
            foreach ($updateSites as $updateSite) {
                $updateSite->extra_query = !empty($data['dlk']) ? 'dlid=' . $data['dlk'] : '';

                $db->updateObject('#__update_sites', $updateSite, 'update_site_id', true);
            }
        }

        $query->clear()
            ->update($db->quoteName('#__extensions'))
            ->set($db->quoteName('params') . ' = ' . $db->quote($params->toString()))
            ->where($db->quoteName('type') . ' = ' . $db->quote('package'))
            ->where($db->quoteName('extension_id') . ' = ' . $db->quote($package->extension_id));

        $db->setQuery($query);
        $db->execute();
    }

    /**
     * Finds an available http transport object for communication
     *
     * @return TransportInterface|boolean  Interface sub-class or boolean false if no adapters are available
     */
    protected static function getHttpAdapter()
    {
        if (self::$adapter === null) {
            $options   = ['follow_location' => true];
            $curlOpts  = [];

            if (defined('CURLOPT_SSL_VERIFYHOST')) {
                $curlOpts[CURLOPT_SSL_VERIFYHOST] = false;
            }

            if (defined('CURLOPT_SSL_VERIFYPEER')) {
                $curlOpts[CURLOPT_SSL_VERIFYPEER] = false;
            }

            if (!empty($curlOpts)) {
                $options['transport.curl'] = $curlOpts;
            }

            $optObject = new Registry();

            $optObject->loadArray($options);

            self::$adapter = HttpFactory::getAvailableDriver($optObject);
        }

        return self::$adapter;
    }

    protected static function encodeData(array $data)
    {
        $defaultData = [
            'host'         => '',
            'dlk'          => '',
            'uname'        => '',
            'access'       => 0,
            'last_checked' => ''
        ];
        $data        = array_replace($defaultData, $data);
        $dataString  = implode('|*|', $data);

        return base64_encode($dataString);
    }

    public static function decodeData($string, $renewHost = false)
    {
        $data = ['host' => '', 'dlk' => '', 'uname' => '', 'access' => 0, 'last_checked' => ''];

        if (empty($string) || !is_string($string)) {
            return $data;
        }

        $string = base64_decode($string);

        if (empty($string) || !is_string($string)) {
            return $data;
        }

        $parts   = !empty($string) && strpos($string, '|*|') !== false ? explode('|*|', $string) : [];
        $newHost = self::getHost();

        if ($renewHost) {
            $data['host']         = $newHost;
            $data['dlk']          = isset($parts[1]) ? self::cleanDlk($parts[1]) : '';
            $data['uname']        = isset($parts[2]) ? $parts[2] : '';
            $data['access']       = isset($parts[3]) ? intval($parts[3]) : 0;
            $data['last_checked'] = isset($parts[4]) ? $parts[4] : null;
        } else {
            $host = '';

            if (!empty($parts[0])) {
                $host = base64_decode($parts[0]);

                if ($host === false) {
                    $host = $parts[0];
                }
            }

            if (!empty($newHost) && $host == $newHost) {
                $data['host']         = $newHost;
                $data['dlk']          = isset($parts[1]) ? self::cleanDlk($parts[1]) : '';
                $data['uname']        = isset($parts[2]) ? $parts[2] : '';
                $data['access']       = isset($parts[3]) ? intval($parts[3]) : 0;
                $data['last_checked'] = isset($parts[4]) ? $parts[4] : null;
            }
        }

        return $data;
    }

    public static function cleanDlk($dlk)
    {
        if (empty($dlk)) {
            return '';
        }

        $dlk = trim($dlk);

        if (empty($dlk)) {
            return '';
        }

        // Is the Download Key too short?
        if (strlen($dlk) < 32) {
            return '';
        }

        if (strlen($dlk) > 32) {
            $dlk = substr($dlk, 0, 32);
        }

        return preg_replace("/[^a-zA-Z0-9]+/", "", $dlk);
    }

    protected static function getHost()
    {
        $host   = Uri::root();
        $result = '';

        if (empty($host)) {
            return $result;
        }

        $parts = parse_url($host);

        if (!empty($parts['host'])) {
            $result .= $parts['host'];
        }

        if (!empty($parts['path'])) {
            $result .= $parts['path'];
        }

        $prefix = 'www.';

        if (!empty($result) && substr($result, 0, strlen($prefix)) == $prefix) {
            $result = substr($result, strlen($prefix));
        }

        $result = rtrim($result, '/');

        return $result;
    }

    protected static function setAccessCode($code)
    {
        self::$accessCode = $code;
    }

    public static function getAccessCode()
    {
        return self::$accessCode;
    }

    protected static function setDate($date)
    {
        self::$date = $date;
    }

    public static function getDate()
    {
        return self::$date;
    }
}
