<?php

declare(strict_types=1);

namespace MauticPlugin\DealFlowBundle\Command;

use MauticPlugin\DealFlowBundle\Service\UpdateServiceInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

#[AsCommand(
    name: 'dealflow:update',
    description: 'Manage Deal Flow plugin updates from the command line',
)]
class UpdateCommand extends Command
{
    private const STAGED_SUFFIX = '.staged';
    private const BACKUP_SUFFIX = '.backup';
    private const ALLOWED_DOWNLOAD_HOST = 'dealflowplugin.com';

    public function __construct(
        private readonly UpdateServiceInterface $updateService,
    ) {
        parent::__construct();
    }

    protected function configure(): void
    {
        $this
            ->addOption('check', null, InputOption::VALUE_NONE, 'Check if an update is available')
            ->addOption('download', null, InputOption::VALUE_NONE, 'Download the latest update to a staging directory')
            ->addOption('apply', null, InputOption::VALUE_NONE, 'Apply the staged update (atomic swap)')
            ->addOption('rollback', null, InputOption::VALUE_NONE, 'Roll back to the previous version from backup')
            ->addOption('force', null, InputOption::VALUE_NONE, 'Force a fresh version check (bypass cache)');
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $io = new SymfonyStyle($input, $output);

        $isCheck = (bool) $input->getOption('check');
        $isDownload = (bool) $input->getOption('download');
        $isApply = (bool) $input->getOption('apply');
        $isRollback = (bool) $input->getOption('rollback');
        $isForce = (bool) $input->getOption('force');

        if ($isCheck) {
            return $this->runCheck($io, $isForce);
        }

        if ($isDownload) {
            return $this->runDownload($io, $isForce);
        }

        if ($isApply) {
            return $this->runApply($io);
        }

        if ($isRollback) {
            return $this->runRollback($io);
        }

        // No option — show usage
        $io->title('Deal Flow Updater');
        $io->text('Run updates via the command line — do not use the web UI for updates.');
        $io->newLine();
        $io->listing([
            '<info>bin/console dealflow:update --check</info>           Check if an update is available',
            '<info>bin/console dealflow:update --download</info>        Download the latest update to staging',
            '<info>bin/console dealflow:update --apply</info>           Apply the staged update (atomic swap)',
            '<info>bin/console dealflow:update --rollback</info>        Roll back to the previous backup',
        ]);
        $io->note('Add --force to bypass the version check cache.');

        return Command::SUCCESS;
    }

    private function runCheck(SymfonyStyle $io, bool $forceRefresh): int
    {
        $io->title('Deal Flow — Version Check');

        $result = $this->updateService->checkForUpdates($forceRefresh);

        if ($result->error) {
            $io->error($result->errorMessage ?? 'Unknown error during version check');

            return Command::FAILURE;
        }

        $io->text(sprintf('Current version: <info>v%s</info>', $result->currentVersion));

        if (!$result->updateAvailable) {
            $io->success(sprintf('You are up to date (v%s).', $result->currentVersion));

            return Command::SUCCESS;
        }

        $io->text(sprintf('Latest version:  <comment>v%s</comment>', $result->latestVersion));
        $io->newLine();
        $io->note(sprintf('Update available: v%s → v%s', $result->currentVersion, $result->latestVersion));

        if ($result->changelog !== null && $result->changelog !== '') {
            $io->section('Changelog');
            $io->text($result->changelog);
        }

        if ($result->requiresMigration) {
            $io->warning('This update includes database migrations. Take a database backup before running: bin/console mautic:plugins:reload. dealflow:update --rollback restores files only and does not revert database migrations.');
        }

        if ($result->updatesUntil !== null) {
            $io->text(sprintf('Updates included through: %s', $result->updatesUntil));
        }

        $io->newLine();
        $io->text('To install: <info>bin/console dealflow:update --download</info> then <info>bin/console dealflow:update --apply</info>');

        return Command::SUCCESS;
    }

