<?php

namespace App\Http\Controllers\Admin;

use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Services\BackupService;
use App\Services\FtpBackupService;
use App\Models\Setting;

/**
 * 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
    {
        $effectiveBytes = BackupService::getMaxZipSize();
        $appBytes       = BackupService::getAppMaxZipSize();
        $phpLimited     = ($effectiveBytes < $appBytes);

        // FTP backup: load settings for the FTP tab
        $ftpService = new FtpBackupService();
        $ftpSettings = $ftpService->getSettings();

        $this->renderAdmin('backup/index', [
            'pageTitle'      => __('admin.backup.title'),
            'auth'           => $this->auth(),
            'csrf'           => $this->csrf(),
            'currentVersion' => $this->getCmsVersion(),
            'zipAvailable'   => class_exists('ZipArchive'),
            'maxZipMb'       => (int) ($effectiveBytes / 1024 / 1024),
            'phpLimited'     => $phpLimited,
            'ftpAvailable'   => true,
            'settings'       => $ftpSettings,
        ]);
    }

    // ─────────────────────────────────────────────────────────────────────────
    // 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', __('messages.zip_extension_missing'));
            Response::redirect(admin_url('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', __('messages.error.backup_error', ['error' => $e->getMessage()]));
            Response::redirect(admin_url('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', __('messages.backup_select_file'));
            Response::redirect(admin_url('backup#restore'));
            return;
        }

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

        // Extension check
        $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
        if ($ext !== 'zip') {
            set_flash('error', __('messages.only_zip_allowed', ['ext' => e($ext)]));
            Response::redirect(admin_url('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', __('messages.backup_save_failed'));
            Response::redirect(admin_url('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', __('messages.backup_invalid', ['error' => $result['error']]));
            Response::redirect(admin_url('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'      => __('admin.backup.confirm_title'),
            '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', __('messages.restore_no_session'));
            Response::redirect(admin_url('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', __('messages.restore_failed', ['error' => $e->getMessage()]));
            Response::redirect(admin_url('backup#restore'));
            return;
        }

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

        set_flash('success', __('messages.restore_success'));
        Response::redirect(admin_url('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', __('messages.update_select_file'));
            Response::redirect(admin_url('backup#update'));
            return;
        }

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

        $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
        if ($ext !== 'zip') {
            set_flash('error', __('messages.only_zip_allowed', ['ext' => e($ext)]));
            Response::redirect(admin_url('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', __('messages.update_save_failed'));
            Response::redirect(admin_url('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', __('messages.update_invalid', ['error' => $result['error']]));
            Response::redirect(admin_url('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', __('messages.update_preflight_failed', ['errors' => $errorList]));
            Response::redirect(admin_url('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'          => __('admin.backup.update_confirm_title'),
            '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', __('messages.update_no_session'));
            Response::redirect(admin_url('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', __('messages.update_failed', ['error' => $e->getMessage()]));
            Response::redirect(admin_url('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', __('messages.update_success', ['version' => $newVersion]));
        Response::redirect(admin_url('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
    {
        $keys = [
            UPLOAD_ERR_INI_SIZE   => 'messages.upload_err_ini_size',
            UPLOAD_ERR_FORM_SIZE  => 'messages.upload_err_form_size',
            UPLOAD_ERR_PARTIAL    => 'messages.upload_err_partial',
            UPLOAD_ERR_NO_FILE    => 'messages.upload_err_no_file',
            UPLOAD_ERR_NO_TMP_DIR => 'messages.upload_err_no_tmp_dir',
            UPLOAD_ERR_CANT_WRITE => 'messages.upload_err_cant_write',
            UPLOAD_ERR_EXTENSION  => 'messages.upload_err_extension',
        ];

        return isset($keys[$code])
            ? __($keys[$code])
            : __('messages.upload_err_unknown', ['code' => $code]);
    }

    // ─────────────────────────────────────────────────────────────────────────
    // FTP BACKUP
    // ─────────────────────────────────────────────────────────────────────────

    /**
     * GET /admin/backup/ftp
     * Display FTP backup settings form.
     */
    public function ftpSettings(Request $request, array $params = []): void
    {
        $ftpService = new FtpBackupService();
        $settings = $ftpService->getSettings();

        $this->renderAdmin('backup/ftp_settings', [
            'pageTitle'      => __('admin.ftp_backup.title'),
            'auth'           => $this->auth(),
            'csrf'           => $this->csrf(),
            'settings'       => $settings,
            'ftpAvailable'   => true,
        ]);
    }

    /**
     * POST /admin/backup/ftp/save
     * Save FTP backup settings.
     */
    public function saveFtpSettings(Request $request, array $params = []): void
    {
        $this->validateCsrf();

        $ftpService = new FtpBackupService();

        // Collect settings from POST
        $settings = [
            'host'           => $request->input('ftp_host', ''),
            'port'           => $request->input('ftp_port', '21'),
            'user'           => $request->input('ftp_user', ''),
            'pass'           => $request->input('ftp_pass', ''),
            'path'           => $request->input('ftp_path', '/backups'),
            'schedule'       => $request->input('ftp_schedule', 'manual'),
            'daily_retain'   => $request->input('ftp_daily_retain', '7'),
            'weekly_retain'  => $request->input('ftp_weekly_retain', '4'),
            'items'          => $request->input('ftp_items', []),
        ];

        try {
            $ftpService->saveSettings($settings);
            set_flash('success', __('admin.ftp_backup.settings_saved'));
        } catch (\Exception $e) {
            set_flash('error', __('messages.error.settings_save_error', ['error' => $e->getMessage()]));
        }

        Response::redirect(admin_url('backup#ftp'));
    }

    /**
     * POST /admin/backup/ftp/test (AJAX)
     * Test FTP connection with current settings.
     */
    public function testFtpConnection(Request $request, array $params = []): void
    {
        $this->validateCsrf();

        $ftpService = new FtpBackupService();
        $result = $ftpService->testConnection();

        header('Content-Type: application/json');
        echo json_encode($result);
        exit;
    }

    /**
     * POST /admin/backup/ftp/backup
     * Manually trigger FTP backup now.
     */
    public function runFtpBackup(Request $request, array $params = []): void
    {
        $this->validateCsrf();

        $db       = $this->app->db();
        $basePath = $this->app->basePath();
        $ftpService = new FtpBackupService();

        try {
            $ftpService->backup($db, $basePath);
            set_flash('success', __('admin.ftp_backup.backup_success'));
        } catch (\Exception $e) {
            set_flash('error', __('admin.ftp_backup.backup_failed') . ': ' . $e->getMessage());
        }

        Response::redirect(admin_url('backup#ftp'));
    }
}
