<?php

/**
 * @package    Grav\Console\Gpm
 *
 * @copyright  Copyright (c) 2015 - 2026 Trilby Media, LLC. All rights reserved.
 * @license    MIT License; see LICENSE file for details.
 */

namespace Grav\Console\Gpm;

use Grav\Common\GPM\GPM;
use Grav\Common\GPM\Installer;
use Grav\Common\GPM\Upgrader;
use Grav\Common\Grav;
use Grav\Console\GpmCommand;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use ZipArchive;
use function array_key_exists;
use function count;

/**
 * Class UpdateCommand
 * @package Grav\Console\Gpm
 */
class UpdateCommand extends GpmCommand
{
    /** @var array */
    protected $data;
    /** @var string */
    protected $destination;
    /** @var string */
    protected $file;
    /** @var array */
    protected $types = ['plugins', 'themes'];
    /** @var GPM  */
    protected $gpm;
    /** @var string */
    protected $all_yes;
    /** @var string */
    protected $overwrite;
    /** @var Upgrader */
    protected $upgrader;

    /**
     * @return void
     */
    protected function configure(): void
    {
        $this
            ->setName('update')
            ->addOption(
                'force',
                'f',
                InputOption::VALUE_NONE,
                'Force re-fetching the data from remote'
            )
            ->addOption(
                'destination',
                'd',
                InputOption::VALUE_OPTIONAL,
                'The grav instance location where the updates should be applied to. By default this would be where the grav cli has been launched from',
                GRAV_ROOT
            )
            ->addOption(
                'all-yes',
                'y',
                InputOption::VALUE_NONE,
                'Assumes yes (or best approach) instead of prompting'
            )
            ->addOption(
                'overwrite',
                'o',
                InputOption::VALUE_NONE,
                'Option to overwrite packages if they already exist'
            )
            ->addOption(
                'plugins',
                'p',
                InputOption::VALUE_NONE,
                'Update only plugins'
            )
            ->addOption(
                'themes',
                't',
                InputOption::VALUE_NONE,
                'Update only themes'
            )
            ->addArgument(
                'package',
                InputArgument::IS_ARRAY | InputArgument::OPTIONAL,
                'The package or packages that is desired to update. By default all available updates will be applied.'
            )
            ->setDescription('Detects and performs an update of plugins and themes when available')
            ->setHelp('The <info>update</info> command updates plugins and themes when a new version is available');
    }

