<?php
/*
 * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
 *
 *  Copyright (C) 2019 - 2024 Jan Böhmer (https://github.com/jbtronics)
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Affero General Public License as published
 *  by the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Affero General Public License for more details.
 *
 *  You should have received a copy of the GNU Affero General Public License
 *  along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

declare(strict_types=1);


namespace App\Controller;

use App\Entity\UserSystem\User;
use App\Services\System\BackupManager;
use App\Services\System\InstallationTypeDetector;
use App\Services\System\UpdateChecker;
use App\Services\System\UpdateExecutor;
use Shivas\VersioningBundle\Service\VersionManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Attribute\Route;

/**
 * Controller for the Update Manager web interface.
 *
 * This provides a read-only view of update status and instructions.
 * Actual updates should be performed via the CLI command for safety.
 */
#[Route('/system/update-manager')]
class UpdateManagerController extends AbstractController
{
    public function __construct(
        private readonly UpdateChecker $updateChecker,
        private readonly UpdateExecutor $updateExecutor,
        private readonly VersionManagerInterface $versionManager,
        private readonly BackupManager $backupManager,
        private readonly InstallationTypeDetector $installationTypeDetector,
        private readonly UserPasswordHasherInterface $passwordHasher,
        #[Autowire(env: 'bool:DISABLE_WEB_UPDATES')]
        private readonly bool $webUpdatesDisabled = false,
        #[Autowire(env: 'bool:DISABLE_BACKUP_RESTORE')]
        private readonly bool $backupRestoreDisabled = false,
        #[Autowire(env: 'bool:DISABLE_BACKUP_DOWNLOAD')]
        private readonly bool $backupDownloadDisabled = false,
    ) {
    }

    /**
     * Check if web updates are disabled and throw exception if so.
     */
    private function denyIfWebUpdatesDisabled(): void
    {
        if ($this->webUpdatesDisabled) {
            throw new AccessDeniedHttpException('Web-based updates are disabled by server configuration. Please use the CLI command instead.');
        }
    }

    /**
     * Check if backup restore is disabled and throw exception if so.
     */
    private function denyIfBackupRestoreDisabled(): void
    {
        if ($this->backupRestoreDisabled) {
            throw new AccessDeniedHttpException('Backup restore is disabled by server configuration.');
        }
    }

    /**
     * Check if backup download is disabled and throw exception if so.
     */
    private function denyIfBackupDownloadDisabled(): void
    {
        if ($this->backupDownloadDisabled) {
            throw new AccessDeniedHttpException('Backup download is disabled by server configuration.');
        }
    }

    /**
     * Main update manager page.
     */
    #[Route('', name: 'admin_update_manager', methods: ['GET'])]
    public function index(): Response
    {
        $this->denyAccessUnlessGranted('@system.show_updates');

        $status = $this->updateChecker->getUpdateStatus();
        $availableUpdates = $this->updateChecker->getAvailableUpdates();
        $validation = $this->updateExecutor->validateUpdatePreconditions();

        return $this->render('admin/update_manager/index.html.twig', [
            'status' => $status,
            'available_updates' => $availableUpdates,
            'all_releases' => $this->updateChecker->getAvailableReleases(10),
            'validation' => $validation,
            'is_locked' => $this->updateExecutor->isLocked(),
            'lock_info' => $this->updateExecutor->getLockInfo(),
            'is_maintenance' => $this->updateExecutor->isMaintenanceMode(),
            'maintenance_info' => $this->updateExecutor->getMaintenanceInfo(),
            'update_logs' => $this->updateExecutor->getUpdateLogs(),
            'backups' => $this->backupManager->getBackups(),
            'web_updates_disabled' => $this->webUpdatesDisabled,
            'backup_restore_disabled' => $this->backupRestoreDisabled,
            'backup_download_disabled' => $this->backupDownloadDisabled,
            'is_docker' => $this->installationTypeDetector->isDocker(),
        ]);
    }

