<?php
declare(strict_types=1);

session_start(['cookie_lifetime' => 0, 'use_strict_mode' => true]);

/*
 * Project name: Flatboard
 * Project URL: https://flatboard.org
 * Author: Frédéric Kaplon and contributors
 * All Flatboard code is released under the MIT license.
 */

/*
 *---------------------------------------------------------------
 * LANGUAGE DETECTION
 *---------------------------------------------------------------
 */
$accept_language = $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? 'en-US';
$language_parts = explode('-', str_replace(',', '-', substr($accept_language, 0, 5)));
$language = ($language_parts[0] ?? 'en') . '-' . strtoupper($language_parts[1] ?? 'US');

/*
 *---------------------------------------------------------------
 * CONSTANTS AND DEFINES
 *---------------------------------------------------------------
 */
const DS = '/';
const PATH_ROOT = __DIR__ . DS;
const VERSION = '4.0';
const BUILD = '2025-11-30';
const APP = 'flatboard';
const COUNTDOWN = 3;
const DEBUG_MODE = true; // Keep true for detailed error logging
define('LANG', $language);

if (DEBUG_MODE) {
    ini_set('error_log', PATH_ROOT . 'error.log');
    error_reporting(E_ALL);
    ini_set('display_errors', '1');
    ini_set('display_startup_errors', '1');
    ini_set('log_errors', '1');
    ini_set('html_errors', '1');
} else {
    error_reporting(E_ALL);
    ini_set('error_log', PATH_ROOT . 'error.log');
    ini_set('log_errors', '1');
    ini_set('display_errors', '0');
    ini_set('display_startup_errors', '0');
}

/*
 *---------------------------------------------------------------
 * PHP VERIFICATIONS
 *---------------------------------------------------------------
 */
if (version_compare(PHP_VERSION, '8.0', '<')) {
    throw new RuntimeException(sprintf('Current PHP version %s, you need >= 8.0. (ERR_202)', PHP_VERSION));
}

$required_modules = ['mbstring', 'json', 'gd', 'dom', 'zip'];
$missing_modules = array_filter($required_modules, fn(string $module): bool => !extension_loaded($module));

if ($missing_modules) {
    $error_message = 'Missing PHP modules: ' . implode(', ', $missing_modules);
    error_log('[ERROR] ' . $error_message);
    throw new RuntimeException($error_message);
}

/*
 *---------------------------------------------------------------
 * TRANSLATIONS
 *---------------------------------------------------------------
 */
