/**
 * @license
 * Copyright 2025 Vybestack LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import { ExtensionUpdateState } from '../ui/state/extensions.js';
import { checkForExtensionUpdate } from '../config/extensions/github.js';
import { loadUserExtensions } from '../config/extension.js';
import { updateExtension, } from '../config/extensions/update.js';
import { Storage, getErrorMessage, } from '@vybestack/llxprt-code-core';
import * as fs from 'node:fs';
import * as path from 'node:path';
const HOUR_IN_MS = 60 * 60 * 1000;
const STATE_FILENAME = 'extension-update-state.json';
function clampIntervalHours(value) {
    if (!value || Number.isNaN(value)) {
        return 24;
    }
    return Math.max(1, value);
}
function resolveSettings(raw) {
    return {
        enabled: raw?.enabled ?? true,
        checkIntervalHours: clampIntervalHours(raw?.checkIntervalHours ?? 24),
        installMode: raw?.installMode ?? 'immediate',
        notificationLevel: raw?.notificationLevel ?? 'toast',
        perExtension: raw?.perExtension ?? {},
    };
}
function createFileStateStore() {
    const dir = Storage.getGlobalLlxprtDir();
    const filePath = path.join(dir, STATE_FILENAME);
    return {
        async read() {
            try {
                await fs.promises.mkdir(dir, { recursive: true });
                const data = await fs.promises.readFile(filePath, 'utf-8');
                return JSON.parse(data);
            }
            catch (error) {
                if (error &&
                    typeof error === 'object' &&
                    'code' in error &&
                    error.code === 'ENOENT') {
                    return {};
                }
                console.warn('[extensions] Failed to read extension auto-update state:', getErrorMessage(error));
                return {};
            }
        },
        async write(state) {
            try {
                await fs.promises.mkdir(dir, { recursive: true });
                await fs.promises.writeFile(filePath, JSON.stringify(state, null, 2), 'utf-8');
            }
            catch (error) {
                console.warn('[extensions] Failed to persist extension auto-update state:', getErrorMessage(error));
            }
        },
    };
}
export class ExtensionAutoUpdater {
    settings;
    workspaceDir;
    notify;
    stateStore;
    extensionLoader;
    updateExecutor;
    updateChecker;
    now;
    intervalHandle = null;
    isChecking = false;
    disposed = false;
    constructor(options = {}) {
        this.settings = resolveSettings(options.settings);
        this.workspaceDir = options.workspaceDir ?? process.cwd();
        this.notify = options.notify;
        this.stateStore = options.stateStore ?? createFileStateStore();
        this.extensionLoader =
            options.extensionLoader ??
                (async () => loadUserExtensions(this.workspaceDir));
        this.updateExecutor =
            options.updateExecutor ??
                ((extension, cwd, currentState, setExtensionUpdateState) => updateExtension(extension, cwd, async () => true, // Auto-approve in background mode
                currentState, (action) => {
                    if (action.type === 'SET_STATE') {
                        setExtensionUpdateState(action.payload.state);
                    }
                }));
        this.updateChecker = options.updateChecker ?? checkForExtensionUpdate;
        this.now = options.now ?? Date.now;
    }
    /**
     * Starts the background auto-update loop.
     * Returns a cleanup function that stops scheduling additional checks.
     */
    start() {
        if (!this.settings.enabled || this.disposed) {
            return () => { };
        }
        void this.runCycle();
        const intervalMs = this.settings.checkIntervalHours * HOUR_IN_MS;
        this.intervalHandle = setInterval(() => {
            void this.runCycle();
        }, intervalMs);
        return () => this.stop();
    }
    stop() {
        if (this.intervalHandle) {
            clearInterval(this.intervalHandle);
            this.intervalHandle = null;
        }
        this.disposed = true;
    }
    async checkNow() {
        await this.runCycle();
    }
    async runCycle() {
        if (!this.settings.enabled || this.disposed || this.isChecking) {
            return;
        }
        this.isChecking = true;
        try {
            const [state, extensions] = await Promise.all([
                this.stateStore.read(),
                this.extensionLoader(),
            ]);
            const extensionsByName = new Map(extensions.map((extension) => [extension.config.name, extension]));
            await this.applyPendingInstalls(state, extensionsByName);
            for (const extension of extensions) {
                await this.processExtension(extension, state);
            }
            await this.stateStore.write(state);
        }
        finally {
            this.isChecking = false;
        }
    }
    getEffectiveSettingsForExtension(extensionName) {
        const override = this.settings.perExtension[extensionName];
        return {
            enabled: override?.enabled ?? this.settings.enabled,
            installMode: override?.installMode ?? this.settings.installMode,
            notificationLevel: override?.notificationLevel ?? this.settings.notificationLevel,
            checkIntervalHours: clampIntervalHours(override?.checkIntervalHours ?? this.settings.checkIntervalHours),
        };
    }
    async applyPendingInstalls(state, extensionsByName) {
        for (const [name, entry] of Object.entries(state)) {
            if (!entry.pendingInstall) {
                continue;
            }
            const extension = extensionsByName.get(name);
            if (!extension || !extension.installMetadata) {
                entry.pendingInstall = false;
                entry.lastError = `Extension "${name}" is no longer installed.`;
                entry.state = ExtensionUpdateState.ERROR;
                continue;
            }
            const settings = this.getEffectiveSettingsForExtension(name);
            await this.performUpdate(extension, entry, settings);
        }
    }
    async processExtension(extension, state) {
        if (!extension.installMetadata) {
            return;
        }
        const settings = this.getEffectiveSettingsForExtension(extension.config.name);
        if (!settings.enabled) {
            return;
        }
        const entry = state[extension.config.name] ?? (state[extension.config.name] = {});
        const now = this.now();
        const intervalMs = settings.checkIntervalHours * HOUR_IN_MS;
        if (entry.lastCheck && now - entry.lastCheck < intervalMs) {
            return;
        }
        entry.lastCheck = now;
        entry.state = ExtensionUpdateState.CHECKING_FOR_UPDATES;
        try {
            // Convert Extension to GeminiCLIExtension for the updateChecker
            const geminiExtension = {
                name: extension.config.name,
                version: extension.config.version,
                isActive: true,
                path: extension.path,
                installMetadata: extension.installMetadata,
            };
            // Call the update checker which will update entry.state via callback
            // Initialize with explicit type to prevent narrowing
            let resultState;
            await this.updateChecker(geminiExtension, (updateState) => {
                resultState = updateState;
                entry.state = updateState;
            });
            entry.lastError = undefined;
            // Check the state after update checker completes
            if (resultState === ExtensionUpdateState.UPDATE_AVAILABLE) {
                await this.handleUpdateAvailable(extension, entry, settings);
            }
            else if (resultState === ExtensionUpdateState.UP_TO_DATE ||
                resultState === ExtensionUpdateState.NOT_UPDATABLE) {
                entry.failureCount = 0;
            }
        }
        catch (error) {
            entry.lastError = getErrorMessage(error);
            entry.failureCount = (entry.failureCount ?? 0) + 1;
            entry.state = ExtensionUpdateState.ERROR;
            this.notifyWithLevel('error', `Failed to check extension "${extension.config.name}" for updates: ${entry.lastError}`, settings.notificationLevel);
        }
    }
    async handleUpdateAvailable(extension, entry, settings) {
        switch (settings.installMode) {
            case 'immediate':
                await this.performUpdate(extension, entry, settings);
                break;
            case 'on-restart':
                entry.pendingInstall = true;
                this.notifyWithLevel('info', `Extension "${extension.config.name}" update queued; it will install on the next restart.`, settings.notificationLevel);
                break;
            case 'manual':
            default:
                this.notifyWithLevel('info', `Update available for extension "${extension.config.name}". Run "llxprt extensions update ${extension.config.name}" to install.`, settings.notificationLevel);
                break;
        }
    }
    async performUpdate(extension, entry, settings) {
        try {
            entry.state = ExtensionUpdateState.UPDATING;
            // Convert Extension to GeminiCLIExtension for the updateExecutor
            const geminiExtension = {
                name: extension.config.name,
                version: extension.config.version,
                isActive: true,
                path: extension.path,
                installMetadata: extension.installMetadata,
            };
            const info = await this.updateExecutor(geminiExtension, this.workspaceDir, entry.state, (updateState) => {
                entry.state = updateState;
            });
            if (!info) {
                throw new Error('Update returned undefined');
            }
            entry.lastUpdate = this.now();
            entry.pendingInstall = false;
            entry.state = ExtensionUpdateState.UPDATED_NEEDS_RESTART;
            entry.failureCount = 0;
            entry.lastError = undefined;
            this.notifyWithLevel('info', `Extension "${info.name}" updated to ${info.updatedVersion}. Restart llxprt-code to load the new version.`, settings.notificationLevel);
        }
        catch (error) {
            entry.lastError = getErrorMessage(error);
            entry.failureCount = (entry.failureCount ?? 0) + 1;
            entry.state = ExtensionUpdateState.ERROR;
            this.notifyWithLevel('error', `Failed to update extension "${extension.config.name}": ${entry.lastError}`, settings.notificationLevel);
        }
    }
    notifyWithLevel(level, message, notificationLevel) {
        if (notificationLevel === 'silent' && level !== 'error') {
            return;
        }
        if (this.notify) {
            this.notify(message, level);
            return;
        }
        const prefix = '[extensions]';
        if (level === 'error') {
            console.error(prefix, message);
        }
        else if (level === 'warn') {
            console.warn(prefix, message);
        }
        else {
            console.log(prefix, message);
        }
    }
}
//# sourceMappingURL=extensionAutoUpdater.js.map