<?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\Factory;
use Joomla\Filesystem\File;
use Joomla\Filesystem\Folder;
use Joomla\Filesystem\Path;
use Joomla\Database\DatabaseInterface;

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

/**
 * File Verifier Helper class of VP Prime Framework.
 */
class FileverifierHelper
{
    /**
     * List of files to be skipped from hashing.
     *
     * @var    array
     */
    protected const FILES_TO_SKIP_FOR_HASH = ['index.html', 'CHANGELOG.txt', 'fileHashes.json', '.htaccess', 'template_bg.jpg'];

    /**
     * List of loaded extensions by template style ID
     *
     * @var    array
     */
    protected static $extensions = [];

    /**
     * Method to build and save file hashes
     *
     * @var    integer    $templateStyleId   The template style ID
     * @var    string     $fileName          The file name in which the hashes to be saved.
     *
     * @return   void
     *
     * @throws   Exception
     */
    public static function buildHashes($templateStyleId, $fileName = 'fileHashes.json'): void
    {
        if (!function_exists('sha1_file')) {
            throw new Exception('File hashing failed. Please enable "sha1_file" function in your PHP settings.');
        }

        $files  = self::getFiles($templateStyleId);
        $hashes = [];

        if (empty($files)) {
            throw new Exception('No extension files found for the provided template style ID: ' . $templateStyleId);
        }

        foreach ($files as $file) {
            if (in_array(basename($file), self::FILES_TO_SKIP_FOR_HASH)) {
                continue;
            }

            // Remove root and standarize directory separator.
            $key = self::cleanPath($file);

            // Add the hash.
            $hashes[$key] = sha1_file($file);
        }

        $template = TemplateHelper::getTemplateStyle($templateStyleId);

        if (empty($template)) {
            throw new Exception('No template style found for the provided template style ID: ' . $templateStyleId);
        }

        $hashFile = JPATH_SITE . '/templates/' . $template->template . '/' . $fileName;
        $buffer   = json_encode($hashes, JSON_PRETTY_PRINT);

        try {
            File::write($hashFile, $buffer);
        } catch (Exception $e) {
            throw new Exception('Failed to write hash file. ' . $e->getMessage());
        }
    }

    /**
     * Method to compare files
     *
     * @var    integer    $templateStyleId   The template style ID
     * @var    string     $fileName          The file name in which the hashes to be saved.
     *
     * @return   array
     *
     * @throws   Exception
     */
    public static function compareHashes($templateStyleId, $fileName = 'fileHashes.json'): array
    {
        if (!function_exists('sha1_file')) {
            throw new Exception('The comparison of files failed. Please enable "sha1_file" function in your PHP settings.');
        }

        $template = TemplateHelper::getTemplateStyle($templateStyleId);

        if (empty($template)) {
            throw new Exception('The comparison of files failed. No template style found for the provided template style ID: ' . $templateStyleId);
        }

        $hashFile = JPATH_SITE . '/templates/' . $template->template . '/' . $fileName;

        if (!is_file($hashFile)) {
            throw new Exception('The comparison of files failed. Hash file is missing for the template.');
        }

        $originalHashesJson = file_get_contents($hashFile);

        if (empty($originalHashesJson)) {
            throw new Exception('The comparison of files failed. Failed to retrieve contents from the hash file. File: ' . self::cleanPath($hashFile));
        }

        $originalHashes = json_decode($originalHashesJson, true);

        if (empty($originalHashes)) {
            throw new Exception('The comparison of files failed. Failed to parse JSON contents from the hash file. File: ' . self::cleanPath($hashFile));
        }

        $files = self::getFiles($templateStyleId);

        if (empty($files)) {
            throw new Exception('The comparison of files failed. No extension files found for the provided template style ID: ' . $templateStyleId);
        }

        $hashes = [];
        $result = [
            'changed'       => [],
            'changed_count' => 0,
            'new'           => [],
            'new_count'     => 0,
            'deleted'       => [],
            'deleted_count' => 0
        ];

        foreach ($files as $file) {
            if (in_array(basename($file), self::FILES_TO_SKIP_FOR_HASH)) {
                continue;
            }

            // Remove root and standarize directory separator.
            $key = self::cleanPath($file);

            // Get the new file hash
            $hashes[$key] = sha1_file($file);

            if (!array_key_exists($key, $originalHashes)) {
                $result['new'][] = $key;
            } elseif ($originalHashes[$key] !== $hashes[$key]) {
                $result['changed'][] = $key;
            }
        }

        // Find the deleted files.
        $result['deleted'] = array_keys(array_diff_key($originalHashes, $hashes));

        // Count all the files.
        $result['changed_count'] = count($result['changed']);
        $result['new_count']     = count($result['new']);
        $result['deleted_count'] = count($result['deleted']);

        return $result;
    }

