<?php

use dokuwiki\Extension\CLIPlugin;
use dokuwiki\plugin\extension\Exception as ExtensionException;
use dokuwiki\plugin\extension\Extension;
use dokuwiki\plugin\extension\Installer;
use dokuwiki\plugin\extension\Local;
use dokuwiki\plugin\extension\Notice;
use dokuwiki\plugin\extension\Repository;
use splitbrain\phpcli\Colors;
use splitbrain\phpcli\Exception;
use splitbrain\phpcli\Options;
use splitbrain\phpcli\TableFormatter;

/**
 * Class cli_plugin_extension
 *
 * Command Line component for the extension manager
 *
 * @license GPL2
 * @author Andreas Gohr <andi@splitbrain.org>
 */
class cli_plugin_extension extends CLIPlugin
{
    /** @inheritdoc */
    protected function setup(Options $options)
    {
        // general setup
        $options->useCompactHelp();
        $options->setHelp(
            "Manage plugins and templates for this DokuWiki instance\n\n" .
            "Status codes:\n" .
            "   i - installed                    " . Notice::symbol(Notice::SECURITY) . " - security issue\n" .
            "   b - bundled with DokuWiki        " . Notice::symbol(Notice::ERROR) . " - extension error\n" .
            "   g - installed via git            " . Notice::symbol(Notice::WARNING) . " - extension warning\n" .
            "   d - disabled                     " . Notice::symbol(Notice::INFO) . " - extension info\n" .
            "   u - update available\n"
        );

        // search
        $options->registerCommand('search', 'Search for an extension');
        $options->registerOption('max', 'Maximum number of results (default 10)', 'm', 'number', 'search');
        $options->registerOption('verbose', 'Show detailed extension information', 'v', false, 'search');
        $options->registerArgument('query', 'The keyword(s) to search for', true, 'search');

        // list
        $options->registerCommand('list', 'List installed extensions');
        $options->registerOption('verbose', 'Show detailed extension information', 'v', false, 'list');
        $options->registerOption('filter', 'Filter by this status', 'f', 'status', 'list');

        // upgrade
        $options->registerCommand('upgrade', 'Update all installed extensions to their latest versions');
        $options->registerOption('git-overwrite', 'Do not skip git-controlled extensions', 'g', false, 'upgrade');

        // install
        $options->registerCommand('install', 'Install or upgrade extensions');
        $options->registerArgument(
            'extensions...',
            'One or more extensions to install. Either by name or download URL',
            true,
            'install'
        );

        // uninstall
        $options->registerCommand('uninstall', 'Uninstall a new extension');
        $options->registerArgument('extensions...', 'One or more extensions to install', true, 'uninstall');

        // enable
        $options->registerCommand('enable', 'Enable installed extensions');
        $options->registerArgument('extensions...', 'One or more extensions to enable', true, 'enable');

        // disable
        $options->registerCommand('disable', 'Disable installed extensions');
        $options->registerArgument('extensions...', 'One or more extensions to disable', true, 'disable');
    }

    /** @inheritdoc */
    protected function main(Options $options)
    {
        $repo = Repository::getInstance();
        try {
            $repo->checkAccess();
        } catch (ExtensionException $e) {
            $this->warning($e->getMessage());
        }

        switch ($options->getCmd()) {
            case 'list':
                $ret = $this->cmdList($options->getOpt('verbose'), $options->getOpt('filter', ''));
                break;
            case 'search':
                $ret = $this->cmdSearch(
                    implode(' ', $options->getArgs()),
                    $options->getOpt('verbose'),
                    (int)$options->getOpt('max', 10)
                );
                break;
            case 'install':
                $ret = $this->cmdInstall($options->getArgs());
                break;
            case 'uninstall':
                $ret = $this->cmdUnInstall($options->getArgs());
                break;
            case 'enable':
                $ret = $this->cmdEnable(true, $options->getArgs());
                break;
            case 'disable':
                $ret = $this->cmdEnable(false, $options->getArgs());
                break;
            case 'upgrade':
                $ret = $this->cmdUpgrade($options->getOpt('git-overwrite', false));
                break;
            default:
                echo $options->help();
                $ret = 0;
        }

        exit($ret);
    }

    /**
     * Upgrade all extensions
     *
     * @return int
     */
    protected function cmdUpgrade($gitOverwrite)
    {
        $local = new Local();
        $extensions = [];
        foreach ($local->getExtensions() as $ext) {
            if ($ext->isGitControlled() && !$gitOverwrite) continue; // skip git controlled extensions
            if ($ext->isUpdateAvailable()) $extensions[] = $ext->getID();
        }
        return $this->cmdInstall($extensions);
    }

    /**
     * Enable or disable one or more extensions
     *
     * @param bool $set
     * @param string[] $extensions
     * @return int
     */
    protected function cmdEnable($set, $extensions)
    {
        $ok = 0;
        foreach ($extensions as $extname) {
            $ext = Extension::createFromId($extname);

            try {
                if ($set) {
                    $ext->enable();
                    $msg = 'msg_enabled';
                } else {
                    $ext->disable();
                    $msg = 'msg_disabled';
                }
                $this->success(sprintf($this->getLang($msg), $ext->getID()));
            } catch (ExtensionException $e) {
                $this->error($e->getMessage());
                ++$ok;
                continue;
            }
        }

        return $ok;
    }

