<?php

/**
 * Trait for package build operations
 */

declare(strict_types=1);

namespace Duplicator\Package;

use Duplicator\Core\Exceptions\DupliException;
use Duplicator\Libs\Snap\SnapException;
use Duplicator\Libs\Snap\SnapIO;
use Duplicator\Libs\Snap\SnapServer;
use Duplicator\Libs\Snap\SnapString;
use Duplicator\Libs\Snap\SnapUtil;
use Duplicator\Libs\WpUtils\WpDbUtils;
use Duplicator\Models\DynamicGlobalEntity;
use Duplicator\Models\GlobalEntity;
use Duplicator\Models\SystemGlobalEntity;
use Duplicator\Package\Archive\PackageArchive;
use Duplicator\Package\Create\BuildComponents;
use Duplicator\Package\Create\BuildStepIterator;
use Duplicator\Utils\Lock\LockUtil;
use Duplicator\Utils\Logging\DupLog;
use Throwable;

/**
 * Trait TraitPackageBuild
 *
 * Handles package build operations including the main build process,
 * build start, build complete, cleanup, integrity checks, and failure handling.
 *
 * @phpstan-require-extends AbstractPackage
 */
trait TraitPackageBuild
{
    /**
     * Starts the Backup build process
     *
     * @param bool $closeOnEnd if true the function will close the log and die on error
     *
     * @return void
     */
    public function runBuild($closeOnEnd = true): void
    {
        try {
            DupLog::trace('Main build step');

            DupLog::open($this->getNameHash());
            $this->build_progress->startTimer();

            $it = new BuildStepIterator($this);
            while ($it->valid()) {
                do_action('duplicator_backup_build_step', $it->current(), $this, [$it, 'stop']);
            }
        } catch (Throwable $e) {
            DupLog::infoTraceException($e, 'Build failed');
            $this->buildFail($e, $closeOnEnd);
        }

        if ($closeOnEnd) {
            DupLog::close();
        }
    }

    /**
     * Run build start
     *
     * @return void
     */
    public function runBuildStart(): void
    {
        $global = GlobalEntity::getInstance();

        DupLog::trace("**** START OF BUILD: " . $this->getNameHash());

        do_action('duplicator_build_before_start', $this);
        $this->timer_start     = microtime(true);
        $this->ziparchive_mode = $global->ziparchive_mode;
        $this->logBuildHeader();
        $this->logScanReport();
        // CREATE DB RECORD
        $this->build_progress->setBuildMode();

        if ($this->Archive->isArchiveEncrypt() && !SettingsUtils::isArchiveEncryptionAvailable()) {
            throw new DupliException(
                "Archive encryption isn't available.",
                DupliException::CODE_ENCRYPTION_UNAVAILABLE,
                __('Archive encryption is not available on this server. Disable encryption or contact your host.', 'duplicator-pro')
            );
        }

        $this->build_progress->initialized = true;
        $this->setStatus(AbstractPackage::STATUS_START);
        do_action('duplicator_build_start', $this);

        // Drop invalid storages, falling back to the default storage when none is valid
        $this->ensureValidStorages();
    }

