<?php
/**
 * Backup & Restore — metadata under securite/.
 *
 * Backup:
 *  - Creates a ZIP of the entire securite/ tree
 *    (database.sqlite, master.key, wallets/*, backups/* …)
 *  - Two modes: save locally (securite/backups/) or download
 *
 * Restore:
 *  - Uploads a ZIP previously created by this page
 *  - Extracts into securite/, overwriting existing files
 *  - A pre-restore backup is created automatically
 */
require_once dirname(__DIR__) . '/bootstrap.php';

$auth->requireRole(ROLE_APP_ADMIN);

if (!Session::has('csrf_token')) {
    Session::set('csrf_token', bin2hex(random_bytes(32)));
}
$csrfToken = Session::get('csrf_token');

$error   = null;
$success = null;
$warning = null;

/* ── Helper: recursively add a directory to a ZipArchive ────────────── */
function backupAddDir(ZipArchive $zip, string $baseDir, string $dir, array $excludePrefixes = []): int
{
    $count = 0;
    $it = new RecursiveIteratorIterator(
        new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS),
        RecursiveIteratorIterator::SELF_FIRST
    );
    foreach ($it as $file) {
        $realPath = $file->getRealPath();
        $relPath  = ltrim(substr($realPath, strlen($baseDir)), '/\\');

        /* Skip excluded prefixes (e.g. backups/) */
        foreach ($excludePrefixes as $pfx) {
            if (str_starts_with($relPath, $pfx)) continue 2;
        }

        if ($file->isDir()) {
            $zip->addEmptyDir($relPath);
        } else {
            $zip->addFile($realPath, $relPath);
            $count++;
        }
    }
    return $count;
}

/* ── POST: actions ──────────────────────────────────────────────────── */
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    if (!hash_equals(Session::get('csrf_token', ''), $_POST['csrf_token'] ?? '')) {
        $error = t('msg.error');
    } else {
        $action = $_POST['action'] ?? '';

        /* ── Backup (local or download) ─────────────────────────────── */
        if ($action === 'backup_local' || $action === 'backup_download') {
            if (!class_exists('ZipArchive')) {
                $error = t('update.zip_ext_missing');
            } elseif (!is_dir(SECURITE_PATH)) {
                $error = t('backup.securite_missing');
            } else {
                $backupDir = SECURITE_PATH . '/backups';
                if (!is_dir($backupDir)) mkdir($backupDir, 0750, true);

                $filename = 'metadata_' . date('Ymd_His') . '.zip';
                $filepath = $backupDir . '/' . $filename;

                $zip = new ZipArchive();
                if ($zip->open($filepath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
                    $error = t('backup.create_error');
                } else {
                    /* Exclude backups/ (nesting). Par sécurité, la clé maître et
                     * le fichier de mot de passe initial sont EXCLUS par défaut :
                     * un backup contenant à la fois master.key ET database.sqlite
                     * annule le chiffrement au repos. L'opérateur doit sauvegarder
                     * master.key séparément (hors bande). Cocher « inclure la clé »
                     * force son inclusion pour un bundle de reprise complet. */
                    $excludes = ['backups/'];
                    if (empty($_POST['include_key'])) {
                        $excludes[] = 'master.key';
                        $excludes[] = 'initial_admin_password.txt';
                    }
                    $fileCount = backupAddDir($zip, SECURITE_PATH, SECURITE_PATH, $excludes);
                    $zip->close();

                    if ($action === 'backup_download') {
                        /* Stream the file then delete */
                        header('Content-Type: application/zip');
                        header('Content-Disposition: attachment; filename="' . $filename . '"');
                        header('Content-Length: ' . filesize($filepath));
                        readfile($filepath);
                        unlink($filepath);
                        exit;
                    }

                    /* Local backup */
                    $success = sprintf(t('backup.created_ok'), $filename, $fileCount);
                }
            }
        }

        /* ── Restore ────────────────────────────────────────────────── */
        if ($action === 'restore') {
            /* Source: uploaded file or local backup */
            $restoreFromLocal = trim($_POST['restore_from_local'] ?? '');
            if ($restoreFromLocal !== '') {
                /* Restore from existing local backup */
                if (!preg_match('/^(metadata|pre_restore|backup)_\d{8}_\d{6}\.zip$/', $restoreFromLocal)) {
                    $error = t('backup.invalid_file');
                } else {
                    $tmpFile = SECURITE_PATH . '/backups/' . $restoreFromLocal;
                    if (!file_exists($tmpFile)) { $error = t('msg.not_found'); }
                }
            } elseif (!isset($_FILES['restore_zip']) || $_FILES['restore_zip']['error'] !== UPLOAD_ERR_OK) {
                $error = t('update.upload_error');
            } else {
                $tmpFile = $_FILES['restore_zip']['tmp_name'];
                $finfo   = new finfo(FILEINFO_MIME_TYPE);
                $mime    = $finfo->file($tmpFile);
                if (!in_array($mime, ['application/zip', 'application/x-zip-compressed', 'application/octet-stream'], true)) {
                    $error = t('update.invalid_zip');
                }
            }

            if (!$error && !class_exists('ZipArchive')) {
                $error = t('update.zip_ext_missing');
            }

            if (!$error) {
                $zip = new ZipArchive();
                if ($zip->open($tmpFile) !== true) {
                    $error = t('update.invalid_zip');
                } else {
                    /* Scan archive */
                    $entries = [];
                    for ($i = 0; $i < $zip->numFiles; $i++) {
                        $name = $zip->getNameIndex($i);
                        if (str_ends_with($name, '/')) continue;
                        if (str_contains($name, '__MACOSX/')) continue;
                        $normalized = str_replace('\\', '/', $name);
                        if (str_contains($normalized, '..')) continue;
                        if (str_starts_with($normalized, 'backups/')) continue;
                        if (str_ends_with($normalized, '-shm') || str_ends_with($normalized, '-wal')) continue;
                        $entries[] = $name;
                    }

                    if (empty($entries)) {
                        $error = t('update.empty_archive');
                        $zip->close();
                    } else {
                        /* Pre-restore backup */
                        $backupDir = SECURITE_PATH . '/backups';
                        if (!is_dir($backupDir)) mkdir($backupDir, 0750, true);
                        $preBackup = 'pre_restore_' . date('Ymd_His') . '.zip';
                        $preBackupPath = $backupDir . '/' . $preBackup;
                        $bkZip = new ZipArchive();
                        if ($bkZip->open($preBackupPath, ZipArchive::CREATE) === true) {
                            backupAddDir($bkZip, SECURITE_PATH, SECURITE_PATH, ['backups/']);
                            $bkZip->close();
                        }

                        /* Clear module config files before extract so
                           old-format backups (configs in DB) get migrated
                           correctly on next bootstrap. */
                        $modCfgDir = SECURITE_PATH . '/modules';
                        if (is_dir($modCfgDir)) {
                            foreach (glob($modCfgDir . '/*.json') ?: [] as $_cf) {
                                @unlink($_cf);
                            }
                        }

                        /* Close current DB before overwriting */
                        Database::close();
                        @unlink(SECURITE_PATH . '/database.sqlite-shm');
                        @unlink(SECURITE_PATH . '/database.sqlite-wal');

                        /* Create target directories */
                        foreach ($entries as $entry) {
                            $destDir = dirname(SECURITE_PATH . '/' . $entry);
                            if (!is_dir($destDir)) @mkdir($destDir, 0750, true);
                        }

                        /* Extract (stream to disk — no memory load) */
                        $ok = $zip->extractTo(SECURITE_PATH, $entries);
                        $restored = $ok ? count($entries) : 0;
                        $errors   = $ok ? [] : ['extractTo failed'];
                        $zip->close();

                        if ($ok && in_array('master.key', $entries, true)) {
                            @chmod(SECURITE_PATH . '/master.key', 0600);
                        }

                        /* Module config migration (DB → files) runs
                           automatically on the next request bootstrap.
                           The pre-extract cleanup above ensures stale
                           JSON files don't shadow restored DB configs. */

                        if (!empty($errors)) {
                            $warning = sprintf(t('update.partial_errors'), count($errors))
                                     . "\n" . implode("\n", $errors);
                        }
                        $success = sprintf(t('backup.restored_ok'), $restored, $preBackup);
                    }
                }
            }
        }

        /* ── Delete a local backup ──────────────────────────────────── */
        if ($action === 'delete_backup') {
            $file = basename($_POST['file'] ?? '');
            if ($file === '' || !preg_match('/^(metadata|pre_restore|backup)_\d{8}_\d{6}\.zip$/', $file)) {
                $error = t('backup.invalid_file');
            } else {
                $path = SECURITE_PATH . '/backups/' . $file;
                if (file_exists($path)) {
                    unlink($path);
                    $success = sprintf(t('backup.deleted_ok'), $file);
                } else {
                    $error = t('msg.not_found');
                }
            }
        }
    }
}

