Your IP : 216.73.216.224


Current Path : /var/www/html/libraries/noboss/src/Util/
Upload File :
Current File : /var/www/html/libraries/noboss/src/Util/NbInstallScriptUtil.php

<?php
/**
 * @package			No Boss Extensions
 * @subpackage  	No Boss Library
 * @author			No Boss Technology <contact@nobosstechnology.com>
 * @copyright		Copyright (C) 2026 No Boss Technology. All rights reserved.
 * @license			GNU Lesser General Public License version 3 or later; see <https://www.gnu.org/licenses/lgpl-3.0.en.html>
 */

namespace Noboss\Library\Util;

use Joomla\CMS\Factory;
use Joomla\Database\DatabaseInterface;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Uri\Uri;
use Joomla\CMS\Installer\Installer;
use Joomla\CMS\Log\Log;
use Noboss\Library\Util\NbCurlUtil;

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

class NbInstallScriptUtil{

    /**
     * Recebe o token do usuário e manda a url do site atual para o servidor da noboss
     *
     * @param String $token token da licença do usuário
     * @return void
     */
    public static function saveAuthorizedUrl($token){
        $url = self::getUrlNbExtensions(). '/index.php?option=com_nbextensoes&task=externallicenses.changeAuthorizedUrl&format=raw';

        // Dados a enviar na requisicao
        $dataPost = array(
            'token' => $token,
            'newUrl' => (str_replace(array('https://www.', 'http://www.', 'https://', 'http://'), '', Uri::root())),
            'overwrite' => false,
            'jversion' => JVERSION
        );
        NbCurlUtil::request('POST', $url, $dataPost, null, 10, null, null);
    }

    /**
     * Verifica se extensao pode ser atualizada a partir de um token
     *
     * @param   String      $token      Token da extensao
     * @return 	Mixed       Boolean 1 se update estiver em dia, 0 se nao estiver em dia ou 'INVALID_TOKEN' se nao localizado
     */
    public static function updateLicenseIsValid($token){
        $url = self::getUrlNbExtensions(). 'index.php?option=com_nbextensoes&task=externallicenses.updateLicenseIsValid&format=raw';

        $dataPost = array(
            'token' => $token
        );

        // Faz uma requisição curl usando a library
        $curlRequest = NbCurlUtil::request('POST', $url, $dataPost, null, 20);

        if ($curlRequest->success) {
            return $curlRequest->data;
        }

        return false;   
   }

   /** 
     * Remove registro da url do site na base da No Boss
     *
     * @param   String      $token      Token da extensao
     * @return 	Boolean      True ou false
     */
    public static function removeUrlSite($token){
        $url = self::getUrlNbExtensions(). 'index.php?option=com_nbextensoes&task=externallicenses.removeUrlSite&format=raw';

        $dataPost = array(
            'token' => $token,
            'url' => base64_encode(str_replace(array('https://www.', 'http://www.', 'https://', 'http://'), '', Uri::root()))
        ); 

         $curlRequest = NbCurlUtil::request('POST', $url, $dataPost, null, 20);

        if ($curlRequest->success) {
            return $curlRequest->data;
        }

        return false; 
   }

    /**
     * Recebe o nome de extensão e o valor da coluna extra_query e atualiza
     *
     * @param String $extensionName Name da extensão
     * @param String $extraQuery valor para a coluna extra_query
     */
    public static function updateExtraQuery($parent, $extraQuery){
        // Pega o nome e os servidores de update da extensão
        $extName = $parent->getName();
        $servers = (array) $parent->getManifest()->updateservers;

        $db = Factory::getContainer()->get(DatabaseInterface::class);
        $query = $db->getQuery(true);
        // Para cada servidor de update
        foreach ($servers as $server) {
            // Busca o id od servidor de update
            $query = $db->getQuery(true);
            $query
            ->select('update_site_id')
            ->from('#__update_sites')
            ->where("location = '{$server}' AND enabled = '1'");
            $db->setQuery($query);
            $id = $db->loadResult();

            // Caso tenha encontrado, apenas faz update do extra_query
            if(!empty($id)){
                $query = $db->getQuery(true);
                $query->update("#__update_sites")
                    ->set("extra_query = '{$extraQuery}'")
                    ->where("update_site_id = '{$id}'");
                $db->setQuery($query);
                $db->execute();
            // Caso não exista no banco, insere um novo registro
            } else {
                $query = $db->getQuery(true);
                $query->insert("#__update_sites")
                    ->columns('name, type, location, enabled, extra_query')
                    ->values("'{$extName}', 'extension', '{$server}', 1, '{$extraQuery}'");
                $db->setQuery($query);
                $db->execute();
            }
        }
    }

    /**
    * Retorna o numero de extensões instaladas no site, não incluindo os extra.
    *
    *  @param		String		Library name of XML
    *
    *  @return		Array		Um array com as extensões dependentes da library
    */
    public static function getDependencies($extraExtensions){
        // Faz uma busca no  banco, na tabela extensions
        $db = Factory::getContainer()->get(DatabaseInterface::class);
        $query = $db->getQuery(true);
        // Array para as buscas
        $likeArray = array('com_noboss', 'plg_noboss', 'mod_noboss', 'tpl_noboss');
        //Monta a query para buscar na tabella extension
        $query->select("COUNT(1)")
        ->from("#__extensions")
        ->where("element NOT IN ('".implode("', '", $extraExtensions)."')");
        $val = '(';
        foreach($likeArray as $key => $like){
            if($key != 0){
                $val .= ' OR ';
            }
            $val .= "element LIKE '{$like}%'";
        }
        $val .= ')';
        $query->where($val);

        $db->setQuery($query);
        $result = $db->loadResult();

        return $result;
    }

    public static function getLibraryUpdateId($parent){
        // Pega o alias da library
        //$libElement = $parent->getElement();

        // Faz uma busca no  banco, na tabela extensions
        $db = Factory::getContainer()->get(DatabaseInterface::class);
        $query = $db->getQuery(true);

        //Monta a query para buscar na tabella extension
        $query->select("b.update_id")
                ->from("#__extensions as a")
                ->join('INNER',"#__updates as b ON a.extension_id = b.extension_id")
                ->where("a.element = 'noboss'");
        $db->setQuery($query);
        $result = $db->loadResult();

        return (int) $result;
    }

    /**
     * Busca por todos os extras e desinstala um por um
     *
     * @return void
     */
    public static function uninstallExtras($extraExtensions){
        $extras = self::getExtrasList($extraExtensions);
        // Para cada extra, executa o processo de desinstalção
        foreach ($extras as $extra) {
            $tmpInstaller = new Installer;
            // Tenta fazer a desinstalação
            try{
                $result = $tmpInstaller->uninstall($extra->type, $extra->extension_id);
                if(!$result){
                    throw new \RuntimeException();
                }
            } catch (\Exception $e){
                // Verifica se existe a constante, caso não exista, monta um texto fixo
                if($msg = Text::sprintf('SCRIPT_EXTRAS_UNINSTALL_ERROR', $extra->element) == 'SCRIPT_EXTRAS_UNINSTALL_ERROR'){
                    $msg = "<p>Houve um erro durante a desinstalação do pacote: {$extra->element}</p>";
                }
                Log::add($msg, Log::WARNING, 'jerror');
            }
        }
    }

    /**
     * Busca no banco por todos os extras
     *
     * @return Array Retorna um array com informações dos extras
     */
    public static function getExtrasList($extraExtensions){
        $db = Factory::getContainer()->get(DatabaseInterface::class);
        $query = $db->getQuery(true);
        //Monta a query para buscar na tabela extension
        $query->select('extension_id, element, type')
        ->from($db->quoteName('#__extensions'))
        ->where("element IN('".implode("', '", $extraExtensions)."')");

        $db->setQuery($query);
        $result = $db->loadObjectList();
        return $result;
    }

    /**
     * Busca por todas as extensões que estão no mesmo pacote que o parametro
     *
     * @param string $element alias do pacote que estamos removendo (Ex: 'pkg_nobossembed')
     * @return array Array com os id das extensçoes que estão no mesmo pacote
     */
    public static function getPackageExtensions($elementPack) {
        $db = Factory::getContainer()->get(DatabaseInterface::class);
        $query = $db->getQuery(true);
        //Monta a query para buscar na tabela extension
        $query->select('a.extension_id, a.type')
        ->from('#__extensions AS a')
        ->where("a.package_id = (SELECT b.package_id FROM #__extensions AS b WHERE b.element='{$elementPack}') AND a.package_id != 0 AND a.extension_id != '{$extensionId}'");

        $db->setQuery($query);
        $result = $db->loadObjectList();
        return $result;
    }

    /**
     * Desistala cada extensão passada no array
     *
     * @return void
     */
    public static function uninstallPackageExtensions($extensions){
        $tmpInstaller = new Installer;
        foreach ($extensions as $extension) {
            // Caso não seja o pacote em si
            if($extension->type != 'package'){
                // Tenta fazer a desinstalação
                $tmpInstaller->uninstall($extension->type, $extension->extension_id);
            } else {
                // Caso seja o pacote, deleta do banco
                $db = Factory::getContainer()->get(DatabaseInterface::class);
                $query = $db->getQuery(true);
                //Monta a query para buscar na tabela extension
                $query
                    ->delete('#__extensions')
                    ->where("extension_id = {$extension->extension_id}");
        
                $db->setQuery($query);
                $db->execute();
            }
        }
    }

    /**
	*	Função que retorna a url base da plataforma No Boss Extensions para a realizacao de requisicoes
	*
	* 	@return 	String 		Url base da plataforma
	**/
	public static function getUrlNbExtensions(){
		// Objeto com dados do config
		$config = Factory::getApplication()->getConfig();

        // Obtem a tag do idioma que esta sendo navegado
        $currentLanguage = Factory::getApplication()->getLanguage()->getTag();

        /* TODO: modificado para pegar sef do idioma direto no idioma corrente cortando estring (ex: extraimos 'pt' de 'pt-BR')
                - Anteriormente buscavamos o sef dos idiomas de conteúdo instalados, mas isso poderia dar problema pq eles podem estar desabilitados sem que o acesso pelo idioma esteja desabilitado
        */
        // $languages = LanguageHelper::getLanguages('lang_code');
        // $langSef = $languages[$currentLanguage];
        // $langSef = $langSef->sef;
        $langSef = substr($currentLanguage, 0, 2);

        // TODO: qnd colocarmos o idioma ingles como default, eh necessario invester a ordem para setar 'pt' somente se usuario estiver em pt

        // Idioma que esta sendo navegado nao eh portugues brasil: forca para colocar idioma ingles na navegacao
        if($langSef != 'pt'){
            $langSef = '/en';
        }
        // Deixa sem tag de idioma para pegar portugues
        else{
            $langSef = '/';
        }

		// Obtem a url definida no config (caso exista)
		$urlNbExtensions = $config->get('url_nb_extensions');

        // Url refinida no config: retorna ela mesmo
		if (isset($urlNbExtensions) && !empty($urlNbExtensions)){
			return $urlNbExtensions.$langSef;
        }

		// Retorna url do ambiente de producao
        return 'https://www.nobossextensions.com'.$langSef;
	}

    /**
     * Funcao que verifica se pasta 'noboss' esta criada dentro de 'layouts' e realiza acoes qnd necessario
     * 
     * Utilizado na area admin dos nossos componentes
     */
    public static function checkLibraryLayoutFolder(){
        // Diretorio 'noboss' nao existe na pasta 'layouts' (provavelmente nao criou na instalacao da library)
        if(!is_dir(JPATH_SITE.'/layouts/noboss')) {
            $urlLibrary = 'https://www.nobossextensions.com/installation/nobosslibrary';
            $urlDocPermission = 'https://docs.nobosstechnology.com/extensoes-joomla/ajustando-permissoes-de-diretorios-e-arquivos-no-joomla';
            
            // Seta mensagem a ser exibida orientando sobre o problema
            $message = "<br>
            We have identified that the 'No Boss Library' that is required for this extension to work is not installed correctly on the site.<br><br>
            The most likely problem is the lack of permissions on the site directory 'layouts/noboss'. <br>
            When the library is installed or updated, it tries to make changes to this directory. <br><br>               
            To resolve the issue, follow the two steps below:<br>
            1. Correct the permissions of the 'layouts/noboss' directory. If you have questions about how to do it, visit our <a href='{$urlDocPermission}' target='_blank'>documentation on setting Joomla file and directory permissions</a>.' <br>
            2. Install the 'No Boss Library' library using url <a href='{$urlLibrary}' target='_blank'>{$urlLibrary}</a>.<br><br>
            If the problem persists, contact the support team. <br><br>";

            // Exibe mensagem como warning (fundo amarelo)
            Factory::getApplication()->enqueueMessage($message, 'Warning');

            return false;
        }

        return true;
    }