    /**
     * Write the Backup creation log header with environment info.
     *
     * Idempotent: writes the header only once per build, so it can be called
     * both at scan start (scheduled builds) and at build start (manual builds).
     *
     * @return void
     */
    public function logBuildHeader(): void
    {
        global $wp_version;

        if ($this->build_progress->header_logged) {
            return;
        }

        $global             = GlobalEntity::getInstance();
        $php_max_time       = @ini_get("max_execution_time");
        $php_max_memory     = @ini_get('memory_limit');
        $php_max_time       = ($php_max_time == 0) ? "(0) no time limit imposed" : "[{$php_max_time}] not allowed";
        $php_max_memory     = ($php_max_memory === false) ? "Unable to set php memory_limit" : WP_MAX_MEMORY_LIMIT . " ({$php_max_memory} default)";
        $architecture       = SnapUtil::getArchitectureString();
        $clientkickoffstate = $this->isClientSideKickoff() ? 'on' : 'off';
        $dGlobal            = DynamicGlobalEntity::getInstance();
        $kickoffOverride    = $dGlobal->getValString(ClientSideKick::KICKOFF_OVERRIDE_KEY, 'auto');
        $kickoffSource      = $kickoffOverride === 'auto' ? 'AUTO' : 'MANUAL';
        $archive_engine     = $global->getArchiveEngine();
        $serverSoftware     = SnapUtil::sanitizeTextInput(INPUT_SERVER, 'SERVER_SOFTWARE', 'unknown');
        $lockSetting        = LockUtil::getProcessLockSettingLabel();
        $lockInfo           = LockUtil::getProcessLockInfo();
        $acquiredLocks      = implode(', ', $lockInfo['acquired']);
        $ajaxProtocolOvr    = $dGlobal->getValString(ClientSideKick::AJAX_PROTOCOL_OVERRIDE_KEY, 'auto');
        $ajaxUrlSource      = $ajaxProtocolOvr === 'auto' ? 'AUTO' : 'MANUAL';
        $workerTime         = GlobalEntity::getMaxWorkerTime();
        $workerTimeLabel    = $workerTime === 0 ? 'unlimited' : "{$workerTime}s";


        $info  = "********************************************************************************\n";
        $info .= "********************************************************************************\n";
        $info .= DUPLICATOR____NAME . " PACKAGE-LOG: " . @date("Y-m-d H:i:s") . "\n";
        $info .= "NOTICE: Do NOT post to public sites or forums \n";
        $info .= "PACKAGE CREATION START\n";
        $info .= "********************************************************************************\n";
        $info .= "********************************************************************************\n";
        $info .= "VERSION:\t" . DUPLICATOR_VERSION . "\n";
        $info .= "WORDPRESS:\t{$wp_version}\n";
        $info .= "----------------------------------------\n";
        $info .= "OS:\t\t" . PHP_OS . " ({$architecture})\n";
        $info .= "WEB SERVER:\t{$serverSoftware}\n";
        $info .= "PHP INFO:\t" . phpversion() . ' | SAPI: ' . php_sapi_name() . "\n";
        $info .= "PHP TIME LIMIT: {$php_max_time}\n";
        $info .= "PHP MAX MEMORY: {$php_max_memory}\n";
        $info .= "MEMORY STACK:\t" . SnapServer::getPHPMemory() . "\n";
        $info .= "SQL ENGINE:\t" . WpDbUtils::getDbEngine() . ' ' . WpDbUtils::getVersion(true) . "\n";
        $info .= "----------------------------------------\n";
        $info .= "RUN TYPE:\t" . PackageUtils::getTypeString($this) . "\n";
        $info .= "CLIENT KICKOFF: {$clientkickoffstate} ({$kickoffSource})\n";
        if (!$this->isClientSideKickoff()) {
            $info .= "AJAX URL:\t" . ClientSideKick::getBackendAjaxUrl() . " ({$ajaxUrlSource})\n";
        }
        $info .= "LOCKS:\t\t{$lockSetting} | Acquired: {$acquiredLocks}\n";
        if ($lockInfo['errors'] !== []) {
            foreach ($lockInfo['errors'] as $lockLabel => $lockError) {
                $info .= "LOCK ERROR {$lockLabel}: {$lockError}\n";
            }
        }
        $info .= "WORKER TIME:\t{$workerTimeLabel}\n";
        $info .= "ARCHIVE ENGINE: {$archive_engine}\n";
        $info .= "SQL BUILD MODE: " . WpDbUtils::getBuildMode() . "\n";
        $info .= "MAX BUILD TIME: {$global->max_package_runtime_in_min} min\n";
        $info .= "MAX XFER TIME:  {$global->max_package_transfer_time_in_min} min\n";

        DupLog::infoTrace($info);

        $this->build_progress->header_logged = true;
    }

    /**
     * Run build complete
     *
     * @return void
     */
    public function runBuildComplete(): void
    {
        DupLog::info("\n********************************************************************************");
        DupLog::info("STORAGE:");
        DupLog::info("********************************************************************************");
        foreach ($this->upload_infos as $upload_info) {
            $storage = $upload_info->getStorage();
            if ($storage->isValid() === false) {
                continue;
            }
            // Protection against deleted storage
            $storage_type_string = strtoupper($storage->getStypeName());
            $storage_path        = $storage->getLocationString();
            DupLog::info($storage_type_string . ": " . $storage->getName() . ', ' . $storage_path);
        }

        $this->buildIntegrityCheck();

        $timerEnd      = microtime(true);
        $timerSum      = SnapString::formattedElapsedTime($timerEnd, $this->timer_start);
        $this->Runtime = $timerSum;
        // FINAL REPORT
        $info  = "\n********************************************************************************\n";
        $info .= "RECORD ID:[{$this->ID}]\n";
        $info .= "TOTAL PROCESS RUNTIME: {$timerSum}\n";
        $info .= "PEAK PHP MEMORY USED: " . SnapServer::getPHPMemory(true) . "\n";
        $info .= "DONE PROCESSING => {$this->name} " . @date("Y-m-d H:i:s") . "\n";
        DupLog::info($info);
        DupLog::trace("Done Backup building");

        //File Cleanup
        $this->buildCleanup();
        do_action('duplicator_build_completed', $this);
    }

