<?php

error_reporting(E_ALL);
ini_set('display_errors', 0);
ini_set('log_errors', 1);

/** Compatível com PHP 7: str_starts_with / str_ends_with existem no PHP 8+. */
if (!function_exists('str_starts_with')) {
	function str_starts_with(string $haystack, string $needle): bool
	{
		return $needle === '' || strncmp($haystack, $needle, strlen($needle)) === 0;
	}
}
if (!function_exists('str_ends_with')) {
	function str_ends_with(string $haystack, string $needle): bool
	{
		if ($needle === '') {
			return true;
		}
		$len = strlen($needle);

		return $len <= strlen($haystack) && substr_compare($haystack, $needle, -$len, $len) === 0;
	}
}

if (session_status() === PHP_SESSION_NONE) {
	session_start([
		'cookie_httponly' => true,
		'cookie_samesite' => 'Lax',
		'use_strict_mode' => true,
	]);
}
if (empty($_SESSION['installer_csrf']) || !is_string($_SESSION['installer_csrf'])) {
	$_SESSION['installer_csrf'] = bin2hex(random_bytes(32));
}

function instaladorCsrfHeader(): string
{
	$h = $_SERVER['HTTP_X_INSTALLER_CSRF'] ?? '';
	return is_string($h) ? trim($h) : '';
}

function instaladorRequireCsrf(): void
{
	if (empty($_SESSION['installer_csrf']) || !is_string($_SESSION['installer_csrf'])) {
		throw new Exception('Sessão inválida. Recarregue a página do instalador.');
	}
	$sent = instaladorCsrfHeader();
	if ($sent === '' || !hash_equals($_SESSION['installer_csrf'], $sent)) {
		throw new Exception('Pedido recusado (CSRF). Recarregue a página e tente novamente.');
	}
}

function instaladorStatePath(): string
{
	return __DIR__ . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'installer_state.json';
}

/** @return array<string,mixed> */
function instaladorReadState(): array
{
	$p = instaladorStatePath();
	if (!is_readable($p)) {
		return [];
	}
	$j = json_decode((string) file_get_contents($p), true);

	return is_array($j) ? $j : [];
}

/** @param array<string,mixed> $merge */
function instaladorWriteStateMerge(array $merge): void
{
	$dir = dirname(instaladorStatePath());
	if (!is_dir($dir) && !@mkdir($dir, 0755, true)) {
		return;
	}
	$cur = instaladorReadState();
	file_put_contents(
		instaladorStatePath(),
		json_encode(array_merge($cur, $merge), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
	);
}

function instaladorPathIsInsideDir(string $path, string $rootDir): bool
{
	$path = realpath($path);
	$root = realpath($rootDir);
	if ($path === false || $root === false) {
		return false;
	}
	$path = str_replace('\\', '/', $path);
	$root = str_replace('\\', '/', $root);

	return $path === $root || str_starts_with($path, $root . '/');
}

function instaladorSanitizeMysqlHost(string $raw): string
{
	$s = trim($raw);
	if ($s === '' || strlen($s) > 255) {
		return '';
	}
	if (!preg_match('/^[a-zA-Z0-9.\-_:]+$/', $s)) {
		return '';
	}

	return $s;
}

function instaladorSanitizeMysqlUser(string $raw): string
{
	$s = trim($raw);
	if ($s === '' || strlen($s) > 128) {
		return '';
	}
	if (!preg_match('/^[a-zA-Z0-9_\-.@]+$/', $s)) {
		return '';
	}

	return $s;
}

function instaladorSanitizeMysqlDatabaseName(string $raw): string
{
	$s = trim($raw);
	if ($s === '' || strlen($s) > 64) {
		return '';
	}
	if (!preg_match('/^[a-zA-Z0-9_]+$/', $s)) {
		return '';
	}

	return $s;
}

/**
 * Extração segura do ZIP (anti Zip Slip); ignora instalador.php embutido.
 *
 * @return list<string>
 */
function instaladorZipExtractSafe(ZipArchive $zip, string $extractDir): array
{
	$extractRoot = realpath($extractDir);
	if ($extractRoot === false) {
		throw new Exception('Diretório de extração inválido.');
	}
	$outNames = [];
	for ($i = 0; $i < $zip->numFiles; $i++) {
		$stat = $zip->statIndex($i);
		if (!is_array($stat) || !isset($stat['name'])) {
			continue;
		}
		$name = (string) $stat['name'];
		if ($name === '' || str_ends_with($name, '/')) {
			continue;
		}
		$norm = str_replace('\\', '/', $name);
		if ($norm[0] === '/' || preg_match('#(^|/)\\.\\.(/|$)#', $norm)) {
			continue;
		}
		if (strcasecmp(basename($norm), 'instalador.php') === 0) {
			continue;
		}
		$destPath = $extractDir . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $norm);
		$destParent = dirname($destPath);
		if (!is_dir($destParent)) {
			if (!@mkdir($destParent, 0755, true)) {
				throw new Exception('Não foi possível criar pastas para extração.');
			}
		}
		$rp = realpath($destParent);
		if ($rp === false || !instaladorPathIsInsideDir($rp, $extractRoot)) {
			throw new Exception('Entrada ZIP inválida (tentativa de path traversal).');
		}
		$bytes = $zip->getFromIndex($i);
		if ($bytes === false) {
			continue;
		}
		if (file_put_contents($destPath, $bytes) !== false) {
			$outNames[] = $norm;
		}
	}

	return $outNames;
}

/*
 * Endpoints JSON — executar antes de qualquer require do projecto (constants/database/functions
 * podem emitir BOM, avisos ou HTML e corromper a resposta; o fetch do browser mostraria «HTML em vez de JSON»).
 */
if (isset($_GET['installer_status']) && ($_SERVER['REQUEST_METHOD'] ?? '') === 'GET') {
	header('Content-Type: application/json; charset=utf-8');
	$dbSql = __DIR__ . DIRECTORY_SEPARATOR . 'db.sql';
	$configDir = __DIR__ . DIRECTORY_SEPARATOR . 'config';
	$state = instaladorReadState();
	echo json_encode([
		'success' => true,
		'requirements' => [
			'php_version_ok' => version_compare(PHP_VERSION, '7.4.0', '>='),
			'php_version' => PHP_VERSION,
			'ext_curl' => extension_loaded('curl'),
			'ext_pdo_mysql' => extension_loaded('pdo_mysql'),
			'ext_zip' => extension_loaded('zip'),
			'config_writable' => is_dir($configDir) ? is_writable($configDir) : is_writable(__DIR__),
			'installer_dir_writable' => is_writable(__DIR__),
		],
		'files' => [
			'db_sql' => is_file($dbSql) && is_readable($dbSql),
			'database_json' => is_file($configDir . DIRECTORY_SEPARATOR . 'database.json'),
		],
		'state' => [
			'package_extracted' => !empty($state['package_extracted']),
			'extracted_at' => $state['extracted_at'] ?? null,
		],
		'csrf_token' => $_SESSION['installer_csrf'] ?? '',
	], JSON_UNESCAPED_UNICODE);
	exit;
}

