'use strict';

var core = require('@nativescript/core');
var zip = require('@nativescript/zip');

function _extends() {
    _extends = Object.assign || function assign(target) {
        for(var i = 1; i < arguments.length; i++){
            var source = arguments[i];
            for(var key in source)if (Object.prototype.hasOwnProperty.call(source, key)) target[key] = source[key];
        }
        return target;
    };
    return _extends.apply(this, arguments);
}

let bundleId;
function getBundleId() {
    if (!bundleId) {
        if (__APPLE__) {
            bundleId = NSBundle.mainBundle.bundleIdentifier;
        } else {
            bundleId = core.Utils.android.getPackageName();
        }
    }
    return bundleId;
}
function getAppVersionInfo() {
    let versionName;
    let buildNumber;
    if (global.isAndroid) {
        const pi = core.Utils.android.getApplicationContext().getPackageManager().getPackageInfo(core.Utils.android.getApplicationContext().getPackageName(), 0);
        try {
            versionName = process.env.APP_VERSION_STRING || pi.versionName;
        } catch (err) {
            versionName = pi.versionName;
        }
        try {
            buildNumber = process.env.APP_BUILD_NUMBER || pi.versionCode.toString();
        } catch (err) {
            buildNumber = pi.versionCode.toString();
        }
    } else {
        try {
            versionName = process.env.APP_VERSION_STRING || NSBundle.mainBundle.objectForInfoDictionaryKey('CFBundleShortVersionString');
        } catch (err) {
            versionName = NSBundle.mainBundle.objectForInfoDictionaryKey('CFBundleShortVersionString');
        }
        try {
            buildNumber = process.env.APP_BUILD_NUMBER || NSBundle.mainBundle.objectForInfoDictionaryKey('CFBundleVersion');
        } catch (err) {
            buildNumber = NSBundle.mainBundle.objectForInfoDictionaryKey('CFBundleVersion');
        }
    }
    return {
        versionName,
        buildNumber
    };
}
/**
 * Get the Norrix fingerprint hash embedded in the native binary.
 *
 * This reads from:
 * - iOS: Info.plist -> NorrixFingerprintHash
 * - Android: BuildConfig.norrixFingerprintHash
 *
 * This is the single source of truth for fingerprint identity, baked into
 * the binary at build time. It cannot be modified by OTA updates.
 *
 * @returns The fingerprint hash string, or undefined if not embedded
 */ function getNativeFingerprintHash() {
    try {
        if (__APPLE__) {
            // Read from Info.plist
            const hash = NSBundle.mainBundle.objectForInfoDictionaryKey('NorrixFingerprintHash');
            if (hash && typeof hash === 'string' && hash.length > 0) {
                return hash;
            }
            // Handle NSString type
            if (hash && hash.toString && hash.toString().length > 0) {
                return hash.toString();
            }
        } else if (global.isAndroid) {
            // Read from BuildConfig
            // BuildConfig is generated at compile time and contains our custom field
            try {
                const context = core.Utils.android.getApplicationContext();
                const packageName = context.getPackageName();
                // Use reflection to access BuildConfig.norrixFingerprintHash
                const buildConfigClass = java.lang.Class.forName(`${packageName}.BuildConfig`);
                const field = buildConfigClass.getDeclaredField('norrixFingerprintHash');
                field.setAccessible(true);
                const hash = field.get(null);
                if (hash && hash.toString().length > 0) {
                    return hash.toString();
                }
            } catch (androidErr) {
                // BuildConfig field may not exist in older builds
                console.log('NorrixUpdates: Could not read fingerprint from BuildConfig:', androidErr);
            }
        }
    } catch (e) {
        console.error('NorrixUpdates: Failed to read native fingerprint hash:', e);
    }
    return undefined;
}
/**
 * Get the Norrix configuration embedded in the native binary.
 *
 * This reads from:
 * - iOS: Info.plist -> NorrixNxConfiguration
 * - Android: BuildConfig.norrixNxConfiguration
 *
 * Note: The native resource keys remain `NorrixNxConfiguration` (iOS) and `norrixNxConfiguration` (Android)
 * for backward compatibility with existing deployed apps.
 *
 * This identifies which configuration/deployment target (e.g., 'stg', 'prod', 'dev')
 * the binary was built for. It's used to ensure OTA updates are delivered
 * only to apps built with the same configuration.
 *
 * @returns The configuration string (e.g., 'stg', 'prod'), or undefined if not embedded
 */ function getNativeNxConfiguration() {
    try {
        if (__APPLE__) {
            // Read from Info.plist
            const config = NSBundle.mainBundle.objectForInfoDictionaryKey('NorrixNxConfiguration');
            if (config && typeof config === 'string' && config.length > 0) {
                return config;
            }
            // Handle NSString type
            if (config && config.toString && config.toString().length > 0) {
                return config.toString();
            }
        } else if (global.isAndroid) {
            // Read from BuildConfig
            try {
                const context = core.Utils.android.getApplicationContext();
                const packageName = context.getPackageName();
                // Use reflection to access BuildConfig.norrixNxConfiguration
                const buildConfigClass = java.lang.Class.forName(`${packageName}.BuildConfig`);
                const field = buildConfigClass.getDeclaredField('norrixNxConfiguration');
                field.setAccessible(true);
                const config = field.get(null);
                if (config && config.toString().length > 0) {
                    return config.toString();
                }
            } catch (androidErr) {
                // BuildConfig field may not exist in older builds
                console.log('NorrixUpdates: Could not read configuration from BuildConfig:', androidErr);
            }
        }
    } catch (e) {
        console.error('NorrixUpdates: Failed to read native configuration:', e);
    }
    return undefined;
}

/**
 * SDK version for telemetry tracking.
 * This value is automatically injected from package.json at build time.
 * The placeholder "2.0.0" is replaced by rollup.
 */ const SDK_VERSION = "2.0.0";
