'use strict';

const nodeFs = require('fs');

/* Require Optional Dependencies */
var fs = nodeFs;
try {
    fs = require('fs-extra');
} catch {
    // Native PostgreSQL mode only needs the Node.js fs implementation.
}
try {
    var unzipper = require('unzipper');
} catch {
    // Validated by the legacy RemoteAuth constructor below.
}
try {
    var archiver = require('archiver');
} catch {
    // Validated by the legacy RemoteAuth constructor below.
}

const path = require('path');
const { Events } = require('./../util/Constants');
const { BrowserSessionBridge } = require('./../session/BrowserSessionBridge');
const { SESSION_STORE_KIND } = require('./../session/PostgresSessionStore');
const BaseAuthStrategy = require('./BaseAuthStrategy');

const comparableJid = (jid) => {
    const normalized = String(jid || '')
        .trim()
        .toLowerCase();
    if (!normalized) return '';
    const separator = normalized.lastIndexOf('@');
    const rawUser =
        separator === -1 ? normalized : normalized.slice(0, separator);
    const rawServer =
        separator === -1 ? 's.whatsapp.net' : normalized.slice(separator + 1);
    const server = rawServer === 'c.us' ? 's.whatsapp.net' : rawServer;
    return `${rawUser.split(':')[0]}@${server}`;
};

/**
 * Remote-based authentication
 * @param {object} options - options
 * @param {object} options.store - Remote database store instance
 * @param {string} options.clientId - Client id to distinguish instances if you are using multiple, otherwise keep null if you are using only one instance
 * @param {string} options.dataPath - Change the default path for saving session files, default is: "./.wwebjs_auth/"
 * @param {number} options.backupSyncIntervalMs - Sets the time interval for periodic session backups. Accepts values starting from 60000ms {1 minute}
 * @param {number} options.initialSyncDelayMs - Delay before the first checkpoint for a newly paired session
 * @param {function} options.identityResolver - Trusted resolver for the companion fingerprint; required for verified native PostgreSQL mode
 * @param {boolean} options.requireFingerprintVerification - Fail readiness unless the trusted fingerprint matches. Defaults to true in production.
 * @param {number} options.rmMaxRetries - Sets the maximum number of retries for removing the session directory
 */
class RemoteAuth extends BaseAuthStrategy {
    constructor({
        clientId,
        dataPath,
        store,
        backupSyncIntervalMs,
        initialSyncDelayMs = 60000,
        identityResolver,
        requireFingerprintVerification = process.env.NODE_ENV === 'production',
        rmMaxRetries,
    } = {}) {
        const nativePostgres = store?.kind === SESSION_STORE_KIND;
        if (!nativePostgres && (!fs.emptyDir || !unzipper || !archiver))
            throw new Error(
                'Optional Dependencies [fs-extra, unzipper, archiver] are required to use RemoteAuth. Make sure to run npm install correctly and remove the --no-optional flag',
            );
        super();

        const idRegex = /^[-_\w]+$/i;
        if (clientId && !idRegex.test(clientId)) {
            throw new Error(
                'Invalid clientId. Only alphanumeric characters, underscores and hyphens are allowed.',
            );
        }
        if (!backupSyncIntervalMs || backupSyncIntervalMs < 60000) {
            throw new Error(
                'Invalid backupSyncIntervalMs. Accepts values starting from 60000ms {1 minute}.',
            );
        }
        if (!Number.isFinite(initialSyncDelayMs) || initialSyncDelayMs < 0) {
            throw new Error('Invalid initialSyncDelayMs.');
        }
        if (!store) throw new Error('Remote database store is required.');
        if (
            identityResolver !== undefined &&
            typeof identityResolver !== 'function'
        ) {
            throw new Error('wwebjs_companion_identity_resolver_invalid');
        }
        if (
            nativePostgres &&
            requireFingerprintVerification &&
            typeof identityResolver !== 'function'
        ) {
            throw new Error('wwebjs_companion_identity_resolver_required');
        }

        this.store = store;
        this.nativePostgres = nativePostgres;
        this.clientId = clientId;
        this.backupSyncIntervalMs = backupSyncIntervalMs;
        this.initialSyncDelayMs = initialSyncDelayMs;
        this.identityResolver = identityResolver;
        this.requireFingerprintVerification = requireFingerprintVerification;
        this.dataPath = path.resolve(dataPath || './.wwebjs_auth/');
        try {
            fs.accessSync(process.cwd(), fs.constants.W_OK);
            this.zipDir = process.cwd();
        } catch {
            this.zipDir = this.dataPath;
        }
        this.tempDir = `${this.dataPath}/wwebjs_temp_session_${this.clientId}`;
        this.requiredDirs = [
            'Default',
            'IndexedDB',
            'Local Storage',
        ]; /* => Required Files & Dirs in WWebJS to restore session */
        this.rmMaxRetries = rmMaxRetries ?? 4;
        this.authReady = false;
        this.shutdownRequested = false;
        this.checkpointTail = Promise.resolve();
        this.periodicCheckpoint = undefined;
        this.profileDirty = true;
        this.criticalCheckpointTimer = undefined;
        this.providerHandoffKey = undefined;
        this.preparedProviderHandoff = undefined;
    }

