<?php

declare(strict_types=1);

namespace App\Console\Commands\Jabali;

use Exception;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\File;
use Symfony\Component\Process\Process;

class UpgradeCommand extends Command
{
    protected $signature = 'jabali:upgrade
                            {--check : Only check for updates without upgrading}
                            {--force : Force upgrade even if already up to date}';

    protected $description = 'Upgrade Jabali Panel to the latest version';

    private string $basePath;

    private string $versionFile;

    public function __construct()
    {
        parent::__construct();
        $this->basePath = base_path();
        $this->versionFile = $this->basePath.'/VERSION';
    }

    public function handle(): int
    {
        if ($this->option('check')) {
            return $this->checkForUpdates();
        }

        return $this->performUpgrade();
    }

    private function checkForUpdates(): int
    {
        $this->info('Checking for updates...');

        $currentVersion = $this->getCurrentVersion();
        $this->line("Current version: <info>{$currentVersion}</info>");

        try {
            $this->configureGitSafeDirectory();
            $this->ensureGitRepository();
            $updateSource = $this->fetchUpdates();

            // Check if there are updates
            $behindCount = trim($this->executeCommandOrFail("git rev-list HEAD..{$updateSource['remoteRef']} --count"));

            if ($behindCount === '0') {
                $this->info('Jabali Panel is up to date!');

                return 0;
            }

            $this->warn("Updates available: {$behindCount} commit(s) behind");

            // Show recent commits
            $this->line("\nRecent changes:");
            $commits = $this->executeCommandOrFail("git log HEAD..{$updateSource['remoteRef']} --oneline -10");
            if ($commits !== '') {
                $this->line($commits);
            }

            return 0;
        } catch (Exception $e) {
            $this->error('Failed to check for updates: '.$e->getMessage());

            return 1;
        }
    }