    /**
     * AJAX endpoint to check update status.
     */
    #[Route('/status', name: 'admin_update_manager_status', methods: ['GET'])]
    public function status(): JsonResponse
    {
        $this->denyAccessUnlessGranted('@system.show_updates');

        return $this->json([
            'status' => $this->updateChecker->getUpdateStatus(),
            'is_locked' => $this->updateExecutor->isLocked(),
            'is_maintenance' => $this->updateExecutor->isMaintenanceMode(),
            'lock_info' => $this->updateExecutor->getLockInfo(),
        ]);
    }

    /**
     * AJAX endpoint to refresh version information.
     */
    #[Route('/refresh', name: 'admin_update_manager_refresh', methods: ['POST'])]
    public function refresh(Request $request): JsonResponse
    {
        $this->denyAccessUnlessGranted('@system.show_updates');

        // Validate CSRF token
        if (!$this->isCsrfTokenValid('update_manager_refresh', $request->request->get('_token'))) {
            return $this->json(['error' => 'Invalid CSRF token'], Response::HTTP_FORBIDDEN);
        }

        $this->updateChecker->refreshVersionInfo();

        return $this->json([
            'success' => true,
            'status' => $this->updateChecker->getUpdateStatus(),
        ]);
    }

    /**
     * View release notes for a specific version.
     */
    #[Route('/release/{tag}', name: 'admin_update_manager_release', methods: ['GET'])]
    public function releaseNotes(string $tag): Response
    {
        $this->denyAccessUnlessGranted('@system.show_updates');

        $releases = $this->updateChecker->getAvailableReleases(20);
        $release = null;

        foreach ($releases as $r) {
            if ($r['tag'] === $tag) {
                $release = $r;
                break;
            }
        }

        if (!$release) {
            throw $this->createNotFoundException('Release not found');
        }

        return $this->render('admin/update_manager/release_notes.html.twig', [
            'release' => $release,
            'current_version' => $this->updateChecker->getCurrentVersionString(),
        ]);
    }

    /**
     * View an update log file.
     */
    #[Route('/log/{filename}', name: 'admin_update_manager_log', methods: ['GET'])]
    public function viewLog(string $filename): Response
    {
        $this->denyAccessUnlessGranted('@system.manage_updates');

        // Security: Only allow viewing files from the update logs directory
        $logs = $this->updateExecutor->getUpdateLogs();
        $logPath = null;

        foreach ($logs as $log) {
            if ($log['file'] === $filename) {
                $logPath = $log['path'];
                break;
            }
        }

        if (!$logPath || !file_exists($logPath)) {
            throw $this->createNotFoundException('Log file not found');
        }

        $content = file_get_contents($logPath);

        return $this->render('admin/update_manager/log_viewer.html.twig', [
            'filename' => $filename,
            'content' => $content,
        ]);
    }

    /**
     * Start an update process.
     */
    #[Route('/start', name: 'admin_update_manager_start', methods: ['POST'])]
    public function startUpdate(Request $request): Response
    {
        $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
        $this->denyAccessUnlessGranted('@system.manage_updates');
        $this->denyIfWebUpdatesDisabled();

        // Validate CSRF token
        if (!$this->isCsrfTokenValid('update_manager_start', $request->request->get('_token'))) {
            $this->addFlash('error', 'Invalid CSRF token');
            return $this->redirectToRoute('admin_update_manager');
        }

        // Check if update is already running
        if ($this->updateExecutor->isLocked() || $this->updateExecutor->isUpdateRunning()) {
            $this->addFlash('error', 'An update is already in progress.');
            return $this->redirectToRoute('admin_update_manager');
        }

        $targetVersion = $request->request->get('version');
        $createBackup = $request->request->getBoolean('backup', true);

        if (!$targetVersion) {
            // Get latest version if not specified
            $latest = $this->updateChecker->getLatestVersion();
            if (!$latest) {
                $this->addFlash('error', 'Could not determine target version.');
                return $this->redirectToRoute('admin_update_manager');
            }
            $targetVersion = $latest['tag'];
        }

        // Validate preconditions
        $validation = $this->updateExecutor->validateUpdatePreconditions();
        if (!$validation['valid']) {
            $this->addFlash('error', implode(' ', $validation['errors']));
            return $this->redirectToRoute('admin_update_manager');
        }

        // Start the background update
        $pid = $this->updateExecutor->startBackgroundUpdate($targetVersion, $createBackup);

        if (!$pid) {
            $this->addFlash('error', 'Failed to start update process.');
            return $this->redirectToRoute('admin_update_manager');
        }

        // Redirect to progress page
        return $this->redirectToRoute('admin_update_manager_progress');
    }