    validateClientOptions(options) {
        if (!this.nativePostgres) return;
        if (options.webVersion !== this.store.webVersion) {
            throw new Error('wwebjs_web_version_incompatible');
        }
        if (
            options.webVersionCache?.type === 'none' ||
            options.webVersionCache?.strict !== true
        ) {
            throw new Error('wwebjs_strict_web_cache_required');
        }
    }

    async beforeBrowserInitialized() {
        this.shutdownRequested = false;
        this.projectionHydrated = false;
        this.shutdownProjection = undefined;
        await this.browserBridge?.close();
        this.browserBridge = undefined;
        const puppeteerOpts = this.client.options.puppeteer;
        const sessionDirName = this.clientId
            ? `RemoteAuth-${this.clientId}`
            : 'RemoteAuth';
        const dirPath = path.join(this.dataPath, sessionDirName);

        if (
            puppeteerOpts.userDataDir &&
            puppeteerOpts.userDataDir !== dirPath
        ) {
            throw new Error(
                'RemoteAuth is not compatible with a user-supplied userDataDir.',
            );
        }

        this.userDataDir = dirPath;
        this.sessionName = sessionDirName;

        if (this.nativePostgres) {
            await this.store.open({
                onLeaseLost: (error) => this.handleLeaseLoss(error),
            });
            try {
                await this.extractNativeSession();
            } catch (error) {
                await this.store.releaseLease().catch(() => {});
                throw error;
            }
        } else {
            await this.extractRemoteSession();
        }

        this.client.options.puppeteer = {
            ...puppeteerOpts,
            userDataDir: dirPath,
        };
    }

    async afterBrowserInitialized() {
        if (!this.nativePostgres) return;
        this.browserBridge = new BrowserSessionBridge({
            page: this.client.pupPage,
            expectedVersion: this.store.webVersion,
            logger: this.store.logger,
            onCriticalMutation: () => this.scheduleCriticalCheckpoint(),
        });
        try {
            await this.browserBridge.install();
        } catch (error) {
            const browser = this.client.pupBrowser;
            if (browser?.isConnected?.()) {
                if (browser.process()) await browser.close();
                else browser.disconnect();
            }
            await this.store.releaseLease().catch(() => {});
            throw error;
        }
    }