    /**
     * Registra mensagem de diagnóstico no Joomla e no log do PHP.
     *
     * @param string $type Tipo da mensagem no Joomla (error, warning, notice, message).
     * @param string $stepName Nome da funcao / etapa de update.
     * @param string $message Texto principal da mensagem de diagnóstico.
     * @param string $extensionName Nome da extensão (ex: 'mod_nobosscalendar').
     * @param \Throwable|null $exception Exceção opcional para enriquecer o contexto.
     * @param string|null $dumpFileName Caminho completo do arquivo de dump, se existir.
     * @param bool $displayOnUi Define se deve exibir a mensagem também na interface do Joomla.
     *
     * @return void
     */
    public static function registerDiagnostic($type, $stepName, $message, $extensionName = '', $exception = null, $dumpFileName = null, $displayOnUi = true){
        $extensionName = strtolower((string) $extensionName);
        $message = "[{$stepName}] {$message}";
    
        // Mensagem completa com todos os detalhes técnicos para o log
        $timestamp = date('Y-m-d H:i:s');
        $fullMessage = "[{$timestamp}] [{$extensionName}] [{$type}] {$message}";

        // Mensagem simplificada para exibição na UI do Joomla
        $uiMessage = $message;

        // Adiciona detalhes da exceção às mensagens
        if ($exception instanceof \Throwable) {
            $exceptionDetails = ' | Exception: ' . get_class($exception)
                . ' | Message: ' . $exception->getMessage()
                . ' | File: ' . $exception->getFile() . ':' . $exception->getLine();
            
            // Detalhes completos vão para o log
            $fullMessage .= $exceptionDetails;
            
            // Na UI, apenas indica que há mais detalhes no log (com quebra de linha)
            if ($type === 'error') {
                $uiMessage .= '<br><br><strong>Error:</strong> ' . $exception->getMessage();
            }
        }

        // Adiciona informação sobre o dump quando aplicável
        if (($type === 'error') && !empty($dumpFileName)) {
            // Extract just the filename from the full path
            $dumpFileBasename = basename($dumpFileName);
            
            // Build restore URL
            $siteUrl = rtrim(Uri::root(), '/');
            $restoreUrl = $siteUrl . '/index.php?option=com_nobossajax&library=noboss.src.Util.NbInstallScriptUtil&method=restoreBackupAjax&extensionName=' . urlencode($extensionName) . '&dumpFileName=' . urlencode($dumpFileBasename) . '&format=raw';
            
            $dumpInfo = ' If necessary, restore the affected database tables using the dump file: ' . $dumpFileName;
            $fullMessage .= $dumpInfo;
            $uiMessage .= '<br><br><strong>Backup:</strong> ' . $dumpInfo;
            $uiMessage .= '<br><strong>Restore Backup:</strong> Click here to <a href="' . $restoreUrl . '" target="_blank" style="color:#4ec9b0;font-weight:bold;">restore from this backup file</a>';
        }

        // Define caminho do arquivo de log específico da extensão
        $logFile = JPATH_ADMINISTRATOR . "/logs/{$extensionName}_update.php";

        // Adiciona referência ao log para erros e warnings (apenas na UI)
        if (($type === 'error' || $type === 'warning') && $displayOnUi) {
            $uiMessage .= "<br><br><strong>Detailed Info:</strong> For detailed technical information, check the log file: <code>{$logFile}</code>";
            
            // URL para re-executar o update manualmente (somente quando nome da extensão existir)
            if (!empty($extensionName)) {
                $siteUrl = rtrim(Uri::root(), '/');
                $manualUpdateUrl = $siteUrl . '/index.php?option=com_nobossajax&library=noboss.src.Util.NbInstallScriptUtil&method=runUpdateManually&extensionName=' . urlencode($extensionName) . '&format=raw';
                $uiMessage .= "<br><br><strong>Manual Update:</strong> Database update scripts can be re-executed directly at: <a href='{$manualUpdateUrl}' target='_blank'>{$manualUpdateUrl}</a>";
            }
            
            // Link para suporte técnico
            $supportUrl = 'https://www.nobossextensions.com/customer-area/request-technical-support';
            $uiMessage .= "<br><br><strong>Technical Support:</strong> If the problem persists, please contact our technical support team providing a screenshot of this error and the details from the log file: <a href='{$supportUrl}' target='_blank'>{$supportUrl}</a>";
        }

        // Exibir mensagem na tela do Joomla
        if ($displayOnUi) {
            try {
                Factory::getApplication()->enqueueMessage($uiMessage, $type);
            } catch (\Throwable $ignored) {
                // Evita interromper o fluxo caso não seja possível exibir mensagem na UI.
            }
        }

        // Cria o arquivo de log com header de segurança do Joomla se não existir
        if (!file_exists($logFile)) {
            $header = "#<?php die('Forbidden.'); ?>\n";
            $header .= "#Date: " . date('Y-m-d H:i:s') . "\n";
            $header .= "#Software: {$extensionName} Update Log\n\n";
            @file_put_contents($logFile, $header);
            @chmod($logFile, 0644);
        }

        // Registra mensagem detalhada no arquivo de log específico
        error_log($fullMessage . PHP_EOL, 3, $logFile);
    }

    /**
     * Executa uma etapa do update e registra erro, quando ocorrer.
     *
     * @param string $stepName Nome da etapa crítica executada.
     * @param callable $callback Callback que executa a etapa.
     * @param string $extensionName Nome da extensão (ex: 'mod_nobosscalendar').
     * @param string|null $dumpFileName Caminho completo do arquivo de dump, se existir.
     *
     * @return void
     */
    public static function runUpdateStep($stepName, $callback, $extensionName = '', $dumpFileName = null){
        try {
            // Executa a funcao recebida como parametro e captura o retorno
            $result = $callback();
            
            // Se a função retornou uma mensagem (string), processa ela
            if (is_string($result) && !empty(trim($result))) {
                // Verifica se o retorno indica erro (texto contém "error" ou "erro")
                if (stripos($result, 'error') !== false || stripos($result, 'erro') !== false) {
                    // Registra como erro no log de diagnóstico
                    self::registerDiagnostic('error', $stepName, $result, $extensionName, null, $dumpFileName);
                } else {
                    // Registra como notice (sucesso ou informação)
                    self::registerDiagnostic('notice', $stepName, $result, $extensionName, null, $dumpFileName, false);
                }
            }
        } catch (\Throwable $e) {
            self::registerDiagnostic('error', $stepName, 'Failure while executing update step.', $extensionName, $e, $dumpFileName);
        }
    }

    /**
     * Grava SQL em disco e aplica retenção de dumps antigos.
     *
     * @return string Mensagem de sucesso ou erro
     */
    private static function persistDumpString(string $dumpString, string $outPath, string $fileName, ?int $daysToKeepDumps): string
    {
        if (!is_dir($outPath)) {
            mkdir($outPath, 0775, true);
        }

        chmod($outPath, 0775);

        if (!is_writable($outPath)) {
            return "Error: The directory '{$outPath}' does not have write permission.";
        }

        if (empty(trim($dumpString))) {
            return "Error: The dump generated is empty. No records found in tables.";
        }

        if (!(file_put_contents($outPath . $fileName, $dumpString))) {
            return "Error: Could not generate the dump file using PHP function 'file_put_contents'.";
        }

        chmod($outPath . $fileName, 0775);

        if ($daysToKeepDumps !== null && is_int($daysToKeepDumps) && $daysToKeepDumps > 0) {
            $currentTime = time();
            $maxAge = $daysToKeepDumps * 24 * 60 * 60;
            $dumpFiles = glob($outPath . '*.sql');

            if (!empty($dumpFiles)) {
                foreach ($dumpFiles as $dumpFile) {
                    if ($dumpFile === $outPath . $fileName) {
                        continue;
                    }

                    $fileAge = $currentTime - filemtime($dumpFile);

                    if ($fileAge > $maxAge) {
                        @unlink($dumpFile);
                    }
                }
            }
        }

        return "Dump generated successfully at {$outPath}{$fileName}";
    }

    /**
     * Informative SQL footer: the site module list is cached as files (com_modules group).
     * There is no portable SQL to purge it; operators should follow these steps after importing the dump.
     */
    private static function appendModuleListCacheHintSql(string $dumpString): string
    {
        return $dumpString . "\n\n"
            . "-- -----------------------------------------------------------------------------\n"
            . "-- Post-import — site module list cache (group `com_modules`, files under cache/):\n"
            . "-- no reliable SQL statement clears this cache. After running this script:\n"
            . "--   • Administrator → System → Clear Cache (include compiled cache if shown); or\n"
            . "--   • On the server: delete the contents of the `cache/com_modules/` folder at the Joomla root.\n"
            . "-- Module rows inserted earlier use placeholder asset lft/rgt (0). If asset-tree or ACL warnings appear:\n"
            . "--   Administrator → System → Maintenance → Rebuild Web Assets (or equivalent in your Joomla version).\n";
    }

    /**
     * Lê title/rules do asset do módulo na origem (geração do dump).
     *
     * @return array{0: string, 1: string} [title, rules json]
     */
    private static function getModuleAssetTitleRulesForDump(
        DatabaseInterface $db,
        string $prefix,
        int $moduleId,
        string $fallbackTitle
    ): array {
        $assetsTable = $prefix . 'assets';
        $name = 'com_modules.module.' . $moduleId;
        $q = $db->getQuery(true)
            ->select($db->quoteName(['title', 'rules']))
            ->from($db->quoteName($assetsTable))
            ->where($db->quoteName('name') . ' = ' . $db->quote($name));
        $db->setQuery($q);
        $assetRow = $db->loadAssoc();
        if ($assetRow === null) {
            return [$fallbackTitle !== '' ? $fallbackTitle : 'Module', '{}'];
        }
        $title = (string) ($assetRow['title'] ?? '');
        if ($title === '') {
            $title = $fallbackTitle !== '' ? $fallbackTitle : 'Module';
        }
        $rules = (string) ($assetRow['rules'] ?? '');
        if ($rules === '') {
            $rules = '{}';
        }

        return [$title, $rules];
    }

    /**
     * SQL: INSERT em #__assets (filho de com_modules) + UPDATE em #__modules.asset_id.
     * lft/rgt = 0 placeholder (rebuild de assets no admin recomendado no destino).
     *
     * @param bool $moduleIdIsUserVariable true: nome do asset e UPDATE usam @nb_mod_{$dumpKeyId}; false: id literal
     */
    private static function buildSqlInsertModuleAssetAndLinkModule(
        DatabaseInterface $db,
        string $prefix,
        int $dumpKeyId,
        bool $moduleIdIsUserVariable,
        string $title,
        string $rules
    ): string {
        $assetsTable = $prefix . 'assets';
        $modulesTable = $prefix . 'modules';
        $parentResolve = 'SELECT ' . $db->quoteName('id') . ' INTO @nb_parent_com_modules FROM ' . $db->quoteName($assetsTable)
            . ' WHERE ' . $db->quoteName('name') . ' = ' . $db->quote('com_modules')
            . ' ORDER BY ' . $db->quoteName('id') . ' ASC LIMIT 1';
        $nameExpr = $moduleIdIsUserVariable
            ? ('CONCAT(' . $db->quote('com_modules.module.') . ', @nb_mod_' . $dumpKeyId . ')')
            : $db->quote('com_modules.module.' . $dumpKeyId);
        $modRef = $moduleIdIsUserVariable ? ('@nb_mod_' . $dumpKeyId) : (string) (int) $dumpKeyId;

        $sql = $parentResolve . ";\n" . 'INSERT INTO ' . $db->quoteName($assetsTable) . ' ('
            . implode(', ', array_map([$db, 'quoteName'], ['parent_id', 'lft', 'rgt', 'level', 'name', 'title', 'rules']))
            . ') VALUES (@nb_parent_com_modules, 0, 0, 2, '
            . $nameExpr . ', '
            . self::quoteSqlDumpScalar($db, $title) . ', '
            . self::quoteSqlDumpScalar($db, $rules !== '' ? $rules : '{}')
            . ");\n";
        $sql .= 'SELECT LAST_INSERT_ID() INTO @nb_ast_' . $dumpKeyId . ";\n";
        $sql .= 'UPDATE ' . $db->quoteName($modulesTable)
            . ' SET ' . $db->quoteName('asset_id') . ' = @nb_ast_' . $dumpKeyId
            . ' WHERE ' . $db->quoteName('id') . ' = ' . $modRef . ";\n";

        return $sql;
    }

    /**
     * Detecta valores de data/hora legados rejeitados em sql_mode estrito (NO_ZERO_DATE / NO_ZERO_IN_DATE).
     */
    private static function shouldExportSqlScalarAsNull($value): bool
    {
        if (!is_string($value)) {
            return false;
        }

        $v = trim($value);

        if ($v === '0000-00-00' || $v === '0000-00-00 00:00:00') {
            return true;
        }

        return preg_match('/^0000-00-00(\s+\d{1,2}:\d{1,2}:\d{1,2}(\.\d+)?)?$/', $v) === 1;
    }

    /**
     * Escapa valor para INSERT no dump SQL (NULL, zero-dates → NULL, demais quoted).
     *
     * @param DatabaseInterface $db
     */
    private static function quoteSqlDumpScalar($db, $value): string
    {
        if ($value === null || self::shouldExportSqlScalarAsNull($value)) {
            return 'NULL';
        }

        return $db->quote($value);
    }

    /**
     * Alguma coluna FK para #__modules aponta para id > 0 que não está no conjunto exportado.
     *
     * @param array<string, mixed>   $row
     * @param array<int, string>     $moduleFkColumns
     * @param array<int, true>       $dumpedModuleIds
     */
    private static function rowReferencesUnknownDumpedModuleId(array $row, array $moduleFkColumns, array $dumpedModuleIds): bool
    {
        foreach ($moduleFkColumns as $col) {
            if (!array_key_exists($col, $row)) {
                continue;
            }
            $v = $row[$col];
            if ($v === null || $v === '') {
                continue;
            }
            $i = (int) $v;
            if ($i > 0 && !isset($dumpedModuleIds[$i])) {
                return true;
            }
        }

        return false;
    }

    /**
     * DELETE multi-tabela (MySQL/MariaDB): linhas em #__modules (alias nbm) e, opcionalmente,
     * #__modules_menu e #__assets ligadas a esses módulos.
     * Em #__assets, o registro do módulo usa `name` = CONCAT('com_modules.module.', nbm.id).
     *
     * @param string $moduleWhereExpr Condição já qualificada com o alias `nbm` (ex.: `nbm`.`module` = 'mod_x').
     */
    private static function buildModulesRelatedMultiDeleteSql(
        DatabaseInterface $db,
        string $prefix,
        string $moduleWhereExpr,
        bool $joinModulesMenu,
        bool $joinAssets
    ): string {
        $nbm = 'nbm';
        $nbmm = 'nbmm';
        $nba = 'nba';
        $tMod = $db->quoteName($prefix . 'modules');

        $deleteTargets = [$db->quoteName($nbm)];
        if ($joinModulesMenu) {
            $deleteTargets[] = $db->quoteName($nbmm);
        }
        if ($joinAssets) {
            $deleteTargets[] = $db->quoteName($nba);
        }

        $sql = 'DELETE ' . implode(', ', $deleteTargets)
            . ' FROM ' . $tMod . ' AS ' . $db->quoteName($nbm);

        if ($joinModulesMenu) {
            $tMm = $db->quoteName($prefix . 'modules_menu');
            $sql .= ' LEFT JOIN ' . $tMm . ' AS ' . $db->quoteName($nbmm)
                . ' ON ' . $db->quoteName($nbmm) . '.' . $db->quoteName('moduleid')
                . ' = ' . $db->quoteName($nbm) . '.' . $db->quoteName('id');
        }

        if ($joinAssets) {
            $tAssets = $db->quoteName($prefix . 'assets');
            $sql .= ' LEFT JOIN ' . $tAssets . ' AS ' . $db->quoteName($nba)
                . ' ON ' . $db->quoteName($nba) . '.' . $db->quoteName('name')
                . ' = CONCAT(' . $db->quote('com_modules.module.') . ', '
                . $db->quoteName($nbm) . '.' . $db->quoteName('id') . ')';
        }

        return $sql . ' WHERE ' . $moduleWhereExpr;
    }