    /**
     * Update progress page.
     */
    #[Route('/progress', name: 'admin_update_manager_progress', methods: ['GET'])]
    public function progress(): Response
    {
        $this->denyAccessUnlessGranted('@system.manage_updates');

        $progress = $this->updateExecutor->getProgress();
        $currentVersion = $this->versionManager->getVersion()->toString();

        // Determine if this is a downgrade
        $isDowngrade = false;
        if ($progress && isset($progress['target_version'])) {
            $targetVersion = ltrim($progress['target_version'], 'v');
            $isDowngrade = version_compare($targetVersion, $currentVersion, '<');
        }

        return $this->render('admin/update_manager/progress.html.twig', [
            'progress' => $progress,
            'is_locked' => $this->updateExecutor->isLocked(),
            'is_maintenance' => $this->updateExecutor->isMaintenanceMode(),
            'is_downgrade' => $isDowngrade,
            'current_version' => $currentVersion,
        ]);
    }

    /**
     * AJAX endpoint to get update progress.
     */
    #[Route('/progress/status', name: 'admin_update_manager_progress_status', methods: ['GET'])]
    public function progressStatus(): JsonResponse
    {
        $this->denyAccessUnlessGranted('@system.show_updates');

        $progress = $this->updateExecutor->getProgress();

        return $this->json([
            'progress' => $progress,
            'is_locked' => $this->updateExecutor->isLocked(),
            'is_maintenance' => $this->updateExecutor->isMaintenanceMode(),
        ]);
    }

    /**
     * Get backup details for restore confirmation.
     */
    #[Route('/backup/{filename}', name: 'admin_update_manager_backup_details', methods: ['GET'])]
    public function backupDetails(string $filename): JsonResponse
    {
        $this->denyAccessUnlessGranted('@system.manage_updates');

        $details = $this->backupManager->getBackupDetails($filename);

        if (!$details) {
            return $this->json(['error' => 'Backup not found'], 404);
        }

        return $this->json($details);
    }

    /**
     * Create a manual backup.
     */
    #[Route('/backup', name: 'admin_update_manager_backup', methods: ['POST'])]
    public function createBackup(Request $request): Response
    {
        $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
        $this->denyAccessUnlessGranted('@system.manage_updates');

        if (!$this->isCsrfTokenValid('update_manager_backup', $request->request->get('_token'))) {
            $this->addFlash('error', 'Invalid CSRF token.');
            return $this->redirectToRoute('admin_update_manager');
        }

        if ($this->updateExecutor->isLocked()) {
            $this->addFlash('error', 'Cannot create backup while an update is in progress.');
            return $this->redirectToRoute('admin_update_manager');
        }

        try {
            $this->backupManager->createBackup(null, 'manual');
            $this->addFlash('success', 'update_manager.backup.created');
        } catch (\Exception $e) {
            $this->addFlash('error', 'Backup failed: ' . $e->getMessage());
        }

        return $this->redirectToRoute('admin_update_manager');
    }

    /**
     * Delete a backup file.
     */
    #[Route('/backup/delete', name: 'admin_update_manager_backup_delete', methods: ['POST'])]
    public function deleteBackup(Request $request): Response
    {
        $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
        $this->denyAccessUnlessGranted('@system.manage_updates');

        if (!$this->isCsrfTokenValid('update_manager_delete', $request->request->get('_token'))) {
            $this->addFlash('error', 'Invalid CSRF token.');
            return $this->redirectToRoute('admin_update_manager');
        }

        $filename = $request->request->get('filename');
        if ($filename && $this->backupManager->deleteBackup($filename)) {
            $this->addFlash('success', 'update_manager.backup.deleted');
        } else {
            $this->addFlash('error', 'update_manager.backup.delete_error');
        }

        return $this->redirectToRoute('admin_update_manager');
    }