    async beforeClientInjected() {
        if (!this.nativePostgres || this.projectionHydrated) return;
        try {
            await this.client.pupPage.waitForFunction(
                'window.Debug?.VERSION != undefined',
                { timeout: this.client.options.authTimeoutMs || 30000 },
            );
            await this.browserBridge.assertCompatible();
            const projection = await this.store.consumePendingProjection();
            if (!projection) return;

            await this.browserBridge.importProjection(projection);
            this.projectionHydrated = true;
            this.store.logger.log('handoff.browser_reload_started');
            await this.client.pupPage.reload({
                waitUntil: 'load',
                timeout: 0,
            });
            await this.client.pupPage.waitForFunction(
                'window.Debug?.VERSION != undefined',
                { timeout: this.client.options.authTimeoutMs || 30000 },
            );
            await this.browserBridge.assertCompatible();
            this.store.logger.log('handoff.browser_reload_completed');
        } catch (error) {
            if (this.store.isHandoffRevision()) {
                await this.store
                    .rollback(error.code || 'handoff_browser_hydration_failed')
                    .catch(() => {});
            }
            await this.browserBridge.close();
            const browser = this.client.pupBrowser;
            if (browser?.isConnected?.()) {
                if (browser.process()) await browser.close();
                else browser.disconnect();
            }
            await this.store.releaseLease();
            throw error;
        }
    }

    async onInitializationFailure(error) {
        if (!this.nativePostgres) return;
        this.stopBackupSync();
        await this.browserBridge?.close().catch(() => {});
        if (this.store.isHandoffRevision()) {
            await this.store
                .rollback(error.code || 'handoff_client_initialization_failed')
                .catch(() => {});
        }
        await this.store.releaseLease().catch(() => {});
        this.store.logger.log(
            'client.initialization_failed',
            { error },
            { force: true },
        );
    }

    async logout() {
        this.shutdownRequested = true;
        this.stopBackupSync();
        await this.checkpointTail.catch(() => {});
        this.authReady = false;
        if (this.nativePostgres) {
            await this.browserBridge?.close();
            await this.store.delete();
            // A server-side/mobile logout navigates the still-open page back
            // to pairing and Client immediately initializes this strategy
            // again. Keep the pool reusable in that path; an explicit
            // Client.logout() has already closed Chromium and can close it.
            if (this.client.pupBrowser?.isConnected?.()) {
                await this.store.releaseLease();
            } else {
                await this.store.close();
            }
        } else {
            await this.deleteRemoteSession();
        }
        await this.removeLocalSession();
    }

    async destroy() {
        await this.shutdown();
    }

    async disconnect() {
        this.shutdownRequested = true;
        this.stopBackupSync();
        // Client calls destroy() immediately after this hook. The final
        // checkpoint belongs to shutdown(); saving here as well would rotate
        // the active/previous revisions twice with the same Chromium profile.
        await this.checkpointTail;
    }

    async beforeBrowserDestroyed() {
        if (!this.nativePostgres || !this.authReady) return;
        this.stopBackupSync();
        await this.checkpointTail;
        if (!this.browserBridge) return;
        await this.browserBridge.flush();
        this.shutdownProjection = await this.browserBridge.exportProjection();
        this.lastProjection = this.shutdownProjection;
        await this.browserBridge.close();
        this.store.logger.log('checkpoint.shutdown_projection_captured', {
            record_count: this.shutdownProjection.records.length,
            size_bytes: this.shutdownProjection.size_bytes,
        });
    }

    /**
     * Persists a final checkpoint without deleting the remote session, then
     * removes the ephemeral Chromium profile.
     */
    async shutdown() {
        this.shutdownRequested = true;
        this.stopBackupSync();
        await this.checkpointTail;
        if (this.authReady) {
            if (this.nativePostgres) {
                if (
                    !this.shutdownProjection &&
                    this.client.pupBrowser?.isConnected?.()
                ) {
                    await this.beforeBrowserDestroyed();
                }
                await this.store.checkpointProfile({
                    profilePath: this.userDataDir,
                    projection: this.shutdownProjection || this.lastProjection,
                    source:
                        this.store.revisionStatus === 'staging'
                            ? this.store.revisionSource || 'pairing'
                            : 'checkpoint',
                });
            } else {
                await this.checkpoint({ force: true, reason: 'shutdown' });
            }
        }
        this.authReady = false;
        await this.browserBridge?.close();
        if (this.nativePostgres) await this.store.close();
        await this.removeLocalSession();
    }