    /**
     * Method to get all extension files by a template style ID
     *
     * @var   integer   $templateStyleId    The template style ID.
     *
     * @return    array
     *
     * @throws    Exception
     */
    public static function getFiles($templateStyleId)
    {
        $extensions = self::getAllExtensions($templateStyleId);

        if (!$extensions) {
            throw new Exception('No extensions found for the provided template style ID: ' . $templateStyleId);
        }

        $allFiles = [];

        foreach ($extensions as $extension) {
            switch ($extension->type) {
                case 'plugin':
                    $files = self::getPluginFiles($extension);
                    break;
                case 'template':
                    $files = self::getTemplateFiles($extension);
                    break;
                case 'module':
                    $files = self::getModuleFiles($extension);
                    break;
                default:
                    $files = [];
            }

            if (is_array($files)) {
                $allFiles = array_merge($allFiles, $files);
            }
        }

        return $allFiles;
    }

    /**
     * Method to get all extensions by a template style ID
     *
     * @var   integer   $templateStyleId    The template style ID.
     *
     * @return    array
     *
     * @throws    Exception
     */
    public static function getAllExtensions(int $templateStyleId): array
    {
        if (array_key_exists($templateStyleId, self::$extensions)) {
            return self::$extensions[$templateStyleId];
        }

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

        $query->select('e.*')
            ->from($db->quoteName('#__extensions') . ' AS ' . $db->quoteName('e'))
            ->join('INNER', $db->quoteName('#__extensions') . ' AS ' . $db->quoteName('p') . ' ON ' . $db->quoteName('p.extension_id') . ' = ' . $db->quoteName('e.package_id'))
            ->join('INNER', $db->quoteName('#__extensions') . ' AS ' . $db->quoteName('t') . ' ON ' . $db->quoteName('t.package_id') . ' = ' . $db->quoteName('p.extension_id'))
            ->join('INNER', $db->quoteName('#__template_styles') . ' AS ' . $db->quoteName('ts') . ' ON ' . $db->quoteName('ts.template') . ' = ' . $db->quoteName('t.element'))
            ->where($db->quoteName('t.type') . ' = ' . $db->quote('template'))
            ->where($db->quoteName('p.type') . ' = ' . $db->quote('package'))
            ->where($db->quoteName('t.enabled') . ' = ' . $db->quote('1'))
            ->where($db->quoteName('p.enabled') . ' = ' . $db->quote('1'))
            ->where($db->quoteName('t.client_id') . ' = ' . $db->quote('0'))
            ->where($db->quoteName('ts.id') . ' = ' . $db->quote($templateStyleId))
            ->group($db->quoteName('e.extension_id'))
            ->order($db->quoteName('e.type') . ' DESC');

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

        if (empty($extensions)) {
            self::$extensions[$templateStyleId] = [];
        } else {
            self::$extensions[$templateStyleId] = $extensions;
        }

        return self::$extensions[$templateStyleId];
    }

