<?php
/**
 * @package         Novarain Extensions Installer
 * @author          Tassos Marinos <info@novarain.com>
 * @link            http://www.novarain.com
 * @copyright       Copyright © 2015 Novarain All Rights Reserved
 * @license         http://www.gnu.org/licenses/gpl-2.0.html GNU/GPL
 */

defined('_JEXEC') or die;

use Joomla\CMS\Installer\Installer;
use Joomla\CMS\Language\Text;
use Joomla\Filesystem\Folder;
use Joomla\Filesystem\File;
use Joomla\CMS\Factory;

class PlgSystemNovaRainInstallerInstallerScript
{
	/**
	 *  Minimum Joomla required version
	 *
	 *  @var  string
	 */
	private $min_joomla_version = '4.0.0';

	/**
	 *  Maximum Joomla supported version
	 *
	 *  @var  string
	 */
	private $max_joomla_version = '6.99.99';

	/**
	 *  Minimum PHP required version
	 *
	 *  @var  string
	 */
	private $min_php_version = '7.4.0';

	/**
	 *  Joomla Global Application object
	 *
	 *  @var  object
	 */
	private $app;

	/**
	 *  Joomla Global Database Object.
	 * 
	 *  @var  object
	 */
	private $db;

	/**
	 *  Class Constructor
	 */
	function __construct()
	{
		$this->app = Factory::getApplication();
		$this->db = Factory::getDbo();
	}

	/**
	 *  Requirements Checks before installation
	 *
	 *  @param   object             $route
	 *  @param   InstallerAdapter  $adapter
	 *
	 *  @return  bool
	 */
	public function preflight($route, $adapter)
	{
		// Load language for messaging
		Factory::getLanguage()->load('plg_system_novaraininstaller', __DIR__);

		// Check Joomla and PHP minimum required version
		if (!$this->passMinimumRequirementVersion("joomla"))
		{
			$this->uninstallInstaller();
			return false;
		}

		if (!$this->passMinimumRequirementVersion("php"))
		{
			$this->uninstallInstaller();
			return false;
		}

		// Check Joomla maximum supported version
		if (!$this->passMaximumRequirementVersion("joomla"))
		{
			$this->uninstallInstaller();
			return false;
		}

		// To prevent XML not found error
		$this->createExtensionRoot();

		return true;
	}

	/**
	 *  Trigger After Installer Installation
	 *
	 *  @param   object             $route    
	 *  @param   InstallerAdapter  $adapter  
	 *
	 *  @return  mixed  False on fail
	 */
	public function postflight($route, $adapter)
	{
		if (!in_array($route, array('install', 'update')))
		{
			return;
		}

		// Invalidate OPcache for all PHP files in packages
		$this->invalidateFiles();

		// Install the Framework
		$this->installFramework();

		// Then install the rest of the packages
		if (!$this->installPackages())
		{
			// Uninstall this installer
			$this->uninstallInstaller();
			return false;
		}

		// Update Update-sites
		$this->updateSites();
		
		// Uninstall Installer
		$this->uninstallInstaller();
	}

	private function createExtensionRoot()
	{
		$destination = JPATH_PLUGINS . '/system/novaraininstaller';
		Folder::create($destination);
		File::copy(__DIR__ . '/novaraininstaller.xml', $destination . '/novaraininstaller.xml');
	}

	private function updateSites()
	{
		// Set Download Key & fix Update Sites
		$upds = new \Tassos\Framework\Updatesites();
		$upds->update();
	}

	private function installPackages()
	{
		$packages = Folder::folders(__DIR__ . '/packages');
		$packages = array_diff($packages, array('plg_system_nrframework'));

		foreach ($packages as $package)
		{
			if (!$this->installPackage($package))
			{
				return false;
			}
		}

		return true;
	}

	private function installPackage($package)
	{
		$packagePath = __DIR__ . '/packages/' . $package;
		
		if (!is_dir($packagePath))
		{
			return;
		}

        $tmpInstaller = new Installer();

		// This is required by Joomla 6.
		// https://github.com/joomla/joomla-cms/issues/45653#issuecomment-3022585847
		$tmpInstaller->setDatabase($this->db);

		return $tmpInstaller->install($packagePath);
	}

	private function installFramework()
	{
		$this->installPackage('plg_system_nrframework');

		// Make sure the latest version of the framework autoloader will be loaded, regardless of the OPcache state.
		$filePath = JPATH_PLUGINS . '/system/nrframework/autoload.php';

		$this->clearFileInOPCache($filePath);

		// Always include the autoloader file to ensure the framework is loaded.
		// Do not use include_once or require_once here, as we want to ensure the file is loaded fresh.
		include $filePath;
	}

	private function passMinimumRequirementVersion($type = "joomla")
	{
		switch ($type)
		{
			case 'joomla':
				if (version_compare(JVERSION, $this->min_joomla_version, '<'))
				{
					$this->app->enqueueMessage(
						Text::sprintf('NRI_NOT_COMPATIBLE_UPDATE', JVERSION, $this->min_joomla_version),
						'error'
					);
					return false;
				}
				break;
			case 'php':
				if (version_compare(PHP_VERSION, $this->min_php_version, '<'))
				{
					$this->app->enqueueMessage(
						Text::sprintf('NRI_NOT_COMPATIBLE_PHP', PHP_VERSION, $this->min_php_version),
						'error'
					);
					return false;
				}
				break;
		}

		return true;
	}

