<?php

namespace App\Http\Controllers\Admin;

use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Services\BackupService;

/**
 * ITSWEBER CMS – Backup & Update Controller (Phase 6e, Hardening)
 *
 * Handles:
 *  - Site backup creation and download
 *  - Backup restore (upload → validate → confirm → execute)
 *  - CMS update package upload, preflight, and application
 *
 * All actions are admin-only (enforced by AuthMiddleware on the route group).
 *
 * Security notes:
 *  - CSRF tokens are validated on every POST.
 *  - Temp files are cleaned up on success and failure.
 *  - Session keys are cleared after use to prevent replay.
 *  - Maintenance mode is handled inside BackupService (enable before, disable after).
 *  - Preflight errors block update execution before any files are touched.
 */
class BackupController extends Controller
{
    // ─────────────────────────────────────────────────────────────────────────
    // INDEX
    // ─────────────────────────────────────────────────────────────────────────

    public function index(Request $request, array $params = []): void
    {
        $this->renderAdmin('backup/index', [
            'pageTitle'      => 'Backup & Update',
            'auth'           => $this->auth(),
            'csrf'           => $this->csrf(),
            'currentVersion' => $this->getCmsVersion(),
            'zipAvailable'   => class_exists('ZipArchive'),
            'maxZipMb'       => (int) (BackupService::getMaxZipSize() / 1024 / 1024),
        ]);
    }

    // ─────────────────────────────────────────────────────────────────────────
    // CREATE BACKUP
    // ─────────────────────────────────────────────────────────────────────────

    /**
     * POST /admin/backup/create
     * Creates a backup ZIP and immediately streams it as a download.
     */
    public function createBackup(Request $request, array $params = []): void
    {
        $this->validateCsrf();

        if (!class_exists('ZipArchive')) {
            set_flash('error', 'Die ZIP-Erweiterung ist auf diesem Server nicht verfügbar. Backup nicht möglich.');
            Response::redirect('/admin/backup');
            return;
        }

        $db       = $this->app->db();
        $basePath = $this->app->basePath();

        try {
            $zipPath = BackupService::createBackup($db, $basePath);

            $siteName     = preg_replace('/[^a-z0-9]/i', '-', \App\Models\Setting::get('site_name', 'site'));
            $siteName     = strtolower(trim($siteName, '-'));
            $downloadName = 'backup_' . ($siteName ?: 'site') . '_' . date('Ymd_His') . '.zip';

            // Stream to browser (exits internally)
            BackupService::streamDownload($zipPath, $downloadName);

        } catch (\Exception $e) {
            set_flash('error', 'Backup fehlgeschlagen: ' . $e->getMessage());
            Response::redirect('/admin/backup');
        }
    }

    // ─────────────────────────────────────────────────────────────────────────
    // RESTORE – Step 1: Upload & Validate
    // ─────────────────────────────────────────────────────────────────────────