    /**
     * Build cleanup
     *
     * @return void
     */
    protected function buildCleanup(): void
    {
        $files = SnapIO::regexGlob(DUPLICATOR_SSDIR_PATH_TMP);
        if (count($files) > 0) {
            $filesToStore = [
                $this->Installer->getInstallerLocalName(),
                $this->Archive->getFileName(),
            ];
            $newPath      = DUPLICATOR_SSDIR_PATH;

            foreach ($files as $file) {
                $fileName = basename($file);

                if (!strstr($fileName, (string) $this->getNameHash())) {
                    continue;
                }

                if (in_array($fileName, $filesToStore)) {
                    if (SnapIO::rename($file, "{$newPath}/{$fileName}") === false) {
                        throw DupliException::fromLastError(
                            "Unable to move a backup file to the final location.\n" .
                            "Moving {$fileName} to {$newPath}",
                            DupliException::CODE_ERROR,
                            __('The backup could not be finalized: unable to move the backup files to the final location.', 'duplicator-pro')
                        );
                    }
                }

                if (file_exists($file)) {
                    unlink($file);
                }
            }
        }
        $this->setStatus(AbstractPackage::STATUS_COPIEDPACKAGE);
    }


    /**
     * Integrity check for the build process
     *
     * @return void
     */
    protected function buildIntegrityCheck()
    {
        //INTEGRITY CHECKS
        //We should not rely on data set in the serialized object, we need to manually check each value
        //indepentantly to have a true integrity check.
        DupLog::info("\n********************************************************************************");
        DupLog::info("INTEGRITY CHECKS:");
        DupLog::info("********************************************************************************");
        //------------------------
        //SQL CHECK:  File should be at minimum 5K.  A base WP install with only Create tables is about 9K
        if (in_array(BuildComponents::COMP_DB, $this->components)) {
            $sql_temp_path = $this->Database->getTempSafeFilePath();
            $sql_temp_size = (int) filesize($sql_temp_path);
            $sql_easy_size = SnapString::byteSize($sql_temp_size);
            $sql_done_txt  = SnapIO::tailFile($sql_temp_path, 3);
            if ($sql_done_txt === false) {
                throw new DupliException(
                    'SQL file missing or unreadable at integrity check.',
                    DupliException::CODE_INTEGRITY_DB_FILE_MISSING,
                    __('The database backup file is missing or could not be read. Check the backup log for details.', 'duplicator-pro')
                );
            }

            if (!strstr($sql_done_txt, (string) DUPLICATOR_DB_EOF_MARKER)) {
                throw new DupliException(
                    'SQL file end of file marker not found.',
                    DupliException::CODE_INTEGRITY_DB_INCOMPLETE,
                    __('The database backup looks incomplete. Check the backup log for details.', 'duplicator-pro')
                );
            }

            // Note: Had to add extra size check of 800 since observed bad sql when filter was on
            if (
                (!$this->Database->FilterOn && $sql_temp_size < DUPLICATOR_MIN_SIZE_DBFILE_WITHOUT_FILTERS) ||
                ($this->Database->FilterOn && $this->Database->info->tablesFinalCount > 0 && $sql_temp_size < DUPLICATOR_MIN_SIZE_DBFILE_WITH_FILTERS)
            ) {
                throw new DupliException(
                    "SQL file too small ($sql_temp_size bytes).",
                    DupliException::CODE_INTEGRITY_DB_TOO_SMALL,
                    __('The database backup file looks too small. Check the backup log for details.', 'duplicator-pro')
                );
            }

            DupLog::info("SQL FILE: {$sql_easy_size}");
        }
        //------------------------
        //INSTALLER CHECK:
        $exe_temp_path = SnapIO::safePath(DUPLICATOR_SSDIR_PATH_TMP . '/' . $this->Installer->getInstallerLocalName());
        $exe_temp_size = @filesize($exe_temp_path);
        $exe_easy_size = SnapString::byteSize($exe_temp_size);
        $exe_done_txt  = SnapIO::tailFile($exe_temp_path, 10);
        if ($exe_done_txt === false) {
            throw new DupliException(
                'Installer file missing or unreadable at integrity check.',
                DupliException::CODE_INTEGRITY_INSTALLER_FILE_MISSING,
                __('The installer file is missing or could not be read. Please try to re-create the backup.', 'duplicator-pro')
            );
        }
        if (!strstr($exe_done_txt, (string) DUPLICATOR_INSTALLER_EOF_MARKER)) {
            throw new DupliException(
                'Installer file not complete. The end of file marker was not found.',
                DupliException::CODE_INTEGRITY_INSTALLER_INCOMPLETE,
                __('The installer file is incomplete. Please try to re-create the backup.', 'duplicator-pro')
            );
        }
        DupLog::info("INSTALLER FILE: {$exe_easy_size}");
        //------------------------
        //ARCHIVE CHECK:
        // Only performs check if we were able to obtain the count
        DupLog::trace("Archive file count is " . $this->Archive->file_count);
        if ($this->Archive->file_count != PackageArchive::FILE_COUNT_UNKNOWN) {
            $zip_easy_size = SnapString::byteSize($this->Archive->Size);
            if (!($this->Archive->Size)) {
                throw new DupliException(
                    "The archive file contains no size. Archive Size: {$zip_easy_size}",
                    DupliException::CODE_INTEGRITY_ARCHIVE_EMPTY,
                    __('The backup archive is empty. Check the backup log for details.', 'duplicator-pro')
                );
            }

            $scan_filepath = DUPLICATOR_SSDIR_PATH_TMP . "/{$this->getNameHash()}_scan.json";
            $json          = '';
            DupLog::trace("***********Does $scan_filepath exist?");
            if (($json = SnapIO::safeFileGetContents($scan_filepath)) === false) {
                throw new DupliException(
                    "Can't find scan file {$scan_filepath} during integrity check.",
                    DupliException::CODE_INTEGRITY_SCANFILE_MISSING,
                    __('A temporary file needed by the build was missing.', 'duplicator-pro')
                );
            }

            $scanReport         = json_decode($json);
            $expected_filecount = (int) ($scanReport->ARC->UDirCount + $scanReport->ARC->UFileCount);
            /** @var int */
            $expected_filecount = apply_filters('duplicator_build_expected_filecount', $expected_filecount, $this);
            DupLog::info("ARCHIVE FILE: {$zip_easy_size} ");
            DupLog::info(sprintf(__('EXPECTED FILE/DIRECTORY COUNT: %1$s', 'duplicator-pro'), number_format($expected_filecount)));
            DupLog::info(sprintf(__('ACTUAL FILE/DIRECTORY COUNT: %1$s', 'duplicator-pro'), number_format($this->Archive->file_count)));
            $this->ExeSize = $exe_easy_size;
            $this->ZipSize = $zip_easy_size;
            /* ------- ZIP Filecount Check -------- */
            // Any zip of over 500 files should be within 2% - this is probably too loose but it will catch gross errors
            DupLog::trace("Expected filecount = $expected_filecount and archive filecount=" . $this->Archive->file_count);
            if ($expected_filecount > 500) {
                $straight_ratio = ($this->Archive->file_count > 0 ? (float) $expected_filecount / (float) $this->Archive->file_count : 0);
                // RSR NEW
                $warning_count = $scanReport->ARC->UnreadableFileCount + $scanReport->ARC->UnreadableDirCount;
                DupLog::trace("Unread counts) unreadfile:{$scanReport->ARC->UnreadableFileCount} unreaddir:{$scanReport->ARC->UnreadableDirCount}");
                $warning_ratio = ((float) ($expected_filecount + $warning_count)) / (float) $this->Archive->file_count;
                DupLog::trace(
                    "Straight ratio is $straight_ratio and warning ratio is $warning_ratio.
                    # Expected=$expected_filecount # Warning=$warning_count and #Archive File {$this->Archive->file_count}"
                );
                // Allow the real file count to exceed the expected by 10% but only allow 1% the other way
                if (($straight_ratio < 0.90) || ($straight_ratio > 1.01)) {
                    // Has to exceed both the straight as well as the warning ratios
                    if (($warning_ratio < 0.90) || ($warning_ratio > 1.01)) {
                        throw new DupliException(
                            sprintf(
                                'File count in archive vs expected suggests a bad archive (%1$d vs %2$d).',
                                $this->Archive->file_count,
                                $expected_filecount
                            ),
                            DupliException::CODE_INTEGRITY_FILE_COUNT_MISMATCH,
                            __('The backup archive has fewer files than expected and may be incomplete.', 'duplicator-pro')
                        );
                    }
                }
            }
        }
    }

