import { getLogger } from '../logging/logger.js';
import { mergeConfigsWithProtection, getPopulatedFields, createInstanceFromConfig, normalizeOriginUrl, } from './mapping.js';
import { DwJsonSource, MobifySource, PackageJsonSource } from './sources/index.js';
import { ResolvedConfigImpl } from './resolved-config.js';
import { globalConfigSourceRegistry } from './config-source-registry.js';
/**
 * Credential groups that must come from the same source.
 *
 * When merging configuration, if any field in a group is already set by a
 * higher-priority source, all fields in that group from lower-priority
 * sources are skipped. This prevents mixing credentials that don't belong together.
 */
const CREDENTIAL_GROUPS = [
    ['clientId', 'clientSecret'],
    ['username', 'password'],
    ['slasClientId', 'slasClientSecret'],
];
/**
 * Get the set of credential groups that are already claimed in the config.
 *
 * A group is "claimed" if any of its fields are set.
 *
 * @param config - The current configuration
 * @returns Set of group indices that are claimed
 */
function getClaimedCredentialGroups(config) {
    const claimed = new Set();
    for (let i = 0; i < CREDENTIAL_GROUPS.length; i++) {
        const group = CREDENTIAL_GROUPS[i];
        if (group.some((f) => config[f] !== undefined)) {
            claimed.add(i);
        }
    }
    return claimed;
}
/**
 * Check if a field belongs to a credential group in the claimed set.
 *
 * @param field - The field name to check
 * @param claimedGroups - Set of group indices that are already claimed
 * @returns true if the field's credential group is claimed
 */
function isFieldInClaimedGroup(field, claimedGroups) {
    const groupIndex = CREDENTIAL_GROUPS.findIndex((g) => g.includes(field));
    if (groupIndex === -1)
        return false;
    return claimedGroups.has(groupIndex);
}
/**
 * Resolves configuration from multiple sources with consistent behavior.
 *
 * ConfigResolver is the preferred high-level API for loading B2C configuration.
 * It provides:
 * - Consistent hostname mismatch protection across SDK and CLI
 * - Extensibility via the ConfigSource interface
 * - Convenience methods for creating B2CInstance and auth credentials
 *
 * ## Resolution Priority
 *
 * Configuration is resolved with the following precedence (highest to lowest):
 * 1. Explicit overrides (passed to resolve methods)
 * 2. Sources in order (dw.json, ~/.mobify by default)
 *
 * ## Usage
 *
 * ```typescript
 * import { createConfigResolver } from '@salesforce/b2c-tooling-sdk/config';
 *
 * const resolver = createConfigResolver();
 *
 * // Simple resolution
 * const { config, warnings } = resolver.resolve({
 *   hostname: process.env.SFCC_SERVER,
 *   clientId: process.env.SFCC_CLIENT_ID,
 * });
 *
 * // Create B2CInstance directly
 * const instance = resolver.createInstance({ hostname: '...' });
 *
 * // Get auth credentials for use with resolveAuthStrategy
 * const credentials = resolver.createAuthCredentials({ clientId: '...' });
 * ```
 *
 * ## Custom Sources
 *
 * You can provide custom configuration sources:
 *
 * ```typescript
 * import { ConfigResolver } from '@salesforce/b2c-tooling-sdk/config';
 *
 * class MySource implements ConfigSource {
 *   name = 'my-source';
 *   load(options) { return { hostname: 'custom.example.com' }; }
 * }
 *
 * const resolver = new ConfigResolver([new MySource()]);
 * ```
 */
