<?php

namespace Duplicator\Core\Upgrade;

use Duplicator\Core\Addons\AddonsManager;
use Duplicator\Core\Bootstrap;
use Duplicator\Core\CapMng;
use Duplicator\Core\Models\AbstractEntity;
use Duplicator\Core\UniqueId;
use Duplicator\Models\ActivityLog\AbstractLogEvent;
use Duplicator\Models\DynamicGlobalEntity;
use Duplicator\Models\GlobalEntity;
use Duplicator\Models\StaticGlobal;
use Duplicator\Models\Storages\AbstractStorageEntity;
use Duplicator\Models\Storages\StoragesUtil;
use Duplicator\Models\SystemGlobalEntity;
use Duplicator\Models\TemplateEntity;
use Duplicator\Package\AbstractPackage;
use Duplicator\Package\DupPackage;
use Duplicator\Utils\AsyncSetupActions;
use Duplicator\Utils\Crypt\CryptBlowfish;
use Duplicator\Utils\ExpireOptions;
use Duplicator\Utils\Logging\DupLog;
use Throwable;

/**
 * Utility class managing when the plugin is updated
 */
class UpgradeFunctions
{
    /**
     * Initialize upgrade hooks
     *
     * Priority scheme:
     * - 4: clearExpireOptions (drops all cached expire options so nothing stale survives the update)
     * - 5: initSecureKey (must run before any entity save to avoid key mismatch)
     * - 10: initTables (creates database tables)
     * - 20: initCapabilities
     * - 50: initEntities (initializes default entities - plugin semi-initialized after this)
     * - 200: initUniqueId
     * - 500: StaticGlobal::saveDefaults (persists default wp_options; runs late so prior
     *        steps that may populate the same keys win over the add_option() defaults)
     * - 1000: updateOptionVersion
     * - 1001: setInstallInfo
     * - 10000: resaveAllEntities (always last)
     * - 10000: Other actions
     *
     * @return void
     */
    public static function init(): void
    {
        add_action('duplicator_upgrade', [self::class, 'clearExpireOptions'], 4, 4);
        add_action('duplicator_upgrade', [self::class, 'initSecureKey'], 5, 4);
        add_action('duplicator_upgrade', [self::class, 'initTables'], 10, 4);
        add_action('duplicator_upgrade', [self::class, 'initCapabilities'], 20, 4);
        add_action('duplicator_upgrade', [self::class, 'initEntities'], 50, 4);
        add_action('duplicator_upgrade', [self::class, 'initUniqueId'], 200, 4);
        add_action('duplicator_upgrade', [self::class, 'saveStaticGlobalDefaults'], 500, 4);
        add_action('duplicator_upgrade', [self::class, 'updateOptionVersion'], 1000, 4);
        add_action('duplicator_upgrade', [self::class, 'setInstallInfo'], 1001, 4);
        add_action('duplicator_upgrade', [self::class, 'resaveAllEntities'], 10000, 4);
        add_action('duplicator_upgrade', [self::class, 'resetAddonsStatus'], 10000, 4);
        add_action('duplicator_upgrade', [self::class, 'cancelActiveBackup'], 10000, 4);
        add_action('duplicator_upgrade', [self::class, 'scheduleAsyncSetup'], 10000, 4);
    }

    /**
     * Cancel any in-progress backup before the upgrade alters schema and entities.
     *
     * @param string       $oldVariant previous variant identifier, '' on fresh install
     * @param false|string $oldVersion previous Duplicator version, false on fresh install
     * @param string       $newVariant current variant identifier
     * @param string       $newVersion current Duplicator version
     *
     * @return void
     */
    public static function cancelActiveBackup($oldVariant, $oldVersion, $newVariant, $newVersion): void
    {
        if ($oldVersion === false) {
            return;
        }

        try {
            /** @var DupPackage[] */
            $activePackages = DupPackage::getPackagesByStatus([
                'relation' => 'AND',
                [
                    'op'     => '>=',
                    'status' => AbstractPackage::STATUS_PRE_PROCESS,
                ],
                [
                    'op'     => '<',
                    'status' => AbstractPackage::STATUS_STORAGE_PROCESSING,
                ],
            ]);

            foreach ($activePackages as $package) {
                DupLog::trace('UPGRADE: cancelling active backup #' . $package->getID() . ' due to plugin update');
                $package->setForCancel();
            }
        } catch (Throwable $e) {
            DupLog::traceError('UPGRADE: failed to cancel active backups — ' . $e->getMessage());
        }
    }