    private function performUpgrade(): int
    {
        $this->info('Starting Jabali Panel upgrade...');
        $this->newLine();

        $currentVersion = $this->getCurrentVersion();
        $this->line("Current version: <info>{$currentVersion}</info>");

        // Step 1: Check git status
        $this->info('[1/9] Checking repository status...');
        try {
            $this->configureGitSafeDirectory();
            $this->ensureGitRepository();
            $this->ensureWritableStorage();
            $statusResult = $this->executeCommand('git status --porcelain');
            if ($statusResult['exitCode'] !== 0) {
                throw new Exception($statusResult['output'] ?: 'Unable to read git status.');
            }

            $status = $statusResult['output'];
            if (! empty(trim($status)) && ! $this->option('force')) {
                $this->warn('Working directory has uncommitted changes:');
                $this->line($status);
                if (! $this->confirm('Continue anyway? Local changes may be overwritten.')) {
                    $this->info('Upgrade cancelled.');

                    return 0;
                }
            }
        } catch (Exception $e) {
            $this->error('Git check failed: '.$e->getMessage());

            return 1;
        }

        // Step 2: Fetch updates
        $this->info('[2/9] Fetching updates from repository...');
        try {
            $updateSource = $this->fetchUpdates();
        } catch (Exception $e) {
            $this->error('Failed to fetch updates: '.$e->getMessage());

            return 1;
        }

        // Step 3: Check if updates available
        $behindCount = trim($this->executeCommandOrFail("git rev-list HEAD..{$updateSource['remoteRef']} --count"));
        if ($behindCount === '0' && ! $this->option('force')) {
            $this->info('Already up to date!');

            return 0;
        }

        // Step 4: Pull changes
        $oldHead = trim($this->executeCommandOrFail('git rev-parse HEAD'));
        $this->info('[3/9] Pulling latest changes...');
        try {
            $pullResult = $this->executeCommand("git pull --ff-only {$updateSource['pullRemote']} main");
            if ($pullResult['exitCode'] !== 0) {
                throw new Exception($pullResult['output'] ?: 'Git pull failed.');
            }
            if ($pullResult['output'] !== '') {
                $this->line($pullResult['output']);
            }
        } catch (Exception $e) {
            $this->error('Failed to pull changes: '.$e->getMessage());
            $this->warn('You may need to resolve conflicts manually.');

            return 1;
        }

        $newHead = trim($this->executeCommandOrFail('git rev-parse HEAD'));
        $changedFiles = $this->getChangedFiles($oldHead, $newHead);

        $hasVendor = File::exists($this->basePath.'/vendor/autoload.php');
        $hasManifest = File::exists($this->basePath.'/public/build/manifest.json');
        $hasPackageJson = File::exists($this->basePath.'/package.json');

        $shouldRunComposer = $this->shouldRunComposerInstall($changedFiles, $this->option('force'), $hasVendor);
        $shouldRunNpm = $this->shouldRunNpmBuild($changedFiles, $this->option('force'), $hasManifest, $hasPackageJson);
        $shouldRunMigrations = $this->shouldRunMigrations($changedFiles, $this->option('force'));

        // Step 5: Install composer dependencies
        $this->info('[4/9] Installing PHP dependencies...');
        if ($shouldRunComposer) {
            try {
                $this->ensureCommandAvailable('composer');
                $composerResult = $this->executeCommand('composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader', 1200);
                if ($composerResult['exitCode'] !== 0) {
                    throw new Exception($composerResult['output'] ?: 'Composer install failed.');
                }
                if ($composerResult['output'] !== '') {
                    $this->line($composerResult['output']);
                }
            } catch (Exception $e) {
                $this->error('Failed to install dependencies: '.$e->getMessage());

                return 1;
            }
        } else {
            $this->line('No composer changes detected, skipping.');
        }

        // Step 5b: Install npm dependencies and build assets
        $this->info('[5/9] Building frontend assets...');
        if ($shouldRunNpm) {
            try {
                $this->ensureCommandAvailable('npm');
                $npmInstall = File::exists($this->basePath.'/package-lock.json') ? 'npm ci' : 'npm install';
                $installResult = $this->executeCommand($npmInstall, 1200);
                if ($installResult['exitCode'] !== 0) {
                    throw new Exception($installResult['output'] ?: 'npm install failed.');
                }
                if ($installResult['output'] !== '') {
                    $this->line($installResult['output']);
                }

                $buildResult = $this->executeCommand('npm run build', 1200);
                if ($buildResult['exitCode'] !== 0) {
                    throw new Exception($buildResult['output'] ?: 'npm build failed.');
                }
                if ($buildResult['output'] !== '') {
                    $this->line($buildResult['output']);
                }
            } catch (Exception $e) {
                $this->error('Asset build failed: '.$e->getMessage());

                return 1;
            }
        } else {
            $this->line('No frontend changes detected, skipping.');
        }

        // Step 6: Run migrations
        $this->info('[6/9] Running database migrations...');
        if ($shouldRunMigrations) {
            try {
                Artisan::call('migrate', ['--force' => true]);
                $this->line(Artisan::output());
            } catch (Exception $e) {
                $this->error('Migration failed: '.$e->getMessage());

                return 1;
            }
        } else {
            $this->line('No migration changes detected, skipping.');
        }

        // Step 7: Clear caches
        $this->info('[7/9] Clearing caches...');
        try {
            Artisan::call('optimize:clear');
            $this->line(Artisan::output());
        } catch (Exception $e) {
            $this->warn('Cache clear warning: '.$e->getMessage());
        }

        // Step 8: Setup Redis ACL if not configured
        $this->info('[8/9] Checking Redis ACL configuration...');
        $this->setupRedisAcl();

        // Step 9: Restart services
        $this->info('[9/9] Restarting services...');
        $this->restartServices();

        $newVersion = $this->getCurrentVersion();
        $this->newLine();
        $this->info("Upgrade complete! Version: {$newVersion}");

        return 0;
    }

    /**
     * @return array{pullRemote: string, remoteRef: string}
     */
    private function fetchUpdates(): array
    {
        $originUrl = trim($this->executeCommandOrFail('git remote get-url origin'));
        $fetchAttempts = [
            ['remote' => 'origin', 'ref' => 'origin/main', 'type' => 'remote'],
        ];

        if ($this->hasRemote('gitea')) {
            $fetchAttempts[] = ['remote' => 'gitea', 'ref' => 'gitea/main', 'type' => 'remote'];
        }

        if ($this->isGithubSshUrl($originUrl)) {
            $fetchAttempts[] = [
                'remote' => $this->githubHttpsUrlFromSsh($originUrl),
                'ref' => 'jabali-upgrade/main',
                'type' => 'url',
            ];
        }

        $lastError = null;
        foreach ($fetchAttempts as $attempt) {
            $result = $attempt['type'] === 'url'
                ? $this->executeCommand("git fetch {$attempt['remote']} main:refs/remotes/{$attempt['ref']}")
                : $this->executeCommand("git fetch {$attempt['remote']} main");

            if (($result['exitCode'] ?? 1) === 0) {
                return [
                    'pullRemote' => $attempt['remote'],
                    'remoteRef' => $attempt['ref'],
                ];
            }

            $lastError = $result['output'] ?? 'Unknown error';
        }

        throw new Exception($lastError ?: 'Unable to fetch updates from any configured remote.');
    }

    private function hasRemote(string $remote): bool
    {
        $result = $this->executeCommand("git remote get-url {$remote}");

        return ($result['exitCode'] ?? 1) === 0;
    }

    private function isGithubSshUrl(string $url): bool
    {
        return str_starts_with($url, 'git@github.com:') || str_starts_with($url, 'ssh://git@github.com/');
    }