	private function passMaximumRequirementVersion($type = "joomla")
	{
		switch ($type)
		{
			case 'joomla':
				if (version_compare(JVERSION, $this->max_joomla_version, '>'))
				{
					// Extract major version (e.g., "6" from "6.99.99")
					$maxVersionParts = explode('.', $this->max_joomla_version);
					$maxVersionFormatted = $maxVersionParts[0] . '.x';

					$this->app->enqueueMessage(
						Text::sprintf('NRI_NOT_COMPATIBLE_MAX_VERSION', JVERSION, $maxVersionFormatted),
						'error'
					);
					return false;
				}
				break;
		}

		return true;
	}

	private function uninstallInstaller()
	{
		if (!is_dir(JPATH_SITE . '/plugins/system/novaraininstaller'))
		{
			return;
		}

		$this->deleteFolders(array(
			JPATH_SITE . '/plugins/system/novaraininstaller/language',
			JPATH_SITE . '/plugins/system/novaraininstaller',
		));

		$db = $this->db;

		$query = $db->getQuery(true)
			->delete('#__extensions')
			->where($db->quoteName('element') . ' = ' . $db->quote('novaraininstaller'))
			->where($db->quoteName('folder') . ' = ' . $db->quote('system'))
			->where($db->quoteName('type') . ' = ' . $db->quote('plugin'));
		$db->setQuery($query);
		$db->execute();
	}
	
	public function deleteFolders($folders = array())
	{
		foreach ($folders as $folder)
		{
			if (!is_dir($folder))
			{
				continue;
			}

			Folder::delete($folder);
		}
	}

	/**
	 * Recursively invalidate OPcache for all PHP files in packages directory and subdirectories.
	 * This ensures that any updated PHP files are recompiled by PHP, avoiding stale bytecode issues.
	 * Measures and logs the time taken for the invalidation process.
	 */
	private function invalidateFiles()
	{
		$packagePaths = Folder::folders(__DIR__ . '/packages');

		foreach ($packagePaths as $element)
		{
			$paths = [];

			if (strpos($element, 'plg_') === 0)
			{
				[$dummy, $folder, $plugin] = explode('_', $element);

				$paths[] = sprintf('%s/%s/%s', JPATH_PLUGINS, $folder, $plugin);
			}
			elseif (strpos($element, 'com_') === 0)
			{
				$paths = [
					sprintf('%s/components/%s', JPATH_ADMINISTRATOR, $element),
					sprintf('%s/components/%s', JPATH_SITE, $element),
				];
			}
			elseif (strpos($element, 'mod_') === 0)
			{
				$paths = [
					sprintf('%s/modules/%s', JPATH_ADMINISTRATOR, $element),
					sprintf('%s/modules/%s', JPATH_SITE, $element),
				];
			}
			else
			{
				continue;
			}

			foreach ($paths as $path)
			{
				$this->clearCacheInDir($path);
			}
		}

		// Refresh Joomla's autoloader cache file
		// This is necessary to ensure that the autoloader reflects any changes made during installation
		$this->clearFileInOPCache(JPATH_CACHE . '/autoload_psr4.php');
	}

	/**
	 * Recursively walk through a directory and invalidate OPcache for each PHP file found.
	 *
	 * @param string $path Directory path to scan
	 */
	private function clearCacheInDir($path)
	{
		if (!@is_dir($path))
		{
			return;
		}

		foreach (new \DirectoryIterator($path) as $file)
		{
			if ($file->isDot() || $file->isLink())
			{
				continue;
			}

			if ($file->isDir())
			{
				// Recurse into subdirectories
				$this->clearCacheInDir($file->getPathname());
				continue;
			}

			if (!$file->isFile())
			{
				continue;
			}

			// Invalidate OPcache for this PHP file
			$this->clearFileInOPCache($file->getPathname());
		}
	}

	/**
	 * Invalidate OPcache for a single PHP file, if OPcache is enabled and available.
	 *
	 * @param string $file Absolute path to the PHP file
	 * @return bool True if invalidated, false otherwise
	 */
	private function clearFileInOPCache($file)
	{
		static $hasOpCache = null;

		if (is_null($hasOpCache))
		{
			$hasOpCache = ini_get('opcache.enable')
				&& function_exists('opcache_invalidate')
				&& (!ini_get('opcache.restrict_api')
					|| stripos(
						realpath($_SERVER['SCRIPT_FILENAME']), ini_get('opcache.restrict_api')
					) === 0);
		}

		if ($hasOpCache && (strtolower(substr($file, -4)) === '.php'))
		{
			// Invalidate the file in OPcache
			$ret = opcache_invalidate($file, true);
			@clearstatcache($file);

			return $ret;
		}

		return false;
	}
}