<?php

/**
 * @package     Dotclear
 * @subpackage  Upgrade
 *
 * @copyright   Olivier Meunier & Association Dotclear
 * @copyright   AGPL-3.0
 */
declare(strict_types=1);

namespace Dotclear\Process\Upgrade;

use Dotclear\App;
use Dotclear\Helper\Html\Form\Caption;
use Dotclear\Helper\Html\Form\Capture;
use Dotclear\Helper\Html\Form\Div;
use Dotclear\Helper\Html\Form\Form;
use Dotclear\Helper\Html\Form\Hidden;
use Dotclear\Helper\Html\Form\Img;
use Dotclear\Helper\Html\Form\Li;
use Dotclear\Helper\Html\Form\Note;
use Dotclear\Helper\Html\Form\Para;
use Dotclear\Helper\Html\Form\Set;
use Dotclear\Helper\Html\Form\Submit;
use Dotclear\Helper\Html\Form\Table;
use Dotclear\Helper\Html\Form\Td;
use Dotclear\Helper\Html\Form\Text;
use Dotclear\Helper\Html\Form\Th;
use Dotclear\Helper\Html\Form\Tr;
use Dotclear\Helper\Html\Form\Ul;
use Dotclear\Helper\Html\Html;
use Dotclear\Helper\Process\TraitProcess;
use Dotclear\Module\ModuleDefine;
use Throwable;

/**
 * @brief   Upgarde process plugins management page.
 *
 * As we are in safe mode, plugins are soft disabled.
 *
 * @since   2.29
 */
class Plugins
{
    use TraitProcess;

    /**
     * @var     array<int, ModuleDefine>    $next_store
     */
    private static ?array $next_store = null;

    /**
     * @var     null|array<string, array<string, bool|string> > $plugins_install
     */
    private static ?array $plugins_install = null;

    public static function init(): bool
    {
        App::upgrade()->page()->checkSuper();

        // Load modules in safe mode
        App::plugins()->safeMode(true);

        try {
            App::plugins()->loadModules(App::config()->pluginsRoot(), 'upgrade', App::lang()->getLang());
        } catch (Throwable) {
            App::error()->add(__('Some plugins could not be loaded.'));
        }

        App::upgrade()->pluginsList()::$allow_multi_install = App::config()->allowMultiModules();

        # -- Execute actions --
        try {
            App::upgrade()->pluginsList()->doActions();
        } catch (Throwable $throwable) {
            App::error()->add($throwable->getMessage());
        }

        if (!empty($_POST['nextstorecheck'])) {
            self::$next_store = App::upgrade()->nextStore()->getDefines(true);
        }

        return self::status(true);
    }

    public static function process(): bool
    {
        // Plugins install
        self::$plugins_install = null;
        if (!App::error()->flag()) {
            $do_install = isset($_REQUEST['do_install']);
            if ($do_install) {
                // Add BACKEND context to run plugins installation if necessary
                App::task()->addContext('BACKEND');
            }

            self::$plugins_install = App::plugins()->installModules();
            if (!$do_install) {
                // Messages, only if not already done by installModules() above
                if (!empty(self::$plugins_install['success'])) {
                    $success = [];
                    foreach (array_keys(self::$plugins_install['success']) as $k) {
                        $info      = implode(' - ', App::upgrade()->pluginsList()->getSettingsUrls($k, true));
                        $success[] = $k . ($info !== '' ? ' → ' . $info : '');
                    }

                    App::upgrade()->notices()->success(
                        (new Div())
                            ->items([
                                (new Note())
                                    ->text(__('Following plugins have been installed:')),
                                (new Ul())
                                    ->items(
                                        array_map(fn (string $item) => (new Li())->text($item), $success)
                                    ),
                            ])
                        ->render(),
                        false,
                        true
                    );
                    unset($success);
                }

                if (!empty(self::$plugins_install['failure'])) {
                    $failure = [];
                    foreach (self::$plugins_install['failure'] as $k => $v) {
                        $failure[] = $k . ' (' . $v . ')';
                    }

                    App::upgrade()->notices()->error(
                        (new Div())
                            ->items([
                                (new Note())
                                    ->text(__('Following plugins have not been installed:')),
                                (new Ul())
                                    ->items(
                                        array_map(fn (string $item) => (new Li())->text($item), $failure)
                                    ),
                            ])
                        ->render(),
                        false,
                        true
                    );
                    unset($failure);
                }
            }
        }

        if (null === App::config()->storePluginUrl()) {
            App::upgrade()->notices()->addMessageNotice(__('Official plugins repository could not be updated as there is no URL set in configuration.'));
        }

        if (!App::error()->flag() && !empty($_GET['nocache'])) {
            App::upgrade()->notices()->addSuccessNotice(__('Manual checking of plugins update done successfully.'));
        }

        return true;
    }