    /**
     * DELETE único por module='mod_xxx': opcionalmente inclui #__modules_menu e #__assets (mesmo comando).
     * $where deve ser no formato module='mod_xxx'.
     *
     * @return string|null SQL ou null se $where não for só module='…' ou se nenhum join extra for solicitado
     */
    private static function buildModulesAndMenuMultiDeleteSql(
        DatabaseInterface $db,
        string $prefix,
        string $where,
        bool $joinModulesMenu,
        bool $joinAssets
    ): ?string {
        if (!$joinModulesMenu && !$joinAssets) {
            return null;
        }

        if (!preg_match("/^\s*module\s*=\s*'([^']+)'\s*$/i", trim($where), $m)) {
            return null;
        }

        $element = $m[1];
        $nbm = 'nbm';
        $qualWhere = $db->quoteName($nbm) . '.' . $db->quoteName('module') . ' = ' . $db->quote($element);

        return self::buildModulesRelatedMultiDeleteSql($db, $prefix, $qualWhere, $joinModulesMenu, $joinAssets);
    }

    /**
     * DELETE único por id + element: opcionalmente inclui #__modules_menu e #__assets (dump de instância única).
     */
    private static function buildModulesAndMenuMultiDeleteSqlByModuleId(
        DatabaseInterface $db,
        string $prefix,
        int $moduleId,
        string $moduleName,
        bool $joinModulesMenu,
        bool $joinAssets
    ): string {
        $nbm = 'nbm';
        $qualWhere = $db->quoteName($nbm) . '.' . $db->quoteName('id') . ' = ' . $moduleId
            . ' AND ' . $db->quoteName($nbm) . '.' . $db->quoteName('module') . ' = ' . $db->quote($moduleName);

        return self::buildModulesRelatedMultiDeleteSql($db, $prefix, $qualWhere, $joinModulesMenu, $joinAssets);
    }

    /**
     * Funcao generica para gerar dump de dados de uma extensao usando PHP puro
     * 
     * Esta funcao eh generica e pode ser usada por qualquer extensao No Boss.
     * Gera um arquivo SQL com INSERTs dos dados das tabelas especificadas.
     * 
     * @param array<int, array<string, mixed>> $tables Array de tabelas e condicoes. Entradas opcionais:
     *        - module_fk_columns: lista de colunas cujo valor referencia #__modules.id (usado com omit_modules_insert_id).
     *        - modules_menu_all_pages (entrada name=modules): DELETE multi-tabela #__modules + opcionalmente #__modules_menu e #__assets num comando; após cada INSERT de módulo (omit id) gera INSERT em modules_menu (moduleid=@nb_mod_*, menuid=0), INSERT em #__assets ligado ao novo id e UPDATE modules.asset_id.
     * @param string $outPath Caminho para salvar o dump
     * @param string $fileName Nome do arquivo de dump
     * @param int|null $daysToKeepDumps Numero de dias para manter dumps antigos. Se fornecido, remove dumps mais antigos que esse prazo (opcional)
     * @param array<string, mixed> $options omit_modules_insert_id: true para não incluir coluna id em INSERTs da tabela modules e usar @nb_mod_{id} nas colunas declaradas em module_fk_columns.
     *        include_deletes_and_drop: false para omitir todos os DELETE e DROP TABLE (estilo append); padrão true.
     *
     * @return string Mensagem de sucesso ou erro
     */
    public static function dumpDataGeneric($tables, $outPath, $fileName, $daysToKeepDumps = null, array $options = []){
        try {
            $db = Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
            $config = Factory::getApplication()->getConfig();
            $prefix = $config->get('dbprefix');

            $omitModulesInsertId = !empty($options['omit_modules_insert_id']);
            $includeDeletesAndDrop = array_key_exists('include_deletes_and_drop', $options)
                ? (bool) $options['include_deletes_and_drop']
                : true;
            $dumpedModuleIds = [];

            $dumpString = '';

            if (!$includeDeletesAndDrop) {
                $dumpString .= "-- Option: DELETE and DROP TABLE omitted (append-only).\n";
            }

            if ($omitModulesInsertId) {
                $dumpString .= "-- Full dump: #__modules INSERT omits `id` (AUTO_INCREMENT).\n";
                $dumpString .= "-- After each module row: SELECT LAST_INSERT_ID() INTO @nb_mod_{oldId} (MySQL/MariaDB, one connection, sequential execution).\n";
                $dumpString .= "-- Related rows: module FK columns use @nb_mod_*; rows referencing module ids not included in this dump are skipped.\n";
                $dumpString .= "-- #__modules + #__modules_menu + #__assets: one DELETE removes menu rows, module asset (name com_modules.module.{id}), and modules;\n";
                $dumpString .= "-- then each module gets modules_menu (menuid=0), new #__assets row (parent com_modules), and UPDATE modules.asset_id.\n";
            } else {
                $dumpString .= "-- Full dump: #__modules INSERT includes explicit `id` (preserve IDs); FK columns keep literal module id values.\n";
                $dumpString .= "-- modules_menu + assets: one row/menuid=0 per module; asset recreated + modules.asset_id updated when `assets` exists.\n";
            }

            $dumpString .= "-- Zero dates (0000-00-00 / 0000-00-00 00:00:00) are exported as NULL for strict sql_mode on target (e.g. NO_ZERO_DATE).\n";

            foreach ($tables as $seedInfo) {
                if ((string) ($seedInfo['name'] ?? '') !== 'modules') {
                    continue;
                }
                $seedTable = $prefix . 'modules';
                $db->setQuery("SHOW TABLES LIKE " . $db->quote($seedTable));
                if (!$db->loadResult()) {
                    break;
                }
                $seedWhere = $seedInfo['where'] ?? null;
                $q = $db->getQuery(true)->select('*')->from($db->quoteName($seedTable));
                if ($seedWhere) {
                    $q->where($seedWhere);
                }
                $db->setQuery($q);
                $seedRows = $db->loadAssocList() ?: [];
                foreach ($seedRows as $seedRow) {
                    $mid = (int) ($seedRow['id'] ?? 0);
                    if ($mid > 0) {
                        $dumpedModuleIds[$mid] = true;
                    }
                }
                break;
            }

            $hasModulesMenuTable = null;
            $hasAssetsTable = null;

            foreach ($tables as $tableInfo) {
                $logicalName = (string) ($tableInfo['name'] ?? '');
                $tableName = $prefix . $logicalName;
                $where = $tableInfo['where'] ?? null;
                $moduleFkColumns = $tableInfo['module_fk_columns'] ?? [];
                if (!is_array($moduleFkColumns)) {
                    $moduleFkColumns = [];
                }

                // Verifica se tabela existe
                $query = "SHOW TABLES LIKE " . $db->quote($tableName);
                $db->setQuery($query);
                if (!$db->loadResult()) {
                    continue; // Tabela não existe, pula
                }

                if ($logicalName === 'modules') {
                    if ($hasModulesMenuTable === null) {
                        $db->setQuery('SHOW TABLES LIKE ' . $db->quote($prefix . 'modules_menu'));
                        $hasModulesMenuTable = (bool) $db->loadResult();
                    }
                    if ($hasAssetsTable === null) {
                        $db->setQuery('SHOW TABLES LIKE ' . $db->quote($prefix . 'assets'));
                        $hasAssetsTable = (bool) $db->loadResult();
                    }
                }

                // Busca todos os registros da tabela
                $query = $db->getQuery(true);
                $query->select('*')->from($db->quoteName($tableName));
                if ($where) {
                    $query->where($where);
                }
                $db->setQuery($query);
                $rows = $db->loadAssocList();

                if (empty($rows)) {
                    continue; // Tabela vazia, pula
                }

                // DROP TABLE e CREATE TABLE (tabelas exclusivas da extensao)
                if ($includeDeletesAndDrop && !empty($tableInfo['drop_create'])) {
                    $dumpString .= "DROP TABLE IF EXISTS " . $db->quoteName($tableName) . ";\n";

                    // Obtem estrutura da tabela
                    $query = "SHOW CREATE TABLE " . $db->quoteName($tableName);
                    $db->setQuery($query);
                    $createTable = $db->loadAssoc();
                    if (!empty($createTable['Create Table'])) {
                        $dumpString .= $createTable['Create Table'] . ";\n";
                    }
                }

                // DELETE (tabelas compartilhadas com outras extensoes)
                if ($includeDeletesAndDrop && !empty($tableInfo['delete']) && $where) {
                    $modulesMenuAllPages = !empty($tableInfo['modules_menu_all_pages']) && $logicalName === 'modules';
                    if ($modulesMenuAllPages && ($hasModulesMenuTable || $hasAssetsTable)) {
                        $multiDel = self::buildModulesAndMenuMultiDeleteSql(
                            $db,
                            $prefix,
                            $where,
                            (bool) $hasModulesMenuTable,
                            (bool) $hasAssetsTable
                        );
                        $dumpString .= ($multiDel !== null ? $multiDel : 'DELETE FROM ' . $db->quoteName($tableName) . " WHERE {$where}") . ";\n";
                    } else {
                        $dumpString .= 'DELETE FROM ' . $db->quoteName($tableName) . " WHERE {$where};\n";
                    }
                }

                // Gera INSERT para cada registro
                foreach ($rows as $row) {
                    if (
                        $logicalName !== 'modules'
                        && $moduleFkColumns !== []
                        && self::rowReferencesUnknownDumpedModuleId($row, $moduleFkColumns, $dumpedModuleIds)
                    ) {
                        continue;
                    }

                    $modulePkForFooter = ($logicalName === 'modules' && isset($row['id']))
                        ? (int) $row['id']
                        : 0;

                    $oldModuleIdForSet = 0;

                    if ($omitModulesInsertId && $logicalName === 'modules' && array_key_exists('id', $row)) {
                        $oldModuleIdForSet = (int) $row['id'];
                        if ($oldModuleIdForSet > 0) {
                            unset($row['id']);
                        } else {
                            $oldModuleIdForSet = 0;
                        }
                    }

                    if (
                        $logicalName === 'modules'
                        && !empty($tableInfo['modules_menu_all_pages'])
                        && $hasAssetsTable
                        && $modulePkForFooter > 0
                    ) {
                        unset($row['asset_id']);
                    }

                    $columns = array_keys($row);
                    $escapedValues = [];

                    foreach ($columns as $columnName) {
                        $value = $row[$columnName];

                        if (
                            $omitModulesInsertId
                            && $logicalName !== 'modules'
                            && $moduleFkColumns !== []
                            && in_array($columnName, $moduleFkColumns, true)
                            && $value !== null && $value !== ''
                            && (int) $value > 0
                        ) {
                            $escapedValues[] = '@nb_mod_' . (int) $value;
                        } else {
                            $escapedValues[] = self::quoteSqlDumpScalar($db, $value);
                        }
                    }

                    $dumpString .= 'INSERT INTO ' . $db->quoteName($tableName)
                        . ' (' . implode(', ', array_map([$db, 'quoteName'], $columns)) . ') '
                        . 'VALUES (' . implode(', ', $escapedValues) . ");\n";

                    if ($logicalName === 'modules' && !empty($tableInfo['modules_menu_all_pages'])) {
                        if ($omitModulesInsertId && $oldModuleIdForSet > 0) {
                            $dumpString .= 'SELECT LAST_INSERT_ID() INTO @nb_mod_' . $oldModuleIdForSet . ";\n";
                            if ($hasModulesMenuTable) {
                                $mmTable = $prefix . 'modules_menu';
                                $dumpString .= 'INSERT INTO ' . $db->quoteName($mmTable)
                                    . ' (' . $db->quoteName('moduleid') . ', ' . $db->quoteName('menuid') . ') '
                                    . 'VALUES (@nb_mod_' . $oldModuleIdForSet . ', 0);' . "\n";
                            }
                            if ($hasAssetsTable) {
                                [$astTitle, $astRules] = self::getModuleAssetTitleRulesForDump(
                                    $db,
                                    $prefix,
                                    $oldModuleIdForSet,
                                    (string) ($row['title'] ?? '')
                                );
                                $dumpString .= self::buildSqlInsertModuleAssetAndLinkModule(
                                    $db,
                                    $prefix,
                                    $oldModuleIdForSet,
                                    true,
                                    $astTitle,
                                    $astRules
                                );
                            }
                        } elseif (!$omitModulesInsertId && $modulePkForFooter > 0) {
                            if ($hasModulesMenuTable) {
                                $mmTable = $prefix . 'modules_menu';
                                $dumpString .= 'INSERT INTO ' . $db->quoteName($mmTable)
                                    . ' (' . $db->quoteName('moduleid') . ', ' . $db->quoteName('menuid') . ') '
                                    . 'VALUES (' . $modulePkForFooter . ', 0);' . "\n";
                            }
                            if ($hasAssetsTable) {
                                [$astTitle, $astRules] = self::getModuleAssetTitleRulesForDump(
                                    $db,
                                    $prefix,
                                    $modulePkForFooter,
                                    (string) ($row['title'] ?? '')
                                );
                                $dumpString .= self::buildSqlInsertModuleAssetAndLinkModule(
                                    $db,
                                    $prefix,
                                    $modulePkForFooter,
                                    false,
                                    $astTitle,
                                    $astRules
                                );
                            }
                        }
                    }
                }
            }

            foreach ($tables as $specEntry) {
                if ((string) ($specEntry['name'] ?? '') === 'modules') {
                    $dumpString = self::appendModuleListCacheHintSql($dumpString);
                    break;
                }
            }

            return self::persistDumpString($dumpString, $outPath, $fileName, $daysToKeepDumps);

        } catch(\Exception $e){
            return "Error generating dump: " . $e->getMessage();
        }
    }

