<?php

/**
 *
 * @package   Duplicator
 * @copyright (c) 2022, Snap Creek LLC
 */

namespace Duplicator\Addons\DupCloudAddon\Models;

use Duplicator\Models\DynamicGlobalEntity;
use Duplicator\Models\GlobalEntity;
use Duplicator\Utils\Logging\DupLog;
use Duplicator\Addons\DupCloudAddon\Utils\DupCloudClient;
use Duplicator\Addons\DupCloudAddon\Utils\DupCloudStorageAdapter;
use Duplicator\Core\Controllers\ControllersManager;
use Duplicator\Core\UniqueId;
use Duplicator\Core\Views\TplMng;
use Duplicator\Installer\Package\ArchiveDescriptor;
use Duplicator\Libs\Snap\SnapIO;
use Duplicator\Libs\Snap\SnapUtil;
use Duplicator\Models\Storages\AbstractStorageEntity;
use Duplicator\Models\Storages\StorageAuthInterface;
use Duplicator\Package\AbstractPackage;
use Duplicator\Package\Create\BuildComponents;
use Duplicator\Package\PackageUtils;
use Duplicator\Package\Storage\UploadInfo;
use Exception;

/**
 * @property ?DupCloudStorageAdapter $adapter
 */
class DupCloudStorage extends AbstractStorageEntity implements StorageAuthInterface
{
    /** @var int */
    const DEFAULT_UPLOAD_CHUNK_SIZE_IN_KB = 10 * 1024;
    /** @var int */
    const UPLOAD_CHUNK_MIN_SIZE_IN_KB = 5 * 1024;
    /** @var int */
    const UPLOAD_CHUNK_MAX_SIZE_IN_KB = 5 * 1024 * 1024;
    /** @var int */
    const DEFAULT_DOWNLOAD_CHUNK_SIZE_IN_KB = 10 * 1024;
    /** @var int */
    const DOWNLOAD_CHUNK_MAX_SIZE_IN_KB = 5 * 1024 * 1024;
    /** @var int */
    const DOWNLOAD_CHUNK_MIN_SIZE_IN_KB = 5 * 1024;

    /**
     * Class constructor
     */
    public function __construct()
    {
        parent::__construct();
        $this->name = __('Duplicator Cloud', "duplicator-pro");
    }

    /**
     * Get priority, used to sort storages.
     * 100 is neutral value, 0 is the highest priority
     *
     * @return int
     */
    public static function getPriority(): int
    {
        return 20;
    }

    /**
     * Mark an upload as failed using the upload info
     *
     * @param UploadInfo $uploadInfo Upload info
     *
     * @return bool True if success, false otherwise
     */
    public function failUpload(UploadInfo $uploadInfo)
    {
        if (!isset($uploadInfo->copyExtraData['UploadUuid'])) {
            DupLog::infoTrace("Can't fail upload, UploadUuid not found");
            return false;
        }

        return $this->getAdapter()->failUpload($uploadInfo->copyExtraData['UploadUuid']);
    }

    /**
     * Cancel an upload using the uplaod info
     *
     * @param UploadInfo $uploadInfo Upload info
     *
     * @return bool True if success, false otherwise
     */
    public function cancelUpload(UploadInfo $uploadInfo)
    {
        if (!isset($uploadInfo->copyExtraData['UploadUuid'])) {
            DupLog::infoTrace("Can't cancel upload, UploadUuid not found");
            return false;
        }

        return $this->getAdapter()->cancelUpload($uploadInfo->copyExtraData['UploadUuid']);
    }

    /**
     * Get new storage object by type
     *
     * @return self
     */
    protected static function getNewStorageInstance(): self
    {
        return new self();
    }

