/**
 * @license
 * Copyright 2025 Vybestack LLC
 * SPDX-License-Identifier: Apache-2.0
 */
/**
 * Authentication precedence utility for providers
 *
 * Implements the authentication precedence chain:
 * 1. Provider-specific auth-key/keyfile (from getProviderSettings)
 * 2. Constructor API key
 * 3. Global auth-key/keyfile (from settings when activeProvider matches)
 * 4. Environment variables
 * 5. OAuth (if enabled)
 */
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import * as os from 'node:os';
import { getActiveProviderRuntimeContext, } from '../runtime/providerRuntimeContext.js';
const runtimeScopedStates = new Map();
let legacyRuntimeScopeWarningEmitted = false;
function maskToken(token) {
    if (!token) {
        return '[redacted]';
    }
    const trimmed = token.replace(/\s+/g, '');
    if (trimmed.length <= 4) {
        return '*'.repeat(trimmed.length);
    }
    if (trimmed.length <= 8) {
        return `${trimmed.slice(0, 2)}***${trimmed.slice(-2)}`;
    }
    return `${trimmed.slice(0, 4)}…${trimmed.slice(-4)}`;
}
function normalizeExpiry(expiry) {
    if (!expiry || Number.isNaN(expiry)) {
        return undefined;
    }
    return expiry > 1_000_000_000_000 ? expiry : expiry * 1000;
}
function resolveProfileId(settingsService) {
    // Prefer explicit profile tracking, fall back to stored value or default.
    const maybeGetName = settingsService.getCurrentProfileName;
    if (typeof maybeGetName === 'function') {
        const profileName = maybeGetName.call(settingsService);
        if (profileName && profileName.trim()) {
            return profileName.trim();
        }
    }
    const currentProfile = settingsService.get('currentProfile');
    if (typeof currentProfile === 'string' && currentProfile.trim()) {
        return currentProfile.trim();
    }
    return 'default';
}
function buildCacheKey(runtimeId, providerId, profileId) {
    return `${runtimeId}::${providerId}::${profileId}`;
}
function toPublicEntry(entry, overrides) {
    const summary = {
        key: entry.key,
        providerId: entry.providerId,
        profileId: entry.profileId,
        runtimeAuthScopeId: entry.runtimeAuthScopeId,
        createdAt: entry.createdAt,
        expiresAt: entry.expiresAt,
        preview: maskToken(entry.token),
        stale: entry.stale,
    };
    return overrides ? { ...summary, ...overrides } : summary;
}
function updateMetadataEntry(state, entry, summary) {
    const snapshot = summary ?? toPublicEntry(entry);
    const existingIndex = state.metadata.cacheEntries.findIndex((item) => item.key === snapshot.key);
    if (existingIndex >= 0) {
        state.metadata.cacheEntries[existingIndex] = snapshot;
    }
    else {
        state.metadata.cacheEntries.push(snapshot);
    }
    state.metadata.metrics.lastUpdated = Date.now();
}
function ensureRuntimeState(context) {
    const runtimeId = context.runtimeId ?? 'legacy-singleton';
    let state = runtimeScopedStates.get(runtimeId);
    if (!state) {
        const metadata = {
            runtimeAuthScopeId: runtimeId,
            cacheEntries: [],
            cancellationHooks: [],
            revokedTokens: [],
            metrics: {
                hits: 0,
                misses: 0,
                lastUpdated: Date.now(),
            },
        };
        state = {
            runtimeAuthScopeId: runtimeId,
            entries: new Map(),
            metadata,
            settingsSubscriptions: [],
        };
        runtimeScopedStates.set(runtimeId, state);
    }
    if (!context.metadata) {
        context.metadata = {};
    }
    context.metadata.runtimeAuthScope = state.metadata;
    if (runtimeId === 'legacy-singleton' &&
        !legacyRuntimeScopeWarningEmitted &&
        process.env.DEBUG) {
        console.warn('AuthPrecedenceResolver invoked without runtimeId; using legacy singleton auth cache.');
        legacyRuntimeScopeWarningEmitted = true;
    }
    return state;
}
function recordCacheHit(state) {
    state.metadata.metrics.hits += 1;
    state.metadata.metrics.lastUpdated = Date.now();
}
function recordCacheMiss(state) {
    state.metadata.metrics.misses += 1;
    state.metadata.metrics.lastUpdated = Date.now();
}
function getValidCachedEntry(state, providerId, profileId) {
    const cacheKey = buildCacheKey(state.runtimeAuthScopeId, providerId, profileId);
    const entry = state.entries.get(cacheKey);
    if (!entry) {
        return null;
    }
    if (entry.stale) {
        return null;
    }
    if (entry.expiresAt && entry.expiresAt <= Date.now()) {
        invalidateEntry(state, cacheKey, 'expired');
        return null;
    }
    entry.updatedAt = Date.now();
    updateMetadataEntry(state, entry);
    return entry;
}
function registerSettingsSubscriptions(state, settingsService, providerId) {
    if (state.settingsService === settingsService) {
        return;
    }
    for (const unsubscribe of state.settingsSubscriptions) {
        try {
            unsubscribe();
        }
        catch (error) {
            if (process.env.DEBUG) {
                console.debug('Failed to remove runtime auth scope subscription:', error);
            }
        }
    }
    state.settingsSubscriptions = [];
    state.settingsService = settingsService;
    const handleProviderChange = (...args) => {
        const [event] = args;
        const payload = (event ?? {});
        if (payload.provider === providerId) {
            invalidateMatchingEntries(state, (entry) => entry.providerId === providerId, 'provider-change');
        }
    };
    settingsService.on('provider-change', handleProviderChange);
    state.settingsSubscriptions.push(() => settingsService.off('provider-change', handleProviderChange));
    const handleProfileChange = (...args) => {
        const [event] = args;
        const payload = (event ?? {});
        if (payload.key === 'currentProfile') {
            invalidateMatchingEntries(state, () => true, 'profile-change');
        }
    };
    settingsService.on('change', handleProfileChange);
    state.settingsSubscriptions.push(() => settingsService.off('change', handleProfileChange));
    const handleCleared = (..._args) => {
        invalidateMatchingEntries(state, () => true, 'settings-cleared');
    };
    settingsService.on('cleared', handleCleared);
    state.settingsSubscriptions.push(() => settingsService.off('cleared', handleCleared));
}
function invalidateMatchingEntries(state, predicate, reason) {
    const summaries = [];
    for (const entry of [...state.entries.values()]) {
        if (predicate(entry)) {
            summaries.push(invalidateEntry(state, entry.key, reason));
        }
    }
    return summaries;
}
function storeRuntimeScopedToken(state, providerId, profileId, token, oauthToken) {
    const cacheKey = buildCacheKey(state.runtimeAuthScopeId, providerId, profileId);
    const expiresAt = normalizeExpiry(oauthToken?.expiry ?? null);
    const now = Date.now();
    let entry = state.entries.get(cacheKey);
    if (!entry) {
        entry = {
            key: cacheKey,
            providerId,
            profileId,
            runtimeAuthScopeId: state.runtimeAuthScopeId,
            token,
            createdAt: now,
            updatedAt: now,
            expiresAt,
            stale: false,
        };
        state.entries.set(cacheKey, entry);
    }
    else {
        entry.token = token;
        entry.updatedAt = now;
        entry.expiresAt = expiresAt;
        entry.stale = false;
    }
    if (entry.cancellationHook) {
        state.metadata.cancellationHooks = state.metadata.cancellationHooks.filter((hook) => hook !== entry.cancellationHook);
    }
    const cancellationHook = () => {
        invalidateEntry(state, cacheKey, 'cancellation-hook');
    };
    entry.cancellationHook = cancellationHook;
    state.metadata.cancellationHooks.push(cancellationHook);
    updateMetadataEntry(state, entry, toPublicEntry(entry));
}
function invalidateEntry(state, cacheKey, reason) {
    const entry = state.entries.get(cacheKey);
    if (!entry) {
        const summary = {
            key: cacheKey,
            providerId: 'unknown',
            profileId: 'default',
            runtimeAuthScopeId: state.runtimeAuthScopeId,
            preview: '[revoked]',
            createdAt: Date.now(),
            expiresAt: undefined,
            stale: true,
            reason,
        };
        state.metadata.revokedTokens.push(summary);
        state.metadata.metrics.lastUpdated = Date.now();
        return summary;
    }
    entry.stale = true;
    entry.updatedAt = Date.now();
    const summary = toPublicEntry(entry, { stale: true, reason });
    state.entries.delete(cacheKey);
    updateMetadataEntry(state, entry, summary);
    if (entry.cancellationHook) {
        state.metadata.cancellationHooks = state.metadata.cancellationHooks.filter((hook) => hook !== entry.cancellationHook);
    }
    state.metadata.revokedTokens.push(summary);
    state.metadata.metrics.lastUpdated = Date.now();
    entry.token = '';
    entry.expiresAt = undefined;
    entry.cancellationHook = undefined;
    return summary;
}
/**
 * @plan PLAN-20251018-STATELESSPROVIDER2.P18
 * @requirement REQ-SP2-004
 * @pseudocode auth-runtime-scope.md lines 7-7
 * Flush scoped credentials for a runtime and return revocation metadata.
 */