    private function githubHttpsUrlFromSsh(string $url): string
    {
        if (str_starts_with($url, 'git@github.com:')) {
            $path = substr($url, strlen('git@github.com:'));

            return 'https://github.com/'.$path;
        }

        if (str_starts_with($url, 'ssh://git@github.com/')) {
            $path = substr($url, strlen('ssh://git@github.com/'));

            return 'https://github.com/'.$path;
        }

        return $url;
    }

    private function getCurrentVersion(): string
    {
        if (! File::exists($this->versionFile)) {
            return 'unknown';
        }

        $content = File::get($this->versionFile);
        if (preg_match('/VERSION=(.+)/', $content, $matches)) {
            return trim($matches[1]);
        }

        return 'unknown';
    }

    protected function executeCommand(string $command, int $timeout = 600): array
    {
        $process = Process::fromShellCommandline($command, $this->basePath, $this->getCommandEnvironment());
        $process->setTimeout($timeout);
        $process->run();
        $output = trim($process->getOutput().$process->getErrorOutput());

        return [
            'exitCode' => $process->getExitCode() ?? 1,
            'output' => $output,
        ];
    }

    protected function executeCommandOrFail(string $command, int $timeout = 600): string
    {
        $result = $this->executeCommand($command, $timeout);
        if ($result['exitCode'] !== 0) {
            throw new Exception($result['output'] ?: "Command failed: {$command}");
        }

        return $result['output'];
    }

    protected function getCommandEnvironment(): array
    {
        $path = getenv('PATH') ?: '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin';

        return [
            'PATH' => $path,
            'COMPOSER_ALLOW_SUPERUSER' => '1',
        ];
    }

    protected function ensureCommandAvailable(string $command): void
    {
        $result = $this->executeCommand("command -v {$command}");
        if ($result['exitCode'] !== 0 || $result['output'] === '') {
            throw new Exception("Required command not found: {$command}");
        }
    }

    protected function ensureGitRepository(): void
    {
        if (! File::isDirectory($this->basePath.'/.git')) {
            throw new Exception('Not a git repository.');
        }
    }

    protected function configureGitSafeDirectory(): void
    {
        foreach ($this->getSafeDirectoryCommands() as $command) {
            $this->executeCommand($command);
        }
    }

    protected function ensureWritableStorage(): void
    {
        foreach ($this->getWritableStorageCommands() as $command) {
            $this->executeCommand($command);
        }
    }

    protected function getSafeDirectoryCommands(): array
    {
        $directory = $this->basePath;
        $commands = [
            "git config --global --add safe.directory {$directory}",
        ];

        if (! $this->isRunningAsRoot()) {
            return $commands;
        }

        $commands[] = "git config --system --add safe.directory {$directory}";

        if ($this->commandExists('sudo') && $this->userExists('www-data')) {
            $commands[] = "sudo -u www-data git config --global --add safe.directory {$directory}";
        }

        return $commands;
    }

    protected function getWritableStorageCommands(): array
    {
        if (! $this->isRunningAsRoot() || ! $this->userExists('www-data')) {
            return [];
        }

        $paths = [
            $this->basePath.'/database',
            $this->basePath.'/storage',
            $this->basePath.'/bootstrap/cache',
        ];

        $escapedPaths = array_map('escapeshellarg', $paths);
        $pathList = implode(' ', $escapedPaths);

        return [
            "chgrp -R www-data {$pathList}",
            "chmod -R g+rwX {$pathList}",
            "find {$pathList} -type d -exec chmod g+s {} +",
        ];
    }

    protected function commandExists(string $command): bool
    {
        $result = $this->executeCommand("command -v {$command}");

        return $result['exitCode'] === 0 && $result['output'] !== '';
    }

    protected function userExists(string $user): bool
    {
        if (function_exists('posix_getpwnam')) {
            return posix_getpwnam($user) !== false;
        }

        return $this->executeCommand("id -u {$user}")['exitCode'] === 0;
    }

    private function getChangedFiles(string $from, string $to): array
    {
        if ($from === $to) {
            return [];
        }

        $output = $this->executeCommandOrFail("git diff --name-only {$from}..{$to}");
        if ($output === '') {
            return [];
        }

        return array_values(array_filter(array_map('trim', explode("\n", $output))));
    }

    protected function shouldRunComposerInstall(array $changedFiles, bool $force, bool $hasVendor): bool
    {
        if ($force || ! $hasVendor) {
            return true;
        }

        return $this->hasChangedFile($changedFiles, ['composer.json', 'composer.lock']);
    }