export class ConfigResolver {
    sources;
    /**
     * Creates a new ConfigResolver.
     *
     * @param sources - Custom configuration sources. If not provided, uses default sources (dw.json, ~/.mobify, package.json).
     *                  Sources are automatically sorted by priority (lower number = higher priority).
     */
    constructor(sources) {
        const configSources = sources ?? [new DwJsonSource(), new MobifySource(), new PackageJsonSource()];
        // Sort sources by priority (lower number = higher priority, undefined = 0)
        this.sources = [...configSources].sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0));
    }
    /**
     * Resolves configuration from all sources.
     *
     * @param overrides - Explicit values that take highest priority
     * @param options - Resolution options
     * @returns Resolution result with config, warnings, and source info
     *
     * @example
     * ```typescript
     * const { config, warnings, sources } = resolver.resolve(
     *   { hostname: process.env.SFCC_SERVER },
     *   { instance: 'staging' }
     * );
     *
     * if (warnings.length > 0) {
     *   console.warn('Config warnings:', warnings);
     * }
     * ```
     */
    async resolve(overrides = {}, options = {}) {
        const sourceInfos = [];
        const sourceWarnings = [];
        const baseConfig = {};
        const hostnameProtection = options.hostnameProtection !== false;
        // Create enriched options that will be updated with accumulated config values.
        // This allows later sources (like plugins) to use values discovered by earlier sources (like dw.json).
        // CLI-provided options always take precedence over accumulated values.
        const enrichedOptions = { ...options };
        // Seed enriched options from overrides (CLI flags) so that all sources—including
        // the first one—can see CLI-provided values like accountManagerHost.
        if (!enrichedOptions.accountManagerHost && overrides.accountManagerHost) {
            enrichedOptions.accountManagerHost = overrides.accountManagerHost;
        }
        if (!enrichedOptions.cloudOrigin && overrides.mrtOrigin) {
            enrichedOptions.cloudOrigin = normalizeOriginUrl(overrides.mrtOrigin);
        }
        // Load from each source in order, merging results
        // Earlier sources have higher priority - later sources only fill in missing values
        for (const source of this.sources) {
            let result;
            try {
                result = await source.load(enrichedOptions);
            }
            catch (error) {
                // Source threw an error (e.g., malformed config file) - create warning and continue
                const message = error instanceof Error ? error.message : String(error);
                sourceWarnings.push({
                    code: 'SOURCE_ERROR',
                    message: `Failed to load configuration from ${source.name}: ${message}`,
                    details: { source: source.name, error: message },
                });
                continue;
            }
            if (result && result.config) {
                const { config: sourceConfig, location } = result;
                const fields = getPopulatedFields(sourceConfig);
                if (fields.length > 0) {
                    // Early hostname mismatch detection: if this source provides a hostname
                    // that conflicts with the override, skip this source entirely.
                    // This prevents instance-bound sources from blocking fields in later
                    // non-instance-bound sources (e.g., password-store providing shortCode).
                    if (hostnameProtection &&
                        overrides.hostname &&
                        sourceConfig.hostname &&
                        sourceConfig.hostname !== overrides.hostname) {
                        sourceWarnings.push({
                            code: 'HOSTNAME_MISMATCH',
                            message: `Server override "${overrides.hostname}" differs from config file "${sourceConfig.hostname}". Config file values ignored.`,
                            details: {
                                providedHostname: overrides.hostname,
                                configHostname: sourceConfig.hostname,
                            },
                        });
                        sourceInfos.push({
                            name: source.name,
                            location,
                            fields: [],
                            fieldsIgnored: fields,
                        });
                        const logger = getLogger();
                        logger.trace({
                            source: source.name,
                            location,
                            fieldsIgnored: fields,
                        }, `[${source.name}] Skipped due to hostname mismatch`);
                        continue;
                    }
                    // Capture which credential groups are already claimed BEFORE processing this source
                    // This allows a single source to provide complete credential pairs
                    const claimedGroups = getClaimedCredentialGroups(baseConfig);
                    // Track which fields are ignored during merge
                    const fieldsIgnored = [];
                    // Merge: source values fill in gaps (don't override existing values)
                    for (const [key, value] of Object.entries(sourceConfig)) {
                        if (value === undefined)
                            continue;
                        const fieldKey = key;
                        // Skip if already set by higher-priority source
                        if (baseConfig[fieldKey] !== undefined) {
                            fieldsIgnored.push(fieldKey);
                            continue;
                        }
                        // Skip if this field's credential group was already claimed by a higher-priority source
                        // This prevents mixing credentials from different sources
                        if (isFieldInClaimedGroup(key, claimedGroups)) {
                            fieldsIgnored.push(fieldKey);
                            continue;
                        }
                        baseConfig[key] = value;
                    }
                    sourceInfos.push({
                        name: source.name,
                        location,
                        fields,
                        fieldsIgnored: fieldsIgnored.length > 0 ? fieldsIgnored : undefined,
                    });
                    const logger = getLogger();
                    logger.trace({
                        source: source.name,
                        location,
                        fields,
                        fieldsIgnored: fieldsIgnored.length > 0 ? fieldsIgnored : undefined,
                    }, `[${source.name}] Contributed fields`);
                    // Enrich options with accumulated config values for subsequent sources.
                    // Only set if not already provided via CLI options or overrides.
                    if (!enrichedOptions.accountManagerHost && baseConfig.accountManagerHost) {
                        enrichedOptions.accountManagerHost = baseConfig.accountManagerHost;
                    }
                    if (!enrichedOptions.cloudOrigin && baseConfig.mrtOrigin) {
                        enrichedOptions.cloudOrigin = baseConfig.mrtOrigin;
                    }
                }
            }
        }
        // Apply overrides with hostname mismatch protection.
        // Instance-bound sources with conflicting hostnames were already skipped above,
        // so baseConfig only contains non-instance-bound fields. The merge handles
        // the case where no source provided a hostname (no mismatch to detect).
        const { config, warnings: mergeWarnings } = mergeConfigsWithProtection(overrides, baseConfig, {
            hostnameProtection: options.hostnameProtection,
        });
        // Normalize mrtOrigin to ensure it always has an https:// prefix.
        // Users may provide a bare hostname (e.g., "cloud.mobify.com") or a full URL.
        config.mrtOrigin = normalizeOriginUrl(config.mrtOrigin);
        // Combine source warnings with merge warnings
        const warnings = [...sourceWarnings, ...mergeWarnings];
        return { config, warnings, sources: sourceInfos };
    }
    /**
     * Creates a B2CInstance from resolved configuration.
     *
     * This is a convenience method that combines configuration resolution
     * with B2CInstance creation.
     *
     * @param overrides - Explicit values that take highest priority
     * @param options - Resolution options
     * @returns Configured B2CInstance
     * @throws Error if hostname is not available in resolved config
     *
     * @example
     * ```typescript
     * const instance = resolver.createInstance({
     *   clientId: process.env.SFCC_CLIENT_ID,
     *   clientSecret: process.env.SFCC_CLIENT_SECRET,
     * });
     *
     * await instance.webdav.put('path/file.txt', content);
     * ```
     */
    async createInstance(overrides = {}, options = {}) {
        const { config, warnings } = await this.resolve(overrides, options);
        // Log warnings (in production, this would use the SDK logger)
        for (const warning of warnings) {
            // Could integrate with getLogger() here if desired
            console.warn(`[ConfigResolver] ${warning.message}`);
        }
        return createInstanceFromConfig(config);
    }
    /**
     * Creates auth credentials from resolved configuration.
     *
     * The returned credentials can be used with `resolveAuthStrategy()`
     * to automatically select the best authentication method.
     *
     * @param overrides - Explicit values that take highest priority
     * @param options - Resolution options
     * @returns Auth credentials suitable for resolveAuthStrategy()
     *
     * @example
     * ```typescript
     * import { resolveAuthStrategy } from '@salesforce/b2c-tooling-sdk';
     *
     * const credentials = resolver.createAuthCredentials({
     *   clientId: process.env.SFCC_CLIENT_ID,
     * });
     *
     * const strategy = resolveAuthStrategy(credentials);
     * ```
     */
    async createAuthCredentials(overrides = {}, options = {}) {
        const { config } = await this.resolve(overrides, options);
        return {
            clientId: config.clientId,
            clientSecret: config.clientSecret,
            scopes: config.scopes,
            username: config.username,
            password: config.password,
            apiKey: config.mrtApiKey,
        };
    }
}
/**
 * Creates a ConfigResolver with default sources (dw.json, ~/.mobify).
 *
 * This is the recommended way to create a ConfigResolver for most use cases.
 *
 * @returns ConfigResolver with default configuration sources
 *
 * @example
 * ```typescript
 * import { createConfigResolver } from '@salesforce/b2c-tooling-sdk/config';
 *
 * const resolver = createConfigResolver();
 * const { config } = resolver.resolve({ hostname: 'example.com' });
 * ```
 */