    async onAuthenticationNeeded() {
        if (this.nativePostgres && this.store.isHandoffRevision()) {
            await this.store.rollback('handoff_requested_qr');
            return {
                failed: true,
                restart: false,
                failureEventPayload: 'wwebjs_handoff_requested_qr',
            };
        }
        return {
            failed: false,
            restart: false,
            failureEventPayload: undefined,
        };
    }

    async afterAuthReady() {
        const sessionExists = this.nativePostgres
            ? this.store.revisionStatus === 'active' ||
              this.store.isHandoffRevision()
            : await this.store.sessionExists({
                  session: this.sessionName,
              });
        this.authReady = false;
        if (!sessionExists && this.initialSyncDelayMs > 0) {
            await this.delay(this.initialSyncDelayMs);
        }
        this.assertNotShuttingDown();

        // READY must only be published after at least one durable checkpoint.
        if (this.nativePostgres) {
            try {
                const resolvedIdentity = this.identityResolver
                    ? await this.identityResolver(this.client)
                    : {};
                const trustedFingerprint =
                    resolvedIdentity?.companionFingerprint ||
                    resolvedIdentity?.deviceFingerprint;
                if (
                    this.requireFingerprintVerification &&
                    !trustedFingerprint
                ) {
                    throw new Error(
                        'wwebjs_companion_fingerprint_not_verified',
                    );
                }
                const browserJid =
                    this.client.info?.wid?._serialized ||
                    this.client.info?.wid?.user;
                if (
                    resolvedIdentity?.jid &&
                    comparableJid(resolvedIdentity.jid) !==
                        comparableJid(browserJid)
                ) {
                    throw new Error('wwebjs_identity_resolver_jid_mismatch');
                }
                const validation = await this.store.validateReadyIdentity({
                    jid: browserJid,
                    companionFingerprint: trustedFingerprint,
                });
                if (
                    this.requireFingerprintVerification &&
                    (!trustedFingerprint ||
                        (!validation.fingerprintVerified &&
                            !validation.fingerprintRegistered))
                ) {
                    throw new Error(
                        'wwebjs_companion_fingerprint_not_verified',
                    );
                }
            } catch (error) {
                this.authReady = false;
                if (this.store.isHandoffRevision()) {
                    await this.store.rollback(
                        error.code || 'handoff_identity_validation_failed',
                    );
                }
                this.store.logger.log(
                    'session.identity_validation_failed',
                    { error },
                    { force: true },
                );
                await this.store.releaseLease();
                const browser = this.client.pupBrowser;
                if (browser?.isConnected?.()) {
                    if (browser.process()) await browser.close();
                    else browser.disconnect();
                }
                throw error;
            }
        }
        let persisted;
        try {
            persisted = await this.checkpoint({
                emit: true,
                force: true,
                reason: 'ready',
            });
        } catch (error) {
            this.authReady = false;
            if (this.nativePostgres) {
                if (this.store.isHandoffRevision()) {
                    await this.store
                        .rollback(error.code || 'handoff_checkpoint_failed')
                        .catch(() => {});
                }
                await this.store.releaseLease().catch(() => {});
                const browser = this.client.pupBrowser;
                if (browser?.isConnected?.()) {
                    if (browser.process()) await browser.close();
                    else browser.disconnect();
                }
            }
            throw error;
        }
        if (!persisted) {
            throw new Error(
                'Unable to persist the initial RemoteAuth checkpoint.',
            );
        }
        this.assertNotShuttingDown();
        this.authReady = true;
        if (
            this.nativePostgres &&
            (this.profileDirty || this.browserBridge?.dirty)
        ) {
            this.scheduleCriticalCheckpoint();
        }
        this.backupSync = setInterval(() => {
            void this.checkpoint({
                periodic: true,
                reason: 'periodic',
            }).catch(() => {});
        }, this.backupSyncIntervalMs);
    }

