<?php
// Copyright (c) Pickware GmbH. All rights reserved.
// This file is part of software that is released under a proprietary license.
// You must not copy, modify, distribute, make publicly available, or execute
// its contents or parts thereof without express permission by the copyright
// holder, unless otherwise permitted by law.

use Shopware\Plugins\ViisonCommon\Classes\Exceptions\InstallationException;
use Shopware\Plugins\ViisonCommon\Classes\Installation\ExpiringLock;
use Shopware\Plugins\ViisonCommon\Classes\Installation\ExpiringLockStatus;
use Shopware\Plugins\ViisonCommon\Classes\Installation\InstallationMessageUtil;
use Shopware\Plugins\ViisonCommon\Components\ExceptionTranslation\ExceptionTranslator;
use Shopware\Plugins\ViisonCommon\Components\Migration\AbstractMigrationSubscriber;
use Shopware\Plugins\ViisonCommon\Components\Migration\MigrationBootstrapAdapter;

if (!class_exists('ViisonCommon_Plugin_BootstrapV14')) {
    require_once('PluginBootstrapV14.php');
}

/**
 * Version 15 of the common plugin Bootstrap class. This version is based on ViisonCommon_Plugin_BootstrapV14, but uses
 * our global migration service.
 *
 * This class needs to be manually loaded in the respective plugin via:
 *
 * if (!class_exists('ViisonCommon_Plugin_BootstrapV15')) {
 *     require_once('ViisonCommon/PluginBootstrapV15.php');
 * }
 */
abstract class ViisonCommon_Plugin_BootstrapV15 extends ViisonCommon_Plugin_BootstrapV14
{
    /**
     * Overwrite this method and return your MigrationSubscriber, if your plugin has Migrations that need to be
     * registered in said Subscriber.
     *
     * @return AbstractMigrationSubscriber|null
     */
    abstract protected function createMigrationSubscriber();

    /**
     * Make method public
     *
     * {@inheritdoc}
     */
    public function getBootstrapSnippetManager()
    {
        return parent::getBootstrapSnippetManager();
    }

    public function registerMigrationSubscriber()
    {
        $subscriber = $this->createMigrationSubscriber();

        if ($subscriber !== null && !$this->isSubscriberRegistered($subscriber)) {
            $eventManager = $this->get('events');
            $eventManager->addSubscriber($subscriber);
        }
    }

    /**
     * @inheritDoc
     */
    public function install()
    {
        $result = parent::install();

        $result = $this->createMigrationBootstrapAdapter()->onAfterInstall($result);
        if ($result['success'] === false) {
            $result['message'] = implode(' ', $result['messages']);
        } else {
            $result['message'] = InstallationMessageUtil::formatUpdateMessage($result['messages']);
        }

        return $result;
    }

    /**
     * @inheritDoc
     */
    public function update($oldVersion)
    {
        $result = parent::update($oldVersion);

        $result = $this->createMigrationBootstrapAdapter()->onAfterUpdate($result);
        if ($result['success'] === false) {
            $result['message'] = implode(' ', $result['messages']);
        } else {
            $result['message'] = InstallationMessageUtil::formatUpdateMessage($result['messages']);
        }

        return $result;
    }

    /**
     * @inheritDoc
     */
    public function uninstall()
    {
        $result = parent::uninstall();

        $result = $this->createMigrationBootstrapAdapter()->onAfterUninstall($result);
        if ($result['success'] === false) {
            $result['message'] = implode(' ', $result['messages']);
        } else {
            $result['message'] = InstallationMessageUtil::formatUpdateMessage($result['messages']);
        }

        return $result;
    }

    /**
     * @inheritDoc
     */
    public function secureUninstall()
    {
        $result = parent::secureUninstall();

        $result = $this->createMigrationBootstrapAdapter()->onAfterSecureUninstall($result);
        if ($result['success'] === false) {
            $result['message'] = implode(' ', $result['messages']);
        } else {
            $result['message'] = InstallationMessageUtil::formatUpdateMessage($result['messages']);
        }

        return $result;
    }

    /**
     * @inheritDoc
     */
    public function enable()
    {
        $executionResult = $this->executeSetupMethod(
            'Activation',
            function () {
                $this->runActivation();
            }
        );
        if ($executionResult['success'] === false) {
            // When executing an activation or a deactivation of the plugin, Shopware would throw their own, generic
            // exception if the method returned false (which the setup execution does, if it catches an exception).
            // Hence the only way to show a custom message for a failed activation or deactivation is to throw our own
            // exception here.
            throw new Exception(implode(' ', $executionResult['messages']));
        }

        $executionResult = $this->createMigrationBootstrapAdapter()->onAfterEnable($executionResult); // TODO: inline
        if ($executionResult['success'] === false) {
            throw new Exception(implode(' ', $executionResult['messages']));
        }

        $executionResult['message'] = InstallationMessageUtil::formatUpdateMessage($executionResult['messages']);

        return $executionResult;
    }