export function createConfigResolver() {
    return new ConfigResolver();
}
/**
 * Resolves configuration from multiple sources and returns a rich config object.
 *
 * This is the preferred high-level API for configuration resolution. It returns
 * a {@link ResolvedB2CConfig} object with validation methods and factory methods
 * for creating SDK objects.
 *
 * ## Resolution Priority
 *
 * 1. Explicit overrides (passed as first argument)
 * 2. Default sources (dw.json, ~/.mobify)
 * 3. Custom sources (via options.sources)
 *
 * ## Example
 *
 * ```typescript
 * import { resolveConfig } from '@salesforce/b2c-tooling-sdk/config';
 *
 * const config = resolveConfig({
 *   hostname: process.env.SFCC_SERVER,
 *   clientId: process.env.SFCC_CLIENT_ID,
 *   mrtApiKey: process.env.MRT_API_KEY,
 * });
 *
 * // Check what's available and create objects
 * if (config.hasB2CInstanceConfig()) {
 *   const instance = config.createB2CInstance();
 *   await instance.webdav.propfind('Cartridges');
 * }
 *
 * if (config.hasMrtConfig()) {
 *   const mrtAuth = config.createMrtAuth();
 * }
 * ```
 *
 * @param overrides - Explicit configuration values (highest priority)
 * @param options - Resolution options
 * @returns Resolved configuration with factory methods
 */