    /**
     * Get default config
     *
     * @return array<string,scalar>
     */
    protected static function getDefaultConfig(): array
    {
        $config = parent::getDefaultConfig();
        return array_merge(
            $config,
            [
                'accessToken' => '',
                'userName'    => '',
                'userEmail'   => '',
                'totalSpace'  => 0,
                'freeSpace'   => 0,
                'authorized'  => false,
                'websiteUuid' => '',
            ]
        );
    }

    /**
     * Storages test
     *
     * @param string $message Test message
     *
     * @return bool return true if success, false otherwise
     */
    public function test(string &$message = ''): bool
    {
        try {
            $this->testLog->reset();
            $message = sprintf(__('Testing %s storage...', 'duplicator-pro'), static::getStypeName());
            $this->testLog->addMessage($message);

            if (static::isSupported() == false) {
                $message = sprintf(__('Storage %s isn\'t supported on current server', 'duplicator-pro'), static::getStypeName());
                $this->testLog->addMessage($message);
                return false;
            }

            $this->testLog->addMessage(__('Check if storage is ready to use.', 'duplicator-pro'));
            $validMessage = '';
            $adapter      = $this->getAdapter();
            if ($adapter->isValid($validMessage) == false) {
                $message = sprintf(
                    __('Storage %1$s is not valid message %2$s', 'duplicator-pro'),
                    static::getStypeName(),
                    $validMessage
                );
                $this->testLog->addMessage($message);
                return false;
            }
            $this->testLog->addMessage(__('Successfully storage test.', 'duplicator-pro'));
            $message = __('Successfully storage test.', 'duplicator-pro');
            return true;
        } catch (Exception $e) {
            $message = $e->getMessage();
            $this->testLog->addMessage(sprintf(__('Error during storage test: %s', 'duplicator-pro'), $message));
            return false;
        }
    }

    /**
     * Get the authorzation url
     *
     * @return string
     */
    public static function getAuthUrl(): string
    {
        return DupCloudClient::getAuthUrl(home_url());
    }

    /**
     * Return the storage type
     *
     * @return int
     */
    public static function getSType(): int
    {
        return 17;
    }

    /**
     * Returns the storage type icon URL
     *
     * @return string Returns the storage icon URL
     */
    public static function getStypeIconURL(): string
    {
        return DUPLICATOR_PRO_IMG_URL . '/duplicator-logo-icon.svg';
    }

    /**
     * Returns the storage type name.
     *
     * @return string
     */
    public static function getStypeName(): string
    {
        return __('Duplicator Cloud', 'duplicator-pro');
    }

    /**
     * Get storage location string
     *
     * @return string
     */
    public function getLocationString(): string
    {
        return 'Manage Backups';
    }

    /**
     * Returns URL to backup list for website
     *
     * @return string
     */
    public function getBackupsUrl(): string
    {
        return SnapIO::trailingslashit(DupCloudClient::manageWebsitesUrl()) . $this->config['websiteUuid'] . '/backups';
    }

    /**
     * Returns an html anchor tag of location
     *
     * @return string Returns an html anchor tag with the storage location as a hyperlink.
     */
    public function getHtmlLocationLink(): string
    {
        if ($this->isAuthorized()) {
            $websitesUrl = $this->getBackupsUrl();
            return '<a href="' . esc_url($websitesUrl) . '" target="_blank" >' .
                esc_html__('Manage Backups', 'duplicator-pro') .
            '</a>';
        } else {
            return '#';
        }
    }

    /**
     * Is unique, if true only one storage of this type can exist
     *
     * @return bool
     */
    public static function isUnique(): bool
    {
        return true;
    }

    /**
     * Check if a grid break should be inserted after this storage type in the selector UI
     *
     * @return bool
     */
    public static function isGridBreakAfter(): bool
    {
        return true;
    }