// In-memory event queue for batching
let eventQueue = [];
let flushTimer = null;
const FLUSH_INTERVAL_MS = 5000; // Flush every 5 seconds
const MAX_QUEUE_SIZE = 50; // Flush if queue reaches this size
// Telemetry configuration
let telemetryUrl = null;
let telemetryEnabled = true;
/**
 * Configure telemetry reporting.
 *
 * @param config.updateUrl The base URL for the Norrix update server
 * @param config.enabled Whether telemetry is enabled (default: true)
 */ function configureTelemetry(config) {
    telemetryUrl = config.updateUrl ? `${config.updateUrl.replace(/\/$/, '')}/api/update/telemetry` : null;
    telemetryEnabled = config.enabled !== false;
}
/**
 * Get a unique device identifier for telemetry.
 * Uses a persisted UUID to ensure consistent tracking across sessions.
 */ function getDeviceId() {
    const key = 'Norrix_Device_Id';
    let deviceId = core.ApplicationSettings.getString(key);
    if (!deviceId) {
        // Generate a UUID v4
        deviceId = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c)=>{
            const r = Math.random() * 16 | 0;
            const v = c === 'x' ? r : r & 0x3 | 0x8;
            return v.toString(16);
        });
        core.ApplicationSettings.setString(key, deviceId);
    }
    return deviceId;
}
/**
 * Get the current platform string.
 */ function getPlatform() {
    const isApple = typeof __APPLE__ !== 'undefined' && !!__APPLE__;
    const isVision = typeof __VISIONOS__ !== 'undefined' && !!__VISIONOS__;
    return isApple ? isVision ? 'visionos' : 'ios' : 'android';
}
/**
 * Get device information for telemetry.
 */ function getDeviceInfo() {
    const appVersionInfo = getAppVersionInfo();
    return {
        deviceId: getDeviceId(),
        deviceModel: core.Device.model,
        osVersion: core.Device.osVersion,
        platform: getPlatform(),
        bundleId: getBundleId(),
        sdkVersion: SDK_VERSION,
        appVersion: appVersionInfo.versionName || '1.0.0',
        buildNumber: appVersionInfo.buildNumber || '1'
    };
}
/**
 * Track an OTA telemetry event.
 *
 * Events are queued and sent in batches to reduce network overhead.
 * If the queue reaches MAX_QUEUE_SIZE or FLUSH_INTERVAL_MS passes,
 * the queue is flushed automatically.
 *
 * @param event The telemetry event type
 * @param properties Additional event properties (merged with device info)
 */ function trackOtaEvent(event, properties) {
    if (!telemetryEnabled) {
        return;
    }
    const baseProps = getDeviceInfo();
    const fullProperties = _extends({}, baseProps, properties);
    const queuedEvent = {
        event,
        properties: fullProperties,
        timestamp: new Date().toISOString()
    };
    eventQueue.push(queuedEvent);
    // Flush immediately if queue is large
    if (eventQueue.length >= MAX_QUEUE_SIZE) {
        flushTelemetry();
        return;
    }
    // Schedule a flush if not already scheduled
    if (!flushTimer) {
        flushTimer = setTimeout(()=>{
            flushTimer = null;
            flushTelemetry();
        }, FLUSH_INTERVAL_MS);
    }
}
/**
 * Flush queued telemetry events to the server.
 *
 * This is called automatically, but can be invoked manually
 * before app suspension or exit.
 */ async function flushTelemetry() {
    if (!telemetryUrl || eventQueue.length === 0) {
        return;
    }
    // Clear any pending timer
    if (flushTimer) {
        clearTimeout(flushTimer);
        flushTimer = null;
    }
    // Grab the current queue and reset
    const eventsToSend = [
        ...eventQueue
    ];
    eventQueue = [];
    try {
        if (eventsToSend.length === 1) {
            // Single event - use POST
            await core.Http.request({
                url: telemetryUrl,
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                content: JSON.stringify(eventsToSend[0])
            });
        } else {
            // Multiple events - use PUT for batch
            await core.Http.request({
                url: telemetryUrl,
                method: 'PUT',
                headers: {
                    'Content-Type': 'application/json'
                },
                content: JSON.stringify({
                    events: eventsToSend
                })
            });
        }
    } catch (error) {
        // Telemetry should never break the app - log and swallow
        console.warn('NorrixTelemetry: Failed to send telemetry:', error);
        // Re-queue failed events (with limit to prevent memory growth)
        if (eventQueue.length < MAX_QUEUE_SIZE * 2) {
            eventQueue.unshift(...eventsToSend);
        }
    }
}
/**
 * Track app launch with OTA context.
 *
 * This should be called when the app launches to report whether
 * it's running from an OTA update or the original binary.
 *
 * @param otaFingerprint The current OTA fingerprint (if running from OTA)
 * @param configuration The current configuration/environment
 */ function trackAppLaunch(otaFingerprint, configuration) {
    const otaVersion = core.ApplicationSettings.getString('Norrix_OTA_Version');
    const otaBuildNumber = core.ApplicationSettings.getString('Norrix_OTA_BuildNumber');
    const isRunningOta = !!otaFingerprint || !!otaVersion;
    trackOtaEvent('ota_app_launch', {
        fingerprintHash: otaFingerprint,
        configuration,
        otaVersion: isRunningOta ? otaVersion || undefined : undefined,
        otaBuildNumber: isRunningOta ? otaBuildNumber || undefined : undefined,
        // Custom property to indicate OTA vs binary launch
        running_from_ota: isRunningOta
    });
}