export async function resolveConfig(overrides = {}, options = {}) {
    // Globally registered sources (from plugins via B2CPluginManager or direct SDK registration).
    // Always included regardless of replaceDefaultSources — global sources are explicitly registered
    // by plugins and should always participate, matching the middleware registry behavior.
    const globalSources = globalConfigSourceRegistry.getSources();
    // Build sources list with priority ordering:
    // 1. sourcesBefore (high priority - override defaults)
    // 2. default sources (dw.json, ~/.mobify, package.json)
    // 3. sourcesAfter (low priority - fill gaps)
    // 4. global registry sources (sorted by their own priority)
    let sources;
    if (options.replaceDefaultSources) {
        // Replace mode: only use provided sources (no default dw.json/~/.mobify/package.json)
        sources = [...(options.sourcesBefore ?? []), ...(options.sourcesAfter ?? []), ...globalSources];
    }
    else {
        // Normal mode: before + defaults + after + global
        const defaultSources = [new DwJsonSource(), new MobifySource(), new PackageJsonSource()];
        // Combine all sources
        sources = [...(options.sourcesBefore ?? []), ...defaultSources, ...(options.sourcesAfter ?? []), ...globalSources];
    }
    // ConfigResolver constructor will sort by priority
    const resolver = new ConfigResolver(sources);
    const { config, warnings, sources: sourceInfos } = await resolver.resolve(overrides, options);
    return new ResolvedConfigImpl(config, warnings, sourceInfos);
}
//# sourceMappingURL=resolver.js.map