/* ── List existing local backups ────────────────────────────────────── */
$backups   = [];
$backupDir = SECURITE_PATH . '/backups';
if (is_dir($backupDir)) {
    foreach (scandir($backupDir, SCANDIR_SORT_DESCENDING) as $f) {
        if ($f === '.' || $f === '..') continue;
        if (!str_ends_with($f, '.zip')) continue;
        $full = $backupDir . '/' . $f;
        $backups[] = [
            'name' => $f,
            'size' => filesize($full),
            'date' => date('Y-m-d H:i:s', filemtime($full)),
        ];
    }
}

/* ── Securite directory size ────────────────────────────────────────── */
$securiteSize = 0;
$securiteFiles = 0;
if (is_dir(SECURITE_PATH)) {
    $it = new RecursiveIteratorIterator(
        new RecursiveDirectoryIterator(SECURITE_PATH, FilesystemIterator::SKIP_DOTS)
    );
    foreach ($it as $file) {
        if ($file->isFile()) {
            $subPath = substr($file->getRealPath(), strlen(SECURITE_PATH) + 1);
            /* Exclude backups/ from the count */
            if (!str_starts_with($subPath, 'backups')) {
                $securiteSize += $file->getSize();
                $securiteFiles++;
            }
        }
    }
}

function fmtSize(int $b): string {
    if ($b >= 1073741824) return number_format($b / 1073741824, 2, '.', ' ') . ' GB';
    if ($b >= 1048576)    return number_format($b / 1048576, 1, '.', ' ') . ' MB';
    if ($b >= 1024)       return number_format($b / 1024, 1, '.', ' ') . ' KB';
    return $b . ' B';
}

View::display(__DIR__ . '/views/backup.php', [
    'error'         => $error,
    'success'       => $success,
    'warning'       => $warning,
    'backups'       => $backups,
    'securiteSize'  => $securiteSize,
    'securiteFiles' => $securiteFiles,
    'csrfToken'     => $csrfToken,
]);