    public static function render(): void
    {
        // -- Page header --
        App::upgrade()->page()->open(
            __('Plugins management'),
            (
                empty($_GET['nocache']) && empty($_GET['showupdate']) ?
                App::upgrade()->page()->jsJson('module_update_url', App::upgrade()->url()->get('upgrade.plugins', ['showupdate' => 1]) . '#update') : ''
            ) .
            App::upgrade()->page()->jsLoad('js/_plugins.js') .
            App::upgrade()->page()->jsPageTabs(),
            App::upgrade()->page()->breadcrumb(
                [
                    __('Dotclear update')    => '',
                    __('Plugins management') => '',
                ]
            )
        );

        // -- Display modules lists --

        $multi_parts = [];

        $parts = [];

        # Activated modules
        $defines = App::upgrade()->pluginsList()->modules->getDefines(
            ['state' => App::upgrade()->pluginsList()->modules->safeMode() ? ModuleDefine::STATE_SOFT_DISABLED : ModuleDefine::STATE_ENABLED]
        );
        if ($defines !== []) {
            $parts[] = (new Set())
                ->items([
                    (new Text(
                        'h3',
                        __('Activated plugins') . ' ' . __('(in normal mode)')
                    )),
                    (new Note())
                        ->class('more-info')
                        ->text(__('You can configure and manage installed plugins from this list.')),
                    (new Capture(App::upgrade()->pluginsList()
                        ->setList('plugin-activate')
                        ->setTab('plugins')
                        ->setDefines($defines)
                        ->displayModules(...), [
                            // cols
                            ['expander', 'name', 'version', 'desc', 'distrib', 'deps'],
                            // actions
                            ['deactivate', 'delete', 'behavior'],
                        ])),
                ]);
        }

        # Deactivated modules
        $defines = App::upgrade()->pluginsList()->modules->getDefines(['state' => ModuleDefine::STATE_HARD_DISABLED]);
        if ($defines !== []) {
            $parts[] = (new Set())
                ->items([
                    (new Text(
                        'h3',
                        __('Deactivated plugins')
                    )),
                    (new Note())
                        ->class('more-info')
                        ->text(__('Deactivated plugins are installed but not usable. You can activate them from here.')),
                    (new Capture(App::upgrade()->pluginsList()
                        ->setList('plugin-deactivate')
                        ->setTab('plugins')
                        ->setDefines($defines)
                        ->displayModules(...), [
                            // cols
                            ['expander', 'name', 'version', 'desc', 'distrib'],
                            // actions
                            ['activate', 'delete'],
                        ])),
                ]);
        }

        $multi_parts[] = (new Div('plugins'))
            ->title(__('Installed plugins'))
            ->class('multi-part')
            ->items($parts);

        // Updated modules from repo
        $defines = App::upgrade()->pluginsList()->store->getDefines(true);
        $updates = $defines === [] ? '' : sprintf(' (%s)', count($defines));

        $parts = [];
        if ($defines === []) {
            $parts[] = (new Note())
                ->text(__('No updates available for plugins.'));
        } else {
            $parts[] = (new Note())
                ->text(sprintf(__('There is one plugin update available:', 'There are %s plugin updates available:', count($defines)), count($defines)));

            $parts[] = (new Capture(App::upgrade()->pluginsList()
                ->setList('plugin-update')
                ->setTab('update')
                ->setDefines($defines)
                ->displayModules(...), [
                    // cols
                    ['checkbox', 'name', 'version', 'repository', 'current_version', 'desc'],
                    // actions
                    ['update'],
                ]));

            $parts[] = (new Note())
                ->class(['info', 'vertical-separator'])
                ->text(sprintf(
                    __('Visit %s plugins repository.'),
                    '<a href="https://dotclear.org/plugin/list">Dotclear</a>'
                ));
        }

        $multi_parts[] = (new Div('update'))
            ->title(Html::escapeHTML(__('Update plugins')) . $updates)
            ->class('multi-part')
            ->items([
                (new Text('h3', __('Update plugins'))),
                (new Form('force-checking'))
                    ->action(App::upgrade()->pluginsList()->getURL('', true, 'update'))
                    ->method('get')
                    ->fields([
                        (new Para())
                        ->items([
                            (new Hidden('nocache', '1')),
                            (new Hidden(['process'], 'Plugins')),
                            (new Submit('force-checking-update', __('Force checking update of plugins'))),
                        ]),
                    ]),
                ... $parts,
            ]);

        // Check all Modules except from ditrib
        $multi_parts[] = self::nextStoreList(explode(',', App::config()->distributedPlugins()), App::upgrade()->url()->get('upgrade.plugins'));

        if (App::upgrade()->pluginsList()->isWritablePath()) {
            # New modules from repo
            $search  = App::upgrade()->pluginsList()->getSearch();
            $defines = $search ? App::upgrade()->pluginsList()->store->searchDefines($search) : App::upgrade()->pluginsList()->store->getDefines();

            if ($search !== null && $search !== '' || $defines !== []) {
                App::upgrade()->pluginsList()
                    ->setList('plugin-new')
                    ->setTab('new')
                    ->setDefines($defines);

                $multi_parts[] = (new Div('new'))
                    ->title(__('Add plugins'))
                    ->class('multi-part')
                    ->items([
                        (new Text('h3', __('Add plugins from repository'))),
                        (new Capture(App::upgrade()->pluginsList()
                            ->displaySearch(...))),
                        (new Capture(App::upgrade()->pluginsList()
                            ->displayIndex(...))),
                        (new Capture(App::upgrade()->pluginsList()
                            ->displayModules(...), [
                                // cols
                                ['expander', 'name', 'score', 'version', 'desc', 'deps'],
                                // actions
                                ['install'],
                                // nav limit
                                true,
                            ])),
                        (new Note())
                            ->class(['info', 'vertical-separator'])
                            ->text(sprintf(
                                __('Visit %s plugins repository.'),
                                '<a href="https://dotclear.org/plugin/list">Dotclear</a>'
                            )),
                    ]);
            }

            # Add a new plugin
            $multi_parts[] = (new Div('addplugin'))
                ->title(__('Install or upgrade manually'))
                ->class('multi-part')
                ->items([
                    (new Text('h3', __('Add plugins from a package'))),
                    (new Note())
                        ->class('more-info')
                        ->text(__('You can install plugins by uploading or downloading zip files.')),
                    (new Capture(App::upgrade()->pluginsList()->displayManualForm(...))),
                ]);
        }

        echo (new Set())
            ->items($multi_parts)
        ->render();

        # -- Notice for super admin --
        if (!App::upgrade()->pluginsList()->isWritablePath()) {
            echo (new Note())
                ->class('warning')
                ->text(__('Some functions are disabled, please give write access to your plugins directory to enable them.'))
            ->render();
        }

        App::upgrade()->page()->helpBlock('core_plugins');
        App::upgrade()->page()->close();
    }