export function flushRuntimeAuthScope(runtimeId) {
    const state = runtimeScopedStates.get(runtimeId);
    if (!state) {
        return { runtimeId, revokedTokens: [] };
    }
    const revokedTokens = [];
    for (const key of [...state.entries.keys()]) {
        revokedTokens.push(invalidateEntry(state, key, 'runtime-dispose'));
    }
    for (const unsubscribe of state.settingsSubscriptions) {
        try {
            unsubscribe();
        }
        catch (error) {
            if (process.env.DEBUG) {
                console.debug('Failed to clean runtime auth scope subscription:', error);
            }
        }
    }
    state.settingsSubscriptions = [];
    state.settingsService = undefined;
    state.metadata.cancellationHooks = [];
    state.metadata.cacheEntries = state.metadata.cacheEntries.map((entry) => ({
        ...entry,
        stale: true,
    }));
    state.metadata.metrics.lastUpdated = Date.now();
    runtimeScopedStates.delete(runtimeId);
    return { runtimeId, revokedTokens };
}
/**
 * Check if authOnly mode is enabled (OAuth-only authentication)
 * @param value The authOnly setting value from SettingsService
 * @returns true if authOnly is enabled, false otherwise
 */
function isAuthOnlyEnabled(value) {
    if (typeof value === 'boolean') {
        return value;
    }
    if (typeof value === 'string') {
        const normalized = value.trim().toLowerCase();
        if (normalized === 'true') {
            return true;
        }
        if (normalized === 'false') {
            return false;
        }
    }
    return false;
}
export class AuthPrecedenceResolver {
    config;
    oauthManager;
    settingsService;
    constructor(config, oauthManager, settingsService) {
        this.config = config;
        this.oauthManager = oauthManager;
        this.settingsService = settingsService;
    }
    /**
     * @plan PLAN-20251018-STATELESSPROVIDER2.P06
     * @requirement REQ-SP2-001
     * @pseudocode base-provider-call-contract.md lines 1-2
     */
    setSettingsService(settingsService) {
        if (!settingsService) {
            this.settingsService = undefined;
            return;
        }
        this.settingsService = settingsService;
    }
    /**
     * @plan PLAN-20251018-STATELESSPROVIDER2.P06
     * @requirement REQ-SP2-001
     * @pseudocode base-provider-call-contract.md lines 1-2
     */
    resolveSettingsService(override) {
        if (override) {
            return override;
        }
        if (this.settingsService) {
            return this.settingsService;
        }
        const context = getActiveProviderRuntimeContext();
        if (!context?.settingsService) {
            throw new Error('Active provider runtime context not available');
        }
        this.settingsService = context.settingsService;
        return this.settingsService;
    }
    /**
     * @plan PLAN-20251018-STATELESSPROVIDER2.P06
     * @requirement REQ-SP2-001
     * @pseudocode base-provider-call-contract.md lines 1-3
     * Resolves authentication using the full precedence chain
     * Returns the first available authentication method or null if none found
     */
    async resolveAuthentication(options) {
        const includeOAuth = options?.includeOAuth ?? false;
        const settingsService = this.resolveSettingsService(options?.settingsService ?? undefined);
        const providerKey = this.normalizeProviderId(this.config.providerId);
        const authOnly = isAuthOnlyEnabled(settingsService.get('authOnly'));
        if (!authOnly) {
            const providerSettings = providerKey && typeof settingsService.getProviderSettings === 'function'
                ? settingsService.getProviderSettings(providerKey)
                : undefined;
            const providerAuthKey = this.normalizeAuthValue(providerSettings?.['auth-key'] ?? providerSettings?.apiKey);
            if (providerAuthKey) {
                return providerAuthKey;
            }
            const providerAuthKeyfile = this.normalizeAuthValue(providerSettings?.['auth-keyfile'] ?? providerSettings?.apiKeyfile);
            if (providerAuthKeyfile) {
                const keyFromFile = await this.readKeyFile(providerAuthKeyfile);
                if (keyFromFile) {
                    return keyFromFile;
                }
            }
            const normalizedConfigKey = this.normalizeAuthValue(this.config.apiKey ?? null);
            if (normalizedConfigKey) {
                return normalizedConfigKey;
            }
            if (this.shouldUseGlobalAuth(settingsService, providerKey)) {
                const authKey = this.normalizeAuthValue(settingsService.get('auth-key'));
                if (authKey) {
                    return authKey;
                }
                const authKeyfile = this.normalizeAuthValue(settingsService.get('auth-keyfile'));
                if (authKeyfile) {
                    const keyFromFile = await this.readKeyFile(authKeyfile);
                    if (keyFromFile) {
                        return keyFromFile;
                    }
                }
            }
            if (this.config.envKeyNames && this.config.envKeyNames.length > 0) {
                for (const envVarName of this.config.envKeyNames) {
                    const envValue = this.normalizeAuthValue(process.env[envVarName]);
                    if (envValue) {
                        return envValue;
                    }
                }
            }
        }
        if (includeOAuth &&
            this.config.isOAuthEnabled &&
            this.config.supportsOAuth &&
            this.oauthManager &&
            this.config.oauthProvider) {
            const providerId = this.resolveProviderIdentifier(providerKey);
            const profileId = resolveProfileId(settingsService);
            let runtimeContext = null;
            let runtimeState = null;
            try {
                runtimeContext = getActiveProviderRuntimeContext();
                runtimeState = ensureRuntimeState(runtimeContext);
                registerSettingsSubscriptions(runtimeState, settingsService, providerId);
            }
            catch {
                runtimeContext = null;
                runtimeState = null;
            }
            const managerWithCheck = this.oauthManager;
            if (managerWithCheck.isOAuthEnabled &&
                typeof managerWithCheck.isOAuthEnabled === 'function') {
                let isEnabledByManager;
                try {
                    const enabledResult = managerWithCheck.isOAuthEnabled(this.config.oauthProvider);
                    isEnabledByManager =
                        typeof enabledResult === 'boolean'
                            ? enabledResult
                            : await enabledResult;
                }
                catch (error) {
                    if (process.env.DEBUG) {
                        console.debug(`Failed to determine OAuth enablement for ${this.config.oauthProvider}:`, error);
                    }
                    isEnabledByManager = undefined;
                }
                if (isEnabledByManager === false) {
                    if (runtimeState) {
                        const cacheKey = buildCacheKey(runtimeState.runtimeAuthScopeId, providerId, profileId);
                        if (runtimeState.entries.has(cacheKey)) {
                            invalidateEntry(runtimeState, cacheKey, 'oauth-disabled');
                        }
                    }
                    return null;
                }
            }
            if (runtimeState) {
                const cachedEntry = getValidCachedEntry(runtimeState, providerId, profileId);
                if (cachedEntry) {
                    recordCacheHit(runtimeState);
                    return cachedEntry.token;
                }
                recordCacheMiss(runtimeState);
            }
            try {
                const requestMetadata = {
                    runtimeAuthScopeId: runtimeState?.runtimeAuthScopeId ?? 'no-runtime',
                    providerId,
                    profileId,
                    cliScope: runtimeContext?.metadata &&
                        typeof runtimeContext.metadata === 'object'
                        ? runtimeContext.metadata
                        : undefined,
                    runtimeMetadata: runtimeContext?.metadata &&
                        typeof runtimeContext.metadata === 'object'
                        ? runtimeContext.metadata
                        : undefined,
                };
                const token = await this.oauthManager.getToken(this.config.oauthProvider, requestMetadata);
                if (token) {
                    let oauthToken;
                    if (typeof this.oauthManager.getOAuthToken === 'function') {
                        try {
                            oauthToken = await this.oauthManager.getOAuthToken(this.config.oauthProvider, requestMetadata);
                        }
                        catch (tokenError) {
                            if (process.env.DEBUG) {
                                console.debug(`Failed to fetch OAuth token metadata for ${this.config.oauthProvider}:`, tokenError);
                            }
                            oauthToken = null;
                        }
                    }
                    if (runtimeState) {
                        storeRuntimeScopedToken(runtimeState, providerId, profileId, token, oauthToken ?? null);
                    }
                    return token;
                }
            }
            catch (error) {
                if (process.env.DEBUG) {
                    console.warn(`Failed to get OAuth token for ${this.config.oauthProvider}:`, error);
                }
            }
        }
        return null;
    }
    /**
     * @plan PLAN-20251018-STATELESSPROVIDER2.P06
     * @requirement REQ-SP2-001
     * @pseudocode base-provider-call-contract.md lines 1-3
     * Check if any authentication method is available without triggering OAuth
     */
    async hasNonOAuthAuthentication(options) {
        const auth = await this.resolveAuthentication({
            ...options,
            includeOAuth: false,
        });
        return auth !== null;
    }
    /**
     * @plan PLAN-20251018-STATELESSPROVIDER2.P06
     * @requirement REQ-SP2-001
     * @pseudocode base-provider-call-contract.md lines 1-3
     * Check if OAuth is the only available authentication method
     */
    async isOAuthOnlyAvailable(options) {
        const hasNonOAuth = await this.hasNonOAuthAuthentication(options);
        return (!hasNonOAuth &&
            this.config.isOAuthEnabled === true &&
            this.config.supportsOAuth === true);
    }
    /**
     * @plan PLAN-20251018-STATELESSPROVIDER2.P06
     * @requirement REQ-SP2-001
     * @pseudocode base-provider-call-contract.md lines 1-3
     * Get authentication method name for debugging/logging
     */
    async getAuthMethodName(options) {
        const settingsService = this.resolveSettingsService(options?.settingsService ?? undefined);
        // Check precedence levels and return method name
        const authKey = settingsService.get('auth-key');
        if (authKey && typeof authKey === 'string' && authKey.trim() !== '') {
            return 'command-key';
        }
        const authKeyfile = settingsService.get('auth-keyfile');
        if (authKeyfile && typeof authKeyfile === 'string') {
            try {
                const keyFromFile = await this.readKeyFile(authKeyfile);
                if (keyFromFile) {
                    return 'command-keyfile';
                }
            }
            catch {
                // Ignore errors for method detection
            }
        }
        if (this.config.apiKey &&
            typeof this.config.apiKey === 'string' &&
            this.config.apiKey.trim() !== '') {
            return 'constructor-apikey';
        }
        if (this.config.envKeyNames && this.config.envKeyNames.length > 0) {
            for (const envVarName of this.config.envKeyNames) {
                const envValue = process.env[envVarName];
                if (envValue && envValue.trim() !== '') {
                    return `env-${envVarName.toLowerCase()}`;
                }
            }
        }
        if (this.config.isOAuthEnabled &&
            this.config.supportsOAuth &&
            this.oauthManager &&
            this.config.oauthProvider) {
            try {
                const isAuthenticated = await this.oauthManager.isAuthenticated(this.config.oauthProvider);
                if (isAuthenticated) {
                    return `oauth-${this.config.oauthProvider}`;
                }
            }
            catch {
                // Ignore errors for method detection
            }
        }
        return null;
    }
    normalizeAuthValue(value) {
        if (typeof value !== 'string') {
            return undefined;
        }
        const trimmed = value.trim();
        if (!trimmed || trimmed.toLowerCase() === 'none') {
            return undefined;
        }
        return trimmed;
    }
    normalizeProviderId(value) {
        if (typeof value !== 'string') {
            return undefined;
        }
        const trimmed = value.trim();
        return trimmed === '' ? undefined : trimmed;
    }
    resolveProviderIdentifier(preferredProviderId) {
        if (preferredProviderId && preferredProviderId.trim()) {
            return preferredProviderId.trim();
        }
        const oauthProvider = this.normalizeProviderId(this.config.oauthProvider);
        if (oauthProvider) {
            return oauthProvider;
        }
        if (this.config.envKeyNames && this.config.envKeyNames.length > 0) {
            return this.config.envKeyNames[0] ?? 'unknown-provider';
        }
        return 'unknown-provider';
    }
    shouldUseGlobalAuth(settingsService, providerId) {
        if (!providerId) {
            return true;
        }
        const activeProvider = settingsService.get('activeProvider');
        if (typeof activeProvider !== 'string') {
            return true;
        }
        const trimmed = activeProvider.trim();
        if (!trimmed) {
            return true;
        }
        return trimmed === providerId;
    }
    /**
     * Reads API key from a file path, handling tilde expansion, absolute and relative paths
     */
    async readKeyFile(filePath) {
        try {
            // Handle tilde expansion for home directory
            const expandedPath = filePath.startsWith('~')
                ? path.join(os.homedir(), filePath.slice(1))
                : filePath;
            // Handle relative paths from current working directory
            const resolvedPath = path.isAbsolute(expandedPath)
                ? expandedPath
                : path.resolve(process.cwd(), expandedPath);
            const content = await fs.readFile(resolvedPath, 'utf-8');
            const key = content.trim();
            if (key === '') {
                if (process.env.DEBUG) {
                    console.warn(`Key file ${filePath} is empty`);
                }
                return null;
            }
            return key;
        }
        catch (error) {
            if (process.env.DEBUG) {
                console.warn(`Failed to read key file ${filePath}:`, error);
            }
            return null;
        }
    }
    /**
     * Updates the configuration
     */
    updateConfig(newConfig) {
        this.config = { ...this.config, ...newConfig };
    }
    /**
     * Updates the OAuth manager
     */
    updateOAuthManager(oauthManager) {
        this.oauthManager = oauthManager;
    }
}
//# sourceMappingURL=precedence.js.map