    /**
     * Delete an update log file.
     */
    #[Route('/log/delete', name: 'admin_update_manager_log_delete', methods: ['POST'])]
    public function deleteLog(Request $request): Response
    {
        $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
        $this->denyAccessUnlessGranted('@system.manage_updates');

        if (!$this->isCsrfTokenValid('update_manager_delete', $request->request->get('_token'))) {
            $this->addFlash('error', 'Invalid CSRF token.');
            return $this->redirectToRoute('admin_update_manager');
        }

        $filename = $request->request->get('filename');
        if ($filename && $this->updateExecutor->deleteLog($filename)) {
            $this->addFlash('success', 'update_manager.log.deleted');
        } else {
            $this->addFlash('error', 'update_manager.log.delete_error');
        }

        return $this->redirectToRoute('admin_update_manager');
    }

    /**
     * Download a backup file.
     * Requires password confirmation as backups contain sensitive data (password hashes, secrets, etc.).
     */
    #[Route('/backup/download', name: 'admin_update_manager_backup_download', methods: ['POST'])]
    public function downloadBackup(Request $request): Response
    {
        $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
        $this->denyAccessUnlessGranted('@system.manage_updates');
        $this->denyIfBackupDownloadDisabled();

        if (!$this->isCsrfTokenValid('update_manager_download', $request->request->get('_token'))) {
            $this->addFlash('error', 'Invalid CSRF token.');
            return $this->redirectToRoute('admin_update_manager');
        }

        // Verify password
        $password = $request->request->get('password', '');
        $user = $this->getUser();
        if (!$user instanceof User || !$this->passwordHasher->isPasswordValid($user, $password)) {
            $this->addFlash('error', 'update_manager.backup.download.invalid_password');
            return $this->redirectToRoute('admin_update_manager');
        }

        $filename = $request->request->get('filename', '');
        $details = $this->backupManager->getBackupDetails($filename);
        if (!$details) {
            throw $this->createNotFoundException('Backup not found');
        }

        $response = new BinaryFileResponse($details['path']);
        $response->setContentDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $details['file']);

        return $response;
    }

    /**
     * Restore from a backup.
     */
    #[Route('/restore', name: 'admin_update_manager_restore', methods: ['POST'])]
    public function restore(Request $request): Response
    {
        $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
        $this->denyAccessUnlessGranted('@system.manage_updates');
        $this->denyIfBackupRestoreDisabled();

        // Validate CSRF token
        if (!$this->isCsrfTokenValid('update_manager_restore', $request->request->get('_token'))) {
            $this->addFlash('error', 'Invalid CSRF token.');
            return $this->redirectToRoute('admin_update_manager');
        }

        // Check if already locked
        if ($this->updateExecutor->isLocked()) {
            $this->addFlash('error', 'An update or restore is already in progress.');
            return $this->redirectToRoute('admin_update_manager');
        }

        $filename = $request->request->get('filename');
        $restoreDatabase = $request->request->getBoolean('restore_database', true);
        $restoreConfig = $request->request->getBoolean('restore_config', false);
        $restoreAttachments = $request->request->getBoolean('restore_attachments', false);

        if (!$filename) {
            $this->addFlash('error', 'No backup file specified.');
            return $this->redirectToRoute('admin_update_manager');
        }

        // Verify the backup exists
        $backupDetails = $this->backupManager->getBackupDetails($filename);
        if (!$backupDetails) {
            $this->addFlash('error', 'Backup file not found.');
            return $this->redirectToRoute('admin_update_manager');
        }

        // Execute restore (this is a synchronous operation for now - could be made async later)
        $result = $this->updateExecutor->restoreBackup(
            $filename,
            $restoreDatabase,
            $restoreConfig,
            $restoreAttachments
        );

        if ($result['success']) {
            $this->addFlash('success', 'Backup restored successfully.');
        } else {
            $this->addFlash('error', 'Restore failed: ' . ($result['error'] ?? 'Unknown error'));
        }

        return $this->redirectToRoute('admin_update_manager');
    }
}