    /**
     * Check if storage is valid
     *
     * @param ?string $errorMsg Reference to store error message
     * @param bool    $force    Force the storage to be revalidated
     *
     * @return bool Return true if storage is valid and ready to use, false otherwise
     */
    public function isValid(?string &$errorMsg = '', bool $force = false): bool
    {
        $adapter = $this->getAdapter();
        $isValid = $adapter->isValid($errorMsg, $force);

        // 'isValid()' can be false when there is no free space
        // 'authorized' determines if the info should be considered for saving
        if ($force === true && $adapter->isAuthorized()) {
            $data = [
                'authorized'  => $adapter->isAuthorized(),
                'userName'    => $adapter->getUserName(),
                'userEmail'   => $adapter->getUserEmail(),
                'totalSpace'  => $adapter->getTotalSpace(),
                'freeSpace'   => $adapter->getFreeSpace(),
                'websiteUuid' => $adapter->getWebsiteUuid(),
            ];

            if ($this->hasConfigChanged($data)) {
                $this->config = array_merge($this->config, $data);
                $this->save();
            }
        }

        return $isValid;
    }

    /**
     * Is autorized
     *
     * @return bool
     */
    public function isAuthorized(): bool
    {
        return $this->config['authorized'];
    }

    /**
     * Authorize
     *
     * @param string $token Token
     *
     * @return bool True if authorized, false if failed
     */
    public function authorize(string $token): bool
    {
        $adapter = new DupCloudStorageAdapter($token);

        $userInfo = $adapter->getUserInfo();

        $this->config['accessToken'] = $token;
        $this->config['userName']    = $userInfo['name'];
        $this->config['userEmail']   = $userInfo['email'];
        $this->config['authorized']  = true;
        $this->config['websiteUuid'] = '';

        return true;
    }

    /**
     * Authorized from HTTP request
     *
     * @param string $message Message
     *
     * @return bool True if authorized, false if failed
     */
    public function authorizeFromRequest(&$message = ''): bool
    {
        // Allow pipe character in the token for compound tokens
        if (($accessToken = SnapUtil::sanitizeDefaultInput(SnapUtil::INPUT_REQUEST, 'access_token', '')) === '') {
            DupLog::trace('No access token found');
            $message = __('No access token provided', 'duplicator-pro');
            return false;
        }

        $this->name  = SnapUtil::sanitizeTextInput(SnapUtil::INPUT_REQUEST, 'name', '');
        $this->notes = SnapUtil::sanitizeDefaultInput(SnapUtil::INPUT_REQUEST, 'notes', '');

        try {
            // Check if this is a compound token (new method) or direct token (legacy)
            if (strpos($accessToken, '.') !== false && strpos($accessToken, '|') !== false) {
                // New compound token authentication (contains both . and |)
                $client = new DupCloudClient();

                // Get site identifier
                $siteIdentifier = UniqueId::getInstance()->getIdentifier();

                DupLog::trace('Authenticating with compound token for site: ' . $siteIdentifier);

                // Authenticate using compound token
                $authResult = $client->authenticateSite($accessToken, $siteIdentifier);

                // Store the permanent token and all data directly
                $this->config['accessToken'] = $authResult['token'];
                $this->config['authorized']  = true;
                $this->config['userName']    = $authResult['user_name'] ?? '';
                $this->config['userEmail']   = $authResult['user_email'] ?? '';
                $this->config['totalSpace']  = $authResult['total_space'] ?? 0;
                $this->config['freeSpace']   = $authResult['free_space'] ?? 0;

                DupLog::trace('Website connected: ' . ($authResult['name'] ?? ''));
                DupLog::trace('Storage info - Total: ' . $this->config['totalSpace'] . ', Free: ' . $this->config['freeSpace']);
                DupLog::trace('User info - Name: ' . $this->config['userName'] . ', Email: ' . $this->config['userEmail']);

                $currentPage    = SnapUtil::sanitizeTextInput(SnapUtil::INPUT_REQUEST, 'current_page', '');
                $storagePageUrl = ControllersManager::getInstance()->getMenuLink(
                    ControllersManager::STORAGE_SUBMENU_SLUG
                );

                $message = TplMng::getInstance()->render(
                    'admin_pages/storages/parts/auth_success_message',
                    [
                        'storagePageUrl' => $storagePageUrl,
                        'storageName'    => $this->getStypeName(),
                        'isSettingsPage' => $currentPage === ControllersManager::SETTINGS_SUBMENU_SLUG,
                    ],
                    false
                );
                return true;
            } else {
                // Legacy method - direct token
                return $this->authorize($accessToken);
            }
        } catch (\Exception $e) {
            DupLog::trace('Authorization failed: ' . $e->getMessage());
            $message = $e->getMessage();
            return false;
        }
    }