    /**
     * {@inheritDoc}
     */
    public function disable()
    {
        $executionResult = $this->executeSetupMethod(
            'Deactivation',
            function () {
                $this->runDeactivation();
            }
        );
        if ($executionResult['success'] === false) {
            // When executing an activation or a deactivation of the plugin, Shopware would throw their own, generic
            // exception if the method returned false (which the setup execution does, if it catches an exception).
            // Hence the only way to show a custom message for a failed activation or deactivation is to throw our own
            // exception here.
            throw new Exception(implode(' ', $executionResult['messages']));
        }

        $executionResult = $this->createMigrationBootstrapAdapter()->onAfterDisable($executionResult);
        if ($executionResult['result'] === false) {
            $executionResult['message'] = implode(' ', $executionResult['messages']);
        } else {
            $executionResult['message'] = InstallationMessageUtil::formatUpdateMessage($executionResult['messages']);
        }

        return $executionResult;
    }

    /**
     * Overwrite this method to get the backend messages returned as array.
     *
     * {@inheritdoc}
     */
    protected function executeSetupMethod($descriptionOfSetupMethod, callable $setupCallback)
    {
        $setupLock = null;
        /** @var ExpiringLockStatus|null $lockStatus */
        $lockStatus = null;
        try {
            $this->logInfo(sprintf(
                '%s for plugin "%s" requested.',
                $descriptionOfSetupMethod,
                $this->getName()
            ));

            if (version_compare($this->getPluginJsonVersion(), $this->getCodeVersion(), '>')) {
                // The plugin's bootstrap file that is cached by the enabled byte code caches (OPcache, APCu etc.) is
                // outdated, hence clear the caches and ask the user to try again
                $this->clearAvailableByteCodeCaches();

                throw InstallationException::byteCodeCacheOutdated($this);
            }

            // Assert the installed shopware version
            if (!$this->assertMinimumVersion($this->getMinRequiredShopwareVersion())) {
                throw InstallationException::shopwareVersionNotSufficient(
                    $this,
                    $this->getMinRequiredShopwareVersion(),
                    $this->get('config')->version
                );
            }

            $setupLock = new ExpiringLock(
                self::LOCK_IDENTIFIER,
                $this->getName(),
                $this->setupLockExpirationInSeconds,
                $this->get('models')
            );

            $this->logInfo(sprintf(
                'Plugin "%s" is trying to acquire a lock to block other plugin setups for the next %d seconds.',
                $this->getName(),
                $this->setupLockExpirationInSeconds
            ));
            $lockStatus = $setupLock->tryAcquireLock();
            if (!$lockStatus->hasBeenAcquired()) {
                throw InstallationException::installationBlockedByActiveLock(
                    $this,
                    $lockStatus->getDescription(),
                    $lockStatus->getRemainingLockTimeInSeconds()
                );
            }

            $this->logInfo(sprintf(
                '%s of plugin "%s" started.',
                $descriptionOfSetupMethod,
                $this->getName()
            ));

            // Execute the actual setup method
            $installationStartTime = microtime(true);
            $setupCallback();
            $installationDurationInSeconds = microtime(true) - $installationStartTime;

            $this->logInfo(sprintf(
                '%s of plugin "%s" has been successfully finished. Run time: %01.3f s',
                $descriptionOfSetupMethod,
                $this->getName(),
                $installationDurationInSeconds
            ));

            $setupLock->releaseOnlyIfAlreadyHeld();
            $this->logInfo(sprintf(
                'Plugin "%s" has released its lock to block other plugin setups.',
                $this->getName()
            ));

            return [
                'success' => true,
                'messages' => $this->growlMessages,
                'invalidateCache' => $this->invalidatedCaches,
            ];
        } catch (\Exception $e) {
            $this->logException(
                sprintf(
                    '%s of plugin "%s" failed with the following error: "%s"',
                    $descriptionOfSetupMethod,
                    $this->getName(),
                    $e->getMessage()
                ),
                $e
            );

            // $setupLock may be null when acquiring failed in try-block.
            if ($setupLock && $lockStatus && $lockStatus->hasBeenAcquired()) {
                $setupLock->releaseOnlyIfAlreadyHeld();
                $this->logInfo(sprintf(
                    'Plugin "%s" has released its lock to block other plugin setups.',
                    $this->getName()
                ));
            }

            $exceptionTranslator = new ExceptionTranslator($this->getBootstrapSnippetManager());
            $translatedErrorMessage = $exceptionTranslator->translate($e);

            return [
                'success' => false,
                'messages' => [$translatedErrorMessage],
            ];
        }
    }

    /**
     * @return MigrationBootstrapAdapter
     */
    protected function createMigrationBootstrapAdapter()
    {
        return new MigrationBootstrapAdapter($this);
    }
}