    /**
     * Register build step action handlers
     *
     * Registers each core step method as a handler on the
     * duplicator_backup_build_step action hook.
     *
     * @return void
     */
    public static function registerBuildStepHandlers(): void
    {
        add_action('duplicator_backup_build_step', [AbstractPackage::class, 'buildStepInitialize'], 10, 3);
        add_action('duplicator_backup_build_step', [AbstractPackage::class, 'buildStepDatabase'], 10, 3);
        add_action('duplicator_backup_build_step', [AbstractPackage::class, 'buildStepArchive'], 10, 3);
        add_action('duplicator_backup_build_step', [AbstractPackage::class, 'buildStepInstaller'], 10, 3);
        add_action('duplicator_backup_build_step', [AbstractPackage::class, 'buildStepFinalize'], 10, 3);
    }

    /**
     * Build step handler: Initialize
     *
     * Runs initialization logic including logging system info,
     * setting build mode, and validating storage configuration.
     *
     * @param string          $step         Current step name
     * @param AbstractPackage $package      The package being built
     * @param callable        $stopCallback Callback to stop the build loop for the current request
     *
     * @return void
     */
    public static function buildStepInitialize(string $step, AbstractPackage $package, callable $stopCallback): void
    {
        if ($step !== BuildStepIterator::STEP_INITIALIZE) {
            return;
        }

        $package->runBuildStart();
    }

