<?php

// SPDX-FileCopyrightText: 2023 Icinga GmbH <https://icinga.com>
// SPDX-License-Identifier: GPL-3.0-or-later

namespace Icinga\Controllers;

use Icinga\Application\Hook\DbMigrationHook;
use Icinga\Application\Icinga;
use Icinga\Application\MigrationManager;
use Icinga\Common\Database;
use Icinga\Exception\MissingParameterException;
use Icinga\Forms\MigrationForm;
use Icinga\Web\Notification;
use Icinga\Web\Widget\ItemList\MigrationList;
use Icinga\Web\Widget\Tabextension\OutputFormat;
use ipl\Html\Attributes;
use ipl\Html\FormElement\SubmitButtonElement;
use ipl\Html\HtmlElement;
use ipl\Html\Text;
use ipl\Web\Compat\CompatController;
use ipl\Web\Widget\ActionLink;

class MigrationsController extends CompatController
{
    use Database;

    public function init()
    {
        Icinga::app()->getModuleManager()->loadModule('setup');
    }

    public function indexAction(): void
    {
        $mm = MigrationManager::instance();

        $this->getTabs()->extend(new OutputFormat(['csv']));
        $this->addTitleTab($this->translate('Migrations'));

        $canApply = $this->hasPermission('application/migrations');
        if (! $canApply) {
            $this->addControl(
                new HtmlElement(
                    'div',
                    Attributes::create(['class' => 'migration-state-banner']),
                    new HtmlElement(
                        'span',
                        null,
                        Text::create(
                            $this->translate('You do not have the required permission to apply pending migrations.')
                        )
                    )
                )
            );
        }

        $migrateListForm = new MigrationForm();
        $migrateListForm->setAttribute('id', $this->getRequest()->protectId('migration-form'));
        $migrateListForm->setRenderDatabaseUserChange(! $mm->validateDatabasePrivileges());

        if ($canApply && $mm->hasPendingMigrations()) {
            $migrateAllButton = new SubmitButtonElement(sprintf('migrate-%s', DbMigrationHook::ALL_MIGRATIONS), [
                'form'  => $migrateListForm->getAttribute('id')->getValue(),
                'label' => $this->translate('Migrate All'),
                'title' => $this->translate('Migrate all pending migrations')
            ]);

            // Is the first button, so will be cloned and that the visible
            // button is outside the form doesn't matter for Web's JS
            $migrateListForm->registerElement($migrateAllButton);

            // Make sure it looks familiar, even if not inside a form
            $migrateAllButton->setWrapper(new HtmlElement('div', Attributes::create(['class' => 'icinga-controls'])));

            $this->controls->getAttributes()->add('class', 'default-layout');
            $this->addControl($migrateAllButton);
        }

        $this->handleFormatRequest($mm->toArray());

        $frameworkList = new MigrationList($mm->yieldMigrations(), $migrateListForm);
        $frameworkListControl = new HtmlElement('div', Attributes::create(['class' => 'migration-list-control']));
        $frameworkListControl->addHtml(new HtmlElement('h2', null, Text::create($this->translate('System'))));
        $frameworkListControl->addHtml($frameworkList);

        $moduleList = new MigrationList($mm->yieldMigrations(true), $migrateListForm);
        $moduleListControl = new HtmlElement('div', Attributes::create(['class' => 'migration-list-control']));
        $moduleListControl->addHtml(new HtmlElement('h2', null, Text::create($this->translate('Modules'))));
        $moduleListControl->addHtml($moduleList);

        $migrateListForm->addHtml($frameworkListControl, $moduleListControl);
        if ($canApply && $mm->hasPendingMigrations()) {
            $frameworkList->ensureAssembled();
            $moduleList->ensureAssembled();

            $this->handleMigrateRequest($migrateListForm);
        }

        $migrations = new HtmlElement('div', Attributes::create(['class' => 'migrations']));
        $migrations->addHtml($migrateListForm);

        $this->addContent($migrations);
    }