    /**
     * POST /admin/backup/restore
     * Receives a backup ZIP upload, validates it, stores it temporarily,
     * and shows a confirmation screen.
     */
    public function restoreUpload(Request $request, array $params = []): void
    {
        $this->validateCsrf();

        $file = $request->file('backup_file') ?? [];

        if (empty($file['name']) || ($file['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_NO_FILE) {
            set_flash('error', 'Bitte wählen Sie eine Backup-Datei aus.');
            Response::redirect('/admin/backup#restore');
            return;
        }

        if ($file['error'] !== UPLOAD_ERR_OK) {
            set_flash('error', $this->uploadErrorMessage($file['error']));
            Response::redirect('/admin/backup#restore');
            return;
        }

        // Extension check
        $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
        if ($ext !== 'zip') {
            set_flash('error', 'Nur ZIP-Dateien werden akzeptiert. Die hochgeladene Datei hat die Endung ".' . htmlspecialchars($ext) . '".');
            Response::redirect('/admin/backup#restore');
            return;
        }

        // Move to temp storage
        $basePath = $this->app->basePath();
        $tempDir  = $basePath . '/storage/temp';
        if (!is_dir($tempDir)) {
            mkdir($tempDir, 0755, true);
        }

        $tempFile = $tempDir . '/restore_' . bin2hex(random_bytes(8)) . '.zip';

        if (!move_uploaded_file($file['tmp_name'], $tempFile)) {
            set_flash('error', 'Die Backup-Datei konnte nicht gespeichert werden. Bitte Schreibrechte auf storage/temp/ prüfen.');
            Response::redirect('/admin/backup#restore');
            return;
        }

        // Validate backup (strict: type, format version, required fields, database.sql presence)
        $result = BackupService::validateBackup($tempFile);

        if (!$result['valid']) {
            @unlink($tempFile);
            set_flash('error', 'Ungültige Backup-Datei: ' . $result['error']);
            Response::redirect('/admin/backup#restore');
            return;
        }

        // Store temp path and manifest in session for the confirm step
        $this->session()->set('restore_temp_file', $tempFile);
        $this->session()->set('restore_manifest',  $result['manifest']);

        // Render confirmation screen
        $this->renderAdmin('backup/restore_confirm', [
            'pageTitle'      => 'Backup wiederherstellen – Bestätigung',
            'auth'           => $this->auth(),
            'csrf'           => $this->csrf(),
            'manifest'       => $result['manifest'],
            'currentVersion' => $this->getCmsVersion(),
        ]);
    }

    // ─────────────────────────────────────────────────────────────────────────
    // RESTORE – Step 2: Execute
    // ─────────────────────────────────────────────────────────────────────────

    /**
     * POST /admin/backup/restore/execute
     * Executes the restore after user confirmation.
     * Maintenance mode is enabled inside BackupService::restore() and cleared afterwards.
     */
    public function restoreExecute(Request $request, array $params = []): void
    {
        $this->validateCsrf();

        $tempFile = $this->session()->get('restore_temp_file', '');
        $manifest = $this->session()->get('restore_manifest', []);

        if (!$tempFile || !is_file($tempFile)) {
            set_flash('error', 'Keine gültige Backup-Sitzung gefunden. Bitte erneut hochladen.');
            Response::redirect('/admin/backup#restore');
            return;
        }

        $db       = $this->app->db();
        $basePath = $this->app->basePath();

        try {
            BackupService::restore($tempFile, $db, $basePath);
        } catch (\Exception $e) {
            @unlink($tempFile);
            $this->session()->remove('restore_temp_file');
            $this->session()->remove('restore_manifest');
            set_flash('error', 'Wiederherstellung fehlgeschlagen: ' . $e->getMessage());
            Response::redirect('/admin/backup#restore');
            return;
        }

        @unlink($tempFile);
        $this->session()->remove('restore_temp_file');
        $this->session()->remove('restore_manifest');

        set_flash('success', 'Wiederherstellung erfolgreich abgeschlossen. Die Website zeigt jetzt den Stand des Backups.');
        Response::redirect('/admin/backup');
    }

    // ─────────────────────────────────────────────────────────────────────────
    // UPDATE – Step 1: Upload, Validate & Preflight
    // ─────────────────────────────────────────────────────────────────────────

    /**
     * POST /admin/backup/update
     * Receives an update package ZIP, validates it, runs a preflight check,
     * and shows the confirmation screen.
     *
     * Preflight errors (write permissions, path traversal) block the process here
     * rather than failing mid-update. Preflight warnings are passed to the confirm
     * screen for display.
     */
    public function updateUpload(Request $request, array $params = []): void
    {
        $this->validateCsrf();

        $file = $request->file('update_file') ?? [];

        if (empty($file['name']) || ($file['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_NO_FILE) {
            set_flash('error', 'Bitte wählen Sie eine Update-Datei aus.');
            Response::redirect('/admin/backup#update');
            return;
        }

        if ($file['error'] !== UPLOAD_ERR_OK) {
            set_flash('error', $this->uploadErrorMessage($file['error']));
            Response::redirect('/admin/backup#update');
            return;
        }

        $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
        if ($ext !== 'zip') {
            set_flash('error', 'Nur ZIP-Dateien werden akzeptiert. Die hochgeladene Datei hat die Endung ".' . htmlspecialchars($ext) . '".');
            Response::redirect('/admin/backup#update');
            return;
        }

        // Move to temp storage
        $basePath = $this->app->basePath();
        $tempDir  = $basePath . '/storage/temp';
        if (!is_dir($tempDir)) {
            mkdir($tempDir, 0755, true);
        }

        $tempFile = $tempDir . '/update_' . bin2hex(random_bytes(8)) . '.zip';

        if (!move_uploaded_file($file['tmp_name'], $tempFile)) {
            set_flash('error', 'Die Update-Datei konnte nicht gespeichert werden. Bitte Schreibrechte auf storage/temp/ prüfen.');
            Response::redirect('/admin/backup#update');
            return;
        }

        // Step 1: Validate manifest (type, format_version, target_version, min_version, etc.)
        $currentVersion = $this->getCmsVersion();
        $result = BackupService::validateUpdate($tempFile, $currentVersion);

        if (!$result['valid']) {
            @unlink($tempFile);
            set_flash('error', 'Ungültiges Update-Paket: ' . $result['error']);
            Response::redirect('/admin/backup#update');
            return;
        }

        // Step 2: Preflight (write permissions, path traversal, protected paths)
        $preflight = BackupService::preflightUpdate($tempFile, $basePath);

        if (!empty($preflight['errors'])) {
            @unlink($tempFile);
            // Show all preflight errors collected, not just the first one
            $errorList = implode(' | ', $preflight['errors']);
            set_flash('error', 'Update-Paket besteht den Preflight-Check nicht: ' . $errorList);
            Response::redirect('/admin/backup#update');
            return;
        }

        // Store temp path and metadata in session
        $this->session()->set('update_temp_file',          $tempFile);
        $this->session()->set('update_manifest',           $result['manifest']);
        $this->session()->set('update_preflight_warnings', $preflight['warnings']);

        $this->renderAdmin('backup/update_confirm', [
            'pageTitle'          => 'CMS-Update – Bestätigung',
            'auth'               => $this->auth(),
            'csrf'               => $this->csrf(),
            'manifest'           => $result['manifest'],
            'currentVersion'     => $currentVersion,
            'preflightWarnings'  => $preflight['warnings'],
        ]);
    }

    // ─────────────────────────────────────────────────────────────────────────
    // UPDATE – Step 2: Execute
    // ─────────────────────────────────────────────────────────────────────────

    /**
     * POST /admin/backup/update/execute
     * Applies the update after user confirmation.
     * Maintenance mode is enabled inside BackupService::applyUpdate() and cleared afterwards.
     */
    public function updateExecute(Request $request, array $params = []): void
    {
        $this->validateCsrf();

        $tempFile = $this->session()->get('update_temp_file', '');
        $manifest = $this->session()->get('update_manifest', []);

        if (!$tempFile || !is_file($tempFile)) {
            set_flash('error', 'Keine gültige Update-Sitzung gefunden. Bitte erneut hochladen.');
            Response::redirect('/admin/backup#update');
            return;
        }

        $db       = $this->app->db();
        $basePath = $this->app->basePath();

        try {
            BackupService::applyUpdate($tempFile, $basePath, $db);
        } catch (\Exception $e) {
            @unlink($tempFile);
            $this->session()->remove('update_temp_file');
            $this->session()->remove('update_manifest');
            $this->session()->remove('update_preflight_warnings');
            set_flash('error', 'Update fehlgeschlagen: ' . $e->getMessage());
            Response::redirect('/admin/backup#update');
            return;
        }

        @unlink($tempFile);
        $this->session()->remove('update_temp_file');
        $this->session()->remove('update_manifest');
        $this->session()->remove('update_preflight_warnings');

        $newVersion = $manifest['target_version'] ?? '?';
        set_flash('success', 'Update erfolgreich eingespielt. Neue Version: ' . $newVersion . '. Bitte den Browser-Cache leeren (Strg+F5).');
        Response::redirect('/admin/backup');
    }

    // ─────────────────────────────────────────────────────────────────────────
    // HELPER
    // ─────────────────────────────────────────────────────────────────────────

    /**
     * Return the installed CMS version.
     *
     * Reads from app/Config/version.php first (canonical, writable by the updater)
     * and falls back to app/Config/app.php for installations that have never been
     * updated (i.e. version.php does not yet exist). Mirrors the logic in
     * BackupService::getCmsVersion() and the cms_version() global helper (ADR-067).
     */
    private function getCmsVersion(): string
    {
        // Canonical source: app/Config/version.php (not in PROTECTED_PATHS)
        $versionFile = $this->app->basePath('app/Config/version.php');
        if (is_file($versionFile)) {
            // phpcs:ignore
            $v = include $versionFile;
            if (is_string($v) && $v !== '') {
                return $v;
            }
        }

        // Fallback: app/Config/app.php (protected – may be stale after updates)
        $config = $this->app->config()->get('version');
        if ($config) {
            return $config;
        }

        $configFile = $this->app->basePath('app/Config/app.php');
        if (is_file($configFile)) {
            $arr = include $configFile;
            if (is_array($arr) && isset($arr['version'])) {
                return $arr['version'];
            }
        }

        return '1.0.0';
    }

    /**
     * Return a human-readable message for a PHP upload error code.
     */
    private function uploadErrorMessage(int $code): string
    {
        $messages = [
            UPLOAD_ERR_INI_SIZE   => 'Die Datei überschreitet die in php.ini festgelegte Maximalgröße (upload_max_filesize).',
            UPLOAD_ERR_FORM_SIZE  => 'Die Datei überschreitet die im Formular angegebene Maximalgröße.',
            UPLOAD_ERR_PARTIAL    => 'Die Datei wurde nur teilweise hochgeladen. Bitte erneut versuchen.',
            UPLOAD_ERR_NO_FILE    => 'Es wurde keine Datei ausgewählt.',
            UPLOAD_ERR_NO_TMP_DIR => 'Temporäres Verzeichnis fehlt. Bitte Serverkonfiguration prüfen.',
            UPLOAD_ERR_CANT_WRITE => 'Datei konnte nicht auf Disk geschrieben werden. Bitte Schreibrechte prüfen.',
            UPLOAD_ERR_EXTENSION  => 'Upload durch eine PHP-Erweiterung abgebrochen.',
        ];

        return $messages[$code] ?? 'Upload-Fehler (Code ' . $code . '). Bitte erneut versuchen.';
    }
}