    /**
     * Queues a serialized durable checkpoint. Periodic calls are coalesced so
     * a slow store can never build an unbounded interval backlog.
     */
    checkpoint({
        emit = false,
        force = false,
        periodic = false,
        reason = 'manual',
    } = {}) {
        if (!force && !this.authReady) return Promise.resolve(false);
        if (
            this.nativePostgres &&
            periodic &&
            !this.profileDirty &&
            !this.browserBridge?.dirty
        ) {
            return Promise.resolve(false);
        }
        if (periodic && this.periodicCheckpoint) {
            return this.periodicCheckpoint;
        }

        const queued = this.checkpointTail.then(() =>
            this.performCheckpoint({ emit, force, reason }),
        );
        this.checkpointTail = queued.catch(() => {});

        if (periodic) {
            this.periodicCheckpoint = queued;
            void queued
                .finally(() => {
                    if (this.periodicCheckpoint === queued) {
                        this.periodicCheckpoint = undefined;
                    }
                })
                .catch(() => {});
        }

        return queued;
    }

    async storeRemoteSession(options = {}) {
        return this.checkpoint({ emit: options.emit, force: true });
    }

    async performCheckpoint({ emit, force, reason }) {
        const pathExists = await this.isValidPath(this.userDataDir);
        if (!pathExists) return false;

        if (this.nativePostgres) {
            if (!this.browserBridge) {
                throw new Error('wwebjs_browser_bridge_unavailable');
            }
            this.store.logger.log('checkpoint.started', { reason });
            const journal = await this.browserBridge.flush();
            if (!force && !this.profileDirty && !journal?.dirty) return false;
            const projection = await this.browserBridge.exportProjection();
            this.lastProjection = projection;
            const source =
                this.store.revisionStatus === 'staging'
                    ? this.store.revisionSource || 'pairing'
                    : 'checkpoint';
            await this.store.checkpointProfile({
                profilePath: this.userDataDir,
                projection,
                source,
            });
            const postCheckpointJournal = await this.browserBridge.flush();
            if (postCheckpointJournal?.dirty) {
                this.profileDirty = true;
                if (postCheckpointJournal.critical) {
                    this.scheduleCriticalCheckpoint();
                }
            } else {
                await this.browserBridge.markClean();
                this.profileDirty = false;
            }
            if (emit) this.client.emit(Events.REMOTE_SESSION_SAVED);
            return true;
        }

        let compressedSessionPath;
        try {
            compressedSessionPath = await this.compressSession();
            await this.store.save({
                session: this.sessionName,
                path: compressedSessionPath,
            });
            if (emit) this.client.emit(Events.REMOTE_SESSION_SAVED);
            return true;
        } finally {
            const paths = [
                this.tempDir,
                ...(compressedSessionPath ? [compressedSessionPath] : []),
            ];
            await Promise.allSettled(
                paths.map((p) =>
                    fs.promises.rm(p, {
                        recursive: true,
                        force: true,
                        maxRetries: this.rmMaxRetries,
                    }),
                ),
            );
        }
    }

    async extractNativeSession() {
        await this.removeLocalSession();
        try {
            const restored = await this.store.restoreProfile({
                profilePath: this.userDataDir,
                selector: 'active',
            });
            if (!restored) {
                if (this.store.revisionStatus === 'active') {
                    throw new Error('wwebjs_active_profile_artifact_missing');
                }
                await fs.promises.mkdir(this.userDataDir, {
                    recursive: true,
                });
            }
        } catch (activeRevisionError) {
            await this.removeLocalSession();
            try {
                await this.store.restoreProfile({
                    profilePath: this.userDataDir,
                    selector: 'previous',
                });
                this.store.logger.log(
                    'artifact.active_restore_failed_previous_restored',
                    { error: activeRevisionError },
                    { force: true },
                );
            } catch (previousRevisionError) {
                throw new AggregateError(
                    [activeRevisionError, previousRevisionError],
                    'Unable to restore active or previous PostgreSQL session revision.',
                );
            }
        }
    }