    public function hintAction(): void
    {
        // The forwarded request doesn't modify the original server query string, but adds the migration param to the
        // request param instead. So, there is no way to access the migration param other than via the request instance.
        /** @var ?string $module */
        $module = $this->getRequest()->getParam(DbMigrationHook::MIGRATION_PARAM);
        if ($module === null) {
            throw new MissingParameterException(
                $this->translate('Required parameter \'%s\' missing'),
                DbMigrationHook::MIGRATION_PARAM
            );
        }

        $mm = MigrationManager::instance();
        if (! $mm->hasMigrations($module)) {
            $this->httpNotFound(sprintf('There are no pending migrations matching the given name: %s', $module));
        }

        $migration = $mm->getMigration($module);
        $this->addTitleTab($this->translate('Error'));
        $this->addContent(
            new HtmlElement(
                'div',
                Attributes::create(['class' => 'pending-migrations-hint']),
                new HtmlElement('h2', null, Text::create($this->translate('Error!'))),
                new HtmlElement(
                    'p',
                    null,
                    Text::create(sprintf($this->translate('%s has pending migrations.'), $migration->getName()))
                ),
                new HtmlElement('p', null, Text::create($this->translate('Please apply the migrations first.'))),
                new ActionLink($this->translate('View pending Migrations'), 'migrations')
            )
        );
    }

    public function migrationAction(): void
    {
        /** @var string $name */
        $name = $this->params->getRequired(DbMigrationHook::MIGRATION_PARAM);

        $this->addTitleTab($this->translate('Migration'));
        $this->getTabs()->disableLegacyExtensions();
        $this->controls->getAttributes()->add('class', 'default-layout');

        $mm = MigrationManager::instance();
        if (! $mm->hasMigrations($name)) {
            $migrations = [];
        } else {
            $hook = $mm->getMigration($name);
            $migrations = array_reverse($hook->getMigrations());
            if (! $this->hasPermission('application/migrations')) {
                $this->addControl(
                    new HtmlElement(
                        'div',
                        Attributes::create(['class' => 'migration-state-banner']),
                        new HtmlElement(
                            'span',
                            null,
                            Text::create(
                                $this->translate('You do not have the required permission to apply pending migrations.')
                            )
                        )
                    )
                );
            } else {
                $this->addControl(
                    new HtmlElement(
                        'div',
                        Attributes::create(['class' => 'migration-controls']),
                        new HtmlElement('span', null, Text::create($hook->getName()))
                    )
                );
            }
        }

        $migrationWidget = new HtmlElement('div', Attributes::create(['class' => 'migrations']));
        $migrationWidget->addHtml((new MigrationList($migrations))->setMinimal(false));
        $this->addContent($migrationWidget);
    }

    public function handleMigrateRequest(MigrationForm $form): void
    {
        $this->assertPermission('application/migrations');

        $form->on(MigrationForm::ON_SUCCESS, function (MigrationForm $form) {
            $mm = MigrationManager::instance();

            /** @var array<string, string> $elevatedPrivileges */
            $elevatedPrivileges = $form->getValue('database_setup');
            if ($elevatedPrivileges !== null && $elevatedPrivileges['grant_privileges'] === 'y') {
                $mm->fixIcingaWebMysqlGrants($this->getDb(), $elevatedPrivileges);
            }

            $pressedButton = $form->getPressedSubmitElement();
            if ($pressedButton) {
                $name = substr($pressedButton->getName(), 8);
                switch ($name) {
                    case DbMigrationHook::ALL_MIGRATIONS:
                        if ($mm->applyAll($elevatedPrivileges)) {
                            Notification::success($this->translate('Applied all migrations successfully'));
                        } else {
                            Notification::error(
                                $this->translate(
                                    'Applied migrations successfully. Though, one or more migration hooks'
                                    . ' failed to run. See logs for details'
                                )
                            );
                        }
                        break;
                    default:
                        $migration = $mm->getMigration($name);
                        if ($mm->apply($migration, $elevatedPrivileges)) {
                            Notification::success($this->translate('Applied pending migrations successfully'));
                        } else {
                            Notification::error(
                                $this->translate('Failed to apply pending migration(s). See logs for details')
                            );
                        }
                }
            }

            $this->sendExtraUpdates(['#col2' => '__CLOSE__']);

            $this->redirectNow('migrations');
        })->handleRequest($this->getServerRequest());
    }

    /**
     * Handle exports
     *
     * @param array<string, mixed> $data
     */
    protected function handleFormatRequest(array $data): void
    {
        $formatJson = $this->params->get('format') === 'json';
        if (! $formatJson && ! $this->getRequest()->isApiRequest()) {
            return;
        }

        $this->getResponse()
            ->json()
            ->setSuccessData($data)
            ->sendResponse();
    }
}