    /**
     * Build step handler: Database
     *
     * Builds the database script using either chunked or single-request mode
     * based on global settings. Always stops the build loop since this step
     * may use chunking across multiple requests.
     *
     * @param string          $step         Current step name
     * @param AbstractPackage $package      The package being built
     * @param callable        $stopCallback Callback to stop the build loop for the current request
     *
     * @return void
     */
    public static function buildStepDatabase(string $step, AbstractPackage $package, callable $stopCallback): void
    {
        if ($step !== BuildStepIterator::STEP_DATABASE) {
            return;
        }

        $global = GlobalEntity::getInstance();

        if ((!$global->package_mysqldump) && ($global->package_phpdump_mode == WpDbUtils::PHPDUMP_MODE_MULTI)) {
            $package->Database->buildInChunks();
        } else {
            $package->Database->build();
            $package->build_progress->database_script_built = true;
            $package->update();
        }

        DupLog::trace("Done building database");
        if ($package->build_progress->database_script_built) {
            DupLog::trace("Set db built for Backup $package->ID");
        }

        $stopCallback();
    }

    /**
     * Build step handler: Archive
     *
     * Builds the archive file for the package. Always stops the build loop
     * since this step may use chunking across multiple requests.
     *
     * @param string          $step         Current step name
     * @param AbstractPackage $package      The package being built
     * @param callable        $stopCallback Callback to stop the build loop for the current request
     *
     * @return void
     */
    public static function buildStepArchive(string $step, AbstractPackage $package, callable $stopCallback): void
    {
        if ($step !== BuildStepIterator::STEP_ARCHIVE) {
            return;
        }

        $package->Archive->buildFile($package);
        $package->update();

        $stopCallback();
    }

    /**
     * Build step handler: Installer
     *
     * Builds the installer and checks for failure. Stops the build loop
     * unless the installer is fully built, allowing the finalize step
     * to run in the same request.
     *
     * @param string          $step         Current step name
     * @param AbstractPackage $package      The package being built
     * @param callable        $stopCallback Callback to stop the build loop for the current request
     *
     * @return void
     */
    public static function buildStepInstaller(string $step, AbstractPackage $package, callable $stopCallback): void
    {
        if ($step !== BuildStepIterator::STEP_INSTALLER) {
            return;
        }

        // Note: Duparchive builds installer within the main build flow not here
        $package->Installer->build($package->build_progress);
        $package->update();

        if (!$package->build_progress->installer_built) {
            $stopCallback();
        }
    }

    /**
     * Build step handler: Finalize
     *
     * Runs post-build logic including integrity checks, cleanup,
     * and marks the finalize step as completed.
     *
     * @param string          $step         Current step name
     * @param AbstractPackage $package      The package being built
     * @param callable        $stopCallback Callback to stop the build loop for the current request
     *
     * @return void
     */
    public static function buildStepFinalize(string $step, AbstractPackage $package, callable $stopCallback): void
    {
        if ($step !== BuildStepIterator::STEP_FINALIZE) {
            return;
        }

        $package->runBuildComplete();
        $package->build_progress->finalized = true;
    }

    /**
     * Backup build fail, this method die the process and set the Backup status to error
     *
     * @param Throwable $exception The failure cause
     * @param bool      $die       If true, the process will die
     *
     * @return void
     */
    public function buildFail(Throwable $exception, bool $die = true): void
    {
        $exception = self::classifyFailureException($exception);
        $dbStatus  = static::getDbStatusById($this->getId());
        if ($dbStatus !== null && $dbStatus < AbstractPackage::STATUS_PRE_PROCESS) {
            // Don't report the failure or fire the failure hooks a second time.
            DupLog::infoTrace(
                "BUILD FAIL SKIPPED: Backup {$this->getId()} already stopped with status {$dbStatus} by another process. " .
                'This may indicate a process lock failure with multiple workers running the same Backup concurrently: ' .
                'another worker declared the failure and cleaned up the build artifacts while this one was still running. ' .
                'Current exception: ' . $exception->getMessage()
            );
            if ($die) {
                DupLog::close();
                die();
            }
            return;
        }

        $previousStatus = $this->getStatus();
        $this->setStatus(AbstractPackage::STATUS_ERROR);
        $this->maybeAddRecommendedFix($exception, $previousStatus);
        do_action('duplicator_build_fail', $this, $previousStatus, $exception);
        $this->addFailureLogEvent($previousStatus, $exception);

        static::deletePackageFilesInDir($this->getNameHash(), DUPLICATOR_SSDIR_PATH_TMP);

        $message  = "Backup creation failed.\n"
            . " EXCEPTION message: " . $exception->getMessage() . "\n";
        $message .= $exception->getFile() . ' LINE: ' . $exception->getLine() . "\n";
        $message .= $exception->getTraceAsString();

        if ($die) {
            DupLog::errorAndDie($message);
        } else {
            DupLog::error($message);
        }
    }

