import { EventEmitter } from "node:events";
import { homedir } from "node:os";
import { resolve } from "node:path";
import { withTimeout } from "../../utils/promise-timeout.js";
const DEFAULT_REFRESH_TIMEOUT_MS = 30000;
export class ProviderSnapshotManager {
    constructor(providerRegistry, logger, options = {}) {
        this.logger = logger;
        this.snapshots = new Map();
        this.providerLoads = new Map();
        this.events = new EventEmitter();
        this.destroyed = false;
        this.providerRegistry = providerRegistry;
        this.refreshTimeoutMs = options.refreshTimeoutMs ?? DEFAULT_REFRESH_TIMEOUT_MS;
    }
    getSnapshot(_cwd) {
        const resolvedCwd = resolveGlobalSnapshotCwd();
        const entries = this.snapshots.get(resolvedCwd);
        if (!entries) {
            const loadingEntries = this.resetSnapshotToLoading(resolvedCwd);
            void this.warmUp(resolvedCwd);
            return entriesToArray(loadingEntries);
        }
        const missingProviders = this.getProviderIds().filter((provider) => !entries.has(provider));
        if (missingProviders.length > 0) {
            for (const provider of missingProviders) {
                entries.set(provider, this.createUnprobedEntry(provider));
            }
        }
        const providerLoads = this.providerLoads.get(resolvedCwd);
        const loadingProviders = Array.from(entries.values())
            .filter((entry) => entry.status === "loading" && !providerLoads?.has(entry.provider))
            .map((entry) => entry.provider);
        if (loadingProviders.length > 0) {
            void this.warmUp(resolvedCwd, loadingProviders);
        }
        return entriesToArray(entries);
    }
    async refreshSnapshotForCwd(options) {
        const snapshotCwd = resolveGlobalSnapshotCwd();
        const providers = this.resolveRefreshProviders(options.providers);
        this.resetSnapshotToLoading(snapshotCwd, providers);
        this.emitChange(snapshotCwd);
        await this.refreshProviders(snapshotCwd, providers ?? this.getProviderIds());
    }
    async refreshSettingsSnapshot(options = {}) {
        const homeCwd = resolveGlobalSnapshotCwd();
        const providers = this.resolveRefreshProviders(options.providers);
        const providersToRefresh = providers ?? this.getProviderIds();
        this.resetSnapshotToLoading(homeCwd, providers);
        this.emitChange(homeCwd);
        await this.refreshProviders(homeCwd, providersToRefresh);
    }
    async warmUpSnapshotForCwd(options) {
        const snapshotCwd = resolveGlobalSnapshotCwd();
        const providers = this.resolveRefreshProviders(options.providers);
        if (options.providers && providers?.length === 0) {
            return;
        }
        const snapshot = this.snapshots.get(snapshotCwd);
        if (!snapshot) {
            this.resetSnapshotToLoading(snapshotCwd, providers);
        }
        else if (providers) {
            const missingProviders = providers.filter((provider) => !snapshot.has(provider));
            if (missingProviders.length > 0) {
                this.resetSnapshotToLoading(snapshotCwd, missingProviders);
            }
        }
        await this.warmUp(snapshotCwd, providers);
    }
    async refresh(options) {
        await this.refreshSnapshotForCwd(options);
    }
    on(event, listener) {
        this.events.on(event, listener);
        return this;
    }
    off(event, listener) {
        this.events.off(event, listener);
        return this;
    }
    destroy() {
        this.destroyed = true;
        this.events.removeAllListeners();
        this.snapshots.clear();
        this.providerLoads.clear();
    }
    replaceRegistry(providerRegistry) {
        this.providerRegistry = providerRegistry;
        for (const cwd of this.snapshots.keys()) {
            this.providerLoads.delete(cwd);
            this.snapshots.set(cwd, this.reconcileSnapshotForRegistry(cwd));
            this.emitChange(cwd);
        }
    }
    createLoadingEntries() {
        const entries = new Map();
        for (const provider of this.getProviderIds()) {
            const definition = this.providerRegistry[provider];
            entries.set(provider, {
                provider,
                status: "loading",
                enabled: definition?.enabled ?? true,
                label: definition?.label,
                description: definition?.description,
                defaultModeId: definition?.defaultModeId ?? null,
            });
        }
        return entries;
    }
    createUnprobedEntry(provider) {
        const definition = this.providerRegistry[provider];
        return {
            provider,
            status: "unavailable",
            enabled: definition?.enabled ?? true,
            label: definition?.label,
            description: definition?.description,
            defaultModeId: definition?.defaultModeId ?? null,
        };
    }
    reconcileSnapshotForRegistry(cwd) {
        const existing = this.snapshots.get(cwd);
        const entries = new Map();
        for (const provider of this.getProviderIds()) {
            const definition = this.providerRegistry[provider];
            const current = existing?.get(provider);
            const metadata = {
                provider,
                enabled: definition?.enabled ?? true,
                label: definition?.label,
                description: definition?.description,
                defaultModeId: definition?.defaultModeId ?? null,
            };
            if (!definition?.enabled || !current || current.status === "loading") {
                entries.set(provider, {
                    ...metadata,
                    status: "unavailable",
                    enabled: definition?.enabled ?? true,
                });
                continue;
            }
            entries.set(provider, {
                ...current,
                ...metadata,
            });
        }
        return entries;
    }
    async warmUp(cwd, providers) {
        const providersToRefresh = providers ?? this.getProviderIds();
        await this.loadProviders({
            cwd,
            providers: providersToRefresh,
            force: false,
        });
    }
    async refreshProviders(cwd, providers) {
        await this.loadProviders({ cwd, providers, force: true });
    }
    async loadProviders(options) {
        await Promise.allSettled(options.providers.map((provider) => this.loadProvider({ ...options, provider })));
    }
    loadProvider(options) {
        const definition = this.providerRegistry[options.provider];
        if (!definition) {
            return Promise.resolve();
        }
        const existingLoad = this.getProviderLoad(options.cwd, options.provider);
        if (existingLoad && !options.force) {
            return existingLoad.promise;
        }
        const load = {
            promise: Promise.resolve(),
        };
        this.setProviderLoad(options.cwd, options.provider, load);
        load.promise = Promise.resolve()
            .then(() => this.refreshProvider({
            cwd: options.cwd,
            provider: options.provider,
            definition,
            load,
            force: options.force,
        }))
            .finally(() => {
            const providerLoads = this.providerLoads.get(options.cwd);
            if (providerLoads?.get(options.provider) === load) {
                providerLoads.delete(options.provider);
            }
            if (providerLoads?.size === 0) {
                this.providerLoads.delete(options.cwd);
            }
        });
        return load.promise;
    }
    async refreshProvider(options) {
        const { cwd, provider, definition, load, force } = options;
        const snapshot = this.getOrCreateSnapshot(options.cwd);
        const base = {
            provider,
            label: definition.label,
            description: definition.description,
            defaultModeId: definition.defaultModeId,
        };
        const setEntry = (entry) => {
            if (!this.isCurrentProviderLoad(cwd, provider, load)) {
                return false;
            }
            snapshot.set(provider, entry);
            this.emitChange(cwd);
            return true;
        };
        try {
            if (!definition.enabled) {
                setEntry({ ...base, status: "unavailable", enabled: false });
                return;
            }
            const client = definition.createClient(this.logger);
            const available = await withTimeout(client.isAvailable(), this.refreshTimeoutMs, `Timed out checking ${definition.label} availability after ${this.refreshTimeoutMs}ms`);
            if (!available) {
                setEntry({ ...base, status: "unavailable", enabled: true });
                return;
            }
            const [models, modes] = await withTimeout(Promise.all([
                definition.fetchModels({ cwd, force }),
                definition.fetchModes({ cwd, force }),
            ]), this.refreshTimeoutMs, `Timed out refreshing ${definition.label} after ${this.refreshTimeoutMs}ms`);
            setEntry({
                ...base,
                status: "ready",
                enabled: true,
                models,
                modes,
                fetchedAt: new Date().toISOString(),
            });
        }
        catch (error) {
            const emitted = setEntry({
                ...base,
                status: "error",
                enabled: true,
                error: toErrorMessage(error),
            });
            if (emitted) {
                this.logger.warn({ err: error, provider, cwd }, "Failed to refresh provider snapshot");
            }
        }
    }
    getProviderLoad(cwdKey, provider) {
        return this.providerLoads.get(cwdKey)?.get(provider);
    }
    setProviderLoad(cwdKey, provider, load) {
        let providerLoads = this.providerLoads.get(cwdKey);
        if (!providerLoads) {
            providerLoads = new Map();
            this.providerLoads.set(cwdKey, providerLoads);
        }
        providerLoads.set(provider, load);
    }
    isCurrentProviderLoad(cwdKey, provider, load) {
        return this.providerLoads.get(cwdKey)?.get(provider) === load;
    }
    emitChange(cwdKey) {
        if (this.destroyed) {
            return;
        }
        const snapshot = this.snapshots.get(cwdKey);
        if (!snapshot) {
            return;
        }
        this.events.emit("change", entriesToArray(snapshot), cwdKey);
    }
    getOrCreateSnapshot(cwdKey) {
        const existing = this.snapshots.get(cwdKey);
        if (existing) {
            return existing;
        }
        const created = this.createLoadingEntries();
        this.snapshots.set(cwdKey, created);
        return created;
    }
    resetSnapshotToLoading(cwdKey, providers) {
        const snapshot = this.getOrCreateSnapshot(cwdKey);
        const loadingEntries = this.createLoadingEntries();
        if (!providers) {
            snapshot.clear();
            for (const [provider, entry] of loadingEntries) {
                snapshot.set(provider, entry);
            }
            return snapshot;
        }
        for (const provider of providers) {
            const loadingEntry = loadingEntries.get(provider);
            if (!loadingEntry)
                continue;
            const existing = snapshot.get(provider);
            snapshot.set(provider, {
                ...loadingEntry,
                models: existing?.models,
                modes: existing?.modes,
                fetchedAt: existing?.fetchedAt,
            });
        }
        return snapshot;
    }
    getProviderIds() {
        return Object.keys(this.providerRegistry);
    }
    resolveRefreshProviders(providers) {
        if (!providers || providers.length === 0) {
            return undefined;
        }
        const providerIds = new Set(this.getProviderIds());
        return Array.from(new Set(providers)).filter((provider) => providerIds.has(provider));
    }
}
export function resolveSnapshotCwd(cwd) {
    const trimmed = cwd?.trim();
    if (!trimmed) {
        return homedir();
    }
    const expanded = trimmed === "~" || trimmed.startsWith("~/") ? `${homedir()}${trimmed.slice(1)}` : trimmed;
    return resolve(expanded);
}
function resolveGlobalSnapshotCwd() {
    return resolveSnapshotCwd();
}
function entriesToArray(entries) {
    return Array.from(entries.values(), cloneEntry);
}
function cloneEntry(entry) {
    return {
        ...entry,
        models: entry.models?.map((model) => ({ ...model })),
        modes: entry.modes?.map((mode) => ({ ...mode })),
    };
}
function toErrorMessage(error) {
    if (error instanceof Error && error.message) {
        return error.message;
    }
    if (typeof error === "string" && error) {
        return error;
    }
    return "Unknown error";
}
//# sourceMappingURL=provider-snapshot-manager.js.map