if (isset($_GET['save_db']) && ($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') {
	header('Content-Type: application/json; charset=utf-8');

	try {
		instaladorRequireCsrf();
		$input = file_get_contents('php://input');
		$dbData = json_decode($input, true);

		if (!$dbData || !is_array($dbData)) {
			throw new Exception('JSON inválido ou vazio.');
		}

		$host = instaladorSanitizeMysqlHost((string) ($dbData['host'] ?? ''));
		$user = instaladorSanitizeMysqlUser((string) ($dbData['user'] ?? ''));
		$dbName = instaladorSanitizeMysqlDatabaseName((string) ($dbData['database'] ?? ''));
		if ($host === '' || $user === '' || $dbName === '') {
			throw new Exception('Host, utilizador ou nome da base de dados inválido (use apenas caracteres permitidos).');
		}

		$password = isset($dbData['password']) && is_string($dbData['password']) ? $dbData['password'] : '';
		if (strlen($password) > 512) {
			throw new Exception('Senha demasiado longa.');
		}

		$configDir = __DIR__ . DIRECTORY_SEPARATOR . 'config';
		if (!file_exists($configDir)) {
			if (!mkdir($configDir, 0755, true)) {
				throw new Exception('Não foi possível criar o diretório config. Verifique as permissões.');
			}
		}

		$configFile = $configDir . DIRECTORY_SEPARATOR . 'database.json';

		$configDataPrev = [];
		if (file_exists($configFile)) {
			$existingContent = file_get_contents($configFile);
			if ($existingContent !== false) {
				$existingData = json_decode($existingContent, true);
				if (is_array($existingData)) {
					$configDataPrev = $existingData;
					unset($configDataPrev['codigo_sistema']);
				}
			}
		}

		$licSrc = array_key_exists('licenca', $dbData)
			? trim((string) $dbData['licenca'])
			: trim((string) ($configDataPrev['licenca'] ?? ''));
		if (strlen($licSrc) > 512) {
			throw new Exception('Campo de licença inválido.');
		}

		$panelUrlSrc = array_key_exists('panel_license_url', $dbData)
			? trim((string) $dbData['panel_license_url'])
			: trim((string) ($configDataPrev['panel_license_url'] ?? ''));

		$configData = [
			'host' => $host,
			'user' => $user,
			'password' => $password,
			'database' => $dbName,
			'licenca' => $licSrc,
		];
		if ($panelUrlSrc !== '') {
			if (strlen($panelUrlSrc) > 512) {
				throw new Exception('URL do painel demasiado longa.');
			}
			$configData['panel_license_url'] = $panelUrlSrc;
		}

		$jsonContent = json_encode($configData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
		$result = file_put_contents($configFile, $jsonContent);

		if ($result === false) {
			throw new Exception('Não foi possível salvar o arquivo. Verifique as permissões do diretório config.');
		}

		error_log('Instalador save_db: database=' . $dbName . ', host=' . $host . ', user=' . $user);

		$dbSqlCheck = __DIR__ . DIRECTORY_SEPARATOR . 'db.sql';
		$importSql = is_file($dbSqlCheck) && is_readable($dbSqlCheck);

		echo json_encode([
			'success' => true,
			'message' => 'Configuração salva com sucesso!',
			'file' => $configFile,
			'import_sql' => $importSql,
		], JSON_UNESCAPED_UNICODE);
	} catch (Exception $e) {
		http_response_code(500);
		echo json_encode([
			'success' => false,
			'message' => $e->getMessage(),
		], JSON_UNESCAPED_UNICODE);
	}

	exit;
}

if (isset($_GET['test_db']) && ($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') {
	header('Content-Type: application/json; charset=utf-8');
	try {
		instaladorRequireCsrf();
		$input = file_get_contents('php://input');
		$d = json_decode($input, true);
		if (!$d || !is_array($d)) {
			throw new Exception('JSON inválido.');
		}
		$host = instaladorSanitizeMysqlHost((string) ($d['host'] ?? ''));
		$user = instaladorSanitizeMysqlUser((string) ($d['user'] ?? ''));
		$dbName = instaladorSanitizeMysqlDatabaseName((string) ($d['database'] ?? ''));
		if ($host === '' || $user === '' || $dbName === '') {
			throw new Exception('Host, utilizador ou nome da base inválidos.');
		}
		$password = isset($d['password']) && is_string($d['password']) ? $d['password'] : '';
		$dsn = 'mysql:host=' . $host . ';charset=utf8mb4';
		$pdo = new PDO($dsn, $user, $password, [
			PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
			PDO::ATTR_TIMEOUT => 8,
		]);
		$stmt = $pdo->prepare('SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = ? LIMIT 1');
		$stmt->execute([$dbName]);
		$exists = (bool) $stmt->fetchColumn();
		echo json_encode([
			'success' => true,
			'message' => $exists
				? 'Ligação OK. A base de dados já existe (será utilizada).'
				: 'Ligação OK. A base de dados ainda não existe (será criada na importação).',
			'database_exists' => $exists,
		], JSON_UNESCAPED_UNICODE);
	} catch (Exception $e) {
		http_response_code(400);
		echo json_encode([
			'success' => false,
			'message' => $e->getMessage(),
		], JSON_UNESCAPED_UNICODE);
	}
	exit;
}

if (isset($_GET['check_progress'])) {
	header('Content-Type: application/json; charset=utf-8');
	$progressFile = __DIR__ . DIRECTORY_SEPARATOR . 'import_progress.json';
	if (file_exists($progressFile)) {
		$progress = json_decode((string) file_get_contents($progressFile), true);
		echo json_encode(is_array($progress) ? $progress : ['step' => 'idle', 'message' => 'Progresso inválido.', 'progress' => 0, 'total' => 0, 'percentage' => 0], JSON_UNESCAPED_UNICODE);
	} else {
		echo json_encode([
			'step' => 'idle',
			'message' => 'Aguardando início da importação...',
			'progress' => 0,
			'total' => 0,
			'percentage' => 0,
		], JSON_UNESCAPED_UNICODE);
	}
	exit;
}

/**
 * Caminho URL deste ficheiro para pedidos fetch (JSON).
 * Se usássemos só SCRIPT_NAME, com rewrite para index.php ou inclusão deste script,
 * o browser chamaria index.php?save_db=1 e recebia HTML em vez de JSON.
 */
function instaladorWebPathSelf(): string {
	$docRoot = isset($_SERVER['DOCUMENT_ROOT']) ? realpath((string) $_SERVER['DOCUMENT_ROOT']) : false;
	$fileReal = realpath(__FILE__);
	if ($docRoot !== false && $fileReal !== false && strpos($fileReal, $docRoot) === 0) {
		$rel = substr($fileReal, strlen($docRoot));
		$rel = str_replace('\\', '/', $rel);
		return '/' . ltrim($rel, '/');
	}
	return isset($_SERVER['SCRIPT_NAME']) ? (string) $_SERVER['SCRIPT_NAME'] : '';
}

// Instalador mínimo: só grava config/database.json localmente — sem carregar database.php / constants.php / functions.php.
$projectRoot = __DIR__;
if (!defined('SITE_NAME')) {
	define('SITE_NAME', 'Instalação');
}
if (!function_exists('formatDate')) {
	function formatDate($date, $format = 'd/m/Y') {
		if (empty($date)) {
			return '-';
		}
		try {
			$dateObj = new DateTime($date);

			return $dateObj->format($format);
		} catch (Exception $e) {
			return $date;
		}
	}
}
$missingFiles = [];

$apiKey = '';
$sistemas = [];
$erro = null;
$carregando = false;

// Função para remover diretório recursivamente
function removeDirectory($dir) {
    if (!file_exists($dir)) {
        return true;
    }
    if (!is_dir($dir)) {
        return unlink($dir);
    }
    foreach (scandir($dir) as $item) {
        if ($item == '.' || $item == '..') {
            continue;
        }
        if (!removeDirectory($dir . DIRECTORY_SEPARATOR . $item)) {
            return false;
        }
    }
    return rmdir($dir);
}

/**
 * Remove todos os ficheiros .sql sob $root (recursivo, ex.: ficheiros extraídos do ZIP).
 */
function instalador_delete_all_sql_files(string $root): int
{
    if (!is_dir($root)) {
        return 0;
    }
    $deleted = 0;
    try {
        $dirIt = new RecursiveDirectoryIterator($root, FilesystemIterator::SKIP_DOTS);
        $it = new RecursiveIteratorIterator($dirIt, RecursiveIteratorIterator::CHILD_FIRST);
        foreach ($it as $file) {
            if ($file->isFile() && strcasecmp(substr($file->getFilename(), -4), '.sql') === 0) {
                if (@unlink($file->getPathname())) {
                    $deleted++;
                }
            }
        }
    } catch (Throwable $e) {
        error_log('instalador_delete_all_sql_files: ' . $e->getMessage());
    }

    return $deleted;
}

// Cancelar instalação: apaga tudo em __DIR__ exceto o próprio instalador (ficheiro actual)
if (isset($_GET['limpar_pasta_instalacao']) && $_SERVER['REQUEST_METHOD'] === 'POST') {
    header('Content-Type: application/json; charset=UTF-8');
    try {
        instaladorRequireCsrf();
        unset($_SESSION['installer']);
        $root = realpath(__DIR__);
        if ($root === false) {
            throw new Exception('Não foi possível resolver o diretório da instalação.');
        }
        $manter = basename(__FILE__);
        $entries = @scandir($root);
        if ($entries === false) {
            throw new Exception('Não foi possível listar a pasta.');
        }
        foreach ($entries as $name) {
            if ($name === '.' || $name === '..') {
                continue;
            }
            if (strcasecmp($name, $manter) === 0) {
                continue;
            }
            $path = $root . DIRECTORY_SEPARATOR . $name;
            if (is_dir($path)) {
                if (!removeDirectory($path)) {
                    throw new Exception('Não foi possível remover a pasta: ' . $name);
                }
            } else {
                if (!@unlink($path)) {
                    throw new Exception('Não foi possível remover o ficheiro: ' . $name);
                }
            }
        }
        echo json_encode([
            'success' => true,
            'message' => 'Pasta limpa. Mantido apenas ' . $manter . '.',
        ], JSON_UNESCAPED_UNICODE);
    } catch (Exception $e) {
        http_response_code(500);
        echo json_encode([
            'success' => false,
            'message' => $e->getMessage(),
        ], JSON_UNESCAPED_UNICODE);
    }
    exit;
}

if (isset($_GET['import_sql']) && $_SERVER['REQUEST_METHOD'] === 'POST') {
    header('Content-Type: application/json');
    
    try {
        instaladorRequireCsrf();
        // Ler configuração do database.json
        $configFile = __DIR__ . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'database.json';
        
        if (!file_exists($configFile)) {
            throw new Exception('Arquivo de configuração não encontrado. Configure o banco de dados primeiro.');
        }
        
        $configContent = file_get_contents($configFile);
        $config = json_decode($configContent, true);
        
        if (!$config) {
            throw new Exception('Erro ao ler arquivo de configuração.');
        }
        
        // Validar configuração (anti injeção em identificadores SQL)
        $config['host'] = instaladorSanitizeMysqlHost((string) ($config['host'] ?? ''));
        $config['user'] = instaladorSanitizeMysqlUser((string) ($config['user'] ?? ''));
        $config['database'] = instaladorSanitizeMysqlDatabaseName((string) ($config['database'] ?? ''));
        if ($config['host'] === '' || $config['user'] === '' || $config['database'] === '') {
            throw new Exception('Configuração de banco de dados inválida ou incompleta.');
        }
        
        // SQL enviado no ZIP (à frente do instalador), nome convencionado: db.sql
        $sqlFile = __DIR__ . DIRECTORY_SEPARATOR . 'db.sql';
        if (!file_exists($sqlFile) || !is_readable($sqlFile)) {
            throw new Exception('Ficheiro db.sql não encontrado nesta pasta (deve vir no pacote ZIP).');
        }
        
        // Ler conteúdo do arquivo SQL
        $sqlContent = file_get_contents($sqlFile);
        
        if (empty($sqlContent)) {
            throw new Exception('Arquivo SQL está vazio.');
        }
        
        // Função para atualizar progresso
        $progressFile = __DIR__ . DIRECTORY_SEPARATOR . 'import_progress.json';
        function updateProgress($step, $message, $progress = 0, $total = 0) {
            global $progressFile;
            $data = [
                'step' => $step,
                'message' => $message,
                'progress' => $progress,
                'total' => $total,
                'percentage' => $total > 0 ? round(($progress / $total) * 100, 1) : 0,
                'timestamp' => time()
            ];
            file_put_contents($progressFile, json_encode($data, JSON_UNESCAPED_UNICODE));
        }
        
        // Limpar arquivo de progresso anterior
        if (file_exists($progressFile)) {
            unlink($progressFile);
        }
        
        updateProgress('connecting', 'Conectando ao servidor MySQL...', 0, 0);
        
        // Conectar ao MySQL
        try {
            $dsn = "mysql:host={$config['host']};charset=utf8mb4";
            $pdo = new PDO($dsn, $config['user'], $config['password'] ?? '', [
                PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
                PDO::ATTR_TIMEOUT => 10
            ]);
            updateProgress('connected', 'Conexão estabelecida com sucesso!', 1, 10);
        } catch (PDOException $e) {
            updateProgress('error', 'Erro ao conectar: ' . $e->getMessage(), 0, 0);
            throw new Exception('Erro ao conectar ao MySQL: ' . $e->getMessage());
        }
        
        // Verificar se o banco de dados existe
        updateProgress('checking_db', 'Verificando se o banco de dados existe...', 2, 10);
        try {
            $chkDb = $pdo->prepare('SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = ? LIMIT 1');
            $chkDb->execute([$config['database']]);
            $dbExists = (bool) $chkDb->fetchColumn();
            
            if ($dbExists) {
                updateProgress('db_exists', 'Banco de dados já existe.', 3, 10);
            } else {
                updateProgress('creating_db', 'Criando banco de dados...', 3, 10);
                $pdo->exec("CREATE DATABASE `{$config['database']}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
                updateProgress('db_created', 'Banco de dados criado com sucesso!', 4, 10);
            }
        } catch (PDOException $e) {
            updateProgress('error', 'Erro ao verificar/criar banco: ' . $e->getMessage(), 0, 0);
            throw new Exception('Erro ao verificar/criar banco de dados: ' . $e->getMessage());
        }
        
        // Selecionar o banco de dados
        updateProgress('selecting_db', 'Selecionando banco de dados...', 4, 10);
        try {
            $pdo->exec("USE `{$config['database']}`");
            updateProgress('db_selected', 'Banco de dados selecionado.', 5, 10);
        } catch (PDOException $e) {
            updateProgress('error', 'Erro ao selecionar banco: ' . $e->getMessage(), 0, 0);
            throw new Exception('Erro ao selecionar banco de dados: ' . $e->getMessage());
        }
        
        updateProgress('parsing_sql', 'Processando arquivo SQL...', 6, 10);
        
        // Processar arquivo SQL
        // Primeiro, processar DELIMITER e blocos com delimitadores alternativos
        $lines = explode("\n", $sqlContent);
        $processedLines = [];
        $inDelimiterBlock = false;
        $currentDelimiter = ';';
        $currentBlock = '';
        
        foreach ($lines as $lineNum => $line) {
            $trimmedLine = trim($line);
            
            // Detectar comandos DELIMITER
            if (preg_match('/^\s*DELIMITER\s+(.+)$/i', $trimmedLine, $matches)) {
                $newDelimiter = trim($matches[1]);
                // Se já estávamos em um bloco, finalizar o bloco anterior
                if ($inDelimiterBlock && !empty($currentBlock)) {
                    $processedLines[] = rtrim($currentBlock) . ';';
                    $currentBlock = '';
                }
                
                if ($newDelimiter === '$$' || $newDelimiter === '//' || $newDelimiter === ';;') {
                    $inDelimiterBlock = true;
                    $currentDelimiter = $newDelimiter;
                } else {
                    // Voltar para delimitador padrão
                    $inDelimiterBlock = false;
                    $currentDelimiter = ';';
                }
                continue; // Não adicionar linha DELIMITER
            }
            
            // Se estamos em um bloco com delimitador alternativo
            if ($inDelimiterBlock) {
                // Verificar se é o fim do bloco (delimitador no final da linha)
                $delimiterPattern = preg_quote($currentDelimiter, '/');
                if (preg_match('/' . $delimiterPattern . '\s*$/', $trimmedLine)) {
                    // Remover delimitador e adicionar ao bloco
                    $lineWithoutDelimiter = preg_replace('/' . $delimiterPattern . '\s*$/', '', $trimmedLine);
                    $currentBlock .= $lineWithoutDelimiter . "\n";
                    // Finalizar bloco
                    $processedLines[] = rtrim($currentBlock) . ';';
                    $currentBlock = '';
                    $inDelimiterBlock = false;
                    $currentDelimiter = ';';
                } else {
                    // Continuar acumulando o bloco
                    $currentBlock .= $line . "\n";
                }
            } else {
                $processedLines[] = $line;
            }
        }
        
        // Se ainda há um bloco aberto, finalizar
        if ($inDelimiterBlock && !empty($currentBlock)) {
            $processedLines[] = rtrim($currentBlock) . ';';
        }
        
        $sqlContent = implode("\n", $processedLines);
        
        // Remover comentários condicionais do MySQL (/*!40101 ... */) - processar o conteúdo
        $sqlContent = preg_replace_callback('/\/\*!\d+\s+(.*?)\s*\*\//s', function($matches) {
            return $matches[1]; // Manter apenas o conteúdo SQL
        }, $sqlContent);
        
        // Remover comentários de linha (-- comentário)
        $sqlContent = preg_replace('/--.*$/m', '', $sqlContent);
        
        // Remover comentários de bloco (/* comentário */) - mas manter os condicionais já processados
        $sqlContent = preg_replace('/\/\*[^!].*?\*\//s', '', $sqlContent);
        
        // Remover comandos SET que podem causar problemas
        $sqlContent = preg_replace('/^\s*SET\s+SQL_MODE\s*=.*?;?\s*$/mi', '', $sqlContent);
        $sqlContent = preg_replace('/^\s*SET\s+time_zone\s*=.*?;?\s*$/mi', '', $sqlContent);
        $sqlContent = preg_replace('/^\s*START\s+TRANSACTION\s*;?\s*$/mi', '', $sqlContent);
        $sqlContent = preg_replace('/^\s*COMMIT\s*;?\s*$/mi', '', $sqlContent);
        
        // Normalizar quebras de linha
        $sqlContent = str_replace(["\r\n", "\r"], "\n", $sqlContent);
        
        // Dividir o SQL em comandos individuais usando uma abordagem mais simples e robusta
        $sqlCommands = [];
        $currentCommand = '';
        $inString = false;
        $stringChar = '';
        $depth = 0;
        $inBeginBlock = false;
        
        $len = strlen($sqlContent);
        for ($i = 0; $i < $len; $i++) {
            $char = $sqlContent[$i];
            $prevChar = ($i > 0) ? $sqlContent[$i - 1] : '';
            $nextChars = ($i < $len - 1) ? substr($sqlContent, $i, min(20, $len - $i)) : '';
            
            // Detectar início de string
            if (!$inString && ($char === '"' || $char === "'" || $char === '`')) {
                $inString = true;
                $stringChar = $char;
                $currentCommand .= $char;
            }
            // Detectar fim de string
            elseif ($inString && $char === $stringChar) {
                // Verificar se não é escape (contar escapes consecutivos)
                $escapeCount = 0;
                $j = $i - 1;
                while ($j >= 0 && $sqlContent[$j] === '\\') {
                    $escapeCount++;
                    $j--;
                }
                if ($escapeCount % 2 === 0) {
                    $inString = false;
                    $stringChar = '';
                }
                $currentCommand .= $char;
            }
            // Processar caracteres fora de strings
            elseif (!$inString) {
                // Detectar BEGIN
                if (preg_match('/\bBEGIN\b/i', $nextChars)) {
                    $inBeginBlock = true;
                }
                
                // Contar parênteses
                if ($char === '(') {
                    $depth++;
                    $currentCommand .= $char;
                } elseif ($char === ')') {
                    $depth--;
                    $currentCommand .= $char;
                }
                // Processar ponto e vírgula
                elseif ($char === ';') {
                    // Se estamos em um bloco BEGIN, verificar se há END
                    if ($inBeginBlock && $depth === 0) {
                        $remaining = substr($sqlContent, $i + 1, 500);
                        // Procurar por END seguido de ponto e vírgula
                        if (preg_match('/^\s*\bEND\b\s*;/i', $remaining)) {
                            // Continuar acumulando até encontrar END;
                            $currentCommand .= $char;
                            continue;
                        }
                    }
                    
                    // Dividir apenas se não estiver dentro de parênteses ou blocos BEGIN
                    if ($depth === 0 && !$inBeginBlock) {
                        $cmd = trim($currentCommand);
                        // Validar comando antes de adicionar
                        if (!empty($cmd) && strlen($cmd) > 3 && 
                            !preg_match('/^(USE|SET\s+@OLD_|SET\s+NAMES)/i', $cmd)) {
                            $sqlCommands[] = $cmd;
                        }
                        $currentCommand = '';
                    } else {
                        $currentCommand .= $char;
                    }
                }
                // Detectar END em blocos BEGIN
                elseif ($inBeginBlock && $depth === 0 && preg_match('/\bEND\b/i', $nextChars)) {
                    // Adicionar END
                    $currentCommand .= $char;
                    // Avançar até o final de END
                    $i++;
                    while ($i < $len && preg_match('/[A-Za-z0-9_]/', $sqlContent[$i])) {
                        $currentCommand .= $sqlContent[$i];
                        $i++;
                    }
                    $i--; // Ajustar para o loop principal
                    
                    // Pular espaços
                    while ($i + 1 < $len && ctype_space($sqlContent[$i + 1])) {
                        $i++;
                    }
                    
                    // Se há ponto e vírgula, finalizar comando
                    if ($i + 1 < $len && $sqlContent[$i + 1] === ';') {
                        $currentCommand .= ';';
                        $i++; // Pular o ;
                        $cmd = trim($currentCommand);
                        if (!empty($cmd) && strlen($cmd) > 3) {
                            $sqlCommands[] = $cmd;
                        }
                        $currentCommand = '';
                        $inBeginBlock = false;
                    } else {
                        $inBeginBlock = false;
                    }
                } else {
                    $currentCommand .= $char;
                }
            } else {
                $currentCommand .= $char;
            }
        }
        
        // Adicionar último comando se houver
        $lastCmd = trim($currentCommand);
        if (!empty($lastCmd) && strlen($lastCmd) > 3 && 
            !preg_match('/^(USE|SET\s+@OLD_|SET\s+NAMES)/i', $lastCmd)) {
            $sqlCommands[] = $lastCmd;
        }
        
        // Filtrar e validar comandos
        $sqlCommands = array_filter($sqlCommands, function($cmd) {
            $cmd = trim($cmd);
            // Remover comandos muito curtos, vazios ou inválidos
            if (empty($cmd) || strlen($cmd) < 4) {
                return false;
            }
            // Ignorar comandos problemáticos
            if (preg_match('/^(USE\s+|SET\s+@OLD_|SET\s+NAMES|COMMIT|START\s+TRANSACTION)/i', $cmd)) {
                return false;
            }
            // Verificar se não contém apenas espaços, quebras de linha ou caracteres especiais
            if (preg_match('/^[\s\n\r;]+$/', $cmd)) {
                return false;
            }
            // Verificar se contém pelo menos uma palavra-chave SQL válida
            $validKeywords = ['CREATE', 'INSERT', 'UPDATE', 'DELETE', 'ALTER', 'DROP', 'SELECT', 'TRIGGER', 'VIEW'];
            $hasValidKeyword = false;
            foreach ($validKeywords as $keyword) {
                if (stripos($cmd, $keyword) !== false) {
                    $hasValidKeyword = true;
                    break;
                }
            }
            if (!$hasValidKeyword) {
                return false;
            }
            // Verificar se não termina apenas com ponto e vírgula ou espaços
            $cmdWithoutSpaces = trim($cmd, " \t\n\r\0\x0B;");
            if (empty($cmdWithoutSpaces)) {
                return false;
            }
            // Verificar se não contém apenas caracteres não imprimíveis
            if (!preg_match('/[a-zA-Z0-9_`\(\)]/', $cmd)) {
                return false;
            }
            return true;
        });
        
        // Reindexar array
        $sqlCommands = array_values($sqlCommands);
        
        $totalCommands = count($sqlCommands);
        updateProgress('parsed', "Arquivo SQL processado. {$totalCommands} comandos encontrados.", 7, 10);
        
        if ($totalCommands === 0) {
            updateProgress('error', 'Nenhum comando SQL válido encontrado no arquivo.', 0, 0);
            throw new Exception('Nenhum comando SQL válido encontrado no arquivo.');
        }
        
        $executed = 0;
        $errors = [];
        
        updateProgress('executing', "Executando comandos SQL... (0/{$totalCommands})", 0, $totalCommands);
        
        // Executar cada comando SQL
        foreach ($sqlCommands as $index => $command) {
            $command = trim($command);
            if (empty($command)) {
                error_log("Comando vazio encontrado no índice: " . $index);
                continue;
            }
            
            // Validação adicional antes de executar
            if (strlen($command) < 4) {
                error_log("Comando muito curto ignorado: " . substr($command, 0, 50));
                continue;
            }
            
            // Validar comando antes de executar
            $cmdTrimmed = trim($command);
            if (empty($cmdTrimmed) || strlen($cmdTrimmed) < 4) {
                error_log("Comando vazio ou muito curto ignorado no índice: " . $index);
                continue;
            }
            
            // Verificar se contém apenas espaços ou caracteres especiais
            if (preg_match('/^[\s\n\r\t;]+$/', $cmdTrimmed)) {
                error_log("Comando contém apenas espaços/caracteres especiais ignorado no índice: " . $index);
                continue;
            }
            
            // Atualizar progresso
            $currentProgress = $index + 1;
            $percentage = round(($currentProgress / $totalCommands) * 100, 1);
            updateProgress('executing', "Executando comando {$currentProgress}/{$totalCommands} ({$percentage}%)", $currentProgress, $totalCommands);
            
            try {
                $pdo->exec($command);
                $executed++;
            } catch (PDOException $e) {
                $errorMsg = $e->getMessage();
                $errorCode = $e->getCode();
                
                // Ignorar erros comuns que não são críticos
                $ignoreErrors = [
                    'already exists',
                    'Duplicate',
                    'Unknown database',
                    'Duplicate key',
                    'Table',
                    'already exist',
                    'Unknown column',
                    'Duplicate entry',
                    'Multiple primary key',
                    'Duplicate column name',
                    'Can\'t DROP',
                    'doesn\'t exist'
                ];
                
                $shouldIgnore = false;
                foreach ($ignoreErrors as $ignorePattern) {
                    if (stripos($errorMsg, $ignorePattern) !== false) {
                        $shouldIgnore = true;
                        break;
                    }
                }
                
                // Ignorar erros de chave primária múltipla (código 1068)
                if (stripos($errorMsg, "Multiple primary key") !== false || 
                    stripos($errorMsg, "1068") !== false ||
                    (stripos($errorMsg, "SQLSTATE[42000]") !== false && stripos($errorMsg, "1068") !== false)) {
                    $shouldIgnore = true;
                    error_log("Erro de chave primária múltipla ignorado (comando #{$index}) - provavelmente a PK já existe");
                }
                
                // Ignorar erros de sintaxe em comandos vazios ou muito curtos
                if (stripos($errorMsg, "near ''") !== false || 
                    stripos($errorMsg, "near '' at line") !== false ||
                    (stripos($errorMsg, "You have an error in your SQL syntax") !== false && 
                    (stripos($errorMsg, "near ''") !== false || strlen($cmdTrimmed) < 10))) {
                    $shouldIgnore = true;
                    error_log("Erro de sintaxe em comando vazio/inválido ignorado (comando #{$index})");
                }
                
                if (!$shouldIgnore) {
                    $errors[] = $errorMsg;
                    $commandPreview = substr($command, 0, 150);
                    error_log("Erro SQL (comando #{$index}): " . $errorMsg);
                    error_log("Comando: " . $commandPreview);
                }
            }
        }
        
        // Se houver erros, verificar se a maioria dos comandos foi executada com sucesso
        if (!empty($errors)) {
            $errorCount = count($errors);
            $successRate = $totalCommands > 0 ? ($executed / $totalCommands) * 100 : 0;
            
            // Se mais de 80% dos comandos foram executados com sucesso, considerar como sucesso
            if ($successRate >= 80) {
                // Log de aviso mas não falha - a maioria foi executada
                error_log("Aviso: " . $errorCount . " erros durante importação SQL, mas " . round($successRate, 1) . "% dos comandos foram executados com sucesso");
                // Não lançar exceção, continuar como sucesso
            } elseif ($errorCount > 10 && $successRate < 50) {
                // Se muitos erros e taxa de sucesso muito baixa, falhar
                updateProgress('error', 'Muitos erros ao executar SQL: ' . implode('; ', array_slice($errors, 0, 3)), 0, 0);
                throw new Exception('Muitos erros ao executar SQL: ' . implode('; ', array_slice($errors, 0, 3)));
            } else {
                // Poucos erros ou taxa de sucesso razoável, apenas log
                error_log("Aviso: " . $errorCount . " erros durante importação SQL (não críticos). Taxa de sucesso: " . round($successRate, 1) . "%");
            }
        }
        
        updateProgress('completed', "Importação concluída! {$executed} comandos executados com sucesso.", $executed > 0 ? $executed : 1, max($totalCommands, 1));

        $sqlRemoved = instalador_delete_all_sql_files(__DIR__);
        $legacyDatabaseDir = __DIR__ . DIRECTORY_SEPARATOR . 'database';
        $removedLegacyDbDir = false;
        if (is_dir($legacyDatabaseDir)) {
            $removedLegacyDbDir = removeDirectory($legacyDatabaseDir);
        }

        // Não apagar import_progress.json aqui: o browser faz polling e precisa de ler step=completed.
        // Apagar progress + instalador só no shutdown, depois da resposta JSON ser enviada.
        $instaladorPath = __FILE__;
        $progressFileShutdown = $progressFile;
        register_shutdown_function(static function () use ($instaladorPath, $progressFileShutdown): void {
            if (is_string($progressFileShutdown) && $progressFileShutdown !== '' && file_exists($progressFileShutdown)) {
                @unlink($progressFileShutdown);
            }
            if ($instaladorPath !== '' && file_exists($instaladorPath)) {
                @unlink($instaladorPath);
            }
        });

        echo json_encode([
            'success' => true,
            'message' => 'Banco de dados criado e importado com sucesso a partir de db.sql.',
            'executed' => $executed,
            'total' => $totalCommands,
            'database' => $config['database'],
            'cleaned' => [
                'sql_files' => $sqlRemoved,
                'legacy_database_dir' => $removedLegacyDbDir,
                'instalador_scheduled' => true,
            ],
        ]);
        
    } catch (Exception $e) {
        http_response_code(500);
        echo json_encode([
            'success' => false,
            'message' => $e->getMessage()
        ]);
    }
    
    exit;
}

// Endpoint para limpeza e finalização
if (isset($_GET['finalizar']) && $_SERVER['REQUEST_METHOD'] === 'POST') {
    header('Content-Type: application/json');
    
    try {
        instaladorRequireCsrf();
        $removedDatabase = false;
        $removedInstalador = false;
        
        // Remover pasta database
        $databaseDir = __DIR__ . DIRECTORY_SEPARATOR . 'database';
        if (file_exists($databaseDir) && is_dir($databaseDir)) {
            try {
                $removedDatabase = removeDirectory($databaseDir);
                if ($removedDatabase) {
                    error_log("Pasta 'database' removida com sucesso.");
                } else {
                    error_log("Aviso: Não foi possível remover a pasta 'database'.");
                }
            } catch (Exception $e) {
                error_log("Erro ao remover pasta 'database': " . $e->getMessage());
            }
        }
        
        // Remover arquivo import_progress.json
        $progressFile = __DIR__ . DIRECTORY_SEPARATOR . 'import_progress.json';
        $removedProgress = false;
        if (file_exists($progressFile)) {
            try {
                if (unlink($progressFile)) {
                    error_log("Arquivo 'import_progress.json' removido com sucesso.");
                    $removedProgress = true;
                } else {
                    error_log("Aviso: Não foi possível remover o arquivo 'import_progress.json'.");
                }
            } catch (Exception $e) {
                error_log("Erro ao remover arquivo 'import_progress.json': " . $e->getMessage());
            }
        }
        
        // Remover arquivo instalador.php
        $instaladorFile = __FILE__;
        if (file_exists($instaladorFile)) {
            try {
                error_log("Removendo arquivo 'instalador.php'.");
                // Marcar para remoção após enviar resposta
                register_shutdown_function(function() use ($instaladorFile) {
                    if (file_exists($instaladorFile)) {
                        @unlink($instaladorFile);
                    }
                });
                $removedInstalador = true;
            } catch (Exception $e) {
                error_log("Erro ao marcar 'instalador.php' para remoção: " . $e->getMessage());
            }
        }
        
        echo json_encode([
            'success' => true,
            'message' => 'Limpeza concluída com sucesso!',
            'cleaned' => [
                'database_dir' => $removedDatabase,
                'import_progress_file' => $removedProgress,
                'instalador_file' => $removedInstalador
            ]
        ]);
        
    } catch (Exception $e) {
        http_response_code(500);
        echo json_encode([
            'success' => false,
            'message' => $e->getMessage()
        ]);
    }
    
    exit;
}

// Função auxiliar para sanitizar (definir antes de usar)
if (!function_exists('sanitize')) {
    function sanitize($input) {
        if (is_null($input)) return '';
        return htmlspecialchars(strip_tags(trim($input)), ENT_QUOTES, 'UTF-8');
    }
}

// Painel remoto (para descarregar o ZIP)
$__instautoPanelBase = 'https://eamsystems.shop/public';
if ($__instautoPanelBase !== null && is_string($__instautoPanelBase) && trim($__instautoPanelBase) !== '') {
    $panelBaseUrl = rtrim(trim($__instautoPanelBase), '/');
} else {
    $env = getenv('NOVAERA_PANEL_URL');
    $panelBaseUrl = is_string($env) && trim($env) !== '' ? rtrim(trim($env), '/') : 'https://eamsystems.shop';
}
$panelBaseUrl = rtrim($panelBaseUrl, '/');

/**
 * Decodifica JSON da API do painel (BOM UTF-8, espaços; extrai o primeiro objeto {…} se houver texto extra).
 *
 * @return array<string,mixed>|null
 */
function instaladorDecodeJsonRespostaPainel(string $raw): ?array
{
    $s = trim($raw);
    if ($s === '') {
        return null;
    }
    if (strncmp($s, "\xEF\xBB\xBF", 3) === 0) {
        $s = substr($s, 3);
    }
    $decoded = json_decode($s, true);
    if (is_array($decoded)) {
        return $decoded;
    }
    $start = strpos($s, '{');
    if ($start === false) {
        return null;
    }
    $depth = 0;
    $len = strlen($s);
    for ($i = $start; $i < $len; $i++) {
        $c = $s[$i];
        if ($c === '{') {
            $depth++;
        } elseif ($c === '}') {
            $depth--;
            if ($depth === 0) {
                $slice = substr($s, $start, $i - $start + 1);
                $decoded = json_decode($slice, true);
                return is_array($decoded) ? $decoded : null;
            }
        }
    }
    return null;
}

/**
 * URLs a tentar para o caminho de API do painel (com e sem /public).
 *
 * @return list<string>
 */
function instaladorMontarUrlsPainelApiPath(string $panelBasePleno, string $pathApi): array
{
    $pathApi = '/' . ltrim($pathApi, '/');
    $b = rtrim(trim($panelBasePleno), '/');
    $list = [$b . $pathApi];
    if (!str_ends_with(strtolower($b), '/public')) {
        $list[] = $b . '/public' . $pathApi;
    }
    return array_values(array_unique($list));
}

// Download e extração — apenas POST + CSRF; API Key vem do body (preferido) ou da sessão.
if (isset($_GET['download_extract']) && ($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') {
    error_reporting(E_ALL);
    ini_set('display_errors', 0);
    ini_set('log_errors', 1);

    header('Content-Type: application/json; charset=utf-8');

    try {
        instaladorRequireCsrf();

        $dbSqlGuard = __DIR__ . DIRECTORY_SEPARATOR . 'db.sql';
        $temSql = is_file($dbSqlGuard) && is_readable($dbSqlGuard) && filesize($dbSqlGuard) > 16;
        if ($temSql) {
            instaladorWriteStateMerge(['package_extracted' => true, 'extracted_at' => time()]);
            throw new Exception('O ficheiro db.sql já existe nesta pasta. Avance para "Base de dados" — não é necessário descarregar de novo.');
        }

        $rawBody = (string) file_get_contents('php://input');
        $bodyJson = json_decode($rawBody, true);
        $sistemaCodigo = 'pacote';
        if (is_array($bodyJson) && isset($bodyJson['sistema'])) {
            $sistemaCodigo = preg_replace('/[^a-zA-Z0-9_-]/', '', trim((string) $bodyJson['sistema']));
        }
        if ($sistemaCodigo === '') {
            $sistemaCodigo = 'pacote';
        }

        $apiKeyDownload = '';
        if (is_array($bodyJson) && isset($bodyJson['api_key']) && is_string($bodyJson['api_key'])) {
            $apiKeyDownload = trim($bodyJson['api_key']);
        }
        if ($apiKeyDownload === '' && !empty($_SESSION['installer']['api_key']) && is_string($_SESSION['installer']['api_key'])) {
            $apiKeyDownload = trim($_SESSION['installer']['api_key']);
        }

        if ($apiKeyDownload === '') {
            http_response_code(401);
            echo json_encode([
                'success' => false,
                'message' => 'API Key em falta para descarregar o ZIP.',
            ], JSON_UNESCAPED_UNICODE);
            exit;
        }

        $issueBody = json_encode(['key' => $apiKeyDownload]);
        if ($issueBody === false) {
            throw new Exception('Erro ao preparar o pedido de download.');
        }

        $resolvedDownloadUrl = '';
        $lastIssueMessage = '';

        foreach (instaladorMontarUrlsPainelApiPath($panelBaseUrl, '/api/download/issue') as $issueUrl) {
            $ch = curl_init();
            curl_setopt_array($ch, [
                CURLOPT_URL => $issueUrl,
                CURLOPT_POST => true,
                CURLOPT_POSTFIELDS => $issueBody,
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_FOLLOWLOCATION => true,
                CURLOPT_ENCODING => '',
                CURLOPT_SSL_VERIFYPEER => true,
                CURLOPT_SSL_VERIFYHOST => 2,
                CURLOPT_TIMEOUT => 120,
                CURLOPT_CONNECTTIMEOUT => 15,
                CURLOPT_HTTPHEADER => [
                    'Content-Type: application/json',
                    'Accept: application/json',
                ],
                CURLOPT_USERAGENT => 'NovaEra-Installer/1.0',
            ]);
            $issueResponse = curl_exec($ch);
            $issueHttp = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
            $issueErr = curl_error($ch);
            curl_close($ch);

            if ($issueResponse === false || $issueErr !== '') {
                $lastIssueMessage = 'Erro ao contactar o painel para emitir o download: ' . ($issueErr !== '' ? $issueErr : 'sem resposta');
                continue;
            }

            /** @var array<string,mixed>|null $issueData */
            $issueData = instaladorDecodeJsonRespostaPainel((string) $issueResponse);

            if ($issueHttp >= 200 && $issueHttp < 300 && is_array($issueData)) {
                $resolvedDownloadUrl = trim((string) ($issueData['download_url'] ?? ''));
                if ($resolvedDownloadUrl !== '') {
                    break;
                }
                $msg = trim((string) ($issueData['error'] ?? $issueData['message'] ?? ''));
                throw new Exception($msg !== '' ? $msg : 'Resposta do painel sem link do ficheiro ZIP.');
            }

            if (is_array($issueData)) {
                $lastIssueMessage = trim((string) ($issueData['error'] ?? $issueData['message'] ?? ''));
                if ($lastIssueMessage === '') {
                    $lastIssueMessage = 'O painel recusou o pedido de download (HTTP ' . $issueHttp . ').';
                }
            } else {
                $trimBody = ltrim((string) $issueResponse);
                $pareceHtml = $trimBody !== '' && ($trimBody[0] === '<' || stripos($trimBody, '<!doctype') === 0);
                $lastIssueMessage = 'Resposta inválida do painel (JSON). HTTP ' . $issueHttp . '.';
                if ($pareceHtml) {
                    $lastIssueMessage .= ' O servidor devolveu HTML em vez da API; tente a URL base com /public.';
                }
            }
        }

        if ($resolvedDownloadUrl === '') {
            throw new Exception($lastIssueMessage !== '' ? $lastIssueMessage : 'Não foi possível obter o link de download do painel.');
        }

        $fileUrl = $resolvedDownloadUrl;

        $tempZipFile = __DIR__ . DIRECTORY_SEPARATOR . $sistemaCodigo . '_' . time() . '.zip';
        $fp = fopen($tempZipFile, 'wb');
        if ($fp === false) {
            throw new Exception('Não foi possível criar ficheiro temporário para o ZIP.');
        }

        $ch = curl_init();
        curl_setopt_array($ch, [
            CURLOPT_URL => $fileUrl,
            CURLOPT_FILE => $fp,
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_SSL_VERIFYHOST => 2,
            CURLOPT_TIMEOUT => 600,
        ]);
        $dlOk = curl_exec($ch);
        $dlHttp = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $dlErr = curl_error($ch);
        curl_close($ch);
        fclose($fp);

        if ($dlOk === false || $dlErr !== '') {
            @unlink($tempZipFile);
            throw new Exception('Erro ao descarregar o ZIP: ' . ($dlErr !== '' ? $dlErr : 'falha de rede'));
        }
        if ($dlHttp < 200 || $dlHttp >= 300) {
            @unlink($tempZipFile);
            throw new Exception('Erro HTTP ' . $dlHttp . ' ao descarregar o ficheiro ZIP.');
        }

        $zipSig = @file_get_contents($tempZipFile, false, null, 0, 2);
        if ($zipSig === false || $zipSig !== 'PK') {
            @unlink($tempZipFile);
            throw new Exception('Ficheiro recebido não é um ZIP válido.');
        }

        $saveResult = false;

        if (!class_exists('ZipArchive')) {
            unlink($tempZipFile);
            throw new Exception('Extensão ZipArchive não está disponível no servidor.');
        }

        $zip = new ZipArchive();
        $result = $zip->open($tempZipFile);
        if ($result !== TRUE) {
            unlink($tempZipFile);
            throw new Exception('Erro ao abrir arquivo ZIP. Código: ' . (string) $result);
        }

        $extractDir = __DIR__;
        $extractedFiles = instaladorZipExtractSafe($zip, $extractDir);
        $zip->close();

        unlink($tempZipFile);

        instaladorWriteStateMerge([
            'package_extracted' => true,
            'extracted_at' => time(),
            'sistema' => $sistemaCodigo,
        ]);

        // Se ainda nao existir database.json, criar esqueleto
        $configDir = __DIR__ . DIRECTORY_SEPARATOR . 'config';
        $configFile = $configDir . DIRECTORY_SEPARATOR . 'database.json';
        if (!file_exists($configFile)) {
            if (!file_exists($configDir)) {
                if (!mkdir($configDir, 0755, true)) {
                    error_log('Aviso: Não foi possível criar o diretório config.');
                }
            }
            $configData = [
                'host' => 'localhost',
                'user' => 'root',
                'password' => '',
                'database' => '',
                'licenca' => $apiKeyDownload,
            ];
            $jsonContent = json_encode($configData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
            $saveResult = file_put_contents($configFile, $jsonContent);
            if ($saveResult === false) {
                error_log('Aviso: Não foi possível criar o database.json após o download.');
            }
        }

        echo json_encode([
            'success' => true,
            'message' => 'Sistema instalado com sucesso!',
            'sistema' => $sistemaCodigo,
            'diretorio' => $extractDir,
            'arquivos' => array_slice($extractedFiles, 0, 20),
            'database_json_updated' => ($saveResult !== false),
        ], JSON_UNESCAPED_UNICODE);

    } catch (Throwable $e) {
        http_response_code(200);
        echo json_encode([
            'success' => false,
            'message' => $e->getMessage(),
        ], JSON_UNESCAPED_UNICODE);
    }

    exit;
}

$dbSqlPathInstalador = __DIR__ . DIRECTORY_SEPARATOR . 'db.sql';
$dbSqlPresenteNoInstalador = is_file($dbSqlPathInstalador) && is_readable($dbSqlPathInstalador)
	&& filesize($dbSqlPathInstalador) > 16;
$autoDbNomeDaPastaInstalador = basename(__DIR__);

// Um único ecrã: base de dados (e texto único de requisitos)
$installerStep = 'database';

$installerCsrfToken = isset($_SESSION['installer_csrf']) && is_string($_SESSION['installer_csrf'])
    ? $_SESSION['installer_csrf']
    : '';

$dbNombreHintForJs = $autoDbNomeDaPastaInstalador;
$dbFormPrefill = [
	'host' => 'localhost',
	'user' => 'root',
	'password' => '',
	'database' => '',
	'licenca' => '',
	'panel_license_url' => '',
];
$pJsonProbe = __DIR__ . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'database.json';
if (is_readable($pJsonProbe)) {
	$pj = json_decode((string) file_get_contents($pJsonProbe), true);
	if (is_array($pj)) {
		foreach (['host', 'user', 'password', 'database', 'licenca', 'panel_license_url'] as $k) {
			if (isset($pj[$k]) && is_string($pj[$k])) {
				$dbFormPrefill[$k] = $pj[$k];
			}
		}
	}
}
?>
<!DOCTYPE html>
<html lang="pt-BR">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title><?php echo htmlspecialchars(SITE_NAME, ENT_QUOTES, 'UTF-8'); ?></title>
    <?php
    $__instaladorScriptSelf = instaladorWebPathSelf();
    ?>
    <script>
    window.__INSTALADOR_SELF = <?php echo json_encode($__instaladorScriptSelf, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_UNESCAPED_UNICODE); ?>;
    window.__INSTALLER_CSRF = <?php echo json_encode($installerCsrfToken, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_UNESCAPED_UNICODE); ?>;
    window.__INSTALLER_STEP = <?php echo json_encode($installerStep, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_UNESCAPED_UNICODE); ?>;
    function instaladorJsonHeaders() {
        return {
            'Content-Type': 'application/json',
            'X-Installer-CSRF': (typeof window.__INSTALLER_CSRF === 'string' ? window.__INSTALLER_CSRF : '')
        };
    }
    function instaladorSelfUrl(querySemInterrogacao) {
        var base = (typeof window.__INSTALADOR_SELF === 'string' && window.__INSTALADOR_SELF.length)
            ? window.__INSTALADOR_SELF
            : window.location.pathname;
        var q = String(querySemInterrogacao || '');
        return base + (base.indexOf('?') === -1 ? '?' : '&') + q;
    }
    function instaladorMensagemRespostaNaoJson() {
        if (window.location.protocol === 'file:') {
            return 'Não use endereço file://. Abra o instalador pelo servidor (HTTP), por exemplo: http://localhost/.../public/instalador/instalador.php';
        }
        var path = (typeof window.__INSTALADOR_SELF === 'string' && window.__INSTALADOR_SELF.length)
            ? window.__INSTALADOR_SELF
            : window.location.pathname;
        try {
            var u = new URL(path, window.location.origin || window.location.href);
            return 'O servidor devolveu HTML ou texto que não é JSON (o pedido pode ter ido para outra página). Abra sempre instalador.php na barra de endereços (com .php visível), não index.php nem apenas a pasta. Ex.: ' + u.href;
        } catch (e1) {
            return 'O servidor devolveu conteúdo que não é JSON. Abra instalador.php via HTTP no navegador e verifique o log de erros do PHP.';
        }
    }
    </script>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        
        body {
            background: #0d0f14;
            color: #e5e7eb;
            min-height: 100vh;
            position: relative;
            overflow-x: hidden;
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
        }
        
        .content-wrapper {
            position: relative;
            z-index: 1;
            min-height: 100vh;
            display: flex;
            align-items: center;
            justify-content: center;
            padding: 3rem 2rem;
        }
        
        .installer-container {
            width: 100%;
            max-width: 1200px;
            margin: 0 auto;
        }
        
        .logo-container {
            text-align: center;
            margin-bottom: 0.5rem;
        }
        
        .logo-container img {
            max-width: min(800px, 100%);
            width: auto;
            height: auto;
            display: inline-block;
            vertical-align: top;
            filter: drop-shadow(0 2px 12px rgba(255, 255, 255, 0.12));
            margin-bottom: 0;
            transition: transform 0.3s ease;
        }
        
        .logo-container img:hover {
            transform: scale(1.05);
        }
        
        .api-form {
            background: rgba(20, 20, 20, 0.9);
            backdrop-filter: blur(15px);
            padding: 2.5rem;
            border-radius: 16px;
            box-shadow: 0 10px 40px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(255, 255, 255, 0.1);
            margin: 0 auto 3rem auto;
            max-width: 500px;
            border: 1px solid rgba(255, 255, 255, 0.1);
            transition: opacity 0.3s ease, transform 0.3s ease, max-height 0.3s ease;
            overflow: hidden;
        }
        
        .api-form.hidden {
            opacity: 0;
            transform: translateY(-20px);
            max-height: 0;
            padding: 0;
            margin: 0;
            pointer-events: none;
            overflow: hidden;
        }
        
        .api-form .form-group label {
            color: #e0e0e0;
        }
        
        .api-form input[type="text"],
        .api-form input[type="password"],
        .api-form input[type="email"] {
            width: 100%;
            background: rgba(10, 10, 10, 0.8);
            border: 1px solid rgba(255, 255, 255, 0.2);
            color: #e0e0e0;
            padding: 0.75rem;
            border-radius: 8px;
            font-size: 1rem;
        }
        
        .api-form input:focus {
            outline: none;
            border-color: #ffffff;
            box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.2);
        }
        
        .api-form small {
            color: #888;
            font-size: 0.875rem;
            display: block;
            margin-top: 0.25rem;
        }
        
        .btn {
            padding: 0.75rem 1.5rem;
            border-radius: 8px;
            font-size: 1rem;
            font-weight: 600;
            cursor: pointer;
            transition: all 0.3s ease;
            border: none;
        }
        
        .btn-primary {
            background: linear-gradient(135deg, #ffffff 0%, #e0e0e0 100%);
            border: none;
            color: #0a0a0a;
            box-shadow: 0 4px 15px rgba(255, 255, 255, 0.2);
            transition: all 0.3s ease;
        }
        
        .btn-primary:hover:not(:disabled) {
            background: linear-gradient(135deg, #ffffff 0%, #f5f5f5 100%);
            box-shadow: 0 6px 20px rgba(255, 255, 255, 0.3);
            transform: translateY(-2px);
        }
        
        .btn-primary:disabled {
            opacity: 0.5;
            cursor: not-allowed;
        }
        
        .sistemas-grid {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
            gap: 2rem;
            margin-top: 2rem;
            justify-items: center;
        }
        
        .sistema-card {
            background: rgba(20, 20, 20, 0.9);
            backdrop-filter: blur(15px);
            border-radius: 16px;
            box-shadow: 0 10px 40px rgba(0, 0, 0, 0.6);
            padding: 2rem;
            cursor: pointer;
            transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
            border: 2px solid rgba(255, 255, 255, 0.1);
            position: relative;
            overflow: hidden;
            width: 100%;
            max-width: 350px;
        }
        
        .sistema-card::before {
            content: '';
            position: absolute;
            top: 0;
            left: -100%;
            width: 100%;
            height: 100%;
            background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.05), transparent);
            transition: left 0.5s ease;
        }
        
        .sistema-card:hover::before {
            left: 100%;
        }
        
        .sistema-card:hover {
            transform: translateY(-5px);
            box-shadow: 0 12px 40px rgba(255, 255, 255, 0.15), 0 0 0 1px rgba(255, 255, 255, 0.2);
            border-color: rgba(255, 255, 255, 0.3);
        }
        
        .sistema-card.ativo {
            border-color: rgba(255, 255, 255, 0.3);
            box-shadow: 0 8px 32px rgba(255, 255, 255, 0.1);
        }
        
        .sistema-card.ativo:hover {
            border-color: rgba(255, 255, 255, 0.5);
            box-shadow: 0 12px 40px rgba(255, 255, 255, 0.2);
        }
        
        .sistema-card.inativo {
            border-color: rgba(158, 158, 158, 0.3);
            opacity: 0.6;
        }
        
        .sistema-card-header {
            display: flex;
            justify-content: space-between;
            align-items: flex-start;
            margin-bottom: 1rem;
        }
        
        .sistema-card h3 {
            margin: 0;
            font-size: 1.25rem;
            color: #e0e0e0;
            flex: 1;
        }
        
        .sistema-status {
            display: inline-block;
            padding: 0.25rem 0.75rem;
            border-radius: 20px;
            font-size: 0.75rem;
            font-weight: 600;
            text-transform: uppercase;
        }
        
        .sistema-status.ativo {
            background: linear-gradient(135deg, #ffffff 0%, #e0e0e0 100%);
            color: #0a0a0a;
            box-shadow: 0 2px 8px rgba(255, 255, 255, 0.2);
        }
        
        .sistema-status.inativo {
            background: rgba(158, 158, 158, 0.3);
            color: #b0b0b0;
        }
        
        .sistema-info {
            margin-top: 1rem;
            padding-top: 1rem;
            border-top: 1px solid rgba(255, 255, 255, 0.1);
        }
        
        .sistema-info-item {
            display: flex;
            justify-content: space-between;
            margin-bottom: 0.5rem;
            font-size: 0.875rem;
        }
        
        .sistema-info-label {
            color: #b0b0b0;
            font-weight: 500;
        }
        
        .sistema-info-value {
            color: #e0e0e0;
            font-weight: 600;
        }
        
        .sistema-card-footer {
            margin-top: 1rem;
            padding-top: 1rem;
            border-top: 1px solid rgba(255, 255, 255, 0.1);
        }
        
        .btn-instalar {
            width: 100%;
            padding: 0.75rem;
            background: linear-gradient(135deg, #ffffff 0%, #e0e0e0 100%);
            color: #0a0a0a;
            border: none;
            border-radius: 8px;
            font-size: 1rem;
            font-weight: 600;
            cursor: pointer;
            transition: all 0.3s ease;
            box-shadow: 0 4px 15px rgba(255, 255, 255, 0.2);
        }
        
        .btn-instalar:hover:not(:disabled) {
            background: linear-gradient(135deg, #ffffff 0%, #f5f5f5 100%);
            transform: scale(1.02);
            box-shadow: 0 6px 20px rgba(255, 255, 255, 0.3);
        }
        
        .btn-instalar:disabled {
            background: rgba(158, 158, 158, 0.2);
            cursor: not-allowed;
            opacity: 0.5;
            box-shadow: none;
        }
        
        .empty-state {
            text-align: center;
            padding: 4rem 2rem;
            background: rgba(20, 20, 20, 0.9);
            backdrop-filter: blur(15px);
            border-radius: 16px;
            box-shadow: 0 10px 40px rgba(0, 0, 0, 0.6);
            border: 1px solid rgba(255, 255, 255, 0.1);
            max-width: 500px;
            margin: 0 auto;
        }
        
        .empty-state h3 {
            color: #e0e0e0;
            margin-top: 1.5rem;
            font-size: 1.5rem;
            font-weight: 600;
        }
        
        .empty-state p {
            color: #b0b0b0;
            margin-top: 0.5rem;
            font-size: 1rem;
        }
        
        .empty-state-icon {
            font-size: 4rem;
            margin-bottom: 1rem;
        }
        
        .loading-spinner {
            display: inline-block;
            width: 20px;
            height: 20px;
            border: 3px solid rgba(255, 255, 255, 0.3);
            border-radius: 50%;
            border-top-color: white;
            animation: spin 1s ease-in-out infinite;
        }
        
        @keyframes spin {
            to { transform: rotate(360deg); }
        }
        
        .alert-installer {
            padding: 1rem;
            border-radius: 8px;
            margin-bottom: 1.5rem;
            border-left: 4px solid;
            backdrop-filter: blur(10px);
        }
        
        .alert-installer.error {
            background: rgba(127, 29, 29, 0.35);
            border-color: #f87171;
            color: #fecaca;
        }
        
        .alert-installer.success {
            background: rgba(22, 101, 52, 0.35);
            border-color: #4ade80;
            color: #bbf7d0;
        }
        
        h2 {
            color: #f3f4f6;
            font-weight: 600;
        }
        
        /* Modal de Confirmação */
        .modal-overlay {
            display: none;
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0, 0, 0, 0.8);
            backdrop-filter: blur(5px);
            z-index: 1000;
            align-items: center;
            justify-content: center;
            animation: fadeIn 0.3s ease;
        }
        
        .modal-overlay.show {
            display: flex;
        }
        
        #sqlProgressModal.modal-overlay {
            z-index: 1100;
        }
        
        @keyframes fadeIn {
            from { opacity: 0; }
            to { opacity: 1; }
        }
        
        .modal-popup {
            background: rgba(20, 20, 20, 0.95);
            backdrop-filter: blur(20px);
            border-radius: 20px;
            padding: 2.5rem;
            max-width: 500px;
            width: 90%;
            box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.1);
            border: 2px solid rgba(255, 255, 255, 0.2);
            animation: slideUp 0.3s ease;
            position: relative;
        }
        
        @keyframes slideUp {
            from {
                transform: translateY(30px);
                opacity: 0;
            }
            to {
                transform: translateY(0);
                opacity: 1;
            }
        }
        
        .modal-header {
            text-align: center;
            margin-bottom: 1.5rem;
        }
        
        .modal-header .modal-icon {
            font-size: 4rem;
            margin-bottom: 1rem;
            display: block;
        }
        
        .modal-header h3 {
            color: #ffffff;
            font-size: 1.75rem;
            font-weight: 700;
            margin-bottom: 0.5rem;
        }
        
        .modal-body {
            text-align: center;
            margin-bottom: 2rem;
        }
        
        .modal-body p {
            color: #b0b0b0;
            font-size: 1rem;
            line-height: 1.6;
            margin-bottom: 1rem;
        }
        
        .modal-info {
            background: rgba(255, 255, 255, 0.05);
            border-radius: 12px;
            padding: 1rem;
            margin: 1rem 0;
            border: 1px solid rgba(255, 255, 255, 0.1);
        }
        
        .modal-info-item {
            display: flex;
            justify-content: space-between;
            margin-bottom: 0.5rem;
            font-size: 0.9rem;
        }
        
        .modal-info-item:last-child {
            margin-bottom: 0;
        }
        
        .modal-info-label {
            color: #b0b0b0;
            font-weight: 500;
        }
        
        .modal-info-value {
            color: #ffffff;
            font-weight: 600;
            font-family: 'Courier New', monospace;
        }
        
        .modal-footer {
            display: flex;
            gap: 1rem;
            justify-content: center;
        }
        
        .modal-btn {
            padding: 0.75rem 2rem;
            border: none;
            border-radius: 12px;
            font-size: 1rem;
            font-weight: 600;
            cursor: pointer;
            transition: all 0.3s ease;
            min-width: 120px;
        }
        
        .modal-btn-confirm {
            background: linear-gradient(135deg, #ffffff 0%, #e0e0e0 100%);
            color: #0a0a0a;
            box-shadow: 0 4px 15px rgba(255, 255, 255, 0.2);
        }
        
        .modal-btn-confirm:hover {
            background: linear-gradient(135deg, #ffffff 0%, #f5f5f5 100%);
            transform: translateY(-2px);
            box-shadow: 0 6px 20px rgba(255, 255, 255, 0.3);
        }
        
        .modal-btn-cancel {
            background: rgba(255, 255, 255, 0.1);
            color: #ffffff;
            border: 2px solid rgba(255, 255, 255, 0.2);
        }
        
        .modal-btn-cancel:hover {
            background: rgba(255, 255, 255, 0.15);
            border-color: rgba(255, 255, 255, 0.3);
        }
        
        .modal-close {
            position: absolute;
            top: 1rem;
            right: 1rem;
            background: rgba(255, 255, 255, 0.1);
            border: none;
            color: #ffffff;
            width: 32px;
            height: 32px;
            border-radius: 50%;
            cursor: pointer;
            font-size: 1.25rem;
            display: flex;
            align-items: center;
            justify-content: center;
            transition: all 0.3s ease;
        }
        
        .modal-close:hover {
            background: rgba(255, 255, 255, 0.2);
            transform: rotate(90deg);
        }
        
        /* Modal de Download */
        .download-progress {
            display: none;
            text-align: center;
            margin: 2rem 0;
        }
        
        .download-progress.show {
            display: block;
        }
        
        .progress-bar-container {
            width: 100%;
            height: 8px;
            background: rgba(255, 255, 255, 0.1);
            border-radius: 10px;
            overflow: hidden;
            margin: 1.5rem 0;
        }
        
        .progress-bar {
            height: 100%;
            background: linear-gradient(90deg, #ffffff 0%, #e0e0e0 100%);
            border-radius: 10px;
            transition: width 0.3s ease;
            width: 0%;
            animation: progressAnimation 5s linear;
        }
        
        @keyframes progressAnimation {
            0% { width: 0%; }
            100% { width: 100%; }
        }
        
        .progress-text {
            color: #ffffff;
            font-size: 1rem;
            font-weight: 600;
            margin-top: 1rem;
        }
        
        .spinner {
            display: inline-block;
            width: 40px;
            height: 40px;
            border: 4px solid rgba(255, 255, 255, 0.2);
            border-top-color: #ffffff;
            border-radius: 50%;
            animation: spin 1s linear infinite;
            margin-bottom: 1rem;
        }
        
        @keyframes spin {
            to { transform: rotate(360deg); }
        }
        
        .modal-btn:disabled {
            opacity: 0.5;
            cursor: not-allowed;
        }
        
        .form-group {
            margin-bottom: 1.5rem;
        }
        
        .form-group label {
            display: block;
            margin-bottom: 0.5rem;
            font-weight: 500;
            color: #e0e0e0;
        }
        
        .form-actions {
            margin-top: 1.5rem;
        }
        
        /* Modal de Configuração de Banco de Dados */
        .db-form-group {
            margin-bottom: 1.5rem;
            text-align: left;
        }
        
        .db-form-group label {
            display: block;
            margin-bottom: 0.5rem;
            font-weight: 500;
            color: #e0e0e0;
            font-size: 0.9rem;
        }
        
        .db-form-group input {
            width: 100%;
            padding: 0.75rem;
            background: rgba(10, 10, 10, 0.8);
            border: 1px solid rgba(255, 255, 255, 0.2);
            border-radius: 8px;
            color: #e0e0e0;
            font-size: 1rem;
            font-family: 'Courier New', monospace;
        }
        
        .db-form-group input:focus {
            outline: none;
            border-color: #ffffff;
            box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.2);
        }
        
        .db-form-group small {
            display: block;
            margin-top: 0.25rem;
            color: #888;
            font-size: 0.8rem;
        }
        
        .db-form-alert {
            display: none;
            margin-bottom: 1rem;
            padding: 0.75rem 1rem;
            border-radius: 8px;
            border: 1px solid #f87171;
            background: rgba(127, 29, 29, 0.35);
            color: #fecaca;
            font-size: 0.875rem;
            line-height: 1.45;
            text-align: left;
            white-space: pre-wrap;
            word-break: break-word;
        }
        
        #instDialogOverlay.modal-overlay {
            z-index: 1200;
        }

        body.step-database .content-wrapper {
            align-items: flex-start;
            padding-top: 2rem;
        }

        .installer-steps {
            display: flex;
            flex-wrap: wrap;
            gap: 0.5rem;
            justify-content: center;
            margin-bottom: 2rem;
            font-size: 0.85rem;
            font-weight: 600;
        }

        .installer-steps span {
            padding: 0.35rem 0.85rem;
            border-radius: 999px;
            background: rgba(255, 255, 255, 0.06);
            color: #9ca3af;
            border: 1px solid rgba(255, 255, 255, 0.12);
        }

        .installer-steps span.active {
            background: rgba(255, 255, 255, 0.18);
            color: #fff;
            border-color: rgba(255, 255, 255, 0.35);
        }

        .wizard-step {
            display: none;
        }
        .wizard-step.active {
            display: block;
        }

        .requirements-panel {
            background: rgba(20, 20, 20, 0.9);
            border: 1px solid rgba(255, 255, 255, 0.1);
            border-radius: 16px;
            padding: 1.5rem 2rem;
            margin-bottom: 2rem;
            max-width: 720px;
            margin-left: auto;
            margin-right: auto;
        }

        .requirements-panel h3 {
            font-size: 1.1rem;
            margin-bottom: 1rem;
            color: #f3f4f6;
        }

        .requirements-panel ul {
            list-style: none;
            text-align: left;
        }

        .requirements-panel li {
            padding: 0.45rem 0;
            border-bottom: 1px solid rgba(255, 255, 255, 0.06);
            display: flex;
            justify-content: space-between;
            gap: 1rem;
            font-size: 0.9rem;
        }

        .requirements-panel li:last-child {
            border-bottom: none;
        }

        .req-ok { color: #86efac; font-weight: 600; }
        .req-fail { color: #fca5a5; font-weight: 600; }

        .db-panel-full {
            max-width: 720px;
            margin-left: auto;
            margin-right: auto;
            text-align: left;
        }

        .db-panel-full h2 {
            text-align: center;
            margin-bottom: 0.75rem;
        }

        .db-panel-full .lead {
            color: #b0b0b0;
            font-size: 0.95rem;
            text-align: center;
            margin-bottom: 1.5rem;
            line-height: 1.55;
        }

        .db-actions-row {
            display: flex;
            flex-wrap: wrap;
            gap: 0.75rem;
            margin-top: 1.25rem;
            justify-content: center;
        }

        .btn-secondary {
            background: rgba(255, 255, 255, 0.12);
            color: #f3f4f6;
            border: 1px solid rgba(255, 255, 255, 0.2);
        }

        .btn-secondary:hover:not(:disabled) {
            background: rgba(255, 255, 255, 0.18);
        }

        .muted-link {
            display: inline-block;
            margin-top: 1.25rem;
            color: #93c5fd;
            text-decoration: none;
            font-size: 0.9rem;
        }

        .muted-link:hover {
            text-decoration: underline;
        }
        
        @media (max-width: 768px) {
            .content-wrapper {
                padding: 1rem;
            }
            
            .sistemas-grid {
                grid-template-columns: 1fr;
                gap: 1.5rem;
            }
            
            .api-form {
                padding: 1.5rem;
            }
            
            .logo-container img {
                max-width: min(800px, 100%);
            }
        }
    </style>
</head>
<body class="<?php echo $installerStep === 'database' ? 'step-database' : ''; ?>">
    <?php
    $__instBase = htmlspecialchars($__instaladorScriptSelf, ENT_QUOTES, 'UTF-8');
    ?>
    <div class="content-wrapper">
        <div class="installer-container">
            <div class="logo-container">
                <img src="https://i.imgur.com/rdf96IZ.png" alt="Logo" id="logo">
            </div>

            <div class="installer-steps" id="wizardSteps" aria-label="Passos da instalação">
                <span id="stepBadge1" class="active">1. Ambiente</span>
                <span id="stepBadge2">2. Chave e Download</span>
                <span id="stepBadge3">3. Configuração</span>
            </div>

            <section class="requirements-panel wizard-step" id="stepEnvironment">
                <h3>🎬 Etapa 1 — Verificação de ambiente</h3>
                <p style="color:#b0b0b0;text-align:center;font-size:0.95rem;line-height:1.55;margin-bottom:1rem;">
                    Confirma se o servidor está pronto antes de continuar.
                </p>
            <?php
    $installerRequirements = [
        ['PHP ≥ 7.4', version_compare(PHP_VERSION, '7.4.0', '>='), 'Versão ' . PHP_VERSION],
        ['Extensão cURL (download do pacote)', extension_loaded('curl'), null],
        ['Extensão ZipArchive (extração do ZIP)', extension_loaded('zip'), null],
        ['Extensão PDO MySQL (teste/importação)', extension_loaded('pdo_mysql'), null],
        ['Pasta do instalador gravável', @is_writable(__DIR__), null],
    ];
    $__cfgProbe = __DIR__ . DIRECTORY_SEPARATOR . 'config';
    $installerRequirements[] = [
        'Pasta config (gravável / criável)',
        is_dir($__cfgProbe) ? @is_writable($__cfgProbe) : @is_writable(__DIR__),
        null,
    ];
    $envAllOk = true;
    foreach ($installerRequirements as $reqTmp) {
        if (empty($reqTmp[1])) {
            $envAllOk = false;
            break;
        }
    }
    ?>
                <ul>
                    <?php foreach ($installerRequirements as $req): ?>
                        <?php
                        [$label, $ok, $extra] = $req;
                        ?>
                        <li>
                            <span><?php echo htmlspecialchars($label); ?><?php echo $extra ? ' <span style="color:#888;font-weight:400;">(' . htmlspecialchars((string) $extra) . ')</span>' : ''; ?></span>
                            <span class="<?php echo $ok ? 'req-ok' : 'req-fail'; ?>"><?php echo $ok ? 'OK' : 'Falta'; ?></span>
                        </li>
                    <?php endforeach; ?>
                </ul>
                <div class="db-actions-row" style="margin-top: 1rem;">
                    <button type="button" class="btn btn-primary" id="btnStepEnvNext" onclick="goToStep(2)" <?php echo !$envAllOk ? 'disabled' : ''; ?>>
                        Continuar para chave e download
                    </button>
                </div>
                <?php if (!$envAllOk): ?>
                    <p style="margin-top:0.75rem;color:#fca5a5;text-align:center;font-size:0.9rem;">Corrija os itens marcados como “Falta” para avançar.</p>
                <?php endif; ?>
            </section>

            <section class="requirements-panel wizard-step" id="downloadZipSection">
                <h3>📦 Sistema disponível para download</h3>
                <p style="color:#b0b0b0;text-align:center;font-size:0.95rem;line-height:1.55;margin-bottom:1rem;">
                    Etapa 2: informe a chave, confirme a instalação e prepare os arquivos do sistema.
                </p>

                <div class="db-form-group" style="margin-bottom: 1rem;">
                    <label for="api_key_download">Chave de licença (API Key para download)</label>
                    <input
                        type="text"
                        id="api_key_download"
                        placeholder="Cole aqui a chave..."
                        maxlength="512"
                        autocomplete="off"
                        value="<?php echo htmlspecialchars($dbFormPrefill['licenca'], ENT_QUOTES, 'UTF-8'); ?>">
                    <small>Enviada ao painel apenas para gerar o download do ZIP. Será gravada em <code>licenca</code>.</small>
                </div>

                <div class="db-actions-row" style="margin-top: 0.75rem;">
                    <button
                        type="button"
                        class="btn btn-secondary"
                        onclick="goToStep(1)"
                    >
                        ← Voltar para ambiente
                    </button>
                    <button
                        type="button"
                        class="btn btn-primary"
                        id="btnDownloadZip"
                        onclick="abrirModal()"
                        <?php echo $dbSqlPresenteNoInstalador ? 'disabled' : ''; ?>
                    >
                        <?php echo $dbSqlPresenteNoInstalador ? '✓ Pacote já extraído' : '📥 Descarregar e extrair ZIP'; ?>
                    </button>
                </div>
            </section>

            <?php if ($dbSqlPresenteNoInstalador): ?>
                <div class="alert-installer success" style="max-width:720px;margin:0 auto 1.5rem auto;">
                    <strong>Pacote local detectado.</strong> O instalador avançou diretamente para a etapa de configuração.
                </div>
            <?php endif; ?>

            <section class="api-form db-panel-full wizard-step" id="databaseStepPanel">
                <h2>Configuração técnica</h2>
                <p class="lead">Etapa 3: preencha os dados técnicos, teste a conexão e salve as definições locais.</p>

                <div id="dbFormError" class="db-form-alert" role="alert" aria-live="polite"></div>

                <form id="dbConfigForm" onsubmit="return false;">
                    <div class="db-form-group">
                        <label for="db_host">Host</label>
                        <input type="text" id="db_host" name="host" placeholder="localhost" required maxlength="255" autocomplete="off" value="<?php echo htmlspecialchars($dbFormPrefill['host'], ENT_QUOTES, 'UTF-8'); ?>">
                    </div>

                    <div class="db-form-group">
                        <label for="db_user">Utilizador</label>
                        <input type="text" id="db_user" name="user" placeholder="root" required maxlength="128" autocomplete="username" value="<?php echo htmlspecialchars($dbFormPrefill['user'], ENT_QUOTES, 'UTF-8'); ?>">
                    </div>

                    <div class="db-form-group">
                        <label for="db_password">Senha</label>
                        <input type="password" id="db_password" name="password" placeholder="(vazio no XAMPP)" maxlength="512" autocomplete="current-password" value="<?php echo htmlspecialchars($dbFormPrefill['password'], ENT_QUOTES, 'UTF-8'); ?>">
                    </div>

                    <div class="db-form-group">
                        <label for="db_database">Identificador técnico</label>
                        <input type="text" id="db_database" name="database" required maxlength="64" pattern="[a-zA-Z0-9_]+" autocomplete="off" value="<?php echo htmlspecialchars($dbFormPrefill['database'], ENT_QUOTES, 'UTF-8'); ?>">
                    </div>

                    <div class="db-form-group">
                        <label for="db_licenca">Chave de licença</label>
                        <input type="text" id="db_licenca" name="licenca" maxlength="512" autocomplete="off" value="<?php echo htmlspecialchars($dbFormPrefill['licenca'], ENT_QUOTES, 'UTF-8'); ?>">
                    </div>

                </form>

                <div id="dbTestHint" style="display:none;margin-top:0.75rem;font-size:0.875rem;color:#93c5fd;text-align:center;"></div>

                <div class="db-actions-row">
                    <button type="button" class="btn btn-secondary" onclick="testarDbLigacao()" id="btnDbTestar">Teste de conexão</button>
                    <button type="button" class="btn btn-primary" onclick="salvarDbConfig()" id="btnDbSalvar">Salvar configurações</button>
                    <button type="button" class="btn btn-secondary" onclick="voltarParaDownloadZip()" id="btnVoltarDownloadZip">Voltar ao download do ZIP</button>
                    <button type="button" class="btn btn-secondary" onclick="cancelarInstaladorELiminarPacote()" id="btnDbCancelar">Limpar pasta…</button>
                </div>
            </section>

        </div>
    </div>
    
    <div class="modal-overlay" id="installModal" aria-hidden="true">
        <div class="modal-popup">
            <button class="modal-close" onclick="fecharModal()">&times;</button>
            <div class="modal-header">
                <span class="modal-icon">📥</span>
                <h3>Confirmar download do ZIP</h3>
            </div>
            <div class="modal-body">
                <p style="margin-bottom:1.25rem;color:#b0b0b0;">
                    O ZIP será descarregado e extraído nesta pasta. Pode demorar alguns minutos.
                </p>

                <div class="download-progress" id="downloadProgress">
                    <div class="spinner"></div>
                    <div class="progress-text" id="progressText">Preparando...</div>
                    <div class="progress-bar-container">
                        <div class="progress-bar" id="progressBar" style="width: 0%;"></div>
                    </div>
                </div>
            </div>
            <div class="modal-footer">
                <button class="modal-btn modal-btn-cancel" onclick="fecharModal()" id="btnCancelar">Cancelar</button>
                <button class="modal-btn modal-btn-confirm" onclick="confirmarInstalacao()" id="btnInstalar">Confirmar</button>
            </div>
        </div>
    </div>
    
    <div class="modal-overlay" id="instDialogOverlay">
        <div class="modal-popup" style="max-width: 440px;">
            <div class="modal-header">
                <span class="modal-icon">❔</span>
                <h3 id="instDialogTitle">Confirmar</h3>
            </div>
            <div class="modal-body">
                <p id="instDialogMsg" style="color: #e0e0e0; line-height: 1.55; margin: 0;"></p>
            </div>
            <div class="modal-footer">
                <button type="button" class="modal-btn modal-btn-cancel" id="instDialogNao" onclick="instDialogResponder(false)">Não</button>
                <button type="button" class="modal-btn modal-btn-confirm" id="instDialogSim" onclick="instDialogResponder(true)">Sim</button>
            </div>
        </div>
    </div>
    
    <script>
        let sistemaSelecionado = null;
        let sqlImportEmAndamento = false;
        let __instDialogResolve = null;
        
        function instDialogResponder(sim) {
            const ov = document.getElementById('instDialogOverlay');
            if (ov) {
                ov.classList.remove('show');
            }
            const fn = __instDialogResolve;
            __instDialogResolve = null;
            if (typeof fn === 'function') {
                fn(!!sim);
            }
        }
        
        function instDialogPerguntar(titulo, mensagem) {
            return new Promise(function (resolve) {
                __instDialogResolve = resolve;
                const t = document.getElementById('instDialogTitle');
                const m = document.getElementById('instDialogMsg');
                if (t) {
                    t.textContent = titulo || 'Confirmar';
                }
                if (m) {
                    m.textContent = mensagem || '';
                }
                const ov = document.getElementById('instDialogOverlay');
                if (ov) {
                    ov.classList.add('show');
                }
            });
        }
        
        function fecharModal() {
            sistemaSelecionado = null;
            const instModal = document.getElementById('installModal');
            if (instModal) {
                instModal.classList.remove('show');
                instModal.setAttribute('aria-hidden', 'true');
            }
            const dp = document.getElementById('downloadProgress');
            if (dp) {
                dp.classList.remove('show');
            }
        }

        function goToStep(step) {
            var s1 = document.getElementById('stepEnvironment');
            var s2 = document.getElementById('downloadZipSection');
            var s3 = document.getElementById('databaseStepPanel');
            var b1 = document.getElementById('stepBadge1');
            var b2 = document.getElementById('stepBadge2');
            var b3 = document.getElementById('stepBadge3');
            [s1, s2, s3].forEach(function (el) { if (el) el.classList.remove('active'); });
            [b1, b2, b3].forEach(function (el) { if (el) el.classList.remove('active'); });
            if (step === 1) {
                if (s1) s1.classList.add('active');
                if (b1) b1.classList.add('active');
            } else if (step === 2) {
                if (s2) s2.classList.add('active');
                if (b2) b2.classList.add('active');
            } else {
                if (s3) s3.classList.add('active');
                if (b3) b3.classList.add('active');
            }
        }
        
        function abrirModal() {
            sistemaSelecionado = { codigo: 'pacote', nome: 'SysLook' };
            const instModal = document.getElementById('installModal');
            if (!instModal) return;

            const dp = document.getElementById('downloadProgress');
            if (dp) dp.classList.remove('show');

            const pb = document.getElementById('progressBar');
            const pt = document.getElementById('progressText');
            if (pb) pb.style.width = '0%';
            if (pt) pt.textContent = 'Preparando...';

            instModal.classList.add('show');
            instModal.setAttribute('aria-hidden', 'false');
        }
        
        async function confirmarInstalacao() {
            const btnInstalar = document.getElementById('btnInstalar');
            const btnCancelar = document.getElementById('btnCancelar');
            const dp = document.getElementById('downloadProgress');
            const pb = document.getElementById('progressBar');
            const pt = document.getElementById('progressText');

            if (btnInstalar) btnInstalar.disabled = true;
            if (btnCancelar) btnCancelar.disabled = true;

            if (dp) dp.classList.add('show');
            if (pb) pb.style.width = '10%';
            if (pt) pt.textContent = 'A descarregar e extrair...';

            const keyEl = document.getElementById('api_key_download') || document.getElementById('db_licenca');
            const apiKey = keyEl ? (keyEl.value || '').trim() : '';

            if (!apiKey) {
                if (pt) pt.textContent = 'Informe a chave de licença para o download do ZIP.';
                if (pt) pt.style.color = '#fc8181';
                if (btnInstalar) btnInstalar.disabled = false;
                if (btnCancelar) btnCancelar.disabled = false;
                return;
            }

            try {
                const response = await fetch(instaladorSelfUrl('download_extract=1'), {
                    method: 'POST',
                    headers: instaladorJsonHeaders(),
                    body: JSON.stringify({ sistema: 'pacote', api_key: apiKey })
                });

                let resultado = null;
                try { resultado = await response.json(); } catch (e) { resultado = null; }
                if (!resultado || typeof resultado !== 'object') {
                    throw new Error(instaladorMensagemRespostaNaoJson());
                }
                if (!resultado.success) {
                    throw new Error(resultado.message || 'Falha ao descarregar ou extrair o ZIP.');
                }

                if (pb) pb.style.width = '100%';
                if (pt) pt.textContent = '✓ ZIP extraído. Agora configure o banco abaixo.';
                if (pt) pt.style.color = '#48bb78';

                const btnDownloadZip = document.getElementById('btnDownloadZip');
                if (btnDownloadZip) {
                    btnDownloadZip.disabled = true;
                    btnDownloadZip.textContent = '✓ Pacote já extraído';
                }

                setTimeout(function () {
                    fecharModal();
                    goToStep(3);
                    scrollToDatabasePanel();
                }, 700);

            } catch (error) {
                const msg = error && error.message ? error.message : String(error);
                if (pt) {
                    pt.textContent = 'Erro: ' + msg;
                    pt.style.color = '#fc8181';
                }
                if (btnInstalar) btnInstalar.disabled = false;
                if (btnCancelar) btnCancelar.disabled = false;
            }
        }

        function scrollToDatabasePanel() {
            const el = document.getElementById('databaseStepPanel') || document.getElementById('dbConfigForm');
            if (!el) return;
            try {
                el.scrollIntoView({ behavior: 'smooth', block: 'start' });
            } catch (e) {
                el.scrollIntoView(true);
            }
        }

        function voltarParaDownloadZip() {
            goToStep(2);
            const el = document.getElementById('downloadZipSection');
            if (!el) return;
            try {
                el.scrollIntoView({ behavior: 'smooth', block: 'start' });
            } catch (e) {
                el.scrollIntoView(true);
            }
        }
        
        // (modal de download ZIP removido — instalador só grava database.json)
        // Fechar modal com ESC
        document.addEventListener('keydown', function(e) {
            if (e.key === 'Escape') {
                const instOv = document.getElementById('instDialogOverlay');
                if (instOv && instOv.classList.contains('show')) {
                    instDialogResponder(false);
                    e.preventDefault();
                    return;
                }
                fecharModal();
                if (!sqlImportEmAndamento) {
                    fecharModalProgresso();
                }
                fecharModalSucesso();
            }
        });
        
        /** Nome do produto → identificador MySQL (máx. 64 caracteres). */
        function sanitizeMysqlDatabaseName(raw) {
            if (!raw || typeof raw !== 'string') {
                return '';
            }
            var s = raw;
            if (typeof s.normalize === 'function') {
                try {
                    s = s.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
                } catch (e1) {
                }
            }
            s = s.toLowerCase().replace(/\s+/g, '_').replace(/[^a-z0-9_]/g, '');
            if (!s) {
                return '';
            }
            if (/^[0-9]/.test(s)) {
                s = 'db_' + s;
            }
            return s.length > 64 ? s.substring(0, 64) : s;
        }

        function clearDbFormError() {
            const el = document.getElementById('dbFormError');
            if (el) {
                el.style.display = 'none';
                el.textContent = '';
            }
        }
        
        function mensagemImportacaoParaFormulario(msg) {
            if (!msg) {
                return 'Não foi possível concluir a importação. Verifique os dados e tente novamente.';
            }
            var s = String(msg);
            if (/Access denied for user|SQLSTATE\[HY000\]\s*\[1045\]|using password:\s*(NO|YES)/i.test(s)) {
                return 'Credenciais MySQL recusadas ou utilizador sem permissão. Confirme utilizador, senha e host.\n\nDetalhe: ' + s;
            }
            return s;
        }
        
        function showDbFormError(msg) {
            const el = document.getElementById('dbFormError');
            if (!el) {
                return;
            }
            el.textContent = mensagemImportacaoParaFormulario(msg);
            el.style.display = 'block';
            try {
                el.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
            } catch (e1) {
            }
        }
        
        function resetDbSaveButtons() {
            const btnSalvar = document.getElementById('btnDbSalvar');
            const btnCancelar = document.getElementById('btnDbCancelar');
            const btnTestar = document.getElementById('btnDbTestar');
            if (btnSalvar) {
                btnSalvar.disabled = false;
                btnSalvar.textContent = 'Salvar configurações';
            }
            if (btnCancelar) {
                btnCancelar.disabled = false;
            }
            if (btnTestar) {
                btnTestar.disabled = false;
                btnTestar.textContent = 'Teste de conexão';
            }
        }

        function preencherSugestaoNomeBase(defaultDatabaseName) {
            var dbEl = document.getElementById('db_database');
            if (!dbEl || dbEl.value.trim() !== '') {
                return;
            }
            var d = (typeof defaultDatabaseName === 'string' && defaultDatabaseName.trim())
                ? defaultDatabaseName.trim()
                : '';
            if (d) {
                dbEl.value = d;
            }
        }
        
        async function cancelarInstaladorELiminarPacote() {
            if (sqlImportEmAndamento) {
                const pt = document.getElementById('sqlProgressText');
                if (pt) {
                    pt.textContent = 'Aguarde a importação do SQL terminar.';
                    pt.style.color = '#fc8181';
                } else {
                    showDbFormError('Aguarde a importação do SQL terminar.');
                }
                return;
            }
            const okLimpar = await instDialogPerguntar(
                'Limpar pasta',
                'Isto apaga todos os ficheiros e pastas desta pasta, exceto instalador.php. Deseja continuar?'
            );
            if (!okLimpar) {
                return;
            }
            const btnCancelar = document.getElementById('btnDbCancelar');
            const btnSalvar = document.getElementById('btnDbSalvar');
            const btnTestar = document.getElementById('btnDbTestar');
            if (btnCancelar) btnCancelar.disabled = true;
            if (btnSalvar) btnSalvar.disabled = true;
            if (btnTestar) btnTestar.disabled = true;
            try {
                const response = await fetch(instaladorSelfUrl('limpar_pasta_instalacao=1'), {
                    method: 'POST',
                    headers: instaladorJsonHeaders(),
                    body: '{}'
                });
                const data = await response.json();
                if (!data.success) {
                    throw new Error(data.message || 'Falha ao limpar a pasta.');
                }
                window.location.href = window.__INSTALADOR_SELF || window.location.pathname;
            } catch (err) {
                showDbFormError(err.message || 'Erro ao limpar a pasta.');
                if (btnCancelar) btnCancelar.disabled = false;
                if (btnSalvar) btnSalvar.disabled = false;
                if (btnTestar) btnTestar.disabled = false;
            }
        }

        async function testarDbLigacao() {
            clearDbFormError();
            const hint = document.getElementById('dbTestHint');
            if (hint) {
                hint.style.display = 'none';
                hint.textContent = '';
            }

            const payload = {
                host: document.getElementById('db_host').value.trim(),
                user: document.getElementById('db_user').value.trim(),
                password: (document.getElementById('db_password').value || '').trim(),
                database: document.getElementById('db_database').value.trim()
            };

            if (!payload.host || !payload.user || !payload.database) {
                showDbFormError('Preencha Host, Utilizador e Identificador técnico antes de testar.');
                return;
            }

            const btnTestar = document.getElementById('btnDbTestar');
            if (btnTestar) {
                btnTestar.disabled = true;
                btnTestar.textContent = 'A testar…';
            }

            try {
                const response = await fetch(instaladorSelfUrl('test_db=1'), {
                    method: 'POST',
                    headers: instaladorJsonHeaders(),
                    body: JSON.stringify(payload)
                });
                const raw = await response.text();
                var data;
                try {
                    data = JSON.parse(raw);
                } catch (e1) {
                    throw new Error(instaladorMensagemRespostaNaoJson());
                }
                if (!response.ok || !data.success) {
                    throw new Error((data && data.message) ? data.message : 'Teste falhou.');
                }
                if (hint) {
                    hint.style.display = 'block';
                    hint.style.color = '#86efac';
                    hint.textContent = data.message || 'Ligação OK.';
                }
            } catch (e) {
                showDbFormError(e.message || 'Erro ao testar ligação.');
            } finally {
                if (btnTestar) {
                    btnTestar.disabled = false;
                    btnTestar.textContent = 'Teste de conexão';
                }
            }
        }
        
        // Funções para modal de progresso de importação SQL
        function mostrarModalProgresso() {
            let sqlModal = document.getElementById('sqlProgressModal');
            if (!sqlModal) {
                // Criar modal se não existir usando a mesma estrutura dos outros modais
                const modalHTML = `
                    <div class="modal-overlay" id="sqlProgressModal">
                        <div class="modal-popup" style="max-width: 500px;">
                            <div class="modal-header">
                                <h3>Importando Banco de Dados</h3>
                            </div>
                            <div class="modal-body" style="text-align: center; padding: 30px;">
                                <div id="sqlProgressBarContainer" style="width: 100%; background: rgba(255,255,255,0.1); border-radius: 10px; overflow: hidden; margin-bottom: 20px;">
                                    <div id="sqlProgressBar" style="width: 0%; height: 30px; background: linear-gradient(90deg, #23730a, #2d8f0f); transition: width 0.3s ease; border-radius: 10px;"></div>
                                </div>
                                <p id="sqlProgressText" style="color: #ffffff; font-size: 14px; margin: 0;">Aguardando início...</p>
                                <p id="sqlProgressDetails" style="color: #a0a0a0; font-size: 12px; margin-top: 10px;">0%</p>
                                <div id="sqlProgressFooter" style="margin-top: 20px; display: none;">
                                    <button class="modal-btn modal-btn-confirm" onclick="redirecionarParaIndex()" id="btnIrAplicacao" style="min-width: 200px;">
                                        Ir para a aplicação (index.php)
                                    </button>
                                </div>
                            </div>
                        </div>
                    </div>
                `;
                document.body.insertAdjacentHTML('beforeend', modalHTML);
                sqlModal = document.getElementById('sqlProgressModal');
            }
            sqlModal.classList.add('show');
        }
        
        function atualizarProgresso(progress) {
            const progressBar = document.getElementById('sqlProgressBar');
            const progressText = document.getElementById('sqlProgressText');
            const progressDetails = document.getElementById('sqlProgressDetails');
            const progressFooter = document.getElementById('sqlProgressFooter');
            
            if (progressBar) {
                progressBar.style.width = progress.percentage + '%';
            }
            if (progressText) {
                progressText.textContent = progress.message || 'Processando...';
                if (progress.step === 'error') {
                    progressText.style.color = '#fc8181';
                } else {
                    progressText.style.color = '#ffffff';
                }
            }
            if (progressDetails) {
                var msg = progress.message || '';
                var m = msg.match(/(\d+)\s*\/\s*(\d+)/);
                if (m) {
                    progressDetails.textContent = m[1] + '/' + m[2] + ' comandos (' + (progress.percentage != null ? progress.percentage : 0) + '%)';
                } else if (progress.total > 0) {
                    progressDetails.textContent = progress.progress + '/' + progress.total + ' (' + (progress.percentage != null ? progress.percentage : 0) + '%)';
                } else {
                    progressDetails.textContent = (progress.percentage != null ? progress.percentage : 0) + '%';
                }
            }
            
            // Mostrar botão quando chegar a 100% ou step for 'completed'
            if (progressFooter) {
                if (progress.percentage >= 100 || progress.step === 'completed') {
                    progressFooter.style.display = 'block';
                    if (progressText) {
                        progressText.style.color = '#48bb78';
                        progressText.textContent = '✅ Importação concluída com sucesso!';
                    }
                } else {
                    progressFooter.style.display = 'none';
                }
            }
        }
        
        function fecharModalProgresso() {
            const sqlModal = document.getElementById('sqlProgressModal');
            if (sqlModal) {
                sqlModal.classList.remove('show');
            }
        }
        
        function mostrarModalSucesso(resultado) {
            let successModal = document.getElementById('successModal');
            if (!successModal) {
                // Criar modal se não existir
                const modalHTML = `
                    <div class="modal-overlay" id="successModal">
                        <div class="modal-popup" style="max-width: 500px;">
                            <div class="modal-header">
                                <span class="modal-icon" style="font-size: 5rem; color: #48bb78;">✅</span>
                                <h3>Instalação Concluída!</h3>
                            </div>
                            <div class="modal-body" style="text-align: center; padding: 20px;">
                                <div id="successMessage" style="color: #e0e0e0; line-height: 1.8; margin-bottom: 20px;"></div>
                                <div style="background: rgba(255,255,255,0.05); border-radius: 12px; padding: 15px; margin: 15px 0; border: 1px solid rgba(255,255,255,0.1);">
                                    <div style="display: flex; justify-content: space-between; margin-bottom: 10px; font-size: 0.9rem;">
                                        <span style="color: #b0b0b0;">Comandos SQL executados:</span>
                                        <span style="color: #ffffff; font-weight: 600;" id="successExecuted">-</span>
                                    </div>
                                    <div style="display: flex; justify-content: space-between; margin-bottom: 10px; font-size: 0.9rem;">
                                        <span style="color: #b0b0b0;">Banco de dados:</span>
                                        <span style="color: #ffffff; font-weight: 600; font-family: 'Courier New', monospace;" id="successDatabase">-</span>
                                    </div>
                                    <div id="successCleaned" style="margin-top: 15px; padding-top: 15px; border-top: 1px solid rgba(255,255,255,0.1); font-size: 0.85rem; color: #a0a0a0;"></div>
                                </div>
                            </div>
                            <div class="modal-footer">
                                <button class="modal-btn modal-btn-confirm" onclick="redirecionarParaIndex()" style="min-width: 200px;">
                                    🚀 Ir para Index.php
                                </button>
                            </div>
                        </div>
                    </div>
                `;
                document.body.insertAdjacentHTML('beforeend', modalHTML);
                successModal = document.getElementById('successModal');
            }
            
            // Preencher informações
            const successMessage = document.getElementById('successMessage');
            const successExecuted = document.getElementById('successExecuted');
            const successDatabase = document.getElementById('successDatabase');
            const successCleaned = document.getElementById('successCleaned');
            
            if (successMessage) {
                successMessage.innerHTML = '<p style="font-size: 1.1rem; margin-bottom: 10px;">Sistema instalado e configurado com sucesso!</p>';
            }
            
            if (successExecuted) {
                successExecuted.textContent = resultado.executed + ' comandos';
            }
            
            if (successDatabase) {
                successDatabase.textContent = resultado.database || 'N/A';
            }
            
            if (successCleaned && resultado.cleaned) {
                let cleanedHTML = '<div style="color: #48bb78; font-weight: 600; margin-bottom: 8px;">🧹 Limpeza automática:</div>';
                if (resultado.cleaned.database_dir) {
                    cleanedHTML += '<div style="margin: 5px 0;">✓ Pasta "database" removida</div>';
                }
                if (resultado.cleaned.instalador_file) {
                    cleanedHTML += '<div style="margin: 5px 0;">✓ Arquivo "instalador.php" removido</div>';
                }
                if (typeof resultado.cleaned.sql_files === 'number' && resultado.cleaned.sql_files > 0) {
                    cleanedHTML += '<div style="margin: 5px 0;">✓ ' + resultado.cleaned.sql_files + ' ficheiro(s) .sql removido(s)</div>';
                }
                if (resultado.cleaned.instalador_scheduled) {
                    cleanedHTML += '<div style="margin: 5px 0;">✓ instalador.php agendado para remoção</div>';
                }
                successCleaned.innerHTML = cleanedHTML;
            } else if (successCleaned) {
                successCleaned.innerHTML = '';
            }
            
            successModal.classList.add('show');
        }
        
        function fecharModalSucesso() {
            const successModal = document.getElementById('successModal');
            if (successModal) {
                successModal.classList.remove('show');
            }
        }
        
        function redirecionarParaIndex() {
            window.location.href = 'index.php';
        }
        
        async function salvarDbConfig() {
            const dbData = {
                host: document.getElementById('db_host').value.trim(),
                user: document.getElementById('db_user').value.trim(),
                password: (document.getElementById('db_password').value || '').trim(),
                database: document.getElementById('db_database').value.trim(),
                licenca: (document.getElementById('db_licenca') && document.getElementById('db_licenca').value || '').trim()
            };

            if (!dbData.host || !dbData.user || !dbData.database) {
                showDbFormError('Preencha os campos obrigatórios: Host, Utilizador e Identificador técnico.');
                return;
            }
            
            clearDbFormError();

            const btnSalvar = document.getElementById('btnDbSalvar');
            const btnCancelar = document.getElementById('btnDbCancelar');
            const btnTestar = document.getElementById('btnDbTestar');

            btnSalvar.disabled = true;
            btnCancelar.disabled = true;
            if (btnTestar) btnTestar.disabled = true;
            btnSalvar.textContent = 'A guardar…';

            try {
                const response = await fetch(instaladorSelfUrl('save_db=1'), {
                    method: 'POST',
                    headers: instaladorJsonHeaders(),
                    body: JSON.stringify(dbData)
                });
                
                const rawSave = await response.text();
                let resultado;
                try {
                    resultado = JSON.parse(rawSave);
                } catch (parseErr) {
                    showDbFormError(instaladorMensagemRespostaNaoJson());
                    btnSalvar.disabled = false;
                    btnCancelar.disabled = false;
                    btnSalvar.textContent = 'Salvar configurações';
                    if (btnTestar) btnTestar.disabled = false;
                    return;
                }

                if (resultado.success) {
                    if (resultado.import_sql) {
                        sqlImportEmAndamento = true;
                        setTimeout(function () {
                            var progressInterval = null;
                            var sqlImportFailureHandled = false;
                            
                            function finishSqlImportFailure(message) {
                                if (sqlImportFailureHandled) {
                                    return;
                                }
                                sqlImportFailureHandled = true;
                                if (progressInterval) {
                                    clearInterval(progressInterval);
                                    progressInterval = null;
                                }
                                fecharModalProgresso();
                                showDbFormError(message);
                                resetDbSaveButtons();
                            }
                            
                            mostrarModalProgresso();
                            
                            const sqlPromise = fetch(instaladorSelfUrl('import_sql=1'), {
                                method: 'POST',
                                headers: instaladorJsonHeaders(),
                                body: '{}'
                            });

                            progressInterval = setInterval(function () {
                                fetch(instaladorSelfUrl('check_progress=1'))
                                    .then(function (r) { return r.json(); })
                                    .then(function (progress) {
                                        atualizarProgresso(progress);
                                        if (progress.step === 'error') {
                                            finishSqlImportFailure(progress.message || 'Erro na importação SQL.');
                                        }
                                    })
                                    .catch(function (e) {
                                        console.error('Erro ao verificar progresso:', e);
                                    });
                            }, 400);

                            sqlPromise.then(function (sqlResponse) {
                                if (progressInterval) {
                                    clearInterval(progressInterval);
                                    progressInterval = null;
                                }
                                return sqlResponse.json().then(function (sqlResultado) {
                                    return { okHttp: sqlResponse.ok, body: sqlResultado };
                                });
                            }).then(function (pack) {
                                var sqlResultado = pack.body || {};
                                if (!pack.okHttp) {
                                    var httpMsg = sqlResultado.message || '';
                                    finishSqlImportFailure(
                                        httpMsg ? ('Erro na importação: ' + httpMsg) : 'Erro HTTP ao importar o SQL (sem mensagem do servidor).'
                                    );
                                    return;
                                }
                                if (sqlResultado.success) {
                                    atualizarProgresso({
                                        step: 'completed',
                                        message: '✅ Configuração concluída. A redirecionar…',
                                        progress: sqlResultado.executed,
                                        total: sqlResultado.total,
                                        percentage: 100
                                    });
                                    window.setTimeout(function () {
                                        window.location.href = 'index.php';
                                    }, 900);
                                } else {
                                    finishSqlImportFailure(sqlResultado.message || 'Erro desconhecido ao importar o banco.');
                                }
                            }).catch(function (e) {
                                finishSqlImportFailure(e && e.message ? e.message : String(e));
                            }).finally(function () {
                                sqlImportEmAndamento = false;
                            });
                        }, 50);
                    } else {
                        clearDbFormError();
                        const hintOk = document.getElementById('dbTestHint');
                        if (hintOk) {
                            hintOk.style.display = 'block';
                            hintOk.style.color = '#86efac';
                            hintOk.textContent = 'Configurações salvas com sucesso.';
                        }
                        resetDbSaveButtons();
                    }
                } else {
                    showDbFormError(resultado.message || 'Erro ao salvar a configuração do banco.');
                    btnSalvar.disabled = false;
                    btnCancelar.disabled = false;
                    if (btnTestar) btnTestar.disabled = false;
                    btnSalvar.textContent = 'Salvar configurações';
                }
            } catch (error) {
                showDbFormError(error.message || 'Erro ao comunicar com o servidor.');
                btnSalvar.disabled = false;
                btnCancelar.disabled = false;
                if (btnTestar) btnTestar.disabled = false;
                btnSalvar.textContent = 'Salvar configurações';
            }
        }
        
        document.addEventListener('DOMContentLoaded', function() {
            const successModal = document.getElementById('successModal');
            if (successModal) {
                successModal.addEventListener('click', function(e) {
                    if (e.target === this) {
                        fecharModalSucesso();
                    }
                });
            }

            const instDlg = document.getElementById('instDialogOverlay');
            if (instDlg) {
                instDlg.addEventListener('click', function (e) {
                    if (e.target === this) {
                        instDialogResponder(false);
                    }
                });
            }

            // Se já existir pacote local, pula direto para a etapa de configuração.
            <?php if ($dbSqlPresenteNoInstalador): ?>
            goToStep(3);
            <?php else: ?>
            goToStep(1);
            <?php endif; ?>

            // Sincroniza a chave usada para download com o campo salvo em database.json
            const apiKeyDownloadInput = document.getElementById('api_key_download');
            const licencaInput = document.getElementById('db_licenca');
            if (apiKeyDownloadInput && licencaInput) {
                if (!licencaInput.value && apiKeyDownloadInput.value) {
                    licencaInput.value = apiKeyDownloadInput.value;
                }
                apiKeyDownloadInput.addEventListener('input', function () {
                    licencaInput.value = (apiKeyDownloadInput.value || '').trim();
                });
            }

            <?php if ($installerStep === 'database'): ?>
            (function () {
                var raw = <?php echo json_encode($dbNombreHintForJs, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_UNESCAPED_UNICODE); ?>;
                var sug = '';
                try {
                    sug = sanitizeMysqlDatabaseName(raw);
                } catch (e2) {
                }
                preencherSugestaoNomeBase(sug);
            })();
            <?php endif; ?>
        });
    </script>
</body>
</html>