    /**
     * Normalize the failure cause: a disk-full I/O failure anywhere in the
     * exception chain is reported as the canonical disk-full domain exception,
     * so it gets handled severity and the matching recommended fix.
     *
     * @param Throwable $exception The failure cause
     *
     * @return Throwable
     */
    private static function classifyFailureException(Throwable $exception): Throwable
    {
        if ($exception instanceof DupliException && $exception->getCode() === DupliException::CODE_DISK_FULL) {
            return $exception;
        }

        if (SnapException::isDiskFullInChain($exception)) {
            return DupliException::diskFull($exception);
        }

        return $exception;
    }

    /**
     * @param Throwable $exception The failure cause
     *
     * @return int one of AbstractPackage::FAIL_REASON_*
     */
    public static function failReasonFromException(Throwable $exception): int
    {
        if (!$exception instanceof DupliException) {
            return AbstractPackage::FAIL_REASON_EXCEPTION;
        }

        switch ($exception->getCode()) {
            case DupliException::CODE_MAX_BUILD_TIME:
                return AbstractPackage::FAIL_REASON_MAX_BUILD_TIME;
            case DupliException::CODE_STUCK:
                return AbstractPackage::FAIL_REASON_STUCK;
            default:
                return AbstractPackage::FAIL_REASON_EXCEPTION;
        }
    }