    /**
     * Uninstall one or more extensions
     *
     * @param string[] $extensions
     * @return int
     */
    protected function cmdUnInstall($extensions)
    {
        $installer = new Installer();

        $ok = 0;
        foreach ($extensions as $extname) {
            $ext = Extension::createFromId($extname);

            try {
                $installer->uninstall($ext);
                $this->success(sprintf($this->getLang('msg_delete_success'), $ext->getID()));
            } catch (ExtensionException $e) {
                $this->debug($e->getTraceAsString());
                $this->error($e->getMessage());
                $ok++; // error code is number of failed uninstalls
            }
        }
        return $ok;
    }

    /**
     * Install one or more extensions
     *
     * @param string[] $extensions
     * @return int
     */
    protected function cmdInstall($extensions)
    {
        $ok = 0;
        foreach ($extensions as $extname) {
            $installer = new Installer(true);

            try {
                if (preg_match("/^https?:\/\//i", $extname)) {
                    $installer->installFromURL($extname);
                } else {
                    $installer->installFromId($extname);
                }
            } catch (ExtensionException $e) {
                $this->debug($e->getTraceAsString());
                $this->error($e->getMessage());
                $ok++; // error code is number of failed installs
            }

            $processed = $installer->getProcessed();
            foreach ($processed as $id => $status) {
                if ($status == Installer::STATUS_INSTALLED) {
                    $this->success(sprintf($this->getLang('msg_install_success'), $id));
                } elseif ($status == Installer::STATUS_UPDATED) {
                    $this->success(sprintf($this->getLang('msg_update_success'), $id));
                }
            }
        }

        return $ok;
    }

    /**
     * Search for an extension
     *
     * @param string $query
     * @param bool $showdetails
     * @param int $max
     * @return int
     * @throws Exception
     */
    protected function cmdSearch($query, $showdetails, $max)
    {
        $repo = Repository::getInstance();
        $result = $repo->searchExtensions($query);
        if ($max) {
            $result = array_slice($result, 0, $max);
        }

        $this->listExtensions($result, $showdetails);
        return 0;
    }

    /**
     * @param bool $showdetails
     * @param string $filter
     * @return int
     * @throws Exception
     */
    protected function cmdList($showdetails, $filter)
    {
        $extensions = (new Local())->getExtensions();
        // initialize remote data in one go
        Repository::getInstance()->initExtensions(array_keys($extensions));

        $this->listExtensions($extensions, $showdetails, $filter);
        return 0;
    }

    /**
     * List the given extensions
     *
     * @param Extension[] $list
     * @param bool $details display details
     * @param string $filter filter for this status
     * @throws Exception
     * @todo break into smaller methods
     */
    protected function listExtensions($list, $details, $filter = '')
    {
        $tr = new TableFormatter($this->colors);
        foreach ($list as $ext) {
            $status = '';
            if ($ext->isInstalled()) {
                $date = $ext->getInstalledVersion();
                $avail = $ext->getLastUpdate();
                $status = 'i';
                if ($avail && $avail > $date) {
                    $vcolor = Colors::C_RED;
                    $status .= 'u';
                } else {
                    $vcolor = Colors::C_GREEN;
                }
                if ($ext->isGitControlled()) $status = 'g';
                if ($ext->isBundled()) {
                    $status = 'b';
                    $date = '<bundled>';
                    $vcolor = null;
                }
                if ($ext->isEnabled()) {
                    $ecolor = Colors::C_BROWN;
                } else {
                    $ecolor = Colors::C_DARKGRAY;
                    $status .= 'd';
                }
            } else {
                $ecolor = null;
                $date = $ext->getLastUpdate();
                $vcolor = null;
            }

            if ($filter && !str_contains($status, $filter)) {
                continue;
            }

            $notices = Notice::list($ext);
            if ($notices[Notice::SECURITY]) $status .= Notice::symbol(Notice::SECURITY);
            if ($notices[Notice::ERROR]) $status .= Notice::symbol(Notice::ERROR);
            if ($notices[Notice::WARNING]) $status .= Notice::symbol(Notice::WARNING);
            if ($notices[Notice::INFO]) $status .= Notice::symbol(Notice::INFO);

            echo $tr->format(
                [20, 5, 12, '*'],
                [
                    $ext->getID(),
                    $status,
                    $date,
                    strip_tags(sprintf(
                        $this->getLang('extensionby'),
                        $ext->getDisplayName(),
                        $this->colors->wrap($ext->getAuthor(), Colors::C_PURPLE)
                    ))
                ],
                [
                    $ecolor,
                    Colors::C_YELLOW,
                    $vcolor,
                    null,
                ]
            );


            if (!$details) continue;

            echo $tr->format(
                [7, '*'],
                ['', $ext->getDescription()],
                [null, Colors::C_CYAN]
            );
            foreach ($notices as $type => $msgs) {
                if (!$msgs) continue;
                foreach ($msgs as $msg) {
                    echo $tr->format(
                        [7, '*'],
                        ['', Notice::symbol($type) . ' ' . $msg],
                        [null, Colors::C_LIGHTBLUE]
                    );
                }
            }
        }
    }
}