    /**
     * @param   array<int, string>  $excludes
     */
    protected static function nextStoreList(array $excludes, string $page_url): Div
    {
        $plugins = App::upgrade()->pluginsList();
        $list    = [];
        // Check ALL modules
        foreach ($plugins->modules->getDefines() as $module) {
            if (!in_array($module->getId(), $excludes)) {
                $list[$module->getId()] = $module;
            }
        }

        $items = [];
        if ($list === []) {
            $items[] = (new Note())
                ->text(__('There is no module to check'))
                ->class('info');
        } elseif (isset(self::$next_store)) {
            $items[] = self::displayNextStoreList($list, self::$next_store);
        }

        return (new Div('nextstore'))
            ->class('multi-part')
            ->title(__('Store version'))
            ->items([
                (new Text('h3', __('Check stores versions'))),
                (new Form('nextstoreform'))
                    ->method('post')
                    ->action($page_url . '#nextstore')
                    ->fields([
                        (new Para())
                            ->items([
                                App::nonce()->formNonce(),
                                (new Submit(['nextstorecheck'], __('Check lastest stores versions'))),
                            ]),
                    ]),
                (new Note())
                    ->text(sprintf(__('You can check repositories for modules written explicitly for Dotclear release greater than %s.'), App::config()->dotclearVersion()))
                    ->class('more-info'),
                ...$items,
            ]);
    }