    scheduleCriticalCheckpoint() {
        this.profileDirty = true;
        if (!this.authReady || this.shutdownRequested) return;
        if (this.criticalCheckpointTimer) return;
        this.criticalCheckpointTimer = setTimeout(() => {
            this.criticalCheckpointTimer = undefined;
            void this.checkpoint({
                force: true,
                reason: 'critical_mutation',
            }).catch((error) => {
                this.store.logger.log(
                    'checkpoint.critical_failed',
                    { error },
                    { force: true },
                );
            });
        }, 100);
    }

    async handleLeaseLoss(error) {
        if (this.shutdownRequested) return;
        this.shutdownRequested = true;
        this.authReady = false;
        this.stopBackupSync();
        await this.browserBridge?.close();
        this.client.emit(Events.DISCONNECTED, error.code);
        const browser = this.client.pupBrowser;
        if (browser?.isConnected?.()) {
            if (browser.process()) await browser.close();
            else browser.disconnect();
        }
    }

    async exportProjection() {
        if (!this.nativePostgres) {
            throw new Error('wwebjs_native_postgres_store_required');
        }
        await this.checkpoint({ force: true, reason: 'export' });
        return this.store.exportProjection();
    }

    async importProjection(options) {
        if (!this.nativePostgres) {
            throw new Error('wwebjs_native_postgres_store_required');
        }
        return this.store.importProjection(options);
    }

    async prepareHandoff(targetProvider, expected = {}) {
        if (!this.nativePostgres) {
            throw new Error('wwebjs_native_postgres_store_required');
        }
        const handoffKey = [
            targetProvider,
            expected.handoffId,
            expected.lifecycleOperationId,
            expected.sourceRevisionId,
        ].join(':');
        if (this.providerHandoffKey && this.providerHandoffKey !== handoffKey) {
            throw new Error('wwebjs_provider_handoff_replay_mismatch');
        }
        this.providerHandoffKey = handoffKey;
        if (this.preparedProviderHandoff) {
            const leaseReleased = await this.store.close({
                requireLeaseRelease: true,
            });
            return { ...this.preparedProviderHandoff, leaseReleased };
        }
        try {
            await this.store.assertAuthorizedHandoff(targetProvider, expected);
        } catch (error) {
            // Authorization happens before any drain/checkpoint side effect.
            // Do not let a rejected request poison a later legitimate target.
            if (this.providerHandoffKey === handoffKey) {
                this.providerHandoffKey = undefined;
            }
            throw error;
        }
        this.shutdownRequested = true;
        this.stopBackupSync();
        this.authReady = false;
        const projection = await this.browserBridge?.exportProjection();
        await this.browserBridge?.close();
        const browser = this.client.pupBrowser;
        if (browser?.isConnected?.()) {
            if (browser.process()) await browser.close();
            else browser.disconnect();
        }
        try {
            if (this.store.revisionStatus === 'active') {
                await this.store.checkpointProfile({
                    profilePath: this.userDataDir,
                    projection,
                    source: 'checkpoint',
                });
            }
            const handoff = await this.store.prepareHandoff(
                targetProvider,
                expected,
            );
            this.preparedProviderHandoff = handoff;
            const leaseReleased = await this.store.close({
                requireLeaseRelease: true,
            });
            await this.removeLocalSession();
            return { ...handoff, leaseReleased };
        } catch (error) {
            if (!this.preparedProviderHandoff) {
                await this.store.close().catch(() => {});
            }
            throw error;
        }
    }

    async promote(revisionId) {
        if (!this.nativePostgres) {
            throw new Error('wwebjs_native_postgres_store_required');
        }
        return this.store.promote(revisionId);
    }

    async rollback(errorCode) {
        if (!this.nativePostgres) {
            throw new Error('wwebjs_native_postgres_store_required');
        }
        return this.store.rollback(errorCode);
    }