    /**
     * Revokes authorization
     *
     * @param string $message Message
     *
     * @return bool True if revoked, false if failed
     */
    public function revokeAuthorization(&$message = ''): bool
    {
        try {
            if (!$this->isAuthorized()) {
                return true;
            }

            if (!$this->getAdapter()->revokeAuthorization()) {
                throw new Exception(__('Error revoking authorization.', 'duplicator-pro'));
            }
        } catch (Exception $e) {
            DupLog::traceException($e, 'REVOKE AUTHORIZATION ERROR BUT COUNTINUE RESETTING CONFIG');
            $message = $e->getMessage();
            return false;
        } finally {
            $this->config = static::getDefaultConfig();
            $this->save();
        }

        $message = __('Duplicator Cloud is disconnected successfully.', 'duplicator-pro');

        return true;
    }

    /**
     * Returns the config fields template path
     *
     * @return string
     */
    protected function getConfigFieldsTemplatePath(): string
    {
        return 'dupcloudaddon/configs/dupcloud';
    }


    /**
     * Returns the config fields template data
     *
     * @return array<string, mixed>
     */
    protected function getConfigFieldsData(): array
    {
        return $this->getDefaultConfigFieldsData();
    }

    /**
     * Returns the default config fields template data
     *
     * @return array<string, mixed>
     */
    protected function getDefaultConfigFieldsData(): array
    {
        return [
            'storage'     => $this,
            'maxPackages' => $this->config['max_packages'],
            'userName'    => $this->config['userName'],
            'userEmail'   => $this->config['userEmail'],
            'totalSpace'  => $this->config['totalSpace'],
            'freeSpace'   => $this->config['freeSpace'],
            'authorized'  => $this->config['authorized'],
            'websiteUuid' => $this->config['websiteUuid'],
        ];
    }

    /**
     * Update data from http request, this method don't save data, just update object properties
     *
     * @param string $message Message
     *
     * @return bool True if success and all data is valid, false otherwise
     */
    public function updateFromHttpRequest(&$message = ''): bool
    {
        if ((parent::updateFromHttpRequest($message) === false)) {
            return false;
        }

        $this->config['max_packages'] = SnapUtil::sanitizeIntInput(SnapUtil::INPUT_REQUEST, 'dupcloud_max_files', 10);

        $message = __('Dupicator Cloud Storage was updated.', 'duplicator-pro');
        return true;
    }

    /**
     * Get the storage adapter
     *
     * @return DupCloudStorageAdapter
     */
    protected function getAdapter(): DupCloudStorageAdapter
    {
        if ($this->adapter === null) {
            $this->adapter = new DupCloudStorageAdapter(
                $this->config['accessToken'],
                DupCloudClient::BACKUP_TYPE_STANDARD,
                $this->config['max_packages']
            );
        }

        return $this->adapter;
    }

    /**
     * Get general data for backup
     *
     * @param AbstractPackage $package the Backup
     *
     * @return array<string,mixed>
     */
    protected function getGeneralExtraData(AbstractPackage $package): array
    {
        return array_merge(
            parent::getGeneralExtraData($package),
            ['backup_details' => self::getBackupDetails($package) ]
        );
    }