    private function runDownload(SymfonyStyle $io, bool $forceRefresh): int
    {
        $io->title('Deal Flow — Download Update');

        $result = $this->updateService->checkForUpdates($forceRefresh);

        if ($result->error) {
            $io->error($result->errorMessage ?? 'Unknown error during version check');

            return Command::FAILURE;
        }

        if (!$result->updateAvailable) {
            $io->success('No update available — you are already running the latest version.');

            return Command::FAILURE;
        }

        if ($result->downloadUrl === null || $result->sha256 === null) {
            $io->error('Update check succeeded but no download URL was returned. Try again later.');

            return Command::FAILURE;
        }

        // Validate download URL
        $parsedUrl = parse_url($result->downloadUrl);
        $host = (string) ($parsedUrl['host'] ?? '');
        $scheme = (string) ($parsedUrl['scheme'] ?? '');

        if ($host !== self::ALLOWED_DOWNLOAD_HOST && !str_ends_with($host, '.'.self::ALLOWED_DOWNLOAD_HOST)) {
            $io->error(sprintf('Download URL host "%s" is not from an allowed domain.', $host));

            return Command::FAILURE;
        }

        if ($scheme !== 'https') {
            $io->error('Download URL must use HTTPS.');

            return Command::FAILURE;
        }

        $pluginDir = $this->getPluginDir();
        $pluginsDir = $this->getPluginsDir();
        $stagedDir = $pluginsDir.'/DealFlowBundle'.self::STAGED_SUFFIX;

        $io->text(sprintf('Downloading v%s…', $result->latestVersion));

        $tempFile = sys_get_temp_dir().'/DealFlowBundle-download-'.time().'.zip';

        try {
            $this->downloadFile($result->downloadUrl, $tempFile);
        } catch (\RuntimeException $e) {
            $io->error('Download failed: '.$e->getMessage());

            return Command::FAILURE;
        }

        // Verify SHA256
        $io->text('Verifying checksum…');
        $actualHash = hash_file('sha256', $tempFile);
        if ($actualHash !== $result->sha256) {
            @unlink($tempFile);
            $io->error(sprintf('Checksum mismatch! Expected %s, got %s.', $result->sha256, $actualHash));

            return Command::FAILURE;
        }

        // Validate zip structure
        $zip = new \ZipArchive();
        if ($zip->open($tempFile) !== true) {
            @unlink($tempFile);
            $io->error('Downloaded file is not a valid zip archive.');

            return Command::FAILURE;
        }

        $hasBundleDir = false;
        for ($i = 0; $i < $zip->numFiles; ++$i) {
            $name = $zip->getNameIndex($i);
            if ($name !== false && str_starts_with($name, 'DealFlowBundle/')) {
                $hasBundleDir = true;
                break;
            }
        }
        $zip->close();

        if (!$hasBundleDir) {
            @unlink($tempFile);
            $io->error('Update package does not contain a DealFlowBundle/ directory.');

            return Command::FAILURE;
        }

        // Remove any existing staged directory
        if (is_dir($stagedDir)) {
            $io->text('Removing previous staged directory…');
            $this->removeDirectory($stagedDir);
        }

        try {
            $extractRoot = $this->createExtractionDirectory($pluginsDir);
        } catch (\RuntimeException $e) {
            @unlink($tempFile);
            $io->error($e->getMessage());

            return Command::FAILURE;
        }

        // Extract to an isolated temp directory, then move the extracted bundle
        // into the staged path so --download never mutates the live plugin.
        $io->text(sprintf('Extracting to %s…', $stagedDir));
        $zip = new \ZipArchive();
        if ($zip->open($tempFile) !== true) {
            @unlink($tempFile);
            $this->removeDirectory($extractRoot);
            $io->error('Downloaded file could not be reopened for extraction.');

            return Command::FAILURE;
        }

        $extractResult = $zip->extractTo($extractRoot);
        $zip->close();
        @unlink($tempFile);

        if (!$extractResult) {
            $this->removeDirectory($extractRoot);
            $io->error('Failed to extract update package.');

            return Command::FAILURE;
        }

        // Rename extracted DealFlowBundle to DealFlowBundle.staged
        $extractedDir = $extractRoot.'/DealFlowBundle';
        if (!is_dir($extractedDir)) {
            $this->removeDirectory($extractRoot);
            $io->error('Extraction did not produce a DealFlowBundle directory.');

            return Command::FAILURE;
        }

        if (!rename($extractedDir, $stagedDir)) {
            $this->removeDirectory($extractRoot);
            $io->error(sprintf('Failed to rename extracted directory to %s.', $stagedDir));

            return Command::FAILURE;
        }

        $this->removeDirectory($extractRoot);

        $io->success(sprintf(
            "Downloaded and staged v%s.\n\nRun: bin/console dealflow:update --apply",
            $result->latestVersion,
        ));

        return Command::SUCCESS;
    }