$translations = [
    'fr-FR' => [
        'Flatboard unpacking' => 'Décompression de Flatboard',
        'Because it’s faster to upload just two files.' => 'Car il est plus rapide de télécharger seulement deux fichiers.',
        'UNPACK' => 'DÉCOMPRESSER',
        'UNPACKED !' => 'DÉCOMPRESSION TERMINÉE !',
        'Select language' => 'Sélectionnez une langue',
        'English' => 'Anglais',
        'French' => 'Français',
        'Russian' => 'Russe',
        'Chinese' => 'Chinois',
        'Oups! I couldn\'t open %file' => 'Oups ! Impossible d’ouvrir %file',
        'YEAH! Archive properly extracted to %path.<br />You will be redirected in <span id="counter">%count</span> second(s).' => 'Super ! Archive extraite vers %path.<br />Vous serez redirigé dans <span id="counter">%count</span> seconde(s).',
        'Failed to delete %item: %error. Check permissions, file locks, or remaining files: %files' => 'Échec de la suppression de %item : %error. Vérifiez les permissions, les verrouillages de fichiers ou les fichiers restants : %files',
        'Hello %name' => 'Bonjour %name',
    ],
    'ru-RU' => [
        'Flatboard unpacking' => 'Распаковка Flatboard',
        'Because it’s faster to upload just two files.' => 'Потому что быстрее загрузить всего два файла.',
        'UNPACK' => 'Распаковать',
        'UNPACKED !' => 'РАСПАКОВКА ЗАВЕРШЕНА!',
        'Select language' => 'Выберите язык',
        'English' => 'Английский',
        'French' => 'Французский',
        'Russian' => 'Русский',
        'Chinese' => 'Китайский',
        'Oups! I couldn\'t open %file' => 'Упс! Не удалось открыть файл %file',
        'YEAH! Archive properly extracted to %path.<br />You will be redirected in <span id="counter">%count</span> second(s).' => 'Ура! Архив успешно извлечен в %path.<br />Вы будете перенаправлены через <span id="counter">%count</span> секунд(ы).',
        'Failed to delete %item: %error. Check permissions, file locks, or remaining files: %files' => 'Не удалось удалить %item: %error. Проверьте права доступа, блокировки файлов или оставшиеся файлы: %files',
        'Hello %name' => 'Привет, %name',
    ],
    'zh-CN' => [
        'Flatboard unpacking' => '解压 Flatboard',
        'Because it’s faster to upload just two files.' => '因为只上传两个文件会更快。',
        'UNPACK' => '解压',
        'UNPACKED !' => '解压完成！',
        'Select language' => '选择语言',
        'Chinese' => '简体中文',
        'English' => '英语',
        'French' => '法语',
        'Russian' => '俄语',
        'Oups! I couldn\'t open %file' => '哎呀！无法打开 %file',
        'YEAH! Archive properly extracted to %path.<br />You will be redirected in <span id="counter">%count</span> second(s).' => '好啦！文件成功解压到 %path。<br />您将在 <span id="counter">%count</span> 秒后被重定向。',
        'Failed to delete %item: %error. Check permissions, file locks, or remaining files: %files' => '无法删除 %item：%error。请检查权限、文件锁定或剩余文件：%files',
        'Hello %name' => '你好 %name',
    ],
];