    /**
     * @return int
     */
    protected function serve(): int
    {
        $input = $this->getInput();
        $io = $this->getIO();

        if (!class_exists(ZipArchive::class)) {
            $io->title('GPM Update');
            $io->error('php-zip extension needs to be enabled!');

            return 1;
        }

        $this->upgrader = new Upgrader($input->getOption('force'));
        $local = $this->upgrader->getLocalVersion();
        $remote = $this->upgrader->getRemoteVersion();
        if (version_compare($local, $remote, '<')) {
            // A new release family is a major upgrade (e.g., 1.7.x -> 1.8.y, or 2.x -> 3.0);
            // from 2.0 on a minor release is an ordinary one.
            $isMajorMinorUpgrade = Upgrader::family($local) !== Upgrader::family($remote);

            if ($isMajorMinorUpgrade) {
                // For major/minor upgrades (e.g., 1.7.x -> 1.8.y), recommend updating plugins FIRST
                $io->writeln('<yellow>WARNING</yellow>: A new major version of Grav is available (v' . $local . ' -> v' . $remote . ').');
                $io->writeln('For major version upgrades, you should update plugins and themes to their latest compatible versions BEFORE upgrading Grav core.');
                $io->writeln('This ensures plugins have any necessary compatibility fixes for the new Grav version.');
                $io->newLine();
                $io->writeln('<green>It is recommended to proceed with updating plugins and themes now.</green>');
            } else {
                // For patch upgrades (e.g., 1.7.45 -> 1.7.46), recommend updating Grav FIRST
                $io->writeln('<yellow>WARNING</yellow>: A new version of Grav is available (v' . $local . ' -> v' . $remote . ').');
                $io->writeln('You should update Grav before updating plugins and themes. If you continue without updating Grav, some plugins or themes may stop working.');
                $io->newLine();
                $question = new ConfirmationQuestion('Continue with the update process? [Y|n] ', true);
                $answer = $io->askQuestion($question);

                if (!$answer) {
                    $io->writeln('<red>Update aborted. Exiting...</red>');

                    return 1;
                }
            }
        }

        $this->gpm = new GPM($input->getOption('force'));

        $this->all_yes = $input->getOption('all-yes');
        $this->overwrite = $input->getOption('overwrite');

        $this->displayGPMRelease();

        $this->destination = realpath($input->getOption('destination'));

        if (!Installer::isGravInstance($this->destination)) {
            $io->writeln('<red>ERROR</red>: ' . Installer::lastErrorMsg());
            exit;
        }
        if ($input->getOption('plugins') === false && $input->getOption('themes') === false) {
            $list_type = ['plugins' => true, 'themes' => true];
        } else {
            $list_type['plugins'] = $input->getOption('plugins');
            $list_type['themes'] = $input->getOption('themes');
        }

        $only_packages = array_map('strtolower', $input->getArgument('package'));
        $this->reportSkippedSymlinks($list_type, $only_packages);

        if ($this->overwrite) {
            $this->data = $this->gpm->getInstallable($list_type);
            $description = ' can be overwritten';
        } else {
            $this->data = $this->gpm->getUpdatable($list_type);
            $description = ' need updating';
        }

        if (!$this->overwrite && !$this->data['total']) {
            $io->writeln('Nothing to update.');

            return 0;
        }

        $io->write("Found <green>{$this->gpm->countInstalled()}</green> packages installed of which <magenta>{$this->data['total']}</magenta>{$description}");

        $limit_to = $this->userInputPackages($only_packages);

        $io->newLine();

        unset($this->data['total'], $limit_to['total']);


        // updates review
        $slugs = [];
        $blocked = [];

        $index = 1;
        foreach ($this->data as $packages) {
            foreach ($packages as $slug => $package) {
                if (!array_key_exists($slug, $limit_to) && count($only_packages)) {
                    continue;
                }

                if (!$package->available) {
                    $package->available = $package->version;
                }

                // Build compatibility badges
                $compat = $package->compatibility ?? null;
                $compatStr = '';
                if (is_array($compat) && !empty($compat['grav'])) {
                    $badges = [];
                    if (in_array('1.7', $compat['grav'], true)) {
                        $badges[] = '<blue>1.7</blue>';
                    }
                    if (in_array('1.8', $compat['grav'], true)) {
                        $badges[] = '<green>1.8</green>';
                    }
                    if (in_array('2.0', $compat['grav'], true)) {
                        $badges[] = '<magenta>2.0</magenta>';
                    }
                    $compatStr = ' ' . implode(' ', $badges);
                }

                $io->writeln(
                    // index
                    str_pad((string)$index++, 2, '0', STR_PAD_LEFT) . '. ' .
                    // name
                    '<cyan>' . str_pad((string) $package->name, 15) . '</cyan> ' .
                    // version
                    "[v<magenta>{$package->version}</magenta> -> v<green>{$package->available}</green>]" .
                    // compat badges
                    $compatStr
                );
                $issue = Installer::getDestinationIssue(
                    $this->destination . DS . $package->install_path,
                    $package->package_type === 'themes'
                );
                if ($issue !== null) {
                    $blocked[] = $slug . ': ' . $issue;
                }
                $slugs[] = $slug;
            }
        }

        if ($blocked) {
            $io->error(array_merge(['No packages were changed. Resolve these destination issues before updating:'], $blocked));
            return 1;
        }
        if (!$slugs) {
            return 0;
        }

        if (!$this->all_yes) {
            // prompt to continue
            $io->newLine();
            $question = new ConfirmationQuestion('Continue with the update process? [Y|n] ', true);
            $answer = $io->askQuestion($question);

            if (!$answer) {
                $io->writeln('<red>Update aborted. Exiting...</red>');

                return 1;
            }
        }

        // finally update
        $install_command = $this->getApplication()->find('install');

        $args = new ArrayInput([
            'command' => 'install',
            'package' => $slugs,
            '-f' => $input->getOption('force'),
            '-d' => $this->destination,
            '-y' => true
        ]);
        $command_exec = $install_command->run($args, $io);

        if ($command_exec != 0) {
            $io->writeln('<red>Error:</red> An error occurred while trying to install the packages');

            return 1;
        }

        return 0;
    }

    /** Report linked packages explicitly, while keeping their targets untouched. */
    protected function reportSkippedSymlinks(array $types, array $onlyPackages): void
    {
        foreach ($types as $type => $enabled) {
            if (!$enabled) {
                continue;
            }
            $installed = $type === 'plugins' ? $this->gpm->getInstalledPlugins() : $this->gpm->getInstalledThemes();
            $repository = $type === 'plugins' ? $this->gpm->getRepositoryPlugins() : $this->gpm->getRepositoryThemes();
            foreach ($installed as $slug => $package) {
                if (!$package->symlink || ($onlyPackages && !in_array($slug, $onlyPackages, true))) {
                    continue;
                }
                $remote = $repository[$slug] ?? null;
                if ($remote && $package->version && $remote->version
                    && version_compare($remote->version, $package->version, '>')) {
                    $this->getIO()->writeln(sprintf(
                        '<comment>Skipped %s (%s → %s): symbolic link; update its target separately.</comment>',
                        $slug, $package->version, $remote->version
                    ));
                }
            }
        }
    }

    /**
     * @param array $only_packages
     * @return array
     */
    private function userInputPackages(array $only_packages): array
    {
        $io = $this->getIO();

        $found = ['total' => 0];
        $ignore = [];

        if (!count($only_packages)) {
            $io->newLine();
        } else {
            foreach ($only_packages as $only_package) {
                $find = $this->gpm->findPackage($only_package);

                if (!$find || (!$this->overwrite && !$this->gpm->isUpdatable($find->slug))) {
                    $name = $find->slug ?? $only_package;
                    $ignore[$name] = $name;
                } else {
                    $found[$find->slug] = $find;
                    $found['total']++;
                }
            }

            if ($found['total']) {
                $list = $found;
                unset($list['total']);
                $list = array_keys($list);

                if ($found['total'] !== $this->data['total']) {
                    $io->write(", only <magenta>{$found['total']}</magenta> will be updated");
                }

                $io->newLine();
                $io->writeln('Limiting updates for only <cyan>' . implode(
                    '</cyan>, <cyan>',
                    $list
                ) . '</cyan>');
            }

            if (count($ignore)) {
                $io->newLine();
                $io->writeln('Packages not found or not requiring updates: <red>' . implode(
                    '</red>, <red>',
                    $ignore
                ) . '</red>');
            }
        }

        return $found;
    }
}