    /**
     * Returns the Backup details
     *
     * @param AbstractPackage $package the Backup
     *
     * @return array<string, mixed>
     */
    public static function getBackupDetails(AbstractPackage $package): array
    {
        return [
            'system'    => [
                'php_version'    => $package->VersionPHP,
                'wp_version'     => $package->VersionWP,
                'plugin_type'    => 'pro',
                'plugin_version' => $package->getVersion(),
            ],
            'file_info' => [
                'backup_filename'         => $package->getArchiveFilename(), // full file name
                'backup_name'             => $package->getName(), // name, first part of file name
                'backup_hash'             => $package->getHash(), // hash [a-z0-9]{20}_[0-9]{14}
                'created'                 => $package->getCreated(),
                'type'                    => PackageUtils::getExecTypeString($package->getExecutionType(), $package->template_id),
                'engine'                  => PackageUtils::getEngineTypeString(
                    $package->build_progress->current_build_mode,
                    $package->ziparchive_mode
                ),
                'secure_mode'             => $package->Installer->OptsSecureOn,
                'runtime'                 => $package->Runtime,
                'notes'                   => $package->notes,
                'installer'               => $package->Installer->getInstallerName(),
                'backup_size_from_plugin' => $package->Archive->Size, // Used on upload start to know the estimated file size
                'filters'                 => [
                    'enabled'     => $package->Archive->FilterOn,
                    'directories' => [
                        'user'      => $package->Archive->FilterInfo->Dirs->Instance,
                        'unredable' => $package->Archive->FilterInfo->Dirs->Unreadable,
                    ],
                    'files'       => [
                        'user'      => $package->Archive->FilterInfo->Files->Instance,
                        'unredable' => $package->Archive->FilterInfo->Files->Unreadable,
                    ],
                    'extensions'  => $package->Archive->FilterInfo->Exts->Instance,
                ],
                'components'              => array_map(fn($component): string => BuildComponents::getLabel($component), $package->components),
            ],
            'db_info'   => [
                'engine'     => $package->Database->info->dbEngine,
                'version'    => $package->VersionDB,
                'name'       => $package->Database->info->name,
                'size'       => $package->Database->info->tablesSizeOnDisk,
                'filters'    => [
                    'enabled' => $package->Database->FilterOn,
                    'tables'  => explode(',', $package->Database->FilterTables),
                ],
                'collations' => $package->Database->info->collationList,
            ],
        ];
    }

    /**
     * Get upload chunk timeout in seconds
     *
     * @return int timeout in microseconds, 0 unlimited
     */
    public function getUploadChunkTimeout(): int
    {
        $global = GlobalEntity::getInstance();
        return (int) ($global->php_max_worker_time_in_sec <= 0 ? 0 :  $global->php_max_worker_time_in_sec * SECONDS_IN_MICROSECONDS);
    }

    /**
     * Duplicator cloud storage handles the pruning of old backups automatically
     *
     * @param array<string> $exclude List of Backups to exclude from deletion
     *
     * @return false|string[] false on failure or array of deleted files of Backups
     */
    public function purgeOldPackages(array $exclude = [])
    {
        DupLog::infoTrace("Old backups are purged automatically by Duplicator Cloud storage");

        return [];
    }

    /**
     * Get upload chunk size in bytes
     *
     * @return int bytes
     */
    public function getUploadChunkSize(): int
    {
        $dGlobal = DynamicGlobalEntity::getInstance();
        return $dGlobal->getValInt(
            'dupcloud_upload_chunk_size_in_kb',
            self::DEFAULT_UPLOAD_CHUNK_SIZE_IN_KB
        ) * KB_IN_BYTES;
    }

    /**
     * Get download chunk size in bytes
     *
     * @return int bytes
     */
    public function getDownloadChunkSize(): int
    {
        $dGlobal = DynamicGlobalEntity::getInstance();
        return $dGlobal->getValInt(
            'dupcloud_download_chunk_size_in_kb',
            self::DEFAULT_DOWNLOAD_CHUNK_SIZE_IN_KB
        ) * KB_IN_BYTES;
    }