$download = filter_input(INPUT_GET, 'download', FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
$lang = filter_input(INPUT_GET, 'l', FILTER_SANITIZE_SPECIAL_CHARS) ?? LANG;
$msg = $_SESSION['msg'] ?? '';
unset($_SESSION['msg']);

/**
 * Décompresse l'archive ZIP, copie les fichiers et supprime les fichiers temporaires.
 * Cette fonction gère le processus complet de décompression de l'archive 'flatboard_pro.zip',
 * copie son contenu dans le répertoire courant, puis supprime le dossier temporaire et l'archive.
 * Elle inclut des mécanismes de réessai pour gérer les problèmes de permissions ou de verrouillage.
 * 
 * @return void
 */
function unpack_(): void {
    $app = APP . '.zip';
    // Vérifie si l'archive ZIP existe
    if (!file_exists($app)) {
        flash('msg', t('Oups! I couldn\'t open %file', ['%file' => $app]), 'warning');
        return;
    }

    // Vérifie si l'archive est accessible en écriture pour la suppression ultérieure
    if (!is_writable($app)) {
        flash('msg', t('Failed to delete %item: %error. Check permissions, file locks, or remaining files: %files', ['%item' => $app, '%error' => 'File is not writable', '%files' => 'N/A']), 'warning');
        return;
    }

    // Récupère le chemin du répertoire parent de l'archive
    $path = pathinfo(realpath($app), PATHINFO_DIRNAME);
    $zip = new ZipArchive;
    
    // Ouvre l'archive ZIP
    if ($zip->open($app) !== true) {
        flash('msg', t('Oups! I couldn\'t open %file', ['%file' => $app]), 'warning');
        return;
    }

    try {
        // Extrait l'archive dans le répertoire parent
        $zip->extractTo($path);
        $zip->close();
        
        // Vérifie si le dossier temporaire est accessible en écriture
        if (is_dir(APP) && !is_writable(APP)) {
            flash('msg', t('Failed to delete %item: %error. Check permissions, file locks, or remaining files: %files', ['%item' => APP, '%error' => 'Directory is not writable', '%files' => 'N/A']), 'warning');
            return;
        }

        // Copie récursivement le contenu du dossier temporaire vers le répertoire courant
        recurse_copy(APP, './');
        
        // Tente de supprimer le dossier temporaire avec un mécanisme de réessai
        for ($attempt = 1; $attempt <= 3; $attempt++) {
            try {
                if (is_dir(APP)) {
                    set_recursive_permissions(APP);
                    removeDirectory(APP);
                }
                if (!is_dir(APP)) {
                    break; // Suppression réussie
                }
                // Liste les fichiers restants pour le débogage
                $remaining_files = scandir(APP) ?: [];
                $remaining_files = array_diff($remaining_files, ['.', '..']);
                throw new Exception('Directory still exists after attempt ' . $attempt . '. Remaining files: ' . implode(', ', $remaining_files));
            } catch (Exception $e) {
                error_log('[ERROR] Delete attempt ' . $attempt . ' for ' . APP . ' failed: ' . $e->getMessage());
                if ($attempt === 3) {
                    flash('msg', t('Failed to delete %item: %error. Check permissions, file locks, or remaining files: %files', ['%item' => APP, '%error' => $e->getMessage(), '%files' => implode(', ', $remaining_files)]), 'warning');
                    return;
                }
                usleep(1500000); // Attend 1.5 secondes avant de réessayer
            }
        }
        
        // Tente de supprimer l'archive ZIP avec un mécanisme de réessai
        for ($attempt = 1; $attempt <= 3; $attempt++) {
            try {
                if (file_exists($app)) {
                    @chmod($app, 0777);
                    // Vérifie si le fichier est verrouillé
                    $handle = @fopen($app, 'r+');
                    if ($handle === false) {
                        throw new Exception('File is locked or inaccessible');
                    }
                    fclose($handle);
                    unlink($app);
                }
                if (!file_exists($app)) {
                    break; // Suppression réussie
                }
                throw new Exception('ZIP file still exists after attempt ' . $attempt);
            } catch (Exception $e) {
                error_log('[ERROR] Delete attempt ' . $attempt . ' for ' . $app . ' failed: ' . $e->getMessage());
                if ($attempt === 3) {
                    flash('msg', t('Failed to delete %item: %error. Check permissions, file locks, or remaining files: %files', ['%item' => $app, '%error' => $e->getMessage(), '%files' => 'N/A']), 'warning');
                    return;
                }
                usleep(1500000); // Attend 1.5 secondes avant de réessayer
            }
        }
        
        // Affiche un message de succès avec redirection
        flash('msg', t('YEAH! Archive properly extracted to %path.<br />You will be redirected in <span id="counter">%count</span> second(s).', ['%path' => $path, '%count' => COUNTDOWN]));
    } catch (Exception $e) {
        flash('msg', t('Oups! I couldn\'t open %file', ['%file' => $app]), 'warning');
        error_log('[ERROR] Unpack failed: ' . $e->getMessage());
    }
}

/**
 * Définit récursivement les permissions d'un fichier ou dossier à 0777.
 * Cette fonction applique des permissions d'écriture maximales pour faciliter la suppression
 * ou la manipulation des fichiers et dossiers. Elle est utilisée avant la suppression pour
 * éviter les erreurs liées aux permissions.
 * 
 * @param string $path Chemin du fichier ou dossier à modifier.
 * @return void
 */
function set_recursive_permissions(string $path): void {
    if (!file_exists($path)) {
        return;
    }
    
    // Si le chemin est un fichier, définit les permissions à 0777
    if (!is_dir($path)) {
        @chmod($path, 0777);
        return;
    }
    
    // Parcourt tous les éléments du dossier
    $items = scandir($path) ?: [];
    foreach ($items as $item) {
        if ($item === '.' || $item === '..') {
            continue;
        }
        $item_path = $path . DS . $item;
        @chmod($item_path, 0777);
        // Si l'élément est un dossier, appelle récursivement la fonction
        if (is_dir($item_path)) {
            set_recursive_permissions($item_path);
        }
    }
    // Définit les permissions du dossier parent
    @chmod($path, 0777);
}

/**
 * Copie récursivement un dossier et son contenu vers une destination.
 * Cette fonction est utilisée pour déplacer les fichiers extraits du dossier temporaire
 * 'flatboard_pro' vers le répertoire courant. Elle gère les sous-dossiers et les fichiers.
 * 
 * @param string $src Chemin du dossier source.
 * @param string $dst Chemin du dossier de destination.
 * @return void
 */
function recurse_copy(string $src, string $dst): void {
    if (!is_dir($src)) {
        return;
    }
    
    // Crée le dossier de destination s'il n'existe pas
    if (!is_dir($dst)) {
        mkdir($dst, 0755, true);
    }
    $dir = opendir($src);
    
    // Parcourt chaque élément du dossier source
    while (($file = readdir($dir)) !== false) {
        if ($file === '.' || $file === '..') {
            continue;
        }
        $src_path = $src . DS . $file;
        $dst_path = $dst . DS . $file;
        
        try {
            // Copie récursivement les dossiers ou directement les fichiers
            is_dir($src_path) ? recurse_copy($src_path, $dst_path) : copy($src_path, $dst_path);
        } catch (Exception $e) {
            error_log('[ERROR] Copy failed for ' . $src_path . ': ' . $e->getMessage());
        }
    }
    
    closedir($dir);
}

/**
 * Supprime récursivement un dossier et son contenu.
 * Cette fonction supprime tous les fichiers et sous-dossiers dans le dossier spécifié,
 * puis supprime le dossier lui-même. Elle vérifie les verrouillages de fichiers et
 * définit les permissions avant la suppression.
 * 
 * @param string $path Chemin du dossier à supprimer.
 * @return void
 */
function removeDirectory(string $path): void {
    if (!is_dir($path)) {
        return;
    }
    
    // Parcourt tous les éléments du dossier
    $items = scandir($path) ?: [];
    foreach ($items as $item) {
        if ($item === '.' || $item === '..') {
            continue;
        }
        $item_path = $path . DS . $item;
        try {
            @chmod($item_path, 0777);
            if (is_dir($item_path)) {
                // Supprime récursivement les sous-dossiers
                removeDirectory($item_path);
            } else {
                // Vérifie si le fichier est verrouillé
                $handle = @fopen($item_path, 'r+');
                if ($handle === false) {
                    throw new Exception('File is locked or inaccessible: ' . $item_path);
                }
                fclose($handle);
                unlink($item_path);
            }
        } catch (Exception $e) {
            error_log('[ERROR] Delete failed for ' . $item_path . ': ' . $e->getMessage());
            throw $e;
        }
    }
    // Supprime le dossier parent
    rmdir($path);
}

/**
 * Gère l'affichage des messages temporaires (notifications).
 * Cette fonction stocke un message dans la session pour affichage unique ou
 * affiche un message stocké avec une classe CSS pour le style (succès, erreur, etc.).
 * 
 * @param string $name Nom de la clé de session pour le message.
 * @param string $message Message à stocker (optionnel).
 * @param string $class Classe CSS pour le style du message (par défaut 'success').
 * @return void
 */
function flash(string $name, string $message = '', string $class = 'success'): void {
    if (empty($name)) {
        return;
    }
    
    // Stocke le message dans la session si fourni
    if (!empty($message) && empty($_SESSION[$name])) {
        unset($_SESSION[$name], $_SESSION[$name . '_class']);
        $_SESSION[$name] = $message;
        $_SESSION[$name . '_class'] = $class;
    } elseif (!empty($_SESSION[$name]) && empty($message)) {
        // Affiche le message stocké avec son style
        $class = $_SESSION[$name . '_class'] ?? 'success';
        echo '<div class="msg ' . htmlspecialchars($class) . '"><span class="msg-close">×</span><h3 class="msg-title">Information</h3><p class="msg-message">' . $_SESSION[$name] . '</p></div>';
        unset($_SESSION[$name], $_SESSION[$name . '_class']);
    }
}

/**
 * Traduit une chaîne de caractères selon la langue sélectionnée.
 * Cette fonction récupère la traduction d'une chaîne à partir du tableau global
 * $translations et remplace les paramètres dynamiques si fournis.
 * 
 * @param string $string Chaîne à traduire.
 * @param array $args Paramètres à remplacer dans la chaîne (optionnel).
 * @param string|null $langcode Code de langue à utiliser (par défaut, langue courante).
 * @return string Chaîne traduite avec les paramètres remplacés.
 */
function t(string $string, array $args = [], ?string $langcode = null): string {
    global $lang, $translations;
    $langcode = $langcode ?? $lang;
    
    // Récupère la traduction ou utilise la chaîne originale si non trouvée
    $string = $translations[$langcode][$string] ?? $string;
    
    if (empty($args)) {
        return $string;
    }
    
    // Remplace les paramètres dans la chaîne traduite
    return strtr($string, array_map('htmlspecialchars', $args));
}
?>
<!DOCTYPE html>
<html lang="<?php echo htmlspecialchars(LANG); ?>">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="description" content="Flatboard Unpacking Script">
    <title><?php echo t('Flatboard unpacking'); ?></title>
    <style>:root{--primary-color:#ff4d4d;--success-color:#28a745;--warning-color:#dc3545;--info-color:#17a2b8;--danger-color:#ffc107;--background-gradient:linear-gradient(135deg,#2c3e50,#4b79a1);--card-background:rgba(255,255,255,0.95);--text-color:#333;--shadow:0 4px 12px rgba(0,0,0,0.1);--transition:all .2s ease}*{box-sizing:border-box;margin:0;padding:0}body{background:var(--background-gradient);min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;font-family:system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;color:var(--text-color)}.card{background:var(--card-background);border-radius:12px;padding:2rem;max-width:500px;width:90%;box-shadow:var(--shadow);text-align:center;animation:fadeIn .5s ease}@keyframes fadeIn{from{opacity:0;transform:translateY(-20px)}to{opacity:1;transform:translateY(0)}}h1{font-size:2rem;margin-bottom:.5rem;color:var(--primary-color)}h3{font-size:1.1rem;font-weight:400;margin-bottom:1.5rem;color:var(--text-color)}.btn{width:160px;height:48px;background:var(--primary-color);color:#fff;border:none;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:1rem;font-weight:500;cursor:pointer;transition:var(--transition);position:relative;margin:1rem auto}.btn:hover{background:#e63939;transform:scale(1.05);box-shadow:var(--shadow)}.btn.disabled{pointer-events:none;opacity:.6}.btn.loading::after{content:'';width:24px;height:24px;border:3px solid #fff;border-top-color:transparent;border-radius:50%;animation:spin 1s linear infinite;position:absolute}@keyframes spin{to{transform:rotate(360deg)}}.btn.shrinking{width:48px;border-radius:50%}.btn.expanding{width:160px;border-radius:8px}.bar{width:100%;height:6px;border-radius:6px;background:#e0e0e0;overflow:hidden;opacity:0;transition:opacity .2s ease;margin:1rem 0}.bar.visible{opacity:1}.bar .p{width:0;height:100%;background:linear-gradient(90deg,var(--primary-color),#ff7676);transition:width 1.8s ease-in-out}.bar .p.full{width:100%}.msg{background:var(--success-color);border-radius:8px;padding:1rem;margin:1rem 0;box-shadow:var(--shadow);position:relative;animation:fadeIn .5s ease}.msg.warning{background:var(--warning-color)}.msg.info{background:var(--info-color)}.msg.danger{background:var(--danger-color)}.msg .msg-close{position:absolute;top:10px;right:10px;cursor:pointer;font-size:1.2rem;color:#fff;transition:var(--transition)}.msg .msg-close:hover{transform:scale(1.2)}.msg .msg-title{font-size:1.2rem;font-weight:600;color:#fff;margin-bottom:.5rem}.msg .msg-message{font-size:.9rem;color:#fff}.select{position:relative;width:200px;margin:1rem auto}.select select{width:100%;padding:.75rem;border:1px solid #ccc;border-radius:8px;font-size:1rem;background:#fff;cursor:pointer;appearance:none;transition:var(--transition)}.select select:focus{border-color:var(--primary-color);outline:none;box-shadow:0 0 5px rgba(255,77,77,0.3)}.select::after{content:'▼';position:absolute;right:1rem;top:50%;transform:translateY(-50%);color:var(--text-color);pointer-events:none}.footer{margin-top:2rem;font-size:.8rem;font-style:italic;color:#fff;opacity:.8}</style>
</head>
<body>
    <div class="card">
        <h1><?php echo t('Flatboard unpacking'); ?></h1>
        <h3><?php echo t('Because it’s faster to upload just two files.'); ?></h3>
        <form class="select">
            <?php
            $options = [
                '' => t('Select language'),
                'en-US' => t('English'),
                'fr-FR' => t('French'),
                'ru-RU' => t('Russian'),
                'zh-CN' => t('Chinese')
            ];
            $selected = $_GET['l'] ?? LANG;
            ?>
            <select onchange="window.location.href='index.php?l=' + encodeURIComponent(this.value)">
                <?php foreach ($options as $value => $option): ?>
                    <option value="<?php echo htmlspecialchars($value); ?>"<?php echo $value === $selected ? ' selected' : ''; ?>>
                        <?php echo htmlspecialchars($option); ?>
                    </option>
                <?php endforeach; ?>
            </select>
        </form>
        <div class="container">
            <?php
            if ($download) {
                unpack_();
                sleep(1);
                flash('msg');
            }
            ?>
        </div>
        <button class="btn">
            <span><?php echo t('UNPACK'); ?></span>
        </button>
        <div class="bar">
            <div class="p"></div>
        </div>
    </div>
    <div class="footer">Version: <?php echo VERSION; ?> - Build: <?php echo BUILD; ?></div>
</body>
<script>const btn=document.querySelector('.btn'),btnSpan=btn.querySelector('span'),bar=document.querySelector('.bar'),progress=bar.querySelector('.p');btn.addEventListener('click',()=>{if(btn.classList.contains('disabled'))return;btn.classList.add('disabled');window.location.href=`index.php?l=<?php echo htmlspecialchars($lang); ?>&download=true`;btnSpan.style.opacity='0';btn.classList.add('shrinking');setTimeout(()=>{btn.classList.remove('shrinking');btn.classList.add('loading');btn.textContent='';bar.classList.add('visible');progress.classList.add('full');setTimeout(()=>{btn.classList.remove('loading');btn.textContent='✔';bar.classList.remove('visible');btn.classList.add('expanding');setTimeout(()=>{btn.classList.remove('expanding','disabled');btn.innerHTML=`<span><?php echo t('UNPACKED !'); ?></span>`;progress.classList.remove('full')},200)},1800)},200)});function countdown(){const counter=document.getElementById('counter');if(!counter)return;const count=parseInt(counter.textContent);if(count<=0){window.location.href='index.php';return}counter.textContent=count-1}setInterval(countdown,1000);document.addEventListener('click',e=>{if(e.target.classList.contains('msg-close')){e.target.parentElement.style.opacity='0';setTimeout(()=>e.target.parentElement.remove(),200)}});</script>
</html>