    /**
     * Display next store list
     *
     * @todo    Documentation about the purpose of this method should be refine, in particular what is next store?
     *
     * @param   array<string,ModuleDefine>     $modules     The known modules
     * @param   array<int,ModuleDefine>        $repos       The repository modules
     */
    protected static function displayNextStoreList(array $modules, array $repos): Div
    {
        // regain module ID
        $store = [];
        foreach ($repos as $module) {
            $store[$module->getId()] = $module;
        }

        $trs = [];
        foreach ($modules as $id => $module) {
            if (!isset($store[$id])) {
                $img = [__('No version available'), 'check-off.svg', 'check-off'];
            } else {
                $dc_min = is_string($dc_min = $store[$id]->get('dc_min')) ? $dc_min : '';
                if (version_compare(App::config()->dotclearVersion(), $dc_min, '>=')) {
                    $img = [__('No update available'), 'check-wrn.svg', 'check-wrn'];
                } else {
                    $img = [__('Newer version available'), 'check-on.svg', 'check-on'];
                }
            }

            $name = is_string($name = $module->get('name')) ? $name : '';

            $tds   = [];
            $tds[] = (new Td())
                ->class('module-icon nowrap')
                ->items([
                    (new Img('images/' . $img[1]))
                        ->alt($img[0])
                        ->title($img[0])
                        ->class('mark mark-' . $img[2]),
                ]);
            $tds[] = (new Td())
                ->class('module-name nowrap')
                ->text(Html::escapeHTML($name) . ($id !== $name ? sprintf(__(' (%s)'), $id) : ''));

            if (isset($store[$id])) {
                $current_version = is_string($current_version = $store[$id]->get('current_version')) ? $current_version : '';
                $version         = is_string($version = $store[$id]->get('version')) ? $version : '';
                $dc_min          = is_string($dc_min = $store[$id]->get('dc_min')) ? $dc_min : '';

                $tds[] = (new Td())
                    ->class('module-version nowrap count')
                    ->text(Html::escapeHTML($current_version));
                $tds[] = (new Td())
                    ->class('module-version nowrap count maximal')
                    ->text(Html::escapeHTML($version));
                $tds[] = (new Td())
                    ->class('module-version nowrap count')
                    ->text(Html::escapeHTML($dc_min));

                if (App::config()->allowRepositories()) {
                    $tds[] = (new Td())
                        ->class('module-repository nowrap count')
                        ->text((empty($module->get('repository')) ? __('Official repository') : __('Third-party repository')));
                }
            } else {
                $version = is_string($version = $module->get('version')) ? $version : '';
                $tds[]   = (new Td())
                    ->class('module-current-version nowrap count')
                    ->text(Html::escapeHTML($version));
                $tds[] = (new Td())
                    ->class('module-version nowrap count maximal')
                    ->colspan(3)
                    ->text(Html::escapeHTML(__('No version available')));
            }

            $trs[] = (new Tr('mvmodules_m_' . Html::escapeHTML((string) $id)))
                ->class('line' . (!isset($store[$id]) || $module->get('version') == $store[$id]->get('version') ? ' offline' : ''))
                ->items($tds);
        }

        return (new Div())
            ->class('table-outer')
            ->items([
                (new Table('mvmodules'))
                    ->class('modules')
                    ->items([
                        (new Caption(Html::escapeHTML(__('Modules list'))))
                            ->class('hidden'),
                        (new Tr())
                            ->items([
                                (new Th())
                                    ->class('first nowrap')
                                    ->colspan(2)
                                    ->text(__('Name')),
                                (new Th())
                                    ->class('nowrap count')
                                    ->scope('col')
                                    ->text(__('Current version')),
                                (new Th())
                                    ->class('nowrap count')
                                    ->scope('col')
                                    ->text(__('Latest version')),
                                (new Th())
                                    ->class('nowrap count')
                                    ->scope('col')
                                    ->text(__('Written for Dotclear')),
                                (new Th())
                                    ->class('nowrap')
                                    ->scope('col')
                                    ->text(__('Repository')),
                            ]),
                        ...$trs,
                    ]),
            ]);
    }
}