    /**
     * Verifica se o usuário está autenticado como administrador do Joomla.
     * 
     * Esta função é uma validação de segurança genérica que pode ser usada por qualquer
     * função AJAX que requeira permissões administrativas.
     * 
    * REQUISITOS DE SEGURANÇA:
    * - Usuário deve estar autenticado (não pode ser guest)
    * - Usuário deve ter permissão 'core.admin' OU pertencer ao grupo 8 (Super Users)
    * - Quando executado via frontend, a autenticação deve existir no frontend OU o site
    *   deve estar configurado para compartilhar sessão entre frontend e administrator
     * 
     * Se a autenticação falhar, exibe uma mensagem de erro HTML e encerra a execução.
     * 
     * @return void (encerra execução com exit se não autenticado)
     */
    public static function checkAdminAuthentication() {
        header('Content-Type: text/html; charset=utf-8');
        
        $app = Factory::getApplication();
        $user = $app->getIdentity();

        // Verifica se o usuário está autenticado
        if ($user->guest) {
            $returnUrl = base64_encode(Uri::getInstance()->toString());
            $frontendLoginUrl = Uri::root() . 'index.php?option=com_users&view=login&return=' . urlencode($returnUrl);

            echo '<html><head><meta charset="utf-8"><title>Access Denied</title></head><body>';
            echo '<style>
                body{font-family:"Segoe UI",Tahoma,Geneva,Verdana,sans-serif;background:#1e1e1e;color:#d4d4d4;padding:20px;text-align:center;}
                .error-box{background:#2d2d2d;border-radius:8px;padding:40px;margin:50px auto;max-width:500px;border:2px solid #f48771;}
                h2{color:#f48771;margin-top:0;}
                p{color:#d4d4d4;line-height:1.6;}
                .icon{font-size:64px;margin-bottom:20px;}
                .btn-login{display:inline-block;background:#4ec9b0;color:#1e1e1e;text-decoration:none;font-weight:bold;padding:12px 20px;border-radius:4px;margin-top:14px;}
                .btn-login:hover{background:#6eddc8;}
                .note{margin-top:20px;background:#252525;border-left:4px solid #dcdcaa;border-radius:4px;padding:14px;text-align:left;}
            </style>';
            echo '<div class="error-box">';
            echo '<div class="icon">🔒</div>';
            echo '<h2>Access Denied</h2>';
            echo '<p><strong>Authentication Required</strong></p>';
            echo '<p>You must be logged in as an administrator to access this function.</p>';
            echo '<a class="btn-login" href="' . htmlspecialchars($frontendLoginUrl) . '">Login no Front-end do Joomla</a>';
            echo '<div class="note">';
            echo '<strong>Important:</strong><br>';
            echo 'This AJAX endpoint runs via frontend (index.php). You must be authenticated in frontend, or your site must be configured to share session between administrator and frontend.';
            echo '</div>';
            echo '</div>';
            echo '</body></html>';
            exit;
        }
        
        // Verifica se o usuário tem permissões administrativas (Super User ou grupos de admin)
        $isAdmin = $user->authorise('core.admin') || in_array(8, $user->groups); // 8 = Super Users
        if (!$isAdmin) {
            echo '<html><head><meta charset="utf-8"><title>Access Denied</title></head><body>';
            echo '<style>
                body{font-family:"Segoe UI",Tahoma,Geneva,Verdana,sans-serif;background:#1e1e1e;color:#d4d4d4;padding:20px;text-align:center;}
                .error-box{background:#2d2d2d;border-radius:8px;padding:40px;margin:50px auto;max-width:500px;border:2px solid #f48771;}
                h2{color:#f48771;margin-top:0;}
                p{color:#d4d4d4;line-height:1.6;}
                .icon{font-size:64px;margin-bottom:20px;}
            </style>';
            echo '<div class="error-box">';
            echo '<div class="icon">🔒</div>';
            echo '<h2>Insufficient Permissions</h2>';
            echo '<p><strong>Administrator Access Required</strong></p>';
            echo '<p>You do not have sufficient permissions to access this function.</p>';
            echo '<p>Only Super Users or Administrators can access this resource.</p>';
            echo '</div>';
            echo '</body></html>';
            exit;
        }
    }

    /**
     * Executa manualmente o método de update da extensão via AJAX.
     *
     * Esta função é genérica e usa o parâmetro extensionName para localizar
     * a extensão no banco e carregar seu script de instalação.
     *
     * Parâmetros da URL:
     * - extensionName (obrigatório): Alias da extensão (ex: mod_nobosscalendar)
     *
     * URL para executar:
     * WEBSITE/index.php?option=com_nobossajax&library=noboss.src.Util.NbInstallScriptUtil&method=runUpdateManually&extensionName=mod_nobosscalendar&format=raw
     *
     * @return void (exibe resultado diretamente na tela)
     */
    public static function runUpdateManually() {
        self::checkAdminAuthentication();

        header('Content-Type: text/html; charset=utf-8');
        header('Access-Control-Allow-Origin: *');

        $input = Factory::getApplication()->input;
        $extensionName = strtolower($input->getString('extensionName', ''));
        

        if (empty($extensionName)) {
            echo "<html><head><meta charset='utf-8'><title>Manual Update Execution</title></head><body>";
            echo "<h2 style='color:#f48771;'>Error: Missing Required Parameter</h2>";
            echo "<p>The parameter <code>extensionName</code> is required.</p>";
            echo "<p>Example: <code>...&extensionName=mod_nobosscalendar</code></p>";
            echo "</body></html>";
            exit;
        }

        // Quando for um componente (com_*), resolve para o módulo de mesmo nome (mod_*)
        if (strpos($extensionName, 'com_') === 0) {
            $moduleName = 'mod_' . substr($extensionName, 4);
        } elseif (strpos($extensionName, 'mod_') === 0) {
            $moduleName = $extensionName;
        } else {
            echo "<html><head><meta charset='utf-8'><title>Manual Update Execution</title></head><body>";
            echo "<h2 style='color:#f48771;'>Unsupported Extension Type</h2>";
            echo "<p>This generic manual update currently supports module (<code>mod_*</code>) and component (<code>com_*</code>) extensions.</p>";
            echo "<p>Received: <code>" . htmlspecialchars($extensionName) . "</code></p>";
            echo "</body></html>";
            exit;
        }

        $classBase = preg_replace('/^mod_/', '', $moduleName);
        $classBase = str_replace(' ', '', ucwords(str_replace('_', ' ', $classBase)));
        $installerClass = "\\mod_{$classBase}InstallerScript";
        $scriptPath = JPATH_ROOT . '/modules/' . $moduleName . '/script.extension.php';

        $confirmed = $input->getString('confirmed', '') === '1';

        $commonStyles = "<style>
            body{font-family:monospace;background:#1e1e1e;color:#d4d4d4;padding:20px;line-height:1.6;}
            .success{color:#4ec9b0;}.error{color:#f48771;}.warning{color:#dcdcaa;}.info{color:#569cd6;}
            pre{background:#2d2d2d;padding:15px;border-radius:5px;overflow-x:auto;}
            hr{border-color:#444;}
            .confirm-box{background:#2d2d2d;border:2px solid #dcdcaa;border-radius:8px;padding:30px;margin:30px auto;max-width:600px;}
            .confirm-box h2{color:#dcdcaa;margin-top:0;}
            .btn{display:inline-block;font-weight:bold;padding:10px 24px;border-radius:4px;text-decoration:none;cursor:pointer;font-family:monospace;font-size:14px;border:none;}
            .btn-confirm{background:#f48771;color:#1e1e1e;margin-right:12px;}
            .btn-confirm:hover{background:#ff6a50;}
            .btn-cancel{background:#444;color:#d4d4d4;}
            .btn-cancel:hover{background:#555;}
        </style>";

        if (!$confirmed) {
            $currentUrl = Uri::getInstance()->toString();
            $confirmUrl = htmlspecialchars(rtrim(Uri::root(), '/') . '/index.php?option=com_nobossajax&library=noboss.src.Util.NbInstallScriptUtil&method=runUpdateManually&extensionName=' . urlencode($extensionName) . '&format=raw&confirmed=1');

            echo "<html><head><meta charset='utf-8'><title>Manual Update Execution</title>{$commonStyles}</head><body>";
            echo "<div class='confirm-box'>";
            echo "<h2>&#9888; Confirm Manual Update</h2>";
            echo "<p>You are about to manually execute the <strong>update()</strong> script for:</p>";
            echo "<p class='info'><strong>Extension:</strong> " . htmlspecialchars($extensionName) . "</p>";
            if ($moduleName !== $extensionName) {
                echo "<p class='warning'><strong>Resolved module:</strong> <code>" . htmlspecialchars($moduleName) . "</code></p>";
            }
            echo "<p class='warning'>This will re-run all database migration steps of the installation script. This action may modify database tables and data.</p>";
            echo "<p>Are you sure you want to proceed?</p>";
            echo "<hr>";
            echo "<a class='btn btn-confirm' href='{$confirmUrl}'>Yes, execute update</a>";
            echo "<a class='btn btn-cancel' href='javascript:history.back()'>Cancel</a>";
            echo "</div>";
            echo "</body></html>";
            exit;
        }

        echo "<html><head><meta charset='utf-8'><title>Manual Update Execution</title>{$commonStyles}</head><body>";
        echo "<h2>Manual database update execution</h2>";
        echo "<p class='info'>Extension: " . htmlspecialchars($extensionName) . "</p>";
        echo "<hr>";

        try {
            ob_start();

            $db = Factory::getContainer()->get(DatabaseInterface::class);
            $query = $db->getQuery(true)
                ->select('*')
                ->from('#__extensions')
                ->where("element = " . $db->quote($moduleName))
                ->where("type = 'module'");
            $db->setQuery($query);
            $extension = $db->loadObject();

            if (!$extension) {
                throw new \Exception("Extension {$moduleName} not found in database");
            }

            $manifestCache = json_decode($extension->manifest_cache);
            $currentVersion = !empty($manifestCache->version) ? $manifestCache->version : 'unknown';

            echo "<p class='info'>Extension found: " . htmlspecialchars($extension->name) . "</p>";
            echo "<p class='info'>Current version: " . htmlspecialchars($currentVersion) . "</p>";
            echo "<hr>";

            $mockParent = new class($moduleName, $currentVersion) {
                public $manifest;

                public function __construct($moduleName, $version) {
                    $this->manifest = new \stdClass();
                    $this->manifest->name = $moduleName;
                    $this->manifest->version = $version;
                }

                public function get($key) {
                    if ($key === 'manifest') {
                        return $this->manifest;
                    }
                    return null;
                }

                public function getManifest() {
                    return $this->manifest;
                }
            };

            if (!file_exists($scriptPath)) {
                throw new \Exception("Installation script not found: {$scriptPath}");
            }

            require_once $scriptPath;

            if (!class_exists($installerClass)) {
                throw new \Exception("Installer class not found: {$installerClass}");
            }

            echo "<p class='warning'>Starting update() method execution...</p>";
            echo "<hr>";
            echo "<div style='background:#2d2d2d;padding:15px;border-radius:5px;'>";

            $installer = new $installerClass();

            $startTime = microtime(true);
            $result = $installer->update($mockParent);
            $endTime = microtime(true);
            $executionTime = round($endTime - $startTime, 2);

            echo "</div>";
            echo "<hr>";

            $app = Factory::getApplication();
            $messages = $app->getMessageQueue();

            $hasErrors = false;
            if (!empty($messages)) {
                foreach ($messages as $msg) {
                    if (strtolower($msg['type'] ?? '') === 'error') {
                        $hasErrors = true;
                        break;
                    }
                }
            }

            if ($result === false || $hasErrors) {
                echo "<p class='error'>The update() method completed with ERRORS</p>";
            } else {
                echo "<p class='success'>Update executed successfully!</p>";
            }

            echo "<p class='info'>Execution time: {$executionTime} seconds</p>";

            if (!empty($messages)) {
                echo "<hr><h3>Joomla System Messages:</h3>";
                foreach ($messages as $msg) {
                    $class = strtolower($msg['type'] ?? 'info');
                    echo "<p class='{$class}'>[" . htmlspecialchars($msg['type']) . "] " . $msg['message'] . "</p>";
                }
            }

            $output = ob_get_clean();
            echo $output;

        } catch (\Exception $e) {
            $output = ob_get_clean();
            echo $output;
            echo "<hr>";
            echo "<h3 class='error'>EXCEPTION CAUGHT:</h3>";
            echo "<pre class='error'>" . htmlspecialchars($e->getMessage()) . "</pre>";
            echo "<p class='error'><strong>File:</strong> " . htmlspecialchars($e->getFile()) . "</p>";
            echo "<p class='error'><strong>Line:</strong> " . $e->getLine() . "</p>";
            echo "<hr>";
            echo "<h4>Stack Trace:</h4>";
            echo "<pre class='error'>" . htmlspecialchars($e->getTraceAsString()) . "</pre>";
        } catch (\Throwable $e) {
            $output = ob_get_clean();
            echo $output;
            echo "<hr>";
            echo "<h3 class='error'>FATAL ERROR:</h3>";
            echo "<pre class='error'>" . htmlspecialchars($e->getMessage()) . "</pre>";
            echo "<p class='error'><strong>File:</strong> " . htmlspecialchars($e->getFile()) . "</p>";
            echo "<p class='error'><strong>Line:</strong> " . $e->getLine() . "</p>";
        }

        echo "<hr>";
        echo "<p><em>Execution finished at " . date('Y-m-d H:i:s') . "</em></p>";
        echo "</body></html>";
        exit;
    }

    /**
     * Valida se o id informado é uma instância do módulo esperado em #__modules.
     *
     * @return \stdClass|null Linha (id, title, module, …) ou null se inválido
     */
    private static function getValidatedModuleInstance(int $moduleId, string $moduleName): ?\stdClass
    {
        if ($moduleId <= 0 || $moduleName === '') {
            return null;
        }

        $db = Factory::getContainer()->get(DatabaseInterface::class);
        $query = $db->getQuery(true);
        $query->select($db->quoteName(['id', 'title', 'module', 'position', 'published']))
            ->from($db->quoteName('#__modules'))
            ->where($db->quoteName('id') . ' = ' . $moduleId)
            ->where($db->quoteName('module') . ' = ' . $db->quote($moduleName));
        $db->setQuery($query);

        $row = $db->loadObject();

        return $row ?: null;
    }

    /**
     * Carrega getDumpTablesSpec() do installer (para UI e filtro do dump).
     *
     * @return array<int, array<string, mixed>>|null
     */
    private static function loadInstallerDumpTablesSpec(string $scriptPath, string $installerClass): ?array
    {
        if (!is_file($scriptPath)) {
            return null;
        }

        if (!class_exists($installerClass)) {
            require_once $scriptPath;
        }

        if (!class_exists($installerClass) || !method_exists($installerClass, 'getDumpTablesSpec')) {
            return null;
        }

        $spec = (array) $installerClass::getDumpTablesSpec();

        return array_values(array_filter($spec, 'is_array'));
    }

    /**
     * Lista lógica única de tabelas no spec (ordem preservada).
     *
     * @param array<int, array<string, mixed>> $spec
     *
     * @return array<int, string>
     */
    private static function getUniqueDumpTableNamesFromSpec(array $spec): array
    {
        $out = [];
        $seen = [];

        foreach ($spec as $entry) {
            $name = (string) ($entry['name'] ?? '');
            if ($name === '' || isset($seen[$name])) {
                continue;
            }
            $seen[$name] = true;
            $out[] = $name;
        }

        return $out;
    }

    /**
     * Emite checkboxes dump_inc[] (só na secção UI «Full extension dump»; todas marcadas por defeito).
     *
     * @param array<int, array<string, mixed>> $spec
     */
    private static function emitFullDumpTableSelectionFieldset(array $spec): void
    {
        $names = self::getUniqueDumpTableNamesFromSpec($spec);

        if ($names === []) {
            echo '<p class="warning">No tables found in <code>getDumpTablesSpec()</code> for selection.</p>';

            return;
        }

        echo '<fieldset class="dump-table-pick" style="border:1px solid #444;border-radius:6px;padding:12px;margin:14px 0;">';
        echo '<legend style="color:#dcdcaa;">Tables to include</legend>';
        echo '<p class="info" style="margin-top:0;font-size:13px;">Uncheck a table to omit it from the file (no DELETE, DROP, CREATE, or INSERT for that table).</p>';

        foreach ($names as $logical) {
            $id = 'dump_inc_' . preg_replace('/[^a-zA-Z0-9_]/', '_', $logical);
            echo '<label style="display:block;margin:6px 0;cursor:pointer;" for="' . htmlspecialchars($id, ENT_QUOTES, 'UTF-8') . '">';
            echo '<input type="checkbox" name="dump_inc[]" id="' . htmlspecialchars($id, ENT_QUOTES, 'UTF-8')
                . '" value="' . htmlspecialchars($logical, ENT_QUOTES, 'UTF-8') . '" checked> ';
            echo '<code>#__' . htmlspecialchars($logical, ENT_QUOTES, 'UTF-8') . '</code>';
            echo '</label>';
        }

        echo '</fieldset>';
    }

    /**
     * Formata uma entrada do array retornado por getDumpTablesSpec() no script de instalação.
     *
     * @param array<string, mixed> $tableInfo
     */
    private static function formatDumpTableSpecEntryHtml(array $tableInfo): string
    {
        $logical = (string) ($tableInfo['name'] ?? '');
        $where = $tableInfo['where'] ?? null;
        $hasDelete = !empty($tableInfo['delete']);
        $hasDropCreate = !empty($tableInfo['drop_create']);

        $tableLabel = '<code>#__' . htmlspecialchars($logical, ENT_QUOTES, 'UTF-8') . '</code>';

        if ($hasDropCreate) {
            $line = "{$tableLabel} — full table (DROP/CREATE + INSERT)";
        } elseif ($hasDelete && $where !== null && $where !== '') {
            $w = htmlspecialchars((string) $where, ENT_QUOTES, 'UTF-8');

            $line = "{$tableLabel} — filtered rows: <code>{$w}</code> (DELETE matching + INSERT)";
        } elseif ($where !== null && $where !== '') {
            $w = htmlspecialchars((string) $where, ENT_QUOTES, 'UTF-8');

            $line = "{$tableLabel} — rows where <code>{$w}</code>";
        } else {
            $line = "{$tableLabel} — all rows (INSERT)";
        }

        if ($logical === 'modules' && ($hasDelete || ($where !== null && $where !== ''))) {
            $line .= ' — <code>id</code> omitted from INSERT; <code>SELECT LAST_INSERT_ID() INTO @nb_mod_{oldId}</code> after each row';
        }

        if ($logical === 'modules' && !empty($tableInfo['modules_menu_all_pages'])) {
            $line .= ' — one DELETE removes matching <code>#__modules</code>, related <code>#__modules_menu</code> (when present), and <code>#__assets</code> rows with <code>name = com_modules.module.{id}</code> (when present); after each dumped module: <code>modules_menu</code> (<code>menuid=0</code>), new <code>#__assets</code> row + <code>UPDATE</code> <code>asset_id</code> (when <code>assets</code> exists; run Rebuild Web Assets on target if needed)';
        }

        $moduleFkCols = $tableInfo['module_fk_columns'] ?? [];
        if (is_array($moduleFkCols) && $moduleFkCols !== []) {
            $listed = '<code>' . htmlspecialchars(implode(', ', $moduleFkCols), ENT_QUOTES, 'UTF-8') . '</code>';
            $line .= " — FK columns to #__modules.id rewritten as <code>@nb_mod_*</code> in dump: {$listed}";
        }

        return $line;
    }

    /**
     * Linhas HTML para o escopo do dump completo (getDumpTablesSpec() no script de instalação).
     *
     * @return array<int, string>
     */
    private static function resolveDumpTableDescriptions(string $scriptPath, string $installerClass): array
    {
        if (is_file($scriptPath)) {
            if (!class_exists($installerClass)) {
                require_once $scriptPath;
            }

            if (class_exists($installerClass) && method_exists($installerClass, 'getDumpTablesSpec')) {
                $spec = (array) $installerClass::getDumpTablesSpec();
                $lines = [];

                foreach ($spec as $entry) {
                    if (!is_array($entry)) {
                        continue;
                    }
                    $lines[] = self::formatDumpTableSpecEntryHtml($entry);
                }

                if ($lines !== []) {
                    return $lines;
                }
            }
        }

        return [
            'Tables defined in this extension\'s <code>dumpBeforeUpdate()</code> (see <code>script.extension.php</code>). Add <code>getDumpTablesSpec()</code> or expect <code>#__modules_menu</code>, <code>#__modules</code>, and extension-specific tables.',
        ];
    }

    /**
     * Dump de uma instância: #__modules (com id, sem asset_id no INSERT se houver #__assets) + #__modules_menu (menuid=0) + #__assets + UPDATE asset_id.
     *
     * @param array<string, mixed> $options include_deletes_and_drop (default true): quando false, omite o bloco DELETE inicial.
     *
     * @return string Mesmo formato de retorno que dumpDataGeneric
     */
    private static function dumpSingleModuleInstanceSql(
        int $moduleId,
        string $moduleName,
        string $outPath,
        string $fileName,
        ?int $daysToKeepDumps,
        array $options = []
    ): string {
        try {
            $includeDeletesAndDrop = array_key_exists('include_deletes_and_drop', $options)
                ? (bool) $options['include_deletes_and_drop']
                : true;

            $db = Factory::getContainer()->get(DatabaseInterface::class);
            $config = Factory::getApplication()->getConfig();
            $prefix = $config->get('dbprefix');
            $tableName = $prefix . 'modules';

            $query = "SHOW TABLES LIKE " . $db->quote($tableName);
            $db->setQuery($query);
            if (!$db->loadResult()) {
                return "Error: Table {$tableName} does not exist.";
            }

            $where = $db->quoteName('id') . ' = ' . $moduleId
                . ' AND ' . $db->quoteName('module') . ' = ' . $db->quote($moduleName);

            $select = $db->getQuery(true);
            $select->select('*')
                ->from($db->quoteName($tableName))
                ->where($where);
            $db->setQuery($select);
            $rows = $db->loadAssocList();

            if (empty($rows)) {
                return "Error: The dump generated is empty. No records found in tables.";
            }

            $dumpString = "-- #__modules + one #__modules_menu row (menuid=0) + #__assets for this instance (full module columns, includes PK).\n";
            $dumpString .= "-- Zero dates (0000-00-00) exported as NULL for strict modes on target.\n";

            $mmTable = $prefix . 'modules_menu';
            $db->setQuery('SHOW TABLES LIKE ' . $db->quote($mmTable));
            $hasModulesMenu = (bool) $db->loadResult();
            $db->setQuery('SHOW TABLES LIKE ' . $db->quote($prefix . 'assets'));
            $hasAssets = (bool) $db->loadResult();

            if ($includeDeletesAndDrop) {
                if ($hasModulesMenu || $hasAssets) {
                    $dumpString .= self::buildModulesAndMenuMultiDeleteSqlByModuleId(
                        $db,
                        $prefix,
                        $moduleId,
                        $moduleName,
                        $hasModulesMenu,
                        $hasAssets
                    ) . ";\n";
                } else {
                    $dumpString .= 'DELETE FROM ' . $db->quoteName($tableName) . ' WHERE ' . $where . ";\n";
                }

                $dumpString .= "\n";
            } else {
                $dumpString .= "-- Omitted DELETE statements (append-only).\n\n";
            }

            $moduleTitleFallback = (string) ($rows[0]['title'] ?? '');

            foreach ($rows as $row) {
                if ($hasAssets) {
                    unset($row['asset_id']);
                }
                $columns = array_keys($row);
                $escapedValues = [];

                foreach ($columns as $columnName) {
                    $escapedValues[] = self::quoteSqlDumpScalar($db, $row[$columnName]);
                }

                $dumpString .= 'INSERT INTO ' . $db->quoteName($tableName)
                    . ' (' . implode(', ', array_map([$db, 'quoteName'], $columns)) . ') '
                    . 'VALUES (' . implode(', ', $escapedValues) . ");\n";
            }

            if ($hasModulesMenu) {
                $dumpString .= 'INSERT INTO ' . $db->quoteName($mmTable)
                    . ' (' . $db->quoteName('moduleid') . ', ' . $db->quoteName('menuid') . ') '
                    . 'VALUES (' . (int) $moduleId . ', 0);' . "\n";
            }

            if ($hasAssets) {
                [$astTitle, $astRules] = self::getModuleAssetTitleRulesForDump(
                    $db,
                    $prefix,
                    $moduleId,
                    $moduleTitleFallback
                );
                $dumpString .= self::buildSqlInsertModuleAssetAndLinkModule(
                    $db,
                    $prefix,
                    $moduleId,
                    false,
                    $astTitle,
                    $astRules
                );
            }

            $dumpString = self::appendModuleListCacheHintSql($dumpString);

            return self::persistDumpString($dumpString, $outPath, $fileName, $daysToKeepDumps);
        } catch (\Exception $e) {
            return 'Error generating dump: ' . $e->getMessage();
        }
    }

    /**
     * Executa dump das tabelas da extensão via AJAX chamando o método dumpBeforeUpdate() do script de instalação.
     *
     * Esta função é genérica e usa o parâmetro extensionName para localizar
     * a extensão e carregar seu script de instalação.
     *
     * REQUISITOS DE SEGURANÇA:
     * - Requer autenticação como administrador do Joomla
     * - Usuário deve ter permissão 'core.admin'
     *
     * Parâmetros da URL:
     * - extensionName (obrigatório): Alias da extensão (ex: mod_nobosscalendar)
     * - id_module (opcional): ID em #__modules; quando informado, a tela inicial permite escolher entre dump
     *   completo (dumpBeforeUpdate) ou escopo reduzido: <code>#__modules</code> + uma linha <code>#__modules_menu</code> (menuid=0) para essa instância
     * - dump_scope (obrigatório após confirmação se id_module foi usado): full | module_only
     * - confirmed: 1 to run after choosing options / scope
     * - dump_opts_sent: 1 when options were submitted from the confirmation form (checkbox state is interpreted only then).
     * - dump_include_deletes: 1 (default when form used) to emit DELETE / DROP TABLE; omit or 0 for append-only (when dump_opts_sent=1).
     * - dump_inc[]: logical table names to include in full dump (from UI checkboxes); required when dump_opts_sent=1 — at least one. Omitted names are skipped entirely.
     * - dump_preserve_module_ids: 1 to keep literal #__modules ids and FKs in the full dump; 0 for AUTO_INCREMENT + @nb_mod_* remapping (when dump_opts_sent=1; default 0).
     *
     * URL para executar:
     * WEBSITE/index.php?option=com_nobossajax&library=noboss.src.Util.NbInstallScriptUtil&method=dumpExtension&extensionName=mod_nobosscalendar&format=raw
     * Com instância específica:
     * ...&id_module=123&format=raw
     *
     * @return void (exibe resultado diretamente na tela)
     */
    public static function dumpExtension() {
        self::checkAdminAuthentication();

        header('Content-Type: text/html; charset=utf-8');
        header('Access-Control-Allow-Origin: *');

        $input = Factory::getApplication()->input;
        $extensionName = strtolower($input->getString('extensionName', ''));
        $idModule = $input->getInt('id_module', 0);
        $dumpScope = $input->getCmd('dump_scope', '');
        $confirmed = $input->getString('confirmed', '') === '1';

        if (empty($extensionName)) {
            echo '<html><head><meta charset="utf-8"><title>Error</title></head><body>';
            echo '<h2 style="color:#f48771;">&#x2717; Error: Missing Required Parameter</h2>';
            echo '<p>The parameter <code>extensionName</code> is required.</p>';
            echo '<p>Example: <code>...&amp;extensionName=mod_nobosscalendar</code></p>';
            echo '</body></html>';
            exit;
        }

        $isModule = strpos($extensionName, 'mod_') === 0;

        // Quando for um componente (com_*), resolve para o módulo de mesmo nome (mod_*)
        if (!$isModule) {
            if (strpos($extensionName, 'com_') === 0) {
                $moduleName = 'mod_' . substr($extensionName, 4);
            } else {
                echo '<html><head><meta charset="utf-8"><title>Error</title></head><body>';
                echo '<h2 style="color:#f48771;">Unsupported Extension Type</h2>';
                echo '<p>This function currently supports module (<code>mod_*</code>) and component (<code>com_*</code>) extensions.</p>';
                echo '<p>Received: <code>' . htmlspecialchars($extensionName) . '</code></p>';
                echo '</body></html>';
                exit;
            }
        } else {
            $moduleName = $extensionName;
        }

        $classBase = preg_replace('/^mod_/', '', $moduleName);
        $classBase = str_replace(' ', '', ucwords(str_replace('_', ' ', $classBase)));
        $installerClass = "\\mod_{$classBase}InstallerScript";
        $scriptPath = JPATH_ROOT . '/modules/' . $moduleName . '/script.extension.php';

        $moduleRow = null;
        if ($idModule > 0) {
            $moduleRow = self::getValidatedModuleInstance($idModule, $moduleName);
            if ($moduleRow === null) {
                echo '<html><head><meta charset="utf-8"><title>Error</title></head><body>';
                echo '<h2 style="color:#f48771;">&#x2717; Invalid module instance</h2>';
                echo '<p>No row in <code>#__modules</code> with <code>id=' . (int) $idModule . '</code> and <code>module=' . htmlspecialchars($moduleName) . '</code>.</p>';
                echo '</body></html>';
                exit;
            }
        }

        $commonStyles = "<style>
            body{font-family:monospace;background:#1e1e1e;color:#d4d4d4;padding:20px;line-height:1.6;}
            .success{color:#4ec9b0;}.error{color:#f48771;}.warning{color:#dcdcaa;}.info{color:#569cd6;}
            hr{border-color:#444;}
            .confirm-box{background:#2d2d2d;border:2px solid #4ec9b0;border-radius:8px;padding:30px;margin:30px auto;max-width:720px;}
            .confirm-box h2{color:#4ec9b0;margin-top:0;}
            .btn{display:inline-block;font-weight:bold;padding:10px 24px;border-radius:4px;text-decoration:none;cursor:pointer;font-family:monospace;font-size:14px;border:none;margin:6px 6px 6px 0;}
            .btn-confirm{background:#4ec9b0;color:#1e1e1e;}
            .btn-confirm:hover{background:#6eddc8;}
            .btn-full{background:#569cd6;color:#1e1e1e;}
            .btn-full:hover{background:#78b4e8;}
            .btn-module{background:#ce9178;color:#1e1e1e;}
            .btn-module:hover{background:#e0a080;}
            .btn-cancel{background:#444;color:#d4d4d4;}
            .btn-cancel:hover{background:#555;}
            code{background:#1e1e1e;padding:2px 6px;border-radius:3px;color:#ce9178;}
            ul.dump-tables{text-align:left;margin:12px 0;padding-left:22px;}
            ul.dump-tables li{margin:4px 0;}
            .scope-block{border:1px solid #444;border-radius:8px;padding:16px;margin:16px 0;background:#252525;}
            .scope-block h3{margin-top:0;color:#dcdcaa;font-size:15px;}
            .dump-options{margin:16px 0;padding:12px;background:#1e1e1e;border-radius:6px;border:1px solid #444;}
            .dump-options label{display:block;margin:8px 0;cursor:pointer;}
            .dump-options input{margin-right:8px;vertical-align:middle;}
            .dump-table-pick{margin:14px 0;}
            .dump-table-pick legend{padding:0 8px;}
        </style>";

        $dumpOptsSent = $input->getInt('dump_opts_sent', 0) === 1;
        $includeDeletesAndDrop = $dumpOptsSent
            ? ($input->getInt('dump_include_deletes', 0) === 1)
            : true;
        $preserveModuleIds = $dumpOptsSent
            ? ($input->getInt('dump_preserve_module_ids', 0) === 1)
            : false;

        if (!$confirmed) {
            $root = rtrim(Uri::root(), '/');
            $actionUrl = htmlspecialchars($root . '/index.php');

            if ($idModule <= 0) {
                $dumpSpecForUi = self::loadInstallerDumpTablesSpec($scriptPath, $installerClass);
                $hasTablePick = $dumpSpecForUi !== null
                    && self::getUniqueDumpTableNamesFromSpec($dumpSpecForUi) !== [];
                $tableDescriptions = $hasTablePick
                    ? []
                    : self::resolveDumpTableDescriptions($scriptPath, $installerClass);

                echo "<html><head><meta charset='utf-8'><title>Confirm Dump</title>{$commonStyles}</head><body>";
                echo "<div class='confirm-box'>";
                echo "<h2>&#128230; Confirm Database Dump</h2>";
                echo "<p>You are about to generate a database dump for:</p>";
                echo "<p class='info'><strong>Extension:</strong> <code>" . htmlspecialchars($extensionName) . "</code></p>";
                if ($moduleName !== $extensionName) {
                    echo "<p class='warning'><strong>Resolved module:</strong> <code>" . htmlspecialchars($moduleName) . "</code></p>";
                }
                echo '<form method="get" action="' . $actionUrl . '" class="dump-options-form">';
                echo '<input type="hidden" name="option" value="com_nobossajax">';
                echo '<input type="hidden" name="library" value="noboss.src.Util.NbInstallScriptUtil">';
                echo '<input type="hidden" name="method" value="dumpExtension">';
                echo '<input type="hidden" name="extensionName" value="' . htmlspecialchars($extensionName, ENT_COMPAT, 'UTF-8') . '">';
                echo '<input type="hidden" name="format" value="raw">';
                echo '<input type="hidden" name="confirmed" value="1">';
                echo '<input type="hidden" name="dump_opts_sent" value="1">';
                echo '<div class="dump-options">';
                echo '<label><input type="checkbox" name="dump_include_deletes" value="1" checked> Include <strong>DELETE</strong> and <strong>DROP TABLE</strong> statements (uncheck for append-only SQL)</label>';
                echo '<label><input type="checkbox" name="dump_preserve_module_ids" value="1" checked> <strong>Preserve module IDs</strong> — keep literal <code>#__modules.id</code> and FK values (otherwise new AUTO_INCREMENT ids and <code>@nb_mod_*</code> variables)</label>';
                echo '</div>';
                echo "<div class='scope-block'>";
                echo "<h3>Full extension dump</h3>";
                echo '<p>Includes all data defined by the installer for this extension.</p>';
                if ($hasTablePick) {
                    self::emitFullDumpTableSelectionFieldset($dumpSpecForUi);
                } else {
                    echo "<ul class='dump-tables'>";
                    foreach ($tableDescriptions as $line) {
                        echo '<li>' . $line . '</li>';
                    }
                    echo '</ul>';
                }
                echo "<p class='warning'>Output is one SQL backup file.</p>";
                echo '<p>Are you sure you want to proceed?</p>';
                echo '<hr>';
                echo '<button type="submit" class="btn btn-confirm">&#10003; Yes, generate full dump</button>';
                echo '</div>';
                echo '</form>';
                echo "<a class='btn btn-cancel' href='javascript:history.back()'>&#10007; Cancel</a>";
                echo "</div>";
                echo "</body></html>";
                exit;
            }

            // id_module presente: escolher escopo antes de confirmar
            $dumpSpecForUi = self::loadInstallerDumpTablesSpec($scriptPath, $installerClass);
            $hasTablePick = $dumpSpecForUi !== null
                && self::getUniqueDumpTableNamesFromSpec($dumpSpecForUi) !== [];
            $tableDescriptions = $hasTablePick
                ? []
                : self::resolveDumpTableDescriptions($scriptPath, $installerClass);

            echo "<html><head><meta charset='utf-8'><title>Choose dump scope</title>{$commonStyles}</head><body>";
            echo "<div class='confirm-box'>";
            echo "<h2>&#128230; Choose backup scope</h2>";
            echo "<p class='info'><strong>Extension:</strong> <code>" . htmlspecialchars($extensionName) . "</code></p>";
            if ($moduleName !== $extensionName) {
                echo "<p class='warning'><strong>Resolved module:</strong> <code>" . htmlspecialchars($moduleName) . "</code></p>";
            }
            echo "<p class='info'><strong>Module instance id:</strong> <code>" . (int) $idModule . "</code> — <strong>Title:</strong> " . htmlspecialchars((string) $moduleRow->title) . "</p>";

            echo '<form method="get" action="' . $actionUrl . '" class="dump-options-form">';
            echo '<input type="hidden" name="option" value="com_nobossajax">';
            echo '<input type="hidden" name="library" value="noboss.src.Util.NbInstallScriptUtil">';
            echo '<input type="hidden" name="method" value="dumpExtension">';
            echo '<input type="hidden" name="extensionName" value="' . htmlspecialchars($extensionName, ENT_COMPAT, 'UTF-8') . '">';
            echo '<input type="hidden" name="id_module" value="' . (int) $idModule . '">';
            echo '<input type="hidden" name="format" value="raw">';
            echo '<input type="hidden" name="confirmed" value="1">';
            echo '<input type="hidden" name="dump_opts_sent" value="1">';
            echo '<div class="dump-options">';
            echo '<label><input type="checkbox" name="dump_include_deletes" value="1" checked> Include <strong>DELETE</strong> and <strong>DROP TABLE</strong> on full dump (module-only respects this for the opening DELETE block only)</label>';
            echo '<label><input type="checkbox" name="dump_preserve_module_ids" value="1"> <strong>Preserve module IDs</strong> (full dump only — literal module PKs and FKs)</label>';
            echo '</div>';

            echo "<div class='scope-block'>";
            echo "<h3>Full extension dump</h3>";
            echo '<p>Includes all data defined by the installer for this extension.</p>';
            if ($hasTablePick) {
                self::emitFullDumpTableSelectionFieldset($dumpSpecForUi);
            } else {
                echo "<ul class='dump-tables'>";
                foreach ($tableDescriptions as $line) {
                    echo '<li>' . $line . '</li>';
                }
                echo '</ul>';
            }
            echo '<button type="submit" name="dump_scope" value="full" class="btn btn-full">&#10003; Generate full dump</button>';
            echo "</div>";

            echo "<div class='scope-block'>";
            echo "<h3>Module row only</h3>";
            echo "<p class='warning'><code>#__modules</code>, <code>#__modules_menu</code> (when present), and <code>#__assets</code> (when present; <code>name = com_modules.module.{id}</code>) for <code>id=" . (int) $idModule . '</code> / <code>module=' . htmlspecialchars($moduleName) . "</code>. SQL runs one <strong>DELETE</strong> (matching rows in those tables) when the option above is enabled, then <strong>INSERT</strong> module (includes PK) and a single <code>modules_menu</code> row with <code>menuid=0</code> when that table exists. Zero legacy dates as <code>NULL</code>. No extension data tables.</p>";
            echo '<button type="submit" name="dump_scope" value="module_only" class="btn btn-module">&#10003; Generate module-row-only dump</button>';
            echo "</div>";

            echo '<hr>';
            echo "<a class='btn btn-cancel' href='javascript:history.back()'>&#10007; Cancel</a>";
            echo '</form>';
            echo "</div></body></html>";
            exit;
        }

        // Confirmado: com id_module exige dump_scope
        if ($idModule > 0) {
            if ($dumpScope !== 'full' && $dumpScope !== 'module_only') {
                echo '<html><head><meta charset="utf-8"><title>Error</title></head><body>';
                echo '<h2 style="color:#f48771;">&#x2717; Missing dump scope</h2>';
                echo '<p>When using <code>id_module</code>, open the dump page without <code>confirmed=1</code> first and choose <strong>full</strong> or <strong>module_only</strong>.</p>';
                echo '</body></html>';
                exit;
            }
        } else {
            $dumpScope = 'full';
        }

        $fullDumpTablesIncluded = [];

        if ($dumpScope === 'module_only') {
            $outPath = JPATH_ROOT . '/tmp/' . $moduleName . '/dumps/';
            $fileName = date('d-m-Y_H-i') . '_module' . (int) $idModule . '.sql';
            $result = self::dumpSingleModuleInstanceSql((int) $idModule, $moduleName, $outPath, $fileName, 30, [
                'include_deletes_and_drop' => $includeDeletesAndDrop,
            ]);
        } else {
            if (!file_exists($scriptPath)) {
                echo '<html><head><meta charset="utf-8"><title>Error</title></head><body>';
                echo '<h2 style="color:#f48771;">&#x2717; Error: Installation Script Not Found</h2>';
                echo '<p>File does not exist: <code>' . htmlspecialchars($scriptPath) . '</code></p>';
                echo '</body></html>';
                exit;
            }

            if (!class_exists($installerClass)) {
                require_once $scriptPath;
            }

            if (!class_exists($installerClass)) {
                echo '<html><head><meta charset="utf-8"><title>Error</title></head><body>';
                echo '<h2 style="color:#f48771;">&#x2717; Error: Installer Class Not Found</h2>';
                echo '<p>Class <code>' . htmlspecialchars($installerClass) . '</code> not found in <code>' . htmlspecialchars($scriptPath) . '</code></p>';
                echo '</body></html>';
                exit;
            }

            if (!method_exists($installerClass, 'getDumpTablesSpec')) {
                echo '<html><head><meta charset="utf-8"><title>Error</title></head><body>';
                echo '<h2 style="color:#f48771;">&#x2717; Error: getDumpTablesSpec Missing</h2>';
                echo '<p>Installer class must define <code>getDumpTablesSpec()</code>.</p>';
                echo '</body></html>';
                exit;
            }

            /** @var array<int, array<string, mixed>> $fullSpec */
            $fullSpec = array_values(array_filter((array) $installerClass::getDumpTablesSpec(), 'is_array'));

            $validNameSet = [];
            foreach ($fullSpec as $e) {
                $n = (string) ($e['name'] ?? '');
                if ($n !== '') {
                    $validNameSet[$n] = true;
                }
            }

            $tables = $fullSpec;

            if ($dumpOptsSent) {
                $reqRaw = $input->get('dump_inc', [], 'array');
                if (!is_array($reqRaw)) {
                    $reqRaw = ($reqRaw !== null && $reqRaw !== '') ? [$reqRaw] : [];
                }

                $reqMap = [];
                foreach ($reqRaw as $v) {
                    $s = is_string($v) ? trim($v) : trim((string) $v);
                    if ($s !== '' && isset($validNameSet[$s])) {
                        $reqMap[$s] = true;
                    }
                }

                $reqList = array_keys($reqMap);
                sort($reqList);

                if ($reqList === []) {
                    echo '<html><head><meta charset="utf-8"><title>Dump Error</title></head><body>';
                    echo '<h2 style="color:#f48771;">&#x2717; Select at least one table</h2>';
                    echo '<p>Choose one or more tables under <strong>Tables to include in full dump</strong> and try again.</p>';
                    echo '<p><a href="javascript:history.back()">Go back</a></p>';
                    echo '</body></html>';
                    exit;
                }

                $pick = array_flip($reqList);
                $tables = array_values(array_filter($fullSpec, static function ($entry) use ($pick) {
                    if (!is_array($entry)) {
                        return false;
                    }
                    $n = (string) ($entry['name'] ?? '');

                    return $n !== '' && isset($pick[$n]);
                }));

                if ($tables === []) {
                    echo '<html><head><meta charset="utf-8"><title>Dump Error</title></head><body>';
                    echo '<h2 style="color:#f48771;">&#x2717; Invalid table selection</h2>';
                    echo '<p>No matching <code>getDumpTablesSpec()</code> entries for the chosen tables.</p>';
                    echo '<p><a href="javascript:history.back()">Go back</a></p>';
                    echo '</body></html>';
                    exit;
                }
            }

            $fullDumpTablesIncluded = self::getUniqueDumpTableNamesFromSpec($tables);

            $outPath = JPATH_ROOT . '/tmp/' . $moduleName . '/dumps/';
            $fileSuffix = '';
            $allSpecTableNames = self::getUniqueDumpTableNamesFromSpec($fullSpec);
            if ($allSpecTableNames !== [] && $fullDumpTablesIncluded !== []) {
                $cmpA = $allSpecTableNames;
                $cmpB = $fullDumpTablesIncluded;
                sort($cmpA, SORT_STRING);
                sort($cmpB, SORT_STRING);
                if ($cmpA === $cmpB) {
                    $fileSuffix = '_complete';
                }
            }
            $fileName = date('d-m-Y_H-i') . $fileSuffix . '.sql';
            $fullPath = $outPath . $fileName;

            $rc = new \ReflectionClass($installerClass);
            if ($rc->hasProperty('lastDumpFileName')) {
                $rp = $rc->getProperty('lastDumpFileName');
                $rp->setAccessible(true);
                if ($rp->isStatic()) {
                    $rp->setValue(null, $fullPath);
                }
            }

            $result = self::dumpDataGeneric($tables, $outPath, $fileName, 30, [
                'omit_modules_insert_id' => !$preserveModuleIds,
                'include_deletes_and_drop' => $includeDeletesAndDrop,
            ]);
        }

        $isSuccess = (strpos($result, 'Dump generated successfully at ') === 0);

        $resultStyles = "<style>
            body{font-family:'Segoe UI',Tahoma,Geneva,Verdana,sans-serif;background:#1e1e1e;color:#d4d4d4;padding:20px;line-height:1.6;}
            h2{margin-top:0;}
            .box{border-radius:8px;padding:30px;margin:40px auto;max-width:640px;}
            .box-success{background:#2d2d2d;border:2px solid #4ec9b0;}
            .box-error{background:#2d2d2d;border:2px solid #f48771;}
            .box h2.success{color:#4ec9b0;}
            .box h2.error{color:#f48771;}
            .file-info{background:#252525;border-radius:4px;padding:16px;margin:20px 0;font-size:14px;}
            .file-info p{margin:6px 0;}
            .file-info strong{color:#dcdcaa;}
            .actions{display:flex;gap:12px;margin-top:24px;flex-wrap:wrap;}
            .btn{display:inline-block;font-weight:bold;padding:10px 22px;border-radius:4px;text-decoration:none;cursor:pointer;font-size:14px;border:none;font-family:inherit;}
            .btn-download{background:#569cd6;color:#1e1e1e;}
            .btn-download:hover{background:#78b4e8;}
            .btn-list{background:#444;color:#d4d4d4;}
            .btn-list:hover{background:#555;}
            code{background:#1e1e1e;padding:2px 6px;border-radius:3px;color:#ce9178;font-size:13px;word-break:break-all;}
        </style>";

        $scopeLabel = $dumpScope === 'module_only'
            ? 'Module instance (#__modules + one #__modules_menu row, menuid=0)'
            : 'Full extension (installer dump)';

        if ($isSuccess) {
            $dumpFilePath = trim(str_replace('Dump generated successfully at ', '', $result));
            $dumpFileBasename = basename($dumpFilePath);
            $fileSize   = file_exists($dumpFilePath) ? number_format(filesize($dumpFilePath) / 1024, 2) . ' KB' : 'N/A';
            $fileDate   = file_exists($dumpFilePath) ? date('Y-m-d H:i:s', filemtime($dumpFilePath)) : 'N/A';

            $downloadUrl = htmlspecialchars(
                rtrim(Uri::root(), '/') . '/index.php?option=com_nobossajax&library=noboss.src.Util.NbInstallScriptUtil&method=restoreBackup'
                . '&extensionName=' . urlencode($extensionName)
                . '&downloadFileName=' . urlencode($dumpFileBasename)
                . '&format=raw'
            );

            $listUrl = htmlspecialchars(
                rtrim(Uri::root(), '/') . '/index.php?option=com_nobossajax&library=noboss.src.Util.NbInstallScriptUtil&method=restoreBackup'
                . '&extensionName=' . urlencode($extensionName)
                . '&format=raw'
            );

            echo "<html><head><meta charset='utf-8'><title>Dump Generated</title>{$resultStyles}</head><body>";
            echo "<div class='box box-success'>";
            echo "<h2 class='success'>&#10003; Dump Generated Successfully</h2>";
            echo "<div class='file-info'>";
            echo "<p><strong>&#128230; Extension:</strong> <code>" . htmlspecialchars($extensionName) . "</code></p>";
            if ($moduleName !== $extensionName) {
                echo "<p><strong>&#128279; Resolved module:</strong> <code>" . htmlspecialchars($moduleName) . "</code></p>";
            }
            echo "<p><strong>Scope:</strong> " . htmlspecialchars($scopeLabel) . "</p>";
            if ($dumpScope === 'full' && $fullDumpTablesIncluded !== []) {
                echo '<p><strong>Tables in this dump:</strong> <code>' . htmlspecialchars(implode(', ', $fullDumpTablesIncluded)) . '</code></p>';
            }
            if ($dumpOptsSent) {
                echo '<p><strong>DELETE / DROP TABLE:</strong> ' . ($includeDeletesAndDrop ? 'included' : 'omitted (append-only)') . '</p>';
                $preserveNote = $dumpScope === 'module_only'
                    ? ' (option applies to full dump; this export always keeps the module PK)'
                    : '';
                echo '<p><strong>Preserve module IDs:</strong> ' . ($preserveModuleIds ? 'yes' : 'no') . htmlspecialchars($preserveNote) . '</p>';
            }
            if ($idModule > 0) {
                echo "<p><strong>id_module:</strong> <code>" . (int) $idModule . "</code></p>";
            }
            echo "<p><strong>&#128196; File:</strong> <code>" . htmlspecialchars($dumpFileBasename) . "</code></p>";
            echo "<p><strong>&#128197; Date:</strong> " . htmlspecialchars($fileDate) . "</p>";
            echo "<p><strong>&#128190; Size:</strong> " . htmlspecialchars($fileSize) . "</p>";
            echo "<p><strong>&#128193; Path:</strong> <code>" . htmlspecialchars($dumpFilePath) . "</code></p>";
            echo "</div>";
            echo "<div class='actions'>";
            echo "<a class='btn btn-download' href='{$downloadUrl}'>&#11015; Download dump</a>";
            echo "<a class='btn btn-list' href='{$listUrl}'>&#128230; Ver lista de dumps</a>";
            echo "</div>";
            echo "</div>";
            echo "</body></html>";
        } else {
            echo "<html><head><meta charset='utf-8'><title>Dump Error</title>{$resultStyles}</head><body>";
            echo "<div class='box box-error'>";
            echo "<h2 class='error'>&#10007; Dump Failed</h2>";
            echo "<p><strong>&#128230; Extension:</strong> <code>" . htmlspecialchars($extensionName) . "</code></p>";
            echo "<p><strong>Scope:</strong> " . htmlspecialchars($scopeLabel) . "</p>";
            echo "<p>" . htmlspecialchars($result) . "</p>";
            echo "</div>";
            echo "</body></html>";
        }

        exit;
    }

    /**
     * Remove um diretório e todo o seu conteúdo recursivamente.
     *
     * @param string $dir Caminho absoluto para o diretório a ser removido.
     *
     * @return bool Retorna true se o diretório foi removido com sucesso, false caso contrário.
     */
    public static function removeDirectory($dir) {
        if (!is_dir($dir)) {
            return false;
        }
    
        $items = scandir($dir);
        foreach ($items as $item) {
            if ($item == '.' || $item == '..') {
                continue;
            }
    
            $path = $dir . DIRECTORY_SEPARATOR . $item;
            if (is_dir($path)) {
                self::removeDirectory($path);
            } else {
                unlink($path);
            }
        }
    
        return rmdir($dir);
    }

    /**
     * Função para restaurar backup do banco de dados a partir de arquivo SQL dump via AJAX
     * 
     * Esta função permite restaurar tabelas do banco de dados a partir de um arquivo de dump gerado previamente.
     * Pode ser chamada diretamente com um arquivo de dump específico ou exibirá lista para seleção.
     * 
     * REQUISITOS DE SEGURANÇA:
     * - Requer autenticação como administrador do Joomla
     * - Usuário deve ter permissão 'core.admin'
     * 
     * Parâmetros da URL:
     * - extensionName (obrigatório): Alias da extensão (ex: 'mod_nobosscalendar')
     * - dumpFileName (opcional): Nome específico do arquivo de dump para restaurar. Se não informado, exibe lista de seleção
     * - confirmed (opcional): Definido como '1' para executar restore imediatamente (após usuário confirmar via modal)
     * 
     * URL para executar: WEBSITE/index.php?option=com_nobossajax&library=noboss.src.Util.NbInstallScriptUtil&method=restoreBackup&extensionName=mod_nobosscalendar&format=raw
     * 
     * @return void (exibe resultado diretamente na tela)
     */
    public static function restoreBackup() {
        // Verifica autenticação administrativa
        self::checkAdminAuthentication();
        
        header('Content-Type: text/html; charset=utf-8');
        header('Access-Control-Allow-Origin: *');
        
        // Obtém parâmetros da URL
        $input = Factory::getApplication()->input;
        $extensionName = strtolower($input->getString('extensionName', ''));
        $dumpFileName = $input->getString('dumpFileName', '');
        $deleteFileName = $input->getString('deleteFileName', '');
        $downloadFileName = $input->getString('downloadFileName', '');
        $confirmed = $input->getInt('confirmed', 0);
        
        // Valida parâmetro obrigatório
        if (empty($extensionName)) {
            echo '<html><head><meta charset="utf-8"><title>Error</title></head><body>';
            echo '<h2 style="color:#f48771;">✗ Error: Missing Required Parameter</h2>';
            echo '<p>The parameter <code>extensionName</code> is required.</p>';
            echo '<p>Example: <code>...&extensionName=mod_nobosscalendar</code></p>';
            echo '</body></html>';
            exit;
        }

        // Quando for um componente (com_*), resolve para o módulo de mesmo nome (mod_*)
        if (strpos($extensionName, 'com_') === 0) {
            $moduleName = 'mod_' . substr($extensionName, 4);
        } elseif (strpos($extensionName, 'mod_') === 0) {
            $moduleName = $extensionName;
        } else {
            echo '<html><head><meta charset="utf-8"><title>Error</title></head><body>';
            echo '<h2 style="color:#f48771;">Unsupported Extension Type</h2>';
            echo '<p>This function currently supports module (<code>mod_*</code>) and component (<code>com_*</code>) extensions.</p>';
            echo '<p>Received: <code>' . htmlspecialchars($extensionName) . '</code></p>';
            echo '</body></html>';
            exit;
        }
        
        // Monta caminho do diretório de dumps: /caminho/completo/do/site/tmp/mod_nobosscalendar/dumps/
        $dumpDir = JPATH_ROOT . '/tmp/' . $moduleName . '/dumps/';
        
        // Verifica se diretório de dumps existe
        if (!is_dir($dumpDir)) {
            echo '<html><head><meta charset="utf-8"><title>Error</title></head><body>';
            echo '<h2 style="color:#f48771;">✗ Error: Dump Directory Not Found</h2>';
            echo '<p>Directory does not exist: <code>' . htmlspecialchars($dumpDir) . '</code></p>';
            echo '<p>No backup files available for this extension.</p>';
            echo '</body></html>';
            exit;
        }
        
        // Download de dump solicitado
        if (!empty($downloadFileName)) {
            $fileToDownload = $dumpDir . basename($downloadFileName);

            if (!file_exists($fileToDownload)) {
                echo '<html><head><meta charset="utf-8"><title>Error</title></head><body>';
                echo '<h2 style="color:#f48771;">&#x2717; Error: Dump File Not Found</h2>';
                echo '<p>File does not exist: <code>' . htmlspecialchars($fileToDownload) . '</code></p>';
                echo '</body></html>';
                exit;
            }

            $safeBasename = basename($fileToDownload);
            header('Content-Description: File Transfer');
            header('Content-Type: application/octet-stream');
            header('Content-Disposition: attachment; filename="' . $safeBasename . '"');
            header('Content-Transfer-Encoding: binary');
            header('Expires: 0');
            header('Cache-Control: must-revalidate');
            header('Pragma: public');
            header('Content-Length: ' . filesize($fileToDownload));
            ob_clean();
            flush();
            readfile($fileToDownload);
            exit;
        }

        // Exclusão de dump solicitada
        if (!empty($deleteFileName)) {
            $fileToDelete = $dumpDir . basename($deleteFileName);
            
            if (!file_exists($fileToDelete)) {
                echo '<html><head><meta charset="utf-8"><title>Error</title></head><body>';
                echo '<h2 style="color:#f48771;">&#x2717; Error: Dump File Not Found</h2>';
                echo '<p>File does not exist: <code>' . htmlspecialchars($fileToDelete) . '</code></p>';
                echo '</body></html>';
                exit;
            }
            
            if (unlink($fileToDelete)) {
                $listUrl = htmlspecialchars(rtrim(Uri::root(), '/') . '/index.php?option=com_nobossajax&library=noboss.src.Util.NbInstallScriptUtil&method=restoreBackup&extensionName=' . urlencode($extensionName) . '&format=raw');
                echo '<html><head><meta charset="utf-8"><title>Dump Deleted</title>';
                echo '<style>body{font-family:monospace;background:#1e1e1e;color:#d4d4d4;padding:20px;}.success{color:#4ec9b0;}.info{color:#569cd6;}hr{border-color:#444;}a{color:#4ec9b0;}</style>';
                echo '</head><body>';
                echo '<h2 class="success">&#10003; Dump Deleted Successfully</h2><hr>';
                echo '<p class="info">File <strong>' . htmlspecialchars(basename($deleteFileName)) . '</strong> has been deleted.</p>';
                echo '<p><a href="' . $listUrl . '">&larr; Back to dump list</a></p>';
                echo '</body></html>';
            } else {
                echo '<html><head><meta charset="utf-8"><title>Error</title></head><body>';
                echo '<h2 style="color:#f48771;">&#x2717; Error: Could Not Delete File</h2>';
                echo '<p>Failed to delete: <code>' . htmlspecialchars($fileToDelete) . '</code></p>';
                echo '</body></html>';
            }
            exit;
        }

        // Determina qual arquivo de dump usar
        $targetDumpFile = '';
        
        if (!empty($dumpFileName)) {
            // Usa arquivo específico fornecido como parâmetro
            $targetDumpFile = $dumpDir . basename($dumpFileName);
            
            if (!file_exists($targetDumpFile)) {
                echo '<html><head><meta charset="utf-8"><title>Error</title></head><body>';
                echo '<h2 style="color:#f48771;">✗ Error: Dump File Not Found</h2>';
                echo '<p>File does not exist: <code>' . htmlspecialchars($targetDumpFile) . '</code></p>';
                echo '</body></html>';
                exit;
            }
        } else {
            // Encontra arquivos .sql no diretório
            $sqlFiles = glob($dumpDir . '*.sql');
            
            if (empty($sqlFiles)) {
                echo '<html><head><meta charset="utf-8"><title>Error</title></head><body>';
                echo '<h2 style="color:#f48771;">✗ Error: No Dump Files Found</h2>';
                echo '<p>No SQL dump files found in: <code>' . htmlspecialchars($dumpDir) . '</code></p>';
                echo '</body></html>';
                exit;
            }
            
            // Ordena por data de modificação (mais recente primeiro)
            usort($sqlFiles, function($a, $b) {
                return filemtime($b) - filemtime($a);
            });
            
            // Exibe lista de arquivos para o usuário escolher
            echo "<html><head><meta charset='utf-8'><title>Select Backup File</title>";
            echo "<style>
                body{font-family:'Segoe UI',Tahoma,Geneva,Verdana,sans-serif;background:#1e1e1e;color:#d4d4d4;padding:20px;line-height:1.6;}
                h2{color:#569cd6;border-bottom:2px solid #444;padding-bottom:15px;}
                .info{background:#2d2d3d;border-left:4px solid #569cd6;padding:15px;margin:20px 0;border-radius:4px;}
                .file-list{background:#2d2d2d;border-radius:8px;padding:20px;margin:20px 0;}
                .file-item{background:#252525;border-radius:4px;padding:15px;margin:10px 0;border:1px solid #444;}
                .file-header{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:10px;}
                .file-name{font-weight:bold;font-size:15px;color:#d4d4d4;}
                .file-meta{color:#808080;font-size:14px;margin-top:10px;}
                .file-meta strong{color:#dcdcaa;}
                .badge{background:#4ec9b0;color:#1e1e1e;padding:2px 8px;border-radius:3px;font-size:12px;font-weight:bold;margin-left:8px;}
                .file-actions{display:flex;gap:8px;flex-shrink:0;}
                .btn{display:inline-block;font-weight:bold;padding:7px 16px;border-radius:4px;text-decoration:none;cursor:pointer;font-size:13px;border:none;font-family:inherit;}
                .btn-restore{background:#4ec9b0;color:#1e1e1e;}
                .btn-restore:hover{background:#6eddc8;}
                .btn-download{background:#569cd6;color:#1e1e1e;}
                .btn-download:hover{background:#78b4e8;}
                .btn-delete{background:#f48771;color:#1e1e1e;}
                .btn-delete:hover{background:#ff6a50;}
                code{background:#1e1e1e;padding:2px 6px;border-radius:3px;color:#ce9178;}
            </style></head><body>";
            
            echo "<h2>&#128230; Select Backup File</h2>";
            
            echo "<div class='info'>";
            echo "<strong>&#128230; Extension:</strong> <code>" . htmlspecialchars($extensionName) . "</code><br>";
            echo "<strong>&#128193; Directory:</strong> <code>" . htmlspecialchars($dumpDir) . "</code><br>";
            echo "<strong>&#128196; Available files:</strong> " . count($sqlFiles);
            echo "</div>";
            
            echo "<div class='file-list'>";
            
            $currentUrl = Uri::getInstance();
            $baseUrl = $currentUrl->toString(['scheme', 'host', 'port', 'path', 'query']);
            
            foreach ($sqlFiles as $index => $filePath) {
                $fileName = basename($filePath);
                $fileSize = filesize($filePath);
                $fileDate = date('Y-m-d H:i:s', filemtime($filePath));
                $fileSizeKB = number_format($fileSize / 1024, 2);
                
                $restoreUrl  = htmlspecialchars($baseUrl . '&dumpFileName='   . urlencode($fileName));
                $downloadUrl = htmlspecialchars($baseUrl . '&downloadFileName=' . urlencode($fileName));
                $deleteUrl   = htmlspecialchars($baseUrl . '&deleteFileName='  . urlencode($fileName));

                $badge = ($index === 0) ? '<span class="badge">MOST RECENT</span>' : '';
                
                echo "<div class='file-item'>";
                echo "<div class='file-header'>";
                echo "<span class='file-name'>&#128196; " . htmlspecialchars($fileName) . $badge . "</span>";
                echo "<div class='file-actions'>";
                echo "<a class='btn btn-restore' href='{$restoreUrl}'>&#9654; Restaurar dump</a>";
                echo "<a class='btn btn-download' href='{$downloadUrl}'>&#11015; Baixar dump</a>";
                echo "<a class='btn btn-delete' href='{$deleteUrl}' onclick=\"return confirm('Excluir o dump &quot;" . htmlspecialchars($fileName, ENT_QUOTES) . "&quot;? Esta ação não pode ser desfeita.')\">&#10007; Excluir dump</a>";
                echo "</div>";
                echo "</div>";
                echo "<div class='file-meta'>";
                echo "<strong>&#128197; Date:</strong> " . htmlspecialchars($fileDate) . " &nbsp;|&nbsp; ";
                echo "<strong>&#128190; Size:</strong> " . $fileSizeKB . " KB";
                echo "</div>";
                echo "</div>";
            }
            
            echo "</div>";
            
            echo "</body></html>";
            exit;
        }
        
        $dumpFileBasename = basename($targetDumpFile);
        $dumpFileSize = filesize($targetDumpFile);
        $dumpFileDate = date('Y-m-d H:i:s', filemtime($targetDumpFile));
        
        // Se não confirmado, exibe modal de confirmação
        if (!$confirmed) {
            echo "<html><head><meta charset='utf-8'><title>Confirm Backup Restore</title>";
            echo "<style>
                body{font-family:'Segoe UI',Tahoma,Geneva,Verdana,sans-serif;background:#1e1e1e;color:#d4d4d4;padding:0;margin:0;display:flex;align-items:center;justify-content:center;min-height:100vh;}
                .modal{background:#2d2d2d;border-radius:8px;box-shadow:0 4px 20px rgba(0,0,0,0.5);max-width:600px;width:90%;padding:30px;}
                h2{margin-top:0;color:#569cd6;border-bottom:2px solid #444;padding-bottom:15px;}
                .warning{background:#3d3d2d;border-left:4px solid #dcdcaa;padding:15px;margin:20px 0;border-radius:4px;}
                .info{background:#2d2d3d;border-left:4px solid #569cd6;padding:15px;margin:20px 0;border-radius:4px;}
                .file-info{background:#252525;padding:15px;border-radius:4px;margin:15px 0;}
                .file-info strong{color:#4ec9b0;}
                .buttons{display:flex;gap:15px;justify-content:center;margin-top:25px;}
                button{padding:12px 30px;border:none;border-radius:4px;font-size:16px;cursor:pointer;font-weight:bold;transition:all 0.3s;}
                .btn-confirm{background:#4ec9b0;color:#1e1e1e;}
                .btn-confirm:hover{background:#6eddc8;}
                .btn-download{background:#569cd6;color:#1e1e1e;}
                .btn-download:hover{background:#78b4e8;}
                .btn-cancel{background:#555;color:#d4d4d4;}
                .btn-cancel:hover{background:#666;}
                code{background:#1e1e1e;padding:2px 6px;border-radius:3px;color:#ce9178;}
            </style></head><body>";
            
            echo "<div class='modal'>";
            echo "<h2>⚠️ Confirm Database Restore</h2>";
            
            echo "<div class='warning'>";
            echo "<strong>⚠️ WARNING:</strong> This action will restore database tables from a backup file. ";
            echo "This operation will overwrite current data in the affected tables.";
            echo "</div>";
            
            echo "<div class='info'>";
            echo "<strong>📦 Extension:</strong> <code>" . htmlspecialchars($extensionName) . "</code>";
            echo "</div>";
            
            echo "<div class='file-info'>";
            echo "<strong>📄 Dump File:</strong> " . htmlspecialchars($dumpFileBasename) . "<br>";
            echo "<strong>📅 Created:</strong> " . htmlspecialchars($dumpFileDate) . "<br>";
            echo "<strong>💾 Size:</strong> " . number_format($dumpFileSize / 1024, 2) . " KB<br>";
            echo "<strong>📁 Path:</strong> <code>" . htmlspecialchars($targetDumpFile) . "</code>";
            echo "</div>";
            
            echo "<div class='info'>";
            echo "<strong>ℹ️ Note:</strong> It is recommended to create a new backup before restoring an old one.";
            echo "</div>";
            
            // Monta URL de confirmação e de download
            $currentUrl  = Uri::getInstance();
            $confirmUrl  = $currentUrl->toString() . '&confirmed=1';
            $baseListUrl = rtrim(Uri::root(), '/') . '/index.php?option=com_nobossajax&library=noboss.src.Util.NbInstallScriptUtil&method=restoreBackup&extensionName=' . urlencode($extensionName) . '&format=raw';
            $downloadUrl = htmlspecialchars($baseListUrl . '&downloadFileName=' . urlencode($dumpFileBasename));

            echo "<div class='buttons'>";
            echo "<button class='btn-confirm' onclick=\"window.location.href='" . htmlspecialchars($confirmUrl) . "'\">✓ Confirm Restore</button>";
            echo "<a class='btn-download' style='padding:12px 30px;font-size:16px;font-weight:bold;border-radius:4px;text-decoration:none;' href='{$downloadUrl}'>&#11015; Download dump</a>";
            echo "<button class='btn-cancel' onclick='window.history.back();'>✗ Cancel</button>";
            echo "</div>";
            
            echo "</div>";
            echo "</body></html>";
            exit;
        }
        
        // Usuário confirmou - prossegue com restore
        echo "<html><head><meta charset='utf-8'><title>Restoring Backup</title>";
        echo "<style>
            body{font-family:monospace;background:#1e1e1e;color:#d4d4d4;padding:20px;line-height:1.6;}
            .success{color:#4ec9b0;}.error{color:#f48771;}.warning{color:#dcdcaa;}.info{color:#569cd6;}
            pre{background:#2d2d2d;padding:15px;border-radius:5px;overflow-x:auto;max-height:400px;}
            hr{border-color:#444;margin:20px 0;}
            .progress{background:#2d2d2d;padding:10px;border-radius:5px;margin:10px 0;}
        </style></head><body>";
        
        echo "<h2>🔄 Restoring Database Backup</h2>";
        echo "<hr>";
        echo "<p class='info'>📦 Extension: " . htmlspecialchars($extensionName) . "</p>";
        echo "<p class='info'>📄 Dump File: " . htmlspecialchars($dumpFileBasename) . "</p>";
        echo "<hr>";
        
        try {
            // Lê conteúdo do arquivo SQL
            echo "<p class='info'>📖 Reading SQL file...</p>";
            $sqlContent = file_get_contents($targetDumpFile);
            
            if ($sqlContent === false) {
                throw new \Exception('Failed to read dump file');
            }
            
            echo "<p class='success'>✓ File read successfully (" . number_format(strlen($sqlContent) / 1024, 2) . " KB)</p>";
            
            // Obtém conexão com o banco de dados
            echo "<p class='info'>🔌 Connecting to database...</p>";
            $db = Factory::getContainer()->get(DatabaseInterface::class);
            echo "<p class='success'>✓ Database connection established</p>";
            
            // Remove comment lines first (line by line)
            echo "<p class='info'>⚙️ Removing comment lines...</p>";
            $lines = explode("\n", $sqlContent);
            $cleanedLines = array_filter($lines, function($line) {
                $trimmed = trim($line);
                // Skip empty lines and comment lines
                return !empty($trimmed) && 
                       !preg_match('/^(--|#)/', $trimmed) && 
                       !preg_match('/^\/\*/', $trimmed);
            });
            $cleanedContent = implode("\n", $cleanedLines);
            
            // Divide SQL em statements individuais (separados por ponto e vírgula)
            echo "<p class='info'>⚙️ Parsing SQL statements...</p>";
            $statements = array_filter(
                array_map('trim', preg_split('/;[\r\n]+/', $cleanedContent)),
                function($stmt) {
                    return !empty($stmt);
                }
            );
            
            $totalStatements = count($statements);
            echo "<p class='success'>✓ Found {$totalStatements} SQL statements to execute</p>";
            echo "<hr>";
            
            // Executa cada statement
            echo "<p class='info'>🚀 Executing SQL statements...</p>";
            echo "<div class='progress'>";
            
            $executedCount = 0;
            $errors = [];
            
            foreach ($statements as $index => $statement) {
                try {
                    $db->setQuery($statement);
                    $db->execute();
                    $executedCount++;
                    
                    // Registra progresso a cada 10 statements
                    if (($executedCount % 10 == 0) || ($executedCount == $totalStatements)) {
                        $percentage = round(($executedCount / $totalStatements) * 100, 1);
                        echo "<p>Progress: {$executedCount}/{$totalStatements} statements ({$percentage}%)</p>";
                        flush();
                        ob_flush();
                    }
                    
                } catch (\Exception $e) {
                    $errors[] = [
                        'statement_num' => $index + 1,
                        'error' => $e->getMessage(),
                        'statement' => substr($statement, 0, 100) . '...'
                    ];
                }
            }
            
            echo "</div>";
            echo "<hr>";
            
            // Exibe resultados
            if (empty($errors)) {
                echo "<h3 class='success'>✓ RESTORE COMPLETED SUCCESSFULLY!</h3>";
                echo "<p class='success'>All {$executedCount} SQL statements executed successfully.</p>";
            } else {
                echo "<h3 class='warning'>⚠️ RESTORE COMPLETED WITH ERRORS</h3>";
                echo "<p class='success'>Successfully executed: {$executedCount}/{$totalStatements} statements</p>";
                echo "<p class='error'>Failed: " . count($errors) . " statements</p>";
                echo "<hr>";
                echo "<h4 class='error'>Errors:</h4>";
                echo "<pre class='error'>";
                foreach ($errors as $error) {
                    echo "Statement #{$error['statement_num']}:\n";
                    echo "Error: {$error['error']}\n";
                    echo "SQL: {$error['statement']}\n\n";
                }
                echo "</pre>";
            }
            
        } catch (\Exception $e) {
            echo "<hr>";
            echo "<h3 class='error'>✗ RESTORE FAILED</h3>";
            echo "<pre class='error'>" . htmlspecialchars($e->getMessage()) . "</pre>";
            echo "<p class='error'><strong>File:</strong> " . htmlspecialchars($e->getFile()) . "</p>";
            echo "<p class='error'><strong>Line:</strong> " . $e->getLine() . "</p>";
        }
        
        echo "<hr>";
        echo "<p><em>Execution finished at " . date('Y-m-d H:i:s') . "</em></p>";
        echo "</body></html>";
        exit;
    }
}