import { EventEmitter } from "node:events";
import { homedir } from "node:os";
import { resolve } from "node:path";
import { expandTilde } from "../../utils/path.js";
import { withTimeout } from "../../utils/promise-timeout.js";
import { buildProviderRegistry, shutdownAgentClients, } from "./provider-registry.js";
import { applyMutableProviderConfigToOverrides } from "../daemon-config-store.js";
const DEFAULT_REFRESH_TIMEOUT_MS = 30000;
export class ProviderSnapshotManager {
    constructor(options) {
        this.snapshots = new Map();
        this.providerLoads = new Map();
        this.events = new EventEmitter();
        this.destroyed = false;
        this.logger = options.logger;
        this.workspaceGitService = options.workspaceGitService;
        this.isDev = options.isDev === true;
        this.extraClients = options.extraClients ?? {};
        this.runtimeSettings = options.runtimeSettings;
        this.providerOverrides = options.providerOverrides;
        this.baseProviderOverrides = options.providerOverrides;
        this.refreshTimeoutMs = options.refreshTimeoutMs ?? DEFAULT_REFRESH_TIMEOUT_MS;
        this.providerRegistry = this.buildRegistry();
        this.providerClients = { ...this.extraClients };
    }
    getSnapshot(cwd) {
        const resolvedCwd = resolveSnapshotCwd(cwd);
        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) {
            const loadingEntries = this.createLoadingEntries();
            for (const provider of missingProviders) {
                const loadingEntry = loadingEntries.get(provider);
                if (loadingEntry) {
                    entries.set(provider, loadingEntry);
                }
            }
            void this.warmUp(resolvedCwd, missingProviders);
        }
        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 = resolveSnapshotCwd(options.cwd);
        const providers = this.resolveRefreshProviders(options.providers);
        this.resetSnapshotToLoading(snapshotCwd, providers, { preserveExisting: false });
        this.emitChange(snapshotCwd);
        await this.refreshProviders(snapshotCwd, providers ?? this.getProviderIds());
    }
    async refreshSettingsSnapshot(options = {}) {
        const homeCwd = resolveSnapshotCwd();
        const providers = this.resolveRefreshProviders(options.providers);
        const providersToRefresh = providers ?? this.getProviderIds();
        this.clearCachedProviders(providers);
        this.resetSnapshotToLoading(homeCwd, providers, { preserveExisting: false });
        this.emitChange(homeCwd);
        await this.refreshProviders(homeCwd, providersToRefresh);
    }
    async warmUpSnapshotForCwd(options) {
        const snapshotCwd = resolveSnapshotCwd(options.cwd);
        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);
    }
    listRegisteredProviderIds() {
        return this.getProviderIds();
    }
    hasProvider(provider) {
        return Object.prototype.hasOwnProperty.call(this.providerRegistry, provider);
    }
    getProviderLabel(provider) {
        return this.providerRegistry[provider]?.label ?? provider;
    }
    getAgentManagerProviderState() {
        const providerDefinitions = {};
        const clients = {};
        for (const [provider, definition] of Object.entries(this.providerRegistry)) {
            providerDefinitions[provider] = {
                enabled: definition.enabled,
                derivedFromProviderId: definition.derivedFromProviderId,
            };
            if (definition.enabled) {
                clients[provider] = this.ensureClient(provider, definition);
            }
        }
        for (const [provider, client] of Object.entries(this.extraClients)) {
            if (client) {
                clients[provider] = client;
            }
        }
        return { providerDefinitions, clients };
    }
    ensureClient(provider, definition) {
        const existing = this.providerClients[provider];
        if (existing) {
            return existing;
        }
        const client = definition.createClient(this.logger);
        this.providerClients[provider] = client;
        return client;
    }
    async listProviders(input = {}) {
        const cwd = resolveSnapshotCwd(input.cwd);
        if (input.wait) {
            await this.warmUpSnapshotForCwd({ cwd, providers: input.providers });
        }
        const providerFilter = input.providers ? new Set(input.providers) : null;
        const entries = this.getSnapshot(cwd);
        return providerFilter ? entries.filter((entry) => providerFilter.has(entry.provider)) : entries;
    }
    async getProvider(input) {
        const entry = (await this.listProviders({ ...input, providers: [input.provider] })).find((candidate) => candidate.provider === input.provider);
        if (!entry) {
            throw new Error(`Provider ${input.provider} is not configured`);
        }
        return entry;
    }
    async listModels(input) {
        const entry = await this.getReadyProvider(input);
        return entry.models ?? [];
    }
    async listModes(input) {
        const entry = await this.getReadyProvider(input);
        return entry.modes ?? [];
    }
    async resolveDefaultModel(input) {
        try {
            const trimmed = input.requestedModel?.trim();
            if (trimmed) {
                return trimmed;
            }
            const models = await this.listModels({
                provider: input.provider,
                cwd: input.cwd ? expandTilde(input.cwd) : undefined,
                wait: true,
            });
            const preferred = models.find((model) => model.isDefault) ?? models[0];
            return preferred?.id;
        }
        catch (error) {
            this.logger.warn({ err: error, provider: input.provider }, "Failed to resolve default model");
            return undefined;
        }
    }
    async resolveCreateConfig(input) {
        const entry = await this.getReadyProvider({
            cwd: input.cwd,
            provider: input.provider,
            wait: true,
        });
        const definition = this.requireProvider(input.provider);
        const parent = input.parent ? this.resolveParent(input.parent) : null;
        return definition.resolveCreateConfig({
            provider: input.provider,
            requestedMode: input.requestedMode,
            featureValues: input.featureValues,
            parent,
            unattended: input.unattended || parent?.isUnattended === true,
            availableModes: entry.modes ?? [],
        });
    }
    async getProviderDiagnostic(provider) {
        const client = this.providerClients[provider];
        if (!client) {
            throw new Error(`Provider ${provider} is not configured`);
        }
        const diagnostic = client.getDiagnostic
            ? (await client.getDiagnostic()).diagnostic
            : "No diagnostic available for this provider.";
        return { provider, diagnostic };
    }
    applyMutableProviderConfig(mutableProviders) {
        this.providerOverrides = applyMutableProviderConfigToOverrides(this.baseProviderOverrides, mutableProviders);
        this.providerRegistry = this.buildRegistry();
        this.providerClients = { ...this.extraClients };
        for (const cwd of this.snapshots.keys()) {
            this.providerLoads.delete(cwd);
            this.snapshots.set(cwd, this.reconcileSnapshotForRegistry(cwd));
            this.emitChange(cwd);
        }
        return this.getAgentManagerProviderState();
    }
    on(event, listener) {
        this.events.on(event, listener);
        return this;
    }
    off(event, listener) {
        this.events.off(event, listener);
        return this;
    }
    async shutdown() {
        // Materialize a client per enabled provider so provider-owned resources
        // (background processes, sockets, etc.) get a chance to release even when
        // a given provider hasn't been touched yet during this daemon's lifetime.
        const state = this.getAgentManagerProviderState();
        const clients = Object.values(state.clients).filter((client) => client !== undefined);
        await shutdownAgentClients(clients, this.logger);
    }
    destroy() {
        this.destroyed = true;
        this.events.removeAllListeners();
        this.snapshots.clear();
        this.providerLoads.clear();
    }
    buildRegistry() {
        const registry = buildProviderRegistry(this.logger, {
            runtimeSettings: this.runtimeSettings,
            providerOverrides: this.providerOverrides,
            workspaceGitService: this.workspaceGitService,
            isDev: this.isDev,
        });
        for (const [provider, client] of Object.entries(this.extraClients)) {
            const definition = registry[provider];
            if (!definition)
                continue;
            registry[provider] = {
                ...definition,
                createClient: () => client,
                resolveCreateConfig: client.resolveCreateConfig?.bind(client) ?? definition.resolveCreateConfig,
                isCreateConfigUnattended: client.isCreateConfigUnattended?.bind(client) ?? definition.isCreateConfigUnattended,
                fetchModels: client.listModels.bind(client),
                fetchModes: client.listModes?.bind(client) ?? definition.fetchModes,
            };
        }
        return registry;
    }
    resolveParent(parent) {
        const definition = this.requireProvider(parent.provider);
        return {
            provider: parent.provider,
            modeId: parent.currentModeId,
            isUnattended: definition.isCreateConfigUnattended({
                modeId: parent.currentModeId,
                config: parent.config,
                features: parent.features,
                availableModes: parent.availableModes ?? definition.modes ?? [],
            }),
        };
    }
    async getReadyProvider(input) {
        const entry = await this.getProvider(input);
        if (!entry.enabled) {
            throw new Error(`Provider '${entry.provider}' is disabled`);
        }
        if (entry.status === "ready") {
            return entry;
        }
        if (entry.status === "error") {
            throw new Error(entry.error ?? `Failed to load provider '${entry.provider}'`);
        }
        throw new Error(`Provider '${entry.provider}' is not available`);
    }
    requireProvider(provider) {
        const definition = this.providerRegistry[provider];
        if (!definition) {
            throw new Error(`Provider ${provider} is not configured`);
        }
        return definition;
    }
    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;
    }
    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 });
    }
    clearCachedProviders(providers) {
        const providerSet = providers ? new Set(providers) : null;
        const loadingEntries = this.createLoadingEntries();
        for (const [cwd, providerLoads] of Array.from(this.providerLoads.entries())) {
            if (!providerSet) {
                this.providerLoads.delete(cwd);
                continue;
            }
            for (const provider of providerSet) {
                providerLoads.delete(provider);
            }
            if (providerLoads.size === 0) {
                this.providerLoads.delete(cwd);
            }
        }
        for (const [cwd, snapshot] of this.snapshots.entries()) {
            if (!providerSet) {
                snapshot.clear();
                for (const [provider, entry] of loadingEntries) {
                    snapshot.set(provider, entry);
                }
                this.emitChange(cwd);
                continue;
            }
            let changed = false;
            for (const provider of providerSet) {
                const loadingEntry = loadingEntries.get(provider);
                if (!loadingEntry)
                    continue;
                snapshot.set(provider, loadingEntry);
                changed = true;
            }
            if (changed) {
                this.emitChange(cwd);
            }
        }
    }
    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 = this.ensureClient(provider, definition);
            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, options = {}) {
        const snapshot = this.getOrCreateSnapshot(cwdKey);
        const loadingEntries = this.createLoadingEntries();
        const preserveExisting = options.preserveExisting ?? true;
        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,
                ...(preserveExisting
                    ? {
                        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 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