    /**
     * Get user name
     *
     * @return string Return user name or false if not available
     */
    public function getUserName(): string
    {
        return $this->config['userName'] ?? __('unknown', 'duplicator-pro');
    }

    /**
     * Get user email
     *
     * @return string Return user email or false if not available
     */
    public function getUserEmail(): string
    {
        return $this->config['userEmail'] ?? __('unknown', 'duplicator-pro');
    }

    /**
     * Get total space
     *
     * @return int
     */
    public function getTotalSpace(): int
    {
        return $this->config['totalSpace'] ?? 0;
    }

    /**
     * Get free space
     *
     * @return int
     */
    public function getUsedSpace(): int
    {
        $freeSpace  = $this->config['freeSpace'] ?? 0;
        $totalSpace = $this->config['totalSpace'] ?? 0;

        return $totalSpace - $freeSpace;
    }

    /**
     * Get free space
     *
     * @return int
     */
    public function getFreeSpace(): int
    {
        return $this->config['freeSpace'] ?? 0;
    }

    /**
     * Get website UUID
     *
     * @return string
     */
    public function getWebsiteUuid(): string
    {
        return $this->config['websiteUuid'] ?? '';
    }

    /**
     * Get unique DupCloud storasge or new tempalte if not exists
     *
     * @return self
     */
    public static function getUniqueStorage(): self
    {
        $storages = static::getAllBySType(self::getSType());
        if (is_array($storages) && count($storages) > 0) {
            return $storages[0];
        } else {
            $storage = new self();
            return $storage;
        }
    }

    /**
     * Register storage type
     *
     * @return void
     */
    public static function registerType(): void
    {
        parent::registerType();

        add_action('duplicator_update_global_storage_settings', function (): void {
            $dGlobal = DynamicGlobalEntity::getInstance();

            foreach (static::getDefaultSettings() as $key => $default) {
                $value = SnapUtil::sanitizeIntInput(SnapUtil::INPUT_REQUEST, $key, $default);
                $dGlobal->setValInt($key, $value);
            }
        });
    }

    /**
     * Get default settings
     *
     * @return array<string, scalar>
     */
    protected static function getDefaultSettings(): array
    {
        return [
            'dupcloud_upload_chunk_size_in_kb'   => self::DEFAULT_UPLOAD_CHUNK_SIZE_IN_KB,
            'dupcloud_download_chunk_size_in_kb' => self::DEFAULT_DOWNLOAD_CHUNK_SIZE_IN_KB,
        ];
    }

    /**
     * Render global options
     *
     * @return void
     */
    public static function renderGlobalOptions(): void
    {
        if (static::isHidden()) {
            return;
        }

        $dGlobal = DynamicGlobalEntity::getInstance();
        TplMng::getInstance()->render(
            'dupcloudaddon/configs/global_options',
            [
                'uploadChunkSizeInKb'   => $dGlobal->getValInt(
                    'dupcloud_upload_chunk_size_in_kb',
                    self::DEFAULT_UPLOAD_CHUNK_SIZE_IN_KB
                ),
                'downloadChunkSizeInKb' => $dGlobal->getValInt(
                    'dupcloud_download_chunk_size_in_kb',
                    self::DEFAULT_DOWNLOAD_CHUNK_SIZE_IN_KB
                ),
            ]
        );
    }

    /**
     * Check if config data has changed
     *
     * @param array<string,mixed> $newData New data to compare
     *
     * @return bool True if changed, false otherwise
     */
    private function hasConfigChanged(array $newData): bool
    {
        foreach ($newData as $key => $value) {
            if (!isset($this->config[$key]) || $this->config[$key] !== $value) {
                return true;
            }
        }
        return false;
    }
}