    /**
     * Drop all cached expire options so no stale cache survives a plugin update.
     *
     * @param string       $oldVariant previous variant identifier, '' on fresh install
     * @param false|string $oldVersion previous Duplicator version, false on fresh install
     * @param string       $newVariant current variant identifier
     * @param string       $newVersion current Duplicator version
     *
     * @return void
     */
    public static function clearExpireOptions($oldVariant, $oldVersion, $newVariant, $newVersion): void
    {
        ExpireOptions::deleteAll();
    }

    /**
     * Schedule async setup actions (server detection, etc.) as a one-shot cron event.
     *
     * @param string       $oldVariant previous variant identifier, '' on fresh install
     * @param false|string $oldVersion previous Duplicator version, false on fresh install
     * @param string       $newVariant current variant identifier
     * @param string       $newVersion current Duplicator version
     *
     * @return void
     */
    public static function scheduleAsyncSetup($oldVariant, $oldVersion, $newVariant, $newVersion): void
    {
        AsyncSetupActions::scheduleDetection();
    }

    /**
     * Materialize StaticGlobal defaults in wp_options (idempotent via add_option).
     *
     * @param string       $oldVariant previous variant identifier, '' on fresh install
     * @param false|string $oldVersion previous Duplicator version, false on fresh install
     * @param string       $newVariant current variant identifier
     * @param string       $newVersion current Duplicator version
     *
     * @return void
     */
    public static function saveStaticGlobalDefaults($oldVariant, $oldVersion, $newVariant, $newVersion): void
    {
        StaticGlobal::saveDefaults();
    }

    /**
     * Rebuild the persisted addon status map for the current fingerprint.
     *
     * @param string       $oldVariant previous variant identifier, '' on fresh install
     * @param false|string $oldVersion previous Duplicator version, false on fresh install
     * @param string       $newVariant current variant identifier
     * @param string       $newVersion current Duplicator version
     *
     * @return void
     */
    public static function resetAddonsStatus($oldVariant, $oldVersion, $newVariant, $newVersion): void
    {
        AddonsManager::resetStatus();
    }

    /**
     * Initialize capabilities manager
     *
     * Must run AFTER any capability-name migration hooked at a lower priority.
     *
     * @param string       $oldVariant previous variant identifier, '' on fresh install
     * @param false|string $oldVersion previous version, false on fresh install
     * @param string       $newVariant current variant
     * @param string       $newVersion current version
     *
     * @return void
     */
    public static function initCapabilities($oldVariant, $oldVersion, $newVariant, $newVersion): void
    {
        CapMng::getInstance();
    }

    /**
     * Initialize database tables.
     *
     * Creates/updates database table structure.
     *
     * @param string       $oldVariant previous variant identifier, '' on fresh install
     * @param false|string $oldVersion previous version, false on fresh install
     * @param string       $newVariant current variant
     * @param string       $newVersion current version
     *
     * @return void
     */
    public static function initTables($oldVariant = '', $oldVersion = false, $newVariant = '', $newVersion = ''): void
    {
        AbstractEntity::initTable();
        AbstractLogEvent::initTable();
        AbstractPackage::initTable();
    }

    /**
     * Initialize default entities and Default storage.
     *
     * Creates default entities if they don't exist.
     *
     * @param string       $oldVariant previous variant identifier, '' on fresh install
     * @param false|string $oldVersion previous version, false on fresh install
     * @param string       $newVariant current variant
     * @param string       $newVersion current version
     *
     * @return void
     */
    public static function initEntities($oldVariant = '', $oldVersion = false, $newVariant = '', $newVersion = ''): void
    {
        GlobalEntity::getInstance()->save();
        DynamicGlobalEntity::getInstance()->save();
        StoragesUtil::initCreateOnInstallStorages();
        TemplateEntity::createDefault();
        TemplateEntity::getManualTemplate(); // If not exists, create it

        // Log upgrade info (safe to call now that tables exist)
        if ($oldVersion === false) {
            DupLog::trace("PLUGIN INSTALLED: VERSION " . $newVersion . " VARIANT " . $newVariant);
        } else {
            DupLog::trace("PLUGIN UPGRADED FROM " . $oldVariant . " " . $oldVersion . " TO " . $newVariant . " " . $newVersion);
        }
    }

    /**
     * Initialize DUP SECURE KEY if not exists
     *
     * @param string       $oldVariant previous variant identifier, '' on fresh install
     * @param false|string $oldVersion previous version, false on fresh install
     * @param string       $newVariant current variant
     * @param string       $newVersion current version
     *
     * @return void
     */
    public static function initSecureKey($oldVariant, $oldVersion, $newVariant, $newVersion): void
    {
        if ($oldVersion === false) {
            // New installation - create key without forcing
            CryptBlowfish::createWpConfigSecureKey(false, false);
        }
    }