    /**
     * Register the recommended quick fix matching the failure cause, if any.
     *
     * @param Throwable $exception      The failure cause
     * @param int       $previousStatus Package status right before the failure, one of AbstractPackage::STATUS_*
     *
     * @return void
     */
    private function maybeAddRecommendedFix(Throwable $exception, int $previousStatus): void
    {
        if (!$exception instanceof DupliException) {
            return;
        }

        $systemGlobal = SystemGlobalEntity::getInstance();
        $message      = $exception->getUserMessage();

        switch ($exception->getCode()) {
            case DupliException::CODE_MAX_BUILD_TIME:
                $this->addMaxBuildTimeRecommendedFix($previousStatus);
                break;
            case DupliException::CODE_SCAN_READ_FAILED:
            case DupliException::CODE_INTEGRITY_SCANFILE_MISSING:
                $systemGlobal->addTextFix(
                    $message,
                    __(
                        'Run the Backup again. If it happens repeatedly, check the build log
                        to find which file keeps disappearing.',
                        'duplicator-pro'
                    )
                );
                break;
            case DupliException::CODE_SCAN_WRITE_FAILED:
            case DupliException::CODE_INSTALLER_BUILD_FAILED:
                $systemGlobal->addTextFix(
                    $message,
                    __(
                        'Check that the Duplicator backup folder is writable and the disk has
                        free space, then run the Backup again.',
                        'duplicator-pro'
                    )
                );
                break;
            case DupliException::CODE_ZIP_PATH_NOT_WRITABLE:
                // Raised by an archive-agnostic utility: the user text belongs here, where the path
                // is known to be a Backup archive. Switching engine cannot fix a permission issue.
                $systemGlobal->addTextFix(
                    __('The Backup archive path already exists but cannot be written to.', 'duplicator-pro'),
                    __(
                        'Check the folder permissions and remove any leftover file with the same
                        name, then run the Backup again.',
                        'duplicator-pro'
                    )
                );
                break;
            case DupliException::CODE_ZIP_NOT_AVAILABLE:
            case DupliException::CODE_ZIP_OPEN_FAILED:
            case DupliException::CODE_ZIP_CLOSE_FAILED:
            case DupliException::CODE_ZIP_RETRY_EXHAUSTED:
            case DupliException::CODE_SHELL_ZIP_FAILED:
            case DupliException::CODE_SHELL_ZIP_FILE_COUNT_FAILED:
            case DupliException::CODE_SHELL_ZIP_ARCHIVE_CORRUPT:
            case DupliException::CODE_SHELL_ZIP_RETRY_EXHAUSTED:
            case DupliException::CODE_SHELL_ZIP_NO_ARCHIVE:
                // Failures raised by archive-agnostic utilities carry no user message: fall back to
                // generic text here, where the archive engine is known, instead of showing the raw one.
                $systemGlobal->addQuickFix(
                    $exception->hasUserMessage() ? $message : __('The archive could not be created with the current archive engine.', 'duplicator-pro'),
                    __('Click to switch the archive engine to DupArchive.', 'duplicator-pro'),
                    ['global' => ['archive_build_mode' => PackageArchive::BUILD_MODE_DUP_ARCHIVE]]
                );
                break;
            case DupliException::CODE_MYSQLDUMP_FAILED:
            case DupliException::CODE_MYSQLDUMP_UNAVAILABLE:
            case DupliException::CODE_MYSQLDUMP_INTERRUPTED:
                $systemGlobal->addQuickFix(
                    $message,
                    __('Click to switch the database engine to PHP.', 'duplicator-pro'),
                    ['global' => ['package_mysqldump' => 0]]
                );
                break;
            case DupliException::CODE_DB_PHP_DUMP_INTERRUPTED:
                $systemGlobal->addQuickFix(
                    $message,
                    __('Click to switch the database engine to multi-threaded PHP mode.', 'duplicator-pro'),
                    ['global' => ['package_phpdump_mode' => WpDbUtils::PHPDUMP_MODE_MULTI]]
                );
                break;
            case DupliException::CODE_DISK_FULL:
                $systemGlobal->addTextFix(
                    $message,
                    __(
                        'Free up disk space by removing unneeded files or old backups, or contact
                        your hosting provider to increase the available space.',
                        'duplicator-pro'
                    )
                );
                break;
            case DupliException::CODE_SHELL_ZIP_FILE_NOT_FOUND:
                $systemGlobal->addTextFix(
                    $message,
                    sprintf(
                        _x(
                            'See FAQ: %1$sHow to resolve "zip warning: No such file or directory"?%2$s',
                            '1 and 2 are opening and closing anchor tags',
                            'duplicator-pro'
                        ),
                        '<a href="' . esc_url(DUPLICATOR_DUPLICATOR_DOCS_URL . 'how-to-resolve-zip-format-related-build-issues') . '" target="_blank">',
                        '</a>'
                    )
                );
                break;
            case DupliException::CODE_DUP_ARCHIVE_32BIT_LIMIT:
                $systemGlobal->addTextFix(
                    $message,
                    sprintf(
                        _x(
                            'Backup build failure due to building a large Backup on 32 bit PHP.
                            Please see %1$sTech docs%2$s for instructions on how to resolve.',
                            '1 and 2 are opening and closing anchor tags',
                            'duplicator-pro'
                        ),
                        '<a href="' . esc_url(DUPLICATOR_DUPLICATOR_DOCS_URL . 'how-to-resolve-file-io-related-build-issues') . '" target="_blank">',
                        '</a>'
                    )
                );
                break;
            case DupliException::CODE_INTEGRITY_DB_INCOMPLETE:
            case DupliException::CODE_INTEGRITY_DB_TOO_SMALL:
                if ($this->Database->DBMode == 'MYSQLDUMP') {
                    $systemGlobal->addQuickFix(
                        $message,
                        __('Click to switch the database engine to PHP.', 'duplicator-pro'),
                        [
                            'global' => [
                                'package_mysqldump'          => 0,
                                'package_mysqldump_qrylimit' => 32768,
                            ],
                        ]
                    );
                } else {
                    $systemGlobal->addQuickFix(
                        $message,
                        __('Click to switch the database engine to MySQLDump.', 'duplicator-pro'),
                        [
                            'global' => [
                                'package_mysqldump'      => 1,
                                'package_mysqldump_path' => '',
                            ],
                        ]
                    );
                }
                break;
            case DupliException::CODE_INTEGRITY_FILE_COUNT_MISMATCH:
                if ($this->build_progress->current_build_mode == PackageArchive::BUILD_MODE_SHELL_EXEC) {
                    $systemGlobal->addQuickFix(
                        $message,
                        __('Click to switch the archive engine to DupArchive.', 'duplicator-pro'),
                        ['global' => ['archive_build_mode' => PackageArchive::BUILD_MODE_DUP_ARCHIVE]]
                    );
                }
                break;
            case DupliException::CODE_INSTALLER_ADD_FAILED:
            case DupliException::CODE_INSTALLER_CONSISTENCY_FAILED:
                if ($this->build_progress->current_build_mode != PackageArchive::BUILD_MODE_DUP_ARCHIVE) {
                    $systemGlobal->addQuickFix(
                        $message,
                        __('Click to switch the archive engine to DupArchive.', 'duplicator-pro'),
                        ['global' => ['archive_build_mode' => PackageArchive::BUILD_MODE_DUP_ARCHIVE]]
                    );
                }
                break;
        }
    }