    async extractRemoteSession() {
        const compressedSessionPath = path.join(
            this.zipDir,
            `${this.sessionName}.zip`,
        );
        const sessionExists = await this.store.sessionExists({
            session: this.sessionName,
        });

        // Remote storage is the source of truth. Local profiles are ephemeral
        // and must never silently override the selected remote revision.
        await this.removeLocalSession();

        if (sessionExists) {
            try {
                await this.extractRevision(
                    this.store.extract.bind(this.store),
                    compressedSessionPath,
                );
            } catch (activeRevisionError) {
                await this.removeLocalSession();
                await fs.promises.rm(compressedSessionPath, { force: true });
                if (typeof this.store.extractPrevious !== 'function') {
                    throw activeRevisionError;
                }

                try {
                    await this.extractRevision(
                        this.store.extractPrevious.bind(this.store),
                        compressedSessionPath,
                    );
                } catch (previousRevisionError) {
                    throw new AggregateError(
                        [activeRevisionError, previousRevisionError],
                        'Unable to restore active or previous remote session revision.',
                    );
                }
            }
        } else {
            await fs.promises.mkdir(this.userDataDir, { recursive: true });
        }
    }

    async extractRevision(extract, compressedSessionPath) {
        await extract({
            session: this.sessionName,
            path: compressedSessionPath,
        });
        await this.unCompressSession(compressedSessionPath);
    }

    async deleteRemoteSession() {
        const sessionExists = await this.store.sessionExists({
            session: this.sessionName,
        });
        if (sessionExists)
            await this.store.delete({ session: this.sessionName });
    }

    stopBackupSync() {
        clearInterval(this.backupSync);
        this.backupSync = undefined;
        clearTimeout(this.criticalCheckpointTimer);
        this.criticalCheckpointTimer = undefined;
    }

    assertNotShuttingDown() {
        if (this.shutdownRequested) {
            throw new Error(
                'RemoteAuth shutdown started before authentication readiness completed.',
            );
        }
    }

    async removeLocalSession() {
        if (!this.userDataDir) return;
        await fs.promises.rm(this.userDataDir, {
            recursive: true,
            force: true,
            maxRetries: this.rmMaxRetries,
        });
    }

    async compressSession() {
        const stageDefaultPath = path.join(this.tempDir, 'Default');
        const userDataDefaultPath = path.join(this.userDataDir, 'Default');

        await fs.emptyDir(stageDefaultPath);
        await this.copyByRequiredDirs(userDataDefaultPath, stageDefaultPath);

        const archive = archiver('zip');
        const outPath = path.join(this.zipDir, `${this.sessionName}.zip`);
        const out = fs.createWriteStream(outPath);

        await new Promise((resolve, reject) => {
            out.once('close', resolve);
            out.once('error', reject);
            archive.once('error', reject);

            archive.pipe(out);
            archive.directory(this.tempDir, false);
            archive.finalize();
        });
        return outPath;
    }

    async unCompressSession(compressedSessionPath) {
        var stream = fs.createReadStream(compressedSessionPath);
        await new Promise((resolve, reject) => {
            stream
                .pipe(
                    unzipper.Extract({
                        path: this.userDataDir,
                        concurrency: 10,
                    }),
                )
                .on('error', (err) => reject(err))
                .on('finish', () => resolve());
        });
        await fs.promises.unlink(compressedSessionPath);
    }

    async copyByRequiredDirs(from, to) {
        for (const d of this.requiredDirs) {
            const src = path.join(from, d);
            if (await this.isValidPath(src)) {
                const dest = path.join(to, path.basename(src));
                await fs.promises.cp(src, dest, {
                    recursive: true,
                    force: true,
                    errorOnExist: false,
                });
            }
        }
    }

    async isValidPath(path) {
        try {
            await fs.promises.access(path);
            return true;
        } catch {
            return false;
        }
    }

    async delay(ms) {
        return new Promise((resolve) => setTimeout(resolve, ms));
    }
}

module.exports = RemoteAuth;