    protected static function getTemplateFiles($template)
    {
        $base     = $template->client_id == 1 ? JPATH_ADMINISTRATOR : JPATH_SITE;
        $baseName = $template->client_id == 1 ? 'administrator' : 'site';
        $files    = [];
        $folders  = [
            $base . '/templates/' . $template->element,
            $base . '/media/templates/' . $baseName . '/' . $template->element,
        ];

        foreach ($folders as $folder) {
            if (!is_dir($folder)) {
                continue;
            }

            $filesInFolder = Folder::files($folder, '.', true, true);

            if (!empty($filesInFolder)) {
                $files = array_merge($files, $filesInFolder);
            }
        }

        $langFolders = self::getLanguageFolders($template->client_id);

        foreach ($langFolders as $langFolder) {
            $langFile    = $langFolder . '/tpl_' . $template->element . '.ini';
            $langSysFile = $langFolder . '/tpl_' . $template->element . '.sys.ini';

            if (is_file($langFile)) {
                $files[] = $langFile;
            }

            if (is_file($langSysFile)) {
                $files[] = $langSysFile;
            }
        }

        return $files;
    }

    protected static function getPluginFiles($plugin)
    {
        $base    = $plugin->client_id == 1 ? JPATH_ADMINISTRATOR : JPATH_SITE;
        $files   = [];
        $folders = [
            $base . '/plugins/' . $plugin->folder . '/' . $plugin->element,
            $base . '/media/' . 'plg_' . $plugin->folder . '_' . $plugin->element,
        ];

        foreach ($folders as $folder) {
            if (!is_dir($folder)) {
                continue;
            }

            $filesInFolder = Folder::files($folder, '.', true, true);

            if (!empty($filesInFolder)) {
                $files = array_merge($files, $filesInFolder);
            }
        }

        $langFolders = self::getLanguageFolders(1);

        foreach ($langFolders as $langFolder) {
            $langFile    = $langFolder . '/plg_' . $plugin->folder . '_' . $plugin->element . '.ini';
            $langSysFile = $langFolder . '/plg_' . $plugin->folder . '_' . $plugin->element . '.sys.ini';

            if (is_file($langFile)) {
                $files[] = $langFile;
            }

            if (is_file($langSysFile)) {
                $files[] = $langSysFile;
            }
        }

        return $files;
    }

    protected static function getModuleFiles($module)
    {
        $base    = $module->client_id == 1 ? JPATH_ADMINISTRATOR : JPATH_SITE;
        $files   = [];
        $folders = [
            $base . '/modules/' . $module->element,
            $base . '/media/' . $module->element,
        ];

        foreach ($folders as $folder) {
            if (!is_dir($folder)) {
                continue;
            }

            $filesInFolder = Folder::files($folder, '.', true, true);

            if (!empty($filesInFolder)) {
                $files = array_merge($files, $filesInFolder);
            }
        }

        $langFolders = self::getLanguageFolders($module->client_id);

        foreach ($langFolders as $langFolder) {
            $langFile    = $langFolder . '/' . $module->element . '.ini';
            $langSysFile = $langFolder . '/' . $module->element . '.sys.ini';

            if (is_file($langFile)) {
                $files[] = $langFile;
            }

            if (is_file($langSysFile)) {
                $files[] = $langSysFile;
            }
        }

        return $files;
    }

    protected static function getLanguageFolders($clientId)
    {
        $base       = $clientId == 1 ? JPATH_ADMINISTRATOR : JPATH_SITE;
        $langFolder = $base . '/language';
        $folders    = [];

        if (is_dir($langFolder)) {
            $folders = Folder::folders($langFolder, '.', false, true);
        }

        return $folders;
    }

    protected static function cleanPath($file)
    {
        $file       = Path::clean($file);
        $root       = JPATH_ROOT . DIRECTORY_SEPARATOR;
        $rootLength = strlen($root);

        if (strpos($file, $root) === 0) {
            $file = substr($file, $rootLength);
        }

        $file = Path::clean($file, '/');

        return $file;
    }
}