exports.SyncStatus = void 0;
(function(SyncStatus) {
    /**
     * The application is up to date.
     */ SyncStatus["UP_TO_DATE"] = "UP_TO_DATE";
    /**
     * An update is available, it has been downloaded, unzipped and copied to the deployment folder.
     * After the completion of the callback invoked with SyncStatus.UPDATE_INSTALLED, the application will be reloaded with the updated code and resources.
     */ SyncStatus["UPDATE_INSTALLED"] = "UPDATE_INSTALLED";
    /**
     * An optional update is available, but the user declined to install it. The update was not downloaded.
     */ SyncStatus["UPDATE_IGNORED"] = "UPDATE_IGNORED";
    /**
     * An error happened during the sync operation. This might be an error while communicating with the server, downloading or unziping the update.
     * The console logs should contain more information about what happened. No update has been applied in this case.
     */ SyncStatus["ERROR"] = "ERROR";
    /**
     * Returned if HMR is enabled and not overridden by the user.
     */ SyncStatus["SKIPPING_BECAUSE_HMR_ENABLED"] = "SKIPPING_BECAUSE_HMR_ENABLED";
    /**
     * There is an ongoing sync in progress, so this attempt to sync has been aborted.
     */ SyncStatus["IN_PROGRESS"] = "IN_PROGRESS";
    /**
     * Intermediate status - the plugin is about to check for updates.
     */ SyncStatus["CHECKING_FOR_UPDATE"] = "CHECKING_FOR_UPDATE";
    /**
     * Intermediate status - a user dialog is about to be displayed. This status will be reported only if user interaction is enabled.
     */ SyncStatus["AWAITING_USER_ACTION"] = "AWAITING_USER_ACTION";
    /**
     * Intermediate status - the update package is about to be downloaded.
     */ SyncStatus["DOWNLOADING_PACKAGE"] = "DOWNLOADING_PACKAGE";
    /**
     * Intermediate status - the update package is about to be installed.
     */ SyncStatus["INSTALLING_UPDATE"] = "INSTALLING_UPDATE";
    /**
     * Intermediate status - the native runtime is being recreated to load the installed update.
     */ SyncStatus["RELOADING_APP"] = "RELOADING_APP";
    /**
     * The installed update could not be loaded through the native soft reboot API.
     */ SyncStatus["RELOAD_FAILED"] = "RELOAD_FAILED";
})(exports.SyncStatus || (exports.SyncStatus = {}));
class NorrixUpdates {
    /**
     * Parse the compatibility reason from the server to extract mismatch details.
     * The server returns reasons like:
     * - "Plugin version mismatch: @nativescript/core (9.0.3 -> 9.0.5)"
     * - "App_Resources hash differs"
     * - "NativeSource hash differs"
     *
     * @param compatibilityReason The raw reason string from the server
     * @returns Parsed mismatch details
     */ _parseMismatchDetails(compatibilityReason) {
        const details = {
            changedPlugins: [],
            appResourcesChanged: false,
            nativeSourcesChanged: false,
            rawReason: compatibilityReason || ''
        };
        if (!compatibilityReason) {
            return details;
        }
        // Check for App_Resources changes
        if (compatibilityReason.toLowerCase().includes('app_resources') || compatibilityReason.toLowerCase().includes('appresources')) {
            details.appResourcesChanged = true;
        }
        // Check for NativeSource changes
        if (compatibilityReason.toLowerCase().includes('nativesource')) {
            details.nativeSourcesChanged = true;
        }
        // Parse plugin version mismatches
        // Format: "Plugin version mismatch: @scope/name (1.0.0 -> 2.0.0)"
        // or "Plugins differ: @scope/name, @scope/other"
        const pluginMismatchRegex = /(?:plugin[s]?\s+(?:version\s+)?(?:mismatch|differ)[s]?:?\s*)([^(]+?)(?:\s*\(([^)]+)\))?(?:,|$)/gi;
        let match;
        while((match = pluginMismatchRegex.exec(compatibilityReason)) !== null){
            const pluginNames = match[1].split(',').map((s)=>s.trim()).filter(Boolean);
            const versionInfo = match[2]; // e.g., "1.0.0 -> 2.0.0"
            for (const name of pluginNames){
                const plugin = {
                    name
                };
                if (versionInfo) {
                    const versionMatch = versionInfo.match(/(\S+)\s*->\s*(\S+)/);
                    if (versionMatch) {
                        plugin.binaryVersion = versionMatch[1];
                        plugin.updateVersion = versionMatch[2];
                    }
                }
                details.changedPlugins.push(plugin);
            }
        }
        // Also try to parse individual plugin mentions
        // Format: "@scope/name: 1.0.0 vs 2.0.0" or just "@scope/name differs"
        const individualPluginRegex = /(@[\w\-./]+|[\w-]+\/[\w-]+)(?::\s*(\S+)\s*(?:vs|->)\s*(\S+)|\s+differs)/gi;
        while((match = individualPluginRegex.exec(compatibilityReason)) !== null){
            const name = match[1];
            // Avoid duplicates
            if (!details.changedPlugins.some((p)=>p.name === name)) {
                details.changedPlugins.push({
                    name,
                    binaryVersion: match[2],
                    updateVersion: match[3]
                });
            }
        }
        return details;
    }
    /**
     * Determine if an update with a fingerprint mismatch should be allowed
     * based on the granular override configuration.
     *
     * @param update The update info from the server
     * @returns true if the update should be allowed, false otherwise
     */ async _shouldAllowMismatchedUpdate(update) {
        if (!update.requiresStoreUpdate) {
            // No mismatch, always allow
            return true;
        }
        // Check the blanket override first
        if (this.config.allowStoreUpdateOverride) {
            return true;
        }
        // Check if any granular overrides are configured
        const hasGranularConfig = this.config.allowedDependencyOverrides && this.config.allowedDependencyOverrides.length > 0 || this.config.allowAppResourcesOverride || this.config.allowNativeSourcesOverride || this.config.onFingerprintMismatch;
        if (!hasGranularConfig) {
            // No overrides configured, don't allow
            return false;
        }
        // Parse the mismatch details
        const details = this._parseMismatchDetails(update.compatibilityReason);
        // Check App_Resources changes
        if (details.appResourcesChanged && !this.config.allowAppResourcesOverride) {
            console.warn('NorrixUpdates: App_Resources differ and allowAppResourcesOverride is not set');
            return false;
        }
        // Check NativeSource changes
        if (details.nativeSourcesChanged && !this.config.allowNativeSourcesOverride) {
            console.warn('NorrixUpdates: NativeSource differ and allowNativeSourcesOverride is not set');
            return false;
        }
        // Check individual plugin changes
        if (details.changedPlugins.length > 0) {
            const allowedPlugins = this.config.allowedDependencyOverrides || [];
            const disallowedChanges = details.changedPlugins.filter((plugin)=>!allowedPlugins.includes(plugin.name));
            if (disallowedChanges.length > 0) {
                console.warn('NorrixUpdates: Plugin changes not in allowedDependencyOverrides:', disallowedChanges.map((p)=>p.name).join(', '));
                return false;
            }
        }
        // If we have a callback, let it make the final decision
        if (this.config.onFingerprintMismatch) {
            try {
                const result = await this.config.onFingerprintMismatch(details);
                return result;
            } catch (e) {
                console.error('NorrixUpdates: onFingerprintMismatch callback threw error:', e);
                return false;
            }
        }
        // All checks passed
        return true;
    }
    /**
     * Utility to get the current native binary fingerprint hash.
     *
     * The fingerprint is the immutable identity of the native binary, baked in
     * at build time. It reads from:
     * - iOS: Info.plist -> NorrixFingerprintHash
     * - Android: BuildConfig.norrixFingerprintHash
     *
     * This is the single source of truth for fingerprint validation. OTA updates
     * cannot modify this value since it's embedded in the native binary.
     *
     * For backward compatibility, if the native fingerprint is not found, we
     * fall back to reading from the assets file (for older builds).
     *
     * @returns fingerprint hash for the native binary
     */ getCurrentFingerprintHash() {
        try {
            // Always read from native resources first (single source of truth).
            // The cache in ApplicationSettings persists across binary upgrades,
            // so we must validate against the actual native value to avoid
            // returning a stale fingerprint from a previous binary.
            const nativeHash = getNativeFingerprintHash();
            if (nativeHash && nativeHash.length > 0) {
                const cached = core.ApplicationSettings.getString('Norrix_Binary_Fingerprint');
                if (cached !== nativeHash) {
                    core.ApplicationSettings.setString('Norrix_Binary_Fingerprint', nativeHash);
                }
                return nativeHash;
            }
            // Fallback: cache (for rare cases where native read fails at runtime)
            const cached = core.ApplicationSettings.getString('Norrix_Binary_Fingerprint');
            if (cached && cached.length > 0) {
                return cached;
            }
            // Fallback: Read from assets file (backward compatibility for older builds)
            // This path will be removed in a future version
            const appFolderPath = core.knownFolders.currentApp().path;
            const fpPath = core.path.join(appFolderPath, 'assets', 'norrix.fingerprint.json');
            if (core.File.exists(fpPath)) {
                const file = core.File.fromPath(fpPath);
                const content = file.readTextSync();
                const json = JSON.parse(content || '{}');
                const hash = json.hash;
                if (typeof hash === 'string' && hash.length > 0) {
                    console.warn('NorrixUpdates: Using legacy fingerprint file. ' + 'Future builds should embed fingerprint in native resources.');
                    core.ApplicationSettings.setString('Norrix_Binary_Fingerprint', hash);
                    return hash;
                }
            }
            return undefined;
        } catch (e) {
            console.error('NorrixUpdates: failed to read fingerprint', e);
            return undefined;
        }
    }
    /**
     * Get the current configuration (deployment target) from native resources.
     *
     * This identifies which environment (e.g., 'stg', 'prod', 'dev') the binary
     * was built for. OTA updates should only be applied if they match the
     * binary's configuration to prevent staging updates from being applied
     * to production apps (or vice versa).
     *
     * Note: The native resource keys remain `NorrixNxConfiguration` (iOS) and `norrixNxConfiguration` (Android)
     * for backward compatibility with existing deployed apps.
     *
     * @returns configuration string for the native binary (e.g., 'stg', 'prod'), or undefined
     */ getCurrentConfiguration() {
        try {
            // Always read from native resources first (single source of truth).
            // The cache in ApplicationSettings persists across binary upgrades,
            // so we must validate against the actual native value to avoid
            // returning a stale configuration from a previous binary (e.g., 'stg'
            // when the current binary is 'prod').
            const nativeConfig = getNativeNxConfiguration();
            if (nativeConfig && nativeConfig.length > 0) {
                const cached = core.ApplicationSettings.getString('Norrix_Binary_NxConfiguration');
                if (cached !== nativeConfig) {
                    core.ApplicationSettings.setString('Norrix_Binary_NxConfiguration', nativeConfig);
                }
                return nativeConfig;
            }
            // Fallback: cache (for rare cases where native read fails at runtime)
            const cached = core.ApplicationSettings.getString('Norrix_Binary_NxConfiguration');
            if (cached && cached.length > 0) {
                return cached;
            }
            return undefined;
        } catch (e) {
            console.error('NorrixUpdates: failed to read configuration', e);
            return undefined;
        }
    }
    /**
     * @deprecated Use getCurrentConfiguration() instead. This method is kept for backward compatibility.
     */ getCurrentNxConfiguration() {
        return this.getCurrentConfiguration();
    }
    /**
     * Check for available updates from the server.
     */ async checkForUpdates() {
        // Track the check event
        if (this.config.enableTelemetry !== false) {
            trackOtaEvent('ota_check', {
                fingerprintHash: this.getCurrentFingerprintHash(),
                configuration: this.getCurrentConfiguration(),
                otaVersion: core.ApplicationSettings.getString('Norrix_OTA_Version') || undefined,
                otaBuildNumber: core.ApplicationSettings.getString('Norrix_OTA_BuildNumber') || undefined
            });
        }
        try {
            // Note: also include buildNumber check when support is there for it
            /**
             * This should always check for any stored OTA version and build number
             * It should compare against the binary version and build number (this.currentVersion, this.currentBuild will always reflect binary build)
             * We will likely want to store this in a documents file instead of applicationsettings.
             * Because users can clear their own apps applicationsettings anytime.
             */ const currentOtaVersion = core.ApplicationSettings.getString('Norrix_OTA_Version');
            const currentOtaBuild = core.ApplicationSettings.getString('Norrix_OTA_BuildNumber');
            const effectiveVersion = currentOtaVersion || this.currentVersion;
            const effectiveBuild = currentOtaBuild || this.currentBuild;
            const fingerprintHash = this.getCurrentFingerprintHash();
            const configuration = this.getCurrentConfiguration();
            const baseUrl = `${this.config.updateUrl}/api/update/check/${this.bundleId}`;
            const params = [
                `currentVersion=${encodeURIComponent(effectiveVersion)}`,
                `currentBuild=${encodeURIComponent(effectiveBuild)}`,
                `platform=${encodeURIComponent(this._getPlatform())}`
            ];
            if (fingerprintHash) {
                params.push(`fingerprint=${encodeURIComponent(fingerprintHash)}`);
            }
            if (configuration) {
                params.push(`configuration=${encodeURIComponent(configuration)}`);
            }
            if (this.config.tags && this.config.tags.length > 0) {
                params.push(`tags=${encodeURIComponent(this.config.tags.join(','))}`);
                params.push(`tagMatch=${this.config.requireAllTags ? 'all' : 'any'}`);
            }
            const url = `${baseUrl}?${params.join('&')}`;
            const response = await core.Http.request({
                url,
                method: 'GET'
            });
            const json = response.content.toJSON();
            const updateAvailable = !!json.updateAvailable;
            const update = {
                updateAvailable,
                platform: json.platform,
                version: json.version,
                buildNumber: json.buildNumber,
                releaseNotes: json.releaseNotes,
                url: json.url,
                expiresAt: json.expiresAt,
                requiresStoreUpdate: json.requiresStoreUpdate,
                compatibilityReason: json.compatibilityReason,
                isRollback: json.isRollback,
                matchedTags: json.matchedTags,
                isTaggedUpdate: json.isTaggedUpdate,
                message: json.message
            };
            // If tags were requested but this is a fallback (untagged) update,
            // and the client opted out of untagged fallback, treat as no update.
            if (update.updateAvailable && this.config.tags && this.config.tags.length > 0 && this.config.allowUntaggedFallback === false && !update.isTaggedUpdate) {
                update.updateAvailable = false;
            }
            return update;
        } catch (error) {
            console.error('Error checking for updates:', error);
            // Track the error
            if (this.config.enableTelemetry !== false) {
                trackOtaEvent('ota_error', {
                    syncStatus: exports.SyncStatus.CHECKING_FOR_UPDATE,
                    error: error instanceof Error ? error.message : String(error)
                });
            }
            return {
                updateAvailable: false,
                message: 'Failed to check for updates'
            };
        }
    }
    /**
     * Full sync operation: check for updates, download, and install if available.
     *
     * This method respects the `installUpdatesAutomatically` config option.
     * Use this method when you want the SDK to handle the full update flow,
     * for example on app resume or when the user triggers a manual check.
     *
     * @returns The update info from the server
     */ async sync() {
        this.statusCallback == null ? void 0 : this.statusCallback.call(this, exports.SyncStatus.CHECKING_FOR_UPDATE, {
            updateAvailable: false,
            message: 'Checking for updates...'
        });
        const update = await this.checkForUpdates();
        const requiresStoreUpdate = !!update.requiresStoreUpdate;
        // Determine if we should allow the update despite fingerprint mismatch
        const shouldAllow = await this._shouldAllowMismatchedUpdate(update);
        if (requiresStoreUpdate && !shouldAllow) {
            // Track store update prompt
            if (this.config.enableTelemetry !== false) {
                trackOtaEvent('ota_store_update_prompted', {
                    otaVersion: update.version,
                    otaBuildNumber: update.buildNumber || undefined,
                    requiresStoreUpdate: true,
                    fingerprintHash: this.getCurrentFingerprintHash(),
                    configuration: this.getCurrentConfiguration()
                });
            }
            this.statusCallback == null ? void 0 : this.statusCallback.call(this, exports.SyncStatus.UPDATE_IGNORED, _extends({}, update, {
                message: update.compatibilityReason || 'Update requires a new app store binary and cannot be installed over-the-air.'
            }));
            return update;
        }
        if (!update.updateAvailable) {
            this.statusCallback == null ? void 0 : this.statusCallback.call(this, exports.SyncStatus.UP_TO_DATE, _extends({}, update, {
                message: 'No update available.'
            }));
            return update;
        }
        // Safety guard: never auto-install a rollback (older version/build).
        // A native binary with a higher build number always supersedes any OTA
        // with a lower build number. This prevents an older active OTA from
        // downgrading a device that just installed a newer build from
        // TestFlight/App Store.
        if (update.isRollback) {
            if (this.config.enableTelemetry !== false) {
                trackOtaEvent('ota_rollback_skipped', {
                    otaVersion: update.version,
                    otaBuildNumber: update.buildNumber || undefined,
                    fingerprintHash: this.getCurrentFingerprintHash(),
                    configuration: this.getCurrentConfiguration(),
                    isRollback: true
                });
            }
            console.warn('NorrixUpdates: Skipping rollback update', update.version, `(build ${update.buildNumber}) — client is on a newer version`);
            this.statusCallback == null ? void 0 : this.statusCallback.call(this, exports.SyncStatus.UP_TO_DATE, _extends({}, update, {
                updateAvailable: false,
                message: 'Rollback update skipped — client is on a newer version.'
            }));
            return update;
        }
        // If automatic installation is enabled, proceed with download and install
        if (this.config.installUpdatesAutomatically) {
            this.updateInProgress = update;
            const success = await this.downloadUpdate(update);
            if (success) {
                this.applyUpdate();
            }
        } else {
            // Just notify that an update is available
            this.statusCallback == null ? void 0 : this.statusCallback.call(this, exports.SyncStatus.AWAITING_USER_ACTION, _extends({}, update, {
                message: 'Update available. Call downloadUpdate() to install.'
            }));
        }
        return update;
    }
    /**
     * Download an update from the server.
     */ async downloadUpdate(update) {
        // Track download started
        if (this.config.enableTelemetry !== false) {
            trackOtaEvent('ota_download_started', {
                otaVersion: update.version,
                otaBuildNumber: update.buildNumber || undefined,
                fingerprintHash: this.getCurrentFingerprintHash(),
                configuration: this.getCurrentConfiguration(),
                isRollback: update.isRollback,
                syncStatus: exports.SyncStatus.DOWNLOADING_PACKAGE
            });
        }
        return new Promise((resolve)=>{
            if (!update.url) {
                console.error('No update URL provided');
                if (this.config.enableTelemetry !== false) {
                    trackOtaEvent('ota_error', {
                        syncStatus: exports.SyncStatus.DOWNLOADING_PACKAGE,
                        error: 'No update URL provided'
                    });
                }
                resolve(false);
            }
            try {
                // Full URL to download the update
                const downloadUrl = update.url;
                const zipFilename = new URL(downloadUrl).pathname.split('/').filter(Boolean).pop();
                // We use the update-<id> for the fingerprint, not the hash
                // This ensures each update folder is unique.
                // Considerations:
                // - Possible we could use same fingerprint on subsequent updates.
                // - Rollback handling could be internal to the SDK and app alone
                // if we stay with update-<id> naming convention. Allowing an option menu to display all available local updates to rollback to.
                // - Cleanup of old OTA folders could be handled internally by the SDK.
                const fingerprint = zipFilename.replace('.zip', '');
                // Debug (local testing):
                // const zipFilename = `norrix-ota-123.zip`;
                // const downloadUrl = `${this.config.updateUrl}/${zipFilename}`;
                console.log('Downloading update from:', downloadUrl);
                const otaFolder = core.path.join(core.knownFolders.documents().path, 'norrix_ota');
                if (!core.Folder.exists(otaFolder)) {
                    core.Folder.fromPath(otaFolder);
                }
                const otaArchivePath = core.path.join(otaFolder, zipFilename);
                const onDownloadSuccess = (file)=>{
                    // Track download complete
                    if (this.config.enableTelemetry !== false) {
                        trackOtaEvent('ota_download_complete', {
                            otaVersion: update.version,
                            otaBuildNumber: update.buildNumber || undefined,
                            fingerprintHash: this.getCurrentFingerprintHash(),
                            configuration: this.getCurrentConfiguration()
                        });
                    }
                    this.statusCallback == null ? void 0 : this.statusCallback.call(this, exports.SyncStatus.INSTALLING_UPDATE, this.updateInProgress);
                    // Track install started
                    if (this.config.enableTelemetry !== false) {
                        trackOtaEvent('ota_install_started', {
                            otaVersion: update.version,
                            otaBuildNumber: update.buildNumber || undefined,
                            fingerprintHash: this.getCurrentFingerprintHash(),
                            configuration: this.getCurrentConfiguration(),
                            syncStatus: exports.SyncStatus.INSTALLING_UPDATE
                        });
                    }
                    const completeHandler = (error)=>{
                        core.ApplicationSettings.setString('Norrix_OTA_Fingerprint', fingerprint);
                        // Reset sentinel for the new OTA — it hasn't been launched yet
                        core.ApplicationSettings.setNumber('Norrix_OTA_Launch_Sentinel', 0);
                        // Persist the OTA version/build so subsequent checks
                        // compare against the most recently applied OTA, not
                        // just the original binary.
                        if (update.version) {
                            core.ApplicationSettings.setString('Norrix_OTA_Version', update.version);
                        }
                        if (update.buildNumber) {
                            core.ApplicationSettings.setString('Norrix_OTA_BuildNumber', update.buildNumber);
                        }
                        // Debug (local testing):
                        //  ApplicationSettings.setString(
                        //   'Norrix_OTA_Fingerprint',
                        //   'norrix-ota-123'
                        // );
                        if (error) {
                            // Track install error
                            if (this.config.enableTelemetry !== false) {
                                trackOtaEvent('ota_error', {
                                    syncStatus: exports.SyncStatus.INSTALLING_UPDATE,
                                    error: (error == null ? void 0 : error.localizedDescription) || String(error),
                                    otaVersion: update.version,
                                    otaBuildNumber: update.buildNumber || undefined
                                });
                                // Flush telemetry immediately on error
                                flushTelemetry();
                            }
                            this.statusCallback == null ? void 0 : this.statusCallback.call(this, exports.SyncStatus.ERROR, _extends({}, this.updateInProgress, {
                                updateAvailable: false,
                                message: error == null ? void 0 : error.localizedDescription
                            }));
                            resolve(true);
                        } else {
                            // Track install complete
                            if (this.config.enableTelemetry !== false) {
                                trackOtaEvent('ota_install_complete', {
                                    otaVersion: update.version,
                                    otaBuildNumber: update.buildNumber || undefined,
                                    fingerprintHash: this.getCurrentFingerprintHash(),
                                    configuration: this.getCurrentConfiguration(),
                                    syncStatus: exports.SyncStatus.UPDATE_INSTALLED,
                                    isRollback: update.isRollback
                                });
                                // Flush telemetry to ensure install complete is sent
                                flushTelemetry();
                            }
                            this.statusCallback == null ? void 0 : this.statusCallback.call(this, exports.SyncStatus.UPDATE_INSTALLED, this.updateInProgress);
                            resolve(true);
                        }
                    };
                    if (__APPLE__) {
                    // Use @nativescript/zip on iOS as well to avoid duplicate-symbol
                    // conflicts with SSZipArchive/ZipArchive when statically linked.
                    }
                    zip.Zip.unzip({
                        archive: file.path,
                        directory: otaFolder,
                        onProgress: this.downloadProgressCallback
                    }).then(()=>{
                        completeHandler();
                    }, (error)=>{
                        completeHandler(error);
                    });
                };
                core.Http.getFile(downloadUrl, otaArchivePath).then(onDownloadSuccess).catch(()=>{
                    setTimeout(()=>{
                        core.Http.getFile(downloadUrl, otaArchivePath).then(onDownloadSuccess).catch(()=>{
                            setTimeout(()=>{
                                core.Http.getFile(downloadUrl, otaArchivePath).then(onDownloadSuccess).catch((e)=>{
                                    // Track download error after all retries
                                    if (this.config.enableTelemetry !== false) {
                                        trackOtaEvent('ota_error', {
                                            syncStatus: exports.SyncStatus.DOWNLOADING_PACKAGE,
                                            error: (e == null ? void 0 : e.message) || String(e),
                                            otaVersion: update.version,
                                            otaBuildNumber: update.buildNumber || undefined
                                        });
                                        flushTelemetry();
                                    }
                                    this.statusCallback == null ? void 0 : this.statusCallback.call(this, exports.SyncStatus.ERROR, e);
                                });
                            }, 3000);
                        });
                    }, 3000);
                });
            } catch (error) {
                console.error('Error downloading update:', error);
                // Track error
                if (this.config.enableTelemetry !== false) {
                    trackOtaEvent('ota_error', {
                        syncStatus: exports.SyncStatus.DOWNLOADING_PACKAGE,
                        error: error instanceof Error ? error.message : String(error),
                        otaVersion: update.version,
                        otaBuildNumber: update.buildNumber || undefined
                    });
                    flushTelemetry();
                }
                this.statusCallback == null ? void 0 : this.statusCallback.call(this, exports.SyncStatus.ERROR, _extends({}, this.updateInProgress, {
                    updateAvailable: false,
                    message: error
                }));
            }
        });
    }
    /**
     * Utility to clear all OTA updates and prompt user to close app.
     * This will reset the app back to the original binary installed version.
     */ resetAllOTAs() {
        core.ApplicationSettings.remove('Norrix_OTA_Fingerprint');
        core.ApplicationSettings.remove('Norrix_OTA_Version');
        core.ApplicationSettings.remove('Norrix_OTA_BuildNumber');
        // Also clear binary marker + cached binary fingerprint so the runtime
        // re-initializes cleanly after a reset.
        core.ApplicationSettings.remove('Norrix_Binary_Marker');
        core.ApplicationSettings.remove('Norrix_Binary_Fingerprint');
        // Clear sentinel and recovery state
        core.ApplicationSettings.remove('Norrix_OTA_Launch_Sentinel');
        core.ApplicationSettings.remove('Norrix_OTA_Crash_Recovery');
        core.ApplicationSettings.remove('Norrix_OTA_Crash_Recovery_Fingerprint');
        core.ApplicationSettings.remove('Norrix_OTA_Crash_Recovery_Version');
        core.ApplicationSettings.remove('Norrix_OTA_Crash_Recovery_BuildNumber');
        // Best-effort: remove all local OTA packages so the app truly
        // returns to the store-binary state.
        try {
            const otaRoot = core.path.join(core.knownFolders.documents().path, 'norrix_ota');
            if (core.Folder.exists(otaRoot)) {
                core.Folder.fromPath(otaRoot).clear();
            }
        } catch (e) {
            console.warn('NorrixUpdates: failed to clear norrix_ota folder', e);
        }
        core.ApplicationSettings.flush();
        setTimeout(()=>{
            core.Dialogs.confirm({
                title: 'Norrix OTA Reset',
                message: 'Would you like to close the app now to reset all OTA updates?',
                okButtonText: 'Yes, Close Now',
                cancelButtonText: 'Will do later.'
            }).then((ok)=>{
                if (ok) {
                    this.closeAppNow();
                }
            });
        }, 1000);
    }
    /**
     * Apply a downloaded update.
     * By default, this preserves the current behavior: installed updates activate
     * on the next cold launch unless promptToRestartAfterInstall or an explicit
     * apply mode is provided. Passing true keeps the legacy process-restart path.
     */ async applyUpdate(closeAppNowOrOptions) {
        try {
            const forceProcessRestart = closeAppNowOrOptions === true;
            const options = typeof closeAppNowOrOptions === 'object' ? closeAppNowOrOptions : undefined;
            const hasExplicitApplyMode = !!(options == null ? void 0 : options.mode) || !!this.config.applyUpdateMode || forceProcessRestart;
            const mode = forceProcessRestart ? 'process-restart' : (options == null ? void 0 : options.mode) || this.config.applyUpdateMode || 'soft-reboot';
            var _options_fallbackToProcessRestart, _ref;
            const fallbackToProcessRestart = (_ref = (_options_fallbackToProcessRestart = options == null ? void 0 : options.fallbackToProcessRestart) != null ? _options_fallbackToProcessRestart : this.config.fallbackToProcessRestart) != null ? _ref : true;
            var _options_prompt;
            const shouldPrompt = !forceProcessRestart && ((_options_prompt = options == null ? void 0 : options.prompt) != null ? _options_prompt : !!this.config.promptToRestartAfterInstall);
            if (!hasExplicitApplyMode && !shouldPrompt) {
                return {
                    applied: false,
                    mode: 'next-launch',
                    message: 'Update will activate on the next app launch.'
                };
            }
            const applyNow = async ()=>{
                if (mode === 'next-launch') {
                    return {
                        applied: false,
                        mode,
                        message: 'Update will activate on the next app launch.'
                    };
                }
                if (mode === 'process-restart') {
                    await flushTelemetry();
                    this.closeAppNow();
                    return {
                        applied: true,
                        mode
                    };
                }
                return this.softRebootApp(fallbackToProcessRestart);
            };
            if (!shouldPrompt) {
                return applyNow();
            }
            const ok = await core.Dialogs.confirm({
                title: 'Update Installed',
                message: 'Would you like to reload the app now to apply the update?',
                okButtonText: 'Apply Now',
                cancelButtonText: 'Later'
            });
            if (!ok) {
                return {
                    applied: false,
                    mode: 'next-launch',
                    message: 'Update will activate on the next app launch.'
                };
            }
            return applyNow();
        } catch (error) {
            console.error('Error applying update:', error);
            return {
                applied: false,
                mode: 'next-launch',
                message: error instanceof Error ? error.message : String(error)
            };
        }
    }
    async softRebootApp(fallbackToProcessRestart) {
        const update = this.updateInProgress || {
            updateAvailable: false,
            message: 'Reloading app...'
        };
        this.statusCallback == null ? void 0 : this.statusCallback.call(this, exports.SyncStatus.RELOADING_APP, _extends({}, update, {
            message: 'Reloading app...'
        }));
        core.ApplicationSettings.flush();
        await flushTelemetry();
        const reload = this.requestNativeScriptReload();
        if (reload.ok) {
            return {
                applied: true,
                mode: 'soft-reboot'
            };
        }
        const message = reload.message || 'NativeScript runtime reload API is not available.';
        console.warn('NorrixUpdates:', message);
        if (!fallbackToProcessRestart) {
            core.ApplicationSettings.setNumber('Norrix_OTA_Launch_Sentinel', 0);
            core.ApplicationSettings.flush();
        }
        if (this.config.enableTelemetry !== false) {
            var _this_updateInProgress, _this_updateInProgress1;
            trackOtaEvent('ota_error', {
                syncStatus: exports.SyncStatus.RELOAD_FAILED,
                error: message,
                otaVersion: (_this_updateInProgress = this.updateInProgress) == null ? void 0 : _this_updateInProgress.version,
                otaBuildNumber: ((_this_updateInProgress1 = this.updateInProgress) == null ? void 0 : _this_updateInProgress1.buildNumber) || undefined
            });
            await flushTelemetry();
        }
        this.statusCallback == null ? void 0 : this.statusCallback.call(this, exports.SyncStatus.RELOAD_FAILED, _extends({}, update, {
            updateAvailable: false,
            message
        }));
        if (fallbackToProcessRestart) {
            this.closeAppNow();
            return {
                applied: true,
                mode: 'process-restart',
                fallbackUsed: true,
                message
            };
        }
        return {
            applied: false,
            mode: 'soft-reboot',
            message
        };
    }
    requestNativeScriptReload() {
        const runtimeApi = this.getNativeScriptRuntimeApi();
        if (!runtimeApi || typeof runtimeApi.reloadApplication !== 'function') {
            const runtimeVersion = globalThis.__runtimeVersion || 'unknown';
            return {
                ok: false,
                message: `NativeScriptRuntime.reloadApplication is not available in this runtime ` + `(@nativescript runtime ${runtimeVersion}). Soft reboot requires the ` + `9.1.0 runtime or newer; falling back.`
            };
        }
        const prepared = this.prepareNativeBundleForReload();
        if (!prepared.ok) {
            return prepared;
        }
        try {
            const accepted = prepared.baseDir ? runtimeApi.reloadApplication(prepared.baseDir) : runtimeApi.reloadApplication();
            return accepted ? {
                ok: true
            } : {
                ok: false,
                message: 'NativeScriptRuntime.reloadApplication rejected reload.'
            };
        } catch (error) {
            return {
                ok: false,
                message: error instanceof Error ? error.message : `NativeScript runtime reload failed: ${String(error)}`
            };
        }
    }
    getNativeScriptRuntimeApi() {
        const scope = globalThis;
        const isApple = typeof __APPLE__ !== 'undefined' && !!__APPLE__;
        const isAndroid = typeof __ANDROID__ !== 'undefined' && !!__ANDROID__;
        if (isApple) {
            return scope.NativeScriptRuntime;
        }
        if (isAndroid) {
            var _scope_com_tns, _scope_com;
            return (_scope_com = scope.com) == null ? void 0 : (_scope_com_tns = _scope_com.tns) == null ? void 0 : _scope_com_tns.NativeScriptRuntime;
        }
        return null;
    }
    prepareNativeBundleForReload() {
        const scope = globalThis;
        const isApple = typeof __APPLE__ !== 'undefined' && !!__APPLE__;
        const isAndroid = typeof __ANDROID__ !== 'undefined' && !!__ANDROID__;
        if (isApple) {
            var _scope_NSBundle_mainBundle, _scope_NSBundle;
            const norrixOTA = scope.NorrixOTA;
            const defaultBaseDir = (_scope_NSBundle = scope.NSBundle) == null ? void 0 : (_scope_NSBundle_mainBundle = _scope_NSBundle.mainBundle) == null ? void 0 : _scope_NSBundle_mainBundle.resourcePath;
            if (!(norrixOTA == null ? void 0 : norrixOTA.applicationPathWithDefault) || !defaultBaseDir) {
                return {
                    ok: false,
                    message: 'NorrixOTA.applicationPathWithDefault is not available for iOS reload.'
                };
            }
            const baseDir = norrixOTA.applicationPathWithDefault(defaultBaseDir);
            if (!baseDir) {
                return {
                    ok: false,
                    message: 'NorrixOTA did not return a bundle path for iOS reload.'
                };
            }
            return {
                ok: true,
                baseDir: String(baseDir)
            };
        }
        if (isAndroid) {
            var _scope_com_tns, _scope_com;
            const context = core.Utils.android.getApplicationContext();
            const norrixOTA = (_scope_com = scope.com) == null ? void 0 : (_scope_com_tns = _scope_com.tns) == null ? void 0 : _scope_com_tns.NorrixOTA;
            if (!context || !(norrixOTA == null ? void 0 : norrixOTA.activatePackage)) {
                return {
                    ok: false,
                    message: 'NorrixOTA.activatePackage is not available for Android reload.'
                };
            }
            norrixOTA.activatePackage(context);
            return {
                ok: true
            };
        }
        return {
            ok: false,
            message: 'NativeScript OTA reload is supported only on iOS and Android.'
        };
    }
    getRestartBaseUrl() {
        try {
            const url = new URL(this.config.updateUrl);
            const hostname = url.hostname;
            return `${url.protocol}//${hostname}/restart`;
        } catch (e) {
            // Fallback to production restart URL
            console.warn('NorrixUpdates: failed to parse updateUrl, using default restart URL', e);
            return 'https://norrix.net/restart';
        }
    }
    /**
     * Utility to close the app.
     * Useful when handling updates that require a restart with user prompts.
     * When useRestartPage is enabled (default), opens a restart page that
     * helps users automatically relaunch the app.
     */ closeAppNow() {
        // Open restart page before closing if enabled (default: true)
        if (this.config.useRestartPage !== false) {
            try {
                // Build restart URL with bundle ID and optional orgId
                const restartBaseUrl = this.getRestartBaseUrl();
                let restartUrl = `${restartBaseUrl}?target=${encodeURIComponent(this.bundleId)}`;
                if (this.config.orgId) {
                    restartUrl += `&orgId=${encodeURIComponent(this.config.orgId)}`;
                }
                core.Utils.openUrl(restartUrl);
            } catch (e) {
                console.warn('NorrixUpdates: failed to open restart page', e);
            }
        }
        setTimeout(()=>{
            const isApple = typeof __APPLE__ !== 'undefined' && !!__APPLE__;
            const isAndroid = typeof __ANDROID__ !== 'undefined' && !!__ANDROID__;
            if (isApple) {
                exit(0);
            } else if (isAndroid) {
                java.lang.System.exit(0);
            }
        }, 300);
    }
    _getPlatform() {
        const isApple = typeof __APPLE__ !== 'undefined' && !!__APPLE__;
        const isVision = typeof __VISIONOS__ !== 'undefined' && !!__VISIONOS__;
        return isApple ? isVision ? 'visionos' : 'ios' : 'android';
    }
    constructor(config){
        this.config = config;
        this.bundleId = getBundleId();
        const appVersionInfo = getAppVersionInfo();
        this.currentVersion = appVersionInfo.versionName || '1.0.0';
        this.currentBuild = appVersionInfo.buildNumber || '1';
        this.statusCallback = config.statusCallback;
        this.downloadProgressCallback = config.downloadProgressCallback;
        // Detect binary upgrade and clear stale caches.
        // ApplicationSettings (UserDefaults / SharedPreferences) persist across
        // binary upgrades, so cached fingerprint, configuration, and OTA version
        // from the previous binary must be invalidated when the binary changes.
        const binaryIdentity = `${this.currentVersion}:${this.currentBuild}`;
        const lastBinaryIdentity = core.ApplicationSettings.getString('Norrix_Last_Binary_Identity');
        if (lastBinaryIdentity && lastBinaryIdentity !== binaryIdentity) {
            core.ApplicationSettings.remove('Norrix_Binary_Fingerprint');
            core.ApplicationSettings.remove('Norrix_Binary_NxConfiguration');
            core.ApplicationSettings.remove('Norrix_OTA_Version');
            core.ApplicationSettings.remove('Norrix_OTA_BuildNumber');
            core.ApplicationSettings.remove('Norrix_OTA_Fingerprint');
            core.ApplicationSettings.remove('Norrix_Binary_Marker');
        }
        core.ApplicationSettings.setString('Norrix_Last_Binary_Identity', binaryIdentity);
        // --- Launch Sentinel: signal successful JS initialization ---
        // This MUST run synchronously before any async operations.
        // If JS got here, the app launched successfully from the OTA bundle.
        // The native layer incremented this before loading the OTA;
        // resetting to 0 tells the native layer the launch was OK.
        core.ApplicationSettings.setNumber('Norrix_OTA_Launch_Sentinel', 0);
        // Check if native layer just recovered from an OTA crash loop
        const crashRecovery = core.ApplicationSettings.getBoolean('Norrix_OTA_Crash_Recovery', false);
        let crashRecoveryFingerprint;
        let crashRecoveryVersion;
        let crashRecoveryBuild;
        if (crashRecovery) {
            crashRecoveryFingerprint = core.ApplicationSettings.getString('Norrix_OTA_Crash_Recovery_Fingerprint') || undefined;
            crashRecoveryVersion = core.ApplicationSettings.getString('Norrix_OTA_Crash_Recovery_Version') || undefined;
            crashRecoveryBuild = core.ApplicationSettings.getString('Norrix_OTA_Crash_Recovery_BuildNumber') || undefined;
            core.ApplicationSettings.remove('Norrix_OTA_Crash_Recovery');
            core.ApplicationSettings.remove('Norrix_OTA_Crash_Recovery_Fingerprint');
            core.ApplicationSettings.remove('Norrix_OTA_Crash_Recovery_Version');
            core.ApplicationSettings.remove('Norrix_OTA_Crash_Recovery_BuildNumber');
            console.warn('NorrixUpdates: Recovered from OTA crash loop, rolled back fingerprint:', crashRecoveryFingerprint, 'version:', crashRecoveryVersion, 'build:', crashRecoveryBuild);
        }
        // Configure telemetry (enabled by default)
        configureTelemetry({
            updateUrl: config.updateUrl,
            enabled: config.enableTelemetry !== false
        });
        // Report crash recovery telemetry (must happen after configureTelemetry)
        if (crashRecovery && config.enableTelemetry !== false) {
            trackOtaEvent('ota_crash_recovery', {
                fingerprintHash: this.getCurrentFingerprintHash(),
                configuration: this.getCurrentConfiguration(),
                otaVersion: crashRecoveryVersion,
                otaBuildNumber: crashRecoveryBuild,
                recoveredFromFingerprint: crashRecoveryFingerprint
            });
            flushTelemetry();
        }
        // Track app launch with current OTA state
        const otaFingerprint = core.ApplicationSettings.getString('Norrix_OTA_Fingerprint');
        const configuration = this.getCurrentConfiguration();
        if (config.enableTelemetry !== false) {
            trackAppLaunch(otaFingerprint || undefined, configuration || undefined);
        }
        // Check for updates on launch if configured
        if (this.config.checkForUpdatesOnLaunch) {
            // Use sync() which handles the full check → download → install flow
            this.sync();
        }
    }
}

let norrix = null;
function initNorrix(config) {
    norrix = new NorrixUpdates(config);
    return norrix;
}
function checkUpdates() {
    if (!norrix) {
        throw new Error('Norrix is not initialized. Please call initNorrix first.');
    }
    return norrix.checkForUpdates();
}

exports.NorrixUpdates = NorrixUpdates;
exports.SDK_VERSION = SDK_VERSION;
exports.checkUpdates = checkUpdates;
exports.configureTelemetry = configureTelemetry;
exports.flushTelemetry = flushTelemetry;
exports.initNorrix = initNorrix;
exports.trackAppLaunch = trackAppLaunch;
exports.trackOtaEvent = trackOtaEvent;