    /**
     * Init Unique identifier
     *
     * @param string       $oldVariant previous variant identifier, '' on fresh install
     * @param false|string $oldVersion previous version, false on fresh install
     * @param string       $newVariant current variant
     * @param string       $newVersion current version
     *
     * @return void
     */
    public static function initUniqueId($oldVariant, $oldVersion, $newVariant, $newVersion): void
    {
        // Initialize the identifier
        UniqueId::getInstance();
    }

    /**
     * Update version + variant + hash options.
     *
     * The version option is stored as "<VERSION>|<VARIANT>" so the variant can be
     * recovered on the next upgrade without decoding the addon hash.
     *
     * @param string       $oldVariant previous variant identifier, '' on fresh install
     * @param false|string $oldVersion previous version, false on fresh install
     * @param string       $newVariant current variant
     * @param string       $newVersion current version
     *
     * @return void
     */
    public static function updateOptionVersion($oldVariant, $oldVersion, $newVariant, $newVersion): void
    {
        self::updateSingleOption(UpgradePlugin::DUP_VERSION_OPT_KEY, DUPLICATOR_VERSION . '|' . DUPLICATOR____TYPE);
        self::updateSingleOption(UpgradePlugin::DUP_HASH_OPT_KEY, Bootstrap::getAddsHash());
    }

    /**
     * Persist a single option with retry on failure
     *
     * @param string $optionKey Option key
     * @param string $value     Value to store
     *
     * @return void
     */
    private static function updateSingleOption(string $optionKey, string $value): void
    {
        if (update_option($optionKey, $value, true) !== false) {
            return;
        }

        DupLog::trace("Couldn't update {$optionKey} so deleting it.");
        delete_option($optionKey);

        if (update_option($optionKey, $value, true) === false) { // @phpstan-ignore-line
            DupLog::trace("Still couldn't update {$optionKey}!");
        } else { // @phpstan-ignore-line
            DupLog::trace("{$optionKey} updated.");
        }
    }

    /**
     * Update install info.
     *
     * @param string       $oldVariant previous variant identifier, '' on fresh install
     * @param false|string $oldVersion previous version, false on fresh install
     * @param string       $newVariant current variant
     * @param string       $newVersion current version
     *
     * @return void
     */
    public static function setInstallInfo($oldVariant, $oldVersion, $newVariant, $newVersion): void
    {
        $installInfo = UpgradePlugin::getStoredInstallInfo();

        if (empty($oldVersion) || $installInfo === false) {
            // If is new installation or install info is not set generate new install info
            $installInfo = [
                'version'    => DUPLICATOR_VERSION,
                'time'       => time(),
                'updateTime' => time(),
            ];
        } else {
            $installInfo['updateTime'] = time();
        }

        delete_option(UpgradePlugin::DUP_INSTALL_INFO_OPT_KEY);
        update_option(UpgradePlugin::DUP_INSTALL_INFO_OPT_KEY, $installInfo, false);
    }

    /**
     * Re-save all entities to update serialization format and version entity
     *
     * @param string       $oldVariant previous variant identifier, '' on fresh install
     * @param false|string $oldVersion previous version, false on fresh install
     * @param string       $newVariant current variant
     * @param string       $newVersion current version
     *
     * @return void
     */
    public static function resaveAllEntities($oldVariant, $oldVersion, $newVariant, $newVersion): void
    {
        $savedCount = 0;
        $errors     = [];

        // Singleton entity classes - use ::class for lazy resolution
        $singletonClasses = [
            GlobalEntity::class,
            DynamicGlobalEntity::class,
            SystemGlobalEntity::class,
        ];

        foreach ($singletonClasses as $class) {
            try {
                $class::getInstance()->save();
                $savedCount++;
            } catch (Throwable $e) {
                $errors[] = "{$class}: " . $e->getMessage();
            }
        }

        // List entity classes - use ::class for lazy resolution
        $listEntityClasses = [
            TemplateEntity::class,
            AbstractStorageEntity::class,
        ];
        $listEntityClasses = apply_filters('duplicator_upgrade_entity_classes', $listEntityClasses);

        foreach ($listEntityClasses as $class) {
            try {
                foreach ($class::getAll() as $entity) {
                    $entity->save();
                    $savedCount++;
                }
            } catch (Throwable $e) {
                $errors[] = "{$class}: " . $e->getMessage();
            }
        }

        // Log any errors that occurred
        if (!empty($errors)) {
            DupLog::trace("UPGRADE: Errors during entity resave: " . implode('; ', $errors));
        }

        DupLog::trace("UPGRADE: Resaved {$savedCount} entities");
    }
}