    private function runApply(SymfonyStyle $io): int
    {
        $io->title('Deal Flow — Apply Update');

        $pluginDir = $this->getPluginDir();
        $pluginsDir = $this->getPluginsDir();
        $stagedDir = $pluginsDir.'/DealFlowBundle'.self::STAGED_SUFFIX;
        $backupDir = $pluginsDir.'/DealFlowBundle'.self::BACKUP_SUFFIX;

        if (!is_dir($stagedDir)) {
            $io->error(sprintf(
                'No staged update found at %s. Run: bin/console dealflow:update --download first.',
                $stagedDir,
            ));

            return Command::FAILURE;
        }

        // Remove old backup if present
        if (is_dir($backupDir)) {
            $io->text('Removing old backup…');
            $this->removeDirectory($backupDir);
        }

        // Atomic swap: current → backup, staged → current
        $io->text('Creating backup of current version…');
        if (!rename($pluginDir, $backupDir)) {
            $io->error(sprintf('Failed to move current plugin to backup at %s.', $backupDir));

            return Command::FAILURE;
        }

        $io->text('Installing staged version…');
        if (!rename($stagedDir, $pluginDir)) {
            $restored = $this->restoreRenamedDirectory($backupDir, $pluginDir);
            $io->error($restored
                ? sprintf('Failed to move staged version to %s. Restored from backup.', $pluginDir)
                : sprintf('Failed to move staged version to %s, and restoring from %s also failed. Manual intervention required.', $pluginDir, $backupDir)
            );

            return Command::FAILURE;
        }

        $newVersion = $this->updateService->getCurrentVersion();
        $this->updateService->clearUpdateCache();

        $io->success(sprintf('Deal Flow updated to v%s.', $newVersion));
        $io->newLine();
        $io->text([
            'Next steps:',
            '  1. Clear Symfony cache: <info>rm -rf var/cache/prod && bin/console cache:warmup</info>',
            '  2. Reload plugin if the update requires migrations: <info>bin/console mautic:plugins:reload</info>',
            '     Take a database backup first. dealflow:update --rollback restores files only.',
            '  3. Restart PHP-FPM or your PHP runtime to clear OPcache.',
        ]);

        return Command::SUCCESS;
    }

    private function runRollback(SymfonyStyle $io): int
    {
        $io->title('Deal Flow — Rollback');

        $pluginDir = $this->getPluginDir();
        $pluginsDir = $this->getPluginsDir();
        $backupDir = $pluginsDir.'/DealFlowBundle'.self::BACKUP_SUFFIX;

        if (!is_dir($backupDir)) {
            $io->error(sprintf(
                'No backup found at %s. Cannot roll back.',
                $backupDir,
            ));

            return Command::FAILURE;
        }

        $failedDir = $pluginsDir.'/DealFlowBundle.failed.'.time();

        if (is_dir($pluginDir)) {
            $io->text('Moving current version aside…');
            if (!rename($pluginDir, $failedDir)) {
                $io->error('Failed to move current plugin directory out of the way.');

                return Command::FAILURE;
            }
        }

        $io->text('Restoring backup…');
        if (!rename($backupDir, $pluginDir)) {
            $restored = $this->restoreRenamedDirectory($failedDir, $pluginDir);
            $io->error($restored
                ? 'Failed to restore backup. Restored current version.'
                : sprintf('Failed to restore backup from %s, and restoring the displaced plugin from %s also failed. Manual intervention required.', $backupDir, $failedDir)
            );

            return Command::FAILURE;
        }

        $restoredVersion = $this->updateService->getCurrentVersion();
        $this->updateService->clearUpdateCache();

        $io->success(sprintf('Rolled back to v%s.', $restoredVersion));
        $io->newLine();
        $io->text([
            'Next steps:',
            '  1. Clear Symfony cache: <info>rm -rf var/cache/prod && bin/console cache:warmup</info>',
            '  2. Restart PHP-FPM or your PHP runtime to clear OPcache.',
        ]);

        return Command::SUCCESS;
    }

    protected function getPluginDir(): string
    {
        return \dirname(__DIR__);
    }

    protected function getPluginsDir(): string
    {
        return \dirname($this->getPluginDir());
    }

    protected function createExtractionDirectory(string $pluginsDir): string
    {
        $extractRoot = $pluginsDir.'/.dealflow-extract-'.str_replace('.', '', uniqid('', true));

        if (!mkdir($extractRoot, 0775) && !is_dir($extractRoot)) {
            throw new \RuntimeException(sprintf('Failed to create extraction directory %s', $extractRoot));
        }

        return $extractRoot;
    }

    protected function restoreRenamedDirectory(string $source, string $target): bool
    {
        return is_dir($source) && !is_dir($target) && rename($source, $target);
    }

    protected function downloadFile(string $url, string $destination): void
    {
        $context = stream_context_create([
            'http' => [
                'timeout' => 60,
                'follow_location' => 1,
                'max_redirects' => 3,
            ],
            'ssl' => [
                'verify_peer' => true,
                'verify_peer_name' => true,
            ],
        ]);

        $data = @file_get_contents($url, false, $context);
        if ($data === false) {
            throw new \RuntimeException(sprintf('Failed to download from %s', $url));
        }

        if (@file_put_contents($destination, $data) === false) {
            throw new \RuntimeException(sprintf('Failed to write download to %s', $destination));
        }
    }

    protected function removeDirectory(string $dir): void
    {
        if (!is_dir($dir)) {
            return;
        }

        $items = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS),
            \RecursiveIteratorIterator::CHILD_FIRST
        );

        foreach ($items as $item) {
            if ($item->isDir()) {
                @rmdir($item->getRealPath());
            } else {
                @unlink($item->getRealPath());
            }
        }

        @rmdir($dir);
    }
}