    /**
     * Register the recommended fix for a Max Build Time failure.
     *
     * The remedy depends on why the time ran out. In client-side kickoff mode
     * elapsed time mostly measures gaps between browser polling sessions, so
     * the guidance targets background processing, never build speed. In
     * server-side mode the phase reached at failure decides between scan,
     * database and archive remedies.
     *
     * @param int $previousStatus Package status right before the failure, one of AbstractPackage::STATUS_*
     *
     * @return void
     */
    private function addMaxBuildTimeRecommendedFix(int $previousStatus): void
    {
        $systemGlobal = SystemGlobalEntity::getInstance();

        if ($this->isClientSideKickoff()) {
            $errorText = __(
                'The Backup ran out of time: on this site backups can only progress while
                a Duplicator admin page is open in the browser, so scheduled backups may
                never be picked up.',
                'duplicator-pro'
            );
            $dGlobal   = DynamicGlobalEntity::getInstance();
            if (isset($_SERVER['AUTH_TYPE']) && $_SERVER['AUTH_TYPE'] == 'Basic' && !$dGlobal->getValBool('basic_auth_enabled')) {
                $systemGlobal->addQuickFix(
                    $errorText,
                    __('Automatically set basic auth username and password', 'duplicator-pro'),
                    ['special' => ['set_basic_auth' => 1]]
                );
            } else {
                $systemGlobal->addTextFix(
                    $errorText,
                    sprintf(
                        _x(
                            'See FAQ: %1$sWhy is the Backup build stuck at 5%%?%2$s',
                            '1 and 2 are opening and closing anchor tags',
                            'duplicator-pro'
                        ),
                        '<a href="' . esc_url(DUPLICATOR_DUPLICATOR_DOCS_URL . 'how-to-resolve-builds-getting-stuck-at-a-certain-point/') .
                            '" target="_blank">',
                        '</a>'
                    )
                );
            }
            return;
        }

        if ($previousStatus <= AbstractPackage::STATUS_AFTER_SCAN) {
            $systemGlobal->addTextFix(
                __('The Backup ran out of time during the site scan phase.', 'duplicator-pro'),
                sprintf(
                    _x(
                        'Reduce the number of files to scan by excluding large folders, or see this %1$sFAQ%2$s.',
                        '1 and 2 are opening and closing anchor tags',
                        'duplicator-pro'
                    ),
                    '<a href="' . esc_url(DUPLICATOR_DUPLICATOR_DOCS_URL . 'how-to-resolve-scanner-warnings-errors-and-timeout-issues') . '" target="_blank">',
                    '</a>'
                )
            );
            return;
        }

        if ($previousStatus >= AbstractPackage::STATUS_DBSTART && $previousStatus <= AbstractPackage::STATUS_DBDONE) {
            $errorText = __('The Backup ran out of time while exporting the database.', 'duplicator-pro');
            if (GlobalEntity::getInstance()->max_package_runtime_in_min < Runner::DEFAULT_MAX_BUILD_TIME_IN_MIN) {
                $this->addRaiseMaxBuildTimeQuickFix($errorText);
            } else {
                $systemGlobal->addTextFix(
                    $errorText,
                    sprintf(
                        _x(
                            'See this %1$sFAQ%2$s on how to speed up a slow Backup build.',
                            '1 and 2 are opening and closing anchor tags',
                            'duplicator-pro'
                        ),
                        '<a href="' . esc_url(DUPLICATOR_DUPLICATOR_DOCS_URL . 'if-the-package-build-is-slow-how-can-i-speed-it-up/') . '" target="_blank">',
                        '</a>'
                    )
                );
            }
            return;
        }

        if ($previousStatus >= AbstractPackage::STATUS_ARCSTART) {
            $errorText = __('The Backup ran out of time while compressing the site files.', 'duplicator-pro');
            if ($this->build_progress->current_build_mode != PackageArchive::BUILD_MODE_DUP_ARCHIVE) {
                $systemGlobal->addQuickFix(
                    $errorText,
                    __('Click to switch the archive engine to DupArchive.', 'duplicator-pro'),
                    ['global' => ['archive_build_mode' => PackageArchive::BUILD_MODE_DUP_ARCHIVE]]
                );
            } elseif (GlobalEntity::getInstance()->max_package_runtime_in_min < Runner::DEFAULT_MAX_BUILD_TIME_IN_MIN) {
                $this->addRaiseMaxBuildTimeQuickFix($errorText);
            } else {
                $systemGlobal->addTextFix(
                    $errorText,
                    __('Exclude large files or folders from the Backup to shorten the build.', 'duplicator-pro')
                );
            }
        }

        // Remaining statuses (e.g. STATUS_START): no reliable remedy to suggest.
    }

    /**
     * Register the one-click fix raising the Max Build Time to the default ceiling.
     *
     * @param string $errorText Failure description shown next to the fix
     *
     * @return void
     */
    private function addRaiseMaxBuildTimeQuickFix(string $errorText): void
    {
        SystemGlobalEntity::getInstance()->addQuickFix(
            $errorText,
            sprintf(
                __('Click to increase the Max Build Time to %d minutes.', 'duplicator-pro'),
                Runner::DEFAULT_MAX_BUILD_TIME_IN_MIN
            ),
            ['global' => ['max_package_runtime_in_min' => Runner::DEFAULT_MAX_BUILD_TIME_IN_MIN]]
        );
    }
}