    protected function shouldRunNpmBuild(array $changedFiles, bool $force, bool $hasManifest, bool $hasPackageJson): bool
    {
        if (! $hasPackageJson) {
            return false;
        }

        if ($force || ! $hasManifest) {
            return true;
        }

        if ($this->hasChangedFile($changedFiles, [
            'package.json',
            'package-lock.json',
            'vite.config.js',
            'postcss.config.js',
            'tailwind.config.js',
        ])) {
            return true;
        }

        return $this->hasChangedPathPrefix($changedFiles, 'resources/');
    }

    protected function shouldRunMigrations(array $changedFiles, bool $force): bool
    {
        if ($force) {
            return true;
        }

        return $this->hasChangedPathPrefix($changedFiles, 'database/migrations/');
    }

    protected function hasChangedFile(array $changedFiles, array $targets): bool
    {
        foreach ($targets as $target) {
            if (in_array($target, $changedFiles, true)) {
                return true;
            }
        }

        return false;
    }

    protected function hasChangedPathPrefix(array $changedFiles, string $prefix): bool
    {
        foreach ($changedFiles as $file) {
            if (str_starts_with($file, $prefix)) {
                return true;
            }
        }

        return false;
    }

    protected function restartServices(): void
    {
        try {
            Artisan::call('queue:restart');
            $this->line('  - queue restarted');
        } catch (Exception $e) {
            $this->warn('Queue restart warning: '.$e->getMessage());
        }

        if (! $this->isRunningAsRoot()) {
            $this->warn('Skipping system service reloads (requires root).');

            return;
        }

        $agentResult = $this->executeCommand('systemctl restart jabali-agent');
        if ($agentResult['exitCode'] === 0) {
            $this->line('  - jabali-agent restarted');
        } else {
            $this->warn('  - jabali-agent restart failed');
        }

        $fpmResult = $this->executeCommand('systemctl reload php*-fpm');
        if ($fpmResult['exitCode'] === 0) {
            $this->line('  - PHP-FPM reloaded (all versions)');
        } else {
            $this->warn('  - PHP-FPM reload failed');
        }
    }

    protected function isRunningAsRoot(): bool
    {
        if (function_exists('posix_geteuid')) {
            return posix_geteuid() === 0;
        }

        return getmyuid() === 0;
    }

    private function setupRedisAcl(): void
    {
        $credFile = '/root/.jabali_redis_credentials';
        $aclFile = '/etc/redis/users.acl';

        // Check if Redis ACL is already configured
        if (File::exists($credFile) && File::exists($aclFile)) {
            $this->line('  - Redis ACL already configured');

            return;
        }

        // Check if we have permission to write to /root/
        if (! is_writable('/root') && ! File::exists($credFile)) {
            $this->line('  - Skipping Redis ACL setup (requires root privileges)');

            return;
        }

        $this->line('  - Setting up Redis ACL...');

        // Generate admin password
        $password = bin2hex(random_bytes(16));

        // Save credentials
        File::put($credFile, "REDIS_ADMIN_PASSWORD={$password}\n");
        chmod($credFile, 0600);

        // Create ACL file
        $aclContent = "user default off\nuser jabali_admin on >{$password} ~* &* +@all\n";
        File::put($aclFile, $aclContent);
        chmod($aclFile, 0640);
        chown($aclFile, 'redis');
        chgrp($aclFile, 'redis');

        // Check if redis.conf has aclfile directive
        $redisConf = '/etc/redis/redis.conf';
        if (File::exists($redisConf)) {
            $conf = File::get($redisConf);
            if (strpos($conf, 'aclfile') === false) {
                // Add aclfile directive
                $conf .= "\n# ACL configuration\naclfile /etc/redis/users.acl\n";
                File::put($redisConf, $conf);
            }
        }

        // Update Laravel .env with Redis credentials
        $envFile = base_path('.env');
        if (File::exists($envFile)) {
            $env = File::get($envFile);

            // Update or add Redis credentials
            if (strpos($env, 'REDIS_USERNAME=') === false) {
                $env = preg_replace(
                    '/REDIS_HOST=.*/m',
                    "REDIS_HOST=127.0.0.1\nREDIS_USERNAME=jabali_admin\nREDIS_PASSWORD={$password}",
                    $env
                );
            } else {
                $env = preg_replace('/REDIS_PASSWORD=.*/m', "REDIS_PASSWORD={$password}", $env);
            }

            File::put($envFile, $env);
        }

        // Restart Redis
        exec('systemctl restart redis-server 2>&1', $output, $code);
        if ($code === 0) {
            $this->line('  - Redis ACL configured and restarted');

            // Migrate existing users
            $this->line('  - Migrating existing users to Redis ACL...');
            try {
                Artisan::call('jabali:migrate-redis-users');
                $this->line(Artisan::output());
            } catch (Exception $e) {
                $this->warn('  - Redis user migration warning: '.$e->getMessage());
            }
        } else {
            $this->warn('  - Redis restart failed, ACL may not be active');
        }
    }
}
