/*
 * Copyright (c) 2025, Salesforce, Inc.
 * SPDX-License-Identifier: Apache-2
 * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
 */
/**
 * dw.json configuration file loading.
 *
 * This module provides utilities for loading B2C Commerce configuration from
 * dw.json files, the standard configuration format used by B2C development tools.
 *
 * @module config
 */
import * as fsp from 'node:fs/promises';
import * as path from 'node:path';
import { getLogger } from '../logging/logger.js';
import { normalizeConfigKeys } from './mapping.js';
/**
 * Finds dw.json by searching upward from the starting directory.
 *
 * @param projectDirectory - Directory to start searching from (defaults to cwd)
 * @returns Path to dw.json if found, undefined otherwise
 *
 * @example
 * const dwPath = findDwJson();
 * if (dwPath) {
 *   console.log(`Found dw.json at ${dwPath}`);
 * }
 */
export async function findDwJson(projectDirectory = process.cwd()) {
    let dir = projectDirectory;
    const root = path.parse(dir).root;
    while (dir !== root) {
        const dwJsonPath = path.join(dir, 'dw.json');
        try {
            await fsp.access(dwJsonPath);
            return dwJsonPath;
        }
        catch {
            // File doesn't exist, continue searching
        }
        dir = path.dirname(dir);
    }
    return undefined;
}
/**
 * Selects the appropriate config from a multi-config dw.json.
 *
 * Selection priority:
 * 1. Named instance (if `instance` option provided)
 * 2. Config marked as `active: true`
 * 3. Root-level config
 */
function selectConfig(json, instanceName) {
    const logger = getLogger();
    // Single config or no configs array
    if (!Array.isArray(json.configs) || json.configs.length === 0) {
        logger.trace({ selection: 'root', instanceName: json.name }, `[DwJsonSource] Selected config "${json.name ?? 'root'}" (single config)`);
        return json;
    }
    // Find by instance name
    if (instanceName) {
        // Check root first
        if (json.name === instanceName) {
            logger.trace({ selection: 'named', instanceName }, `[DwJsonSource] Selected config "${instanceName}" by name (root)`);
            return json;
        }
        // Then check configs array
        const found = json.configs.find((c) => c.name === instanceName);
        if (found) {
            logger.trace({ selection: 'named', instanceName }, `[DwJsonSource] Selected config "${instanceName}" by name`);
            return found;
        }
        // Instance explicitly requested but not found - return undefined
        logger.trace({ requestedInstance: instanceName }, `[DwJsonSource] Named instance "${instanceName}" not found`);
        return undefined;
    }
    // Find active config
    const activeConfig = json.configs.find((c) => c.active === true);
    if (activeConfig) {
        logger.trace({ selection: 'active', instanceName: activeConfig.name }, `[DwJsonSource] Selected config "${activeConfig.name}" by active flag`);
        return activeConfig;
    }
    // Default to root config
    logger.trace({ selection: 'root', instanceName: json.name }, `[DwJsonSource] Selected config "${json.name ?? 'root'}" (default to root)`);
    return json;
}
/**
 * Load the raw dw.json file without selecting a specific instance.
 *
 * This is useful for instance management operations that need to work
 * with the full configs array.
 *
 * @param options - Loading options
 * @returns The raw multi-config structure and path, or undefined if not found
 */
export async function loadFullDwJson(options = {}) {
    const logger = getLogger();
    const dwJsonPath = options.path ?? path.join(options.projectDirectory ?? options.workingDirectory ?? process.cwd(), 'dw.json');
    logger.trace({ path: dwJsonPath }, '[DwJsonSource] Checking for config file');
    try {
        await fsp.access(dwJsonPath);
    }
    catch {
        logger.trace({ path: dwJsonPath }, '[DwJsonSource] No config file found');
        return undefined;
    }
    try {
        const content = await fsp.readFile(dwJsonPath, 'utf8');
        const json = JSON.parse(content);
        return { config: json, path: dwJsonPath };
    }
    catch (error) {
        const message = error instanceof Error ? error.message : String(error);
        logger.trace({ path: dwJsonPath, error: message }, '[DwJsonSource] Failed to parse config file');
        throw error;
    }
}
/**
 * Save a dw.json configuration to disk.
 *
 * @param config - The configuration to save
 * @param filePath - Path to save to
 */
export async function saveDwJson(config, filePath) {
    const content = JSON.stringify(config, null, 2) + '\n';
    await fsp.writeFile(filePath, content, 'utf8');
}
/**
 * Add a new instance to dw.json.
 *
 * If dw.json doesn't exist, creates a new one. If an instance with the same
 * name already exists, throws an error.
 *
 * @param instance - The instance configuration to add
 * @param options - Options for adding
 * @throws Error if instance with same name already exists
 */
export async function addInstance(instance, options = {}) {
    const dwJsonPath = options.path ?? path.join(options.projectDirectory || options.workingDirectory || process.cwd(), 'dw.json');
    let existing = {};
    try {
        const content = await fsp.readFile(dwJsonPath, 'utf8');
        existing = JSON.parse(content);
    }
    catch (error) {
        // File doesn't exist - start with empty config
        if (error.code !== 'ENOENT') {
            throw error;
        }
    }
    // Check if instance name already exists
    const instanceName = instance.name;
    if (!instanceName) {
        throw new Error('Instance must have a name');
    }
    // Check root config
    if (existing.name === instanceName) {
        throw new Error(`Instance "${instanceName}" already exists`);
    }
    // Check configs array
    if (existing.configs?.some((c) => c.name === instanceName)) {
        throw new Error(`Instance "${instanceName}" already exists`);
    }
    // Handle setActive - clear other active flags
    if (options.setActive) {
        instance.active = true;
        // Clear active on root if it has it
        if (existing.active !== undefined) {
            existing.active = false;
        }
        // Clear active on all other configs
        if (existing.configs) {
            for (const c of existing.configs) {
                if (c.active !== undefined) {
                    c.active = false;
                }
            }
        }
    }
    // Initialize configs array if needed
    if (!existing.configs) {
        existing.configs = [];
    }
    // Add the new instance
    existing.configs.push(instance);
    await saveDwJson(existing, dwJsonPath);
}
/**
 * Remove an instance from dw.json.
 *
 * @param name - Name of the instance to remove
 * @param options - Options for removal
 * @throws Error if instance not found or dw.json doesn't exist
 */
export async function removeInstance(name, options = {}) {
    const dwJsonPath = options.path ?? path.join(options.projectDirectory || options.workingDirectory || process.cwd(), 'dw.json');
    let content;
    try {
        content = await fsp.readFile(dwJsonPath, 'utf8');
    }
    catch (error) {
        if (error.code === 'ENOENT') {
            throw new Error('No dw.json file found');
        }
        throw error;
    }
    const existing = JSON.parse(content);
    // Check if trying to remove root config
    if (existing.name === name) {
        throw new Error(`Cannot remove root instance "${name}". Edit dw.json manually to remove root config.`);
    }
    // Find and remove from configs array
    if (!existing.configs || !existing.configs.some((c) => c.name === name)) {
        throw new Error(`Instance "${name}" not found`);
    }
    existing.configs = existing.configs.filter((c) => c.name !== name);
    await saveDwJson(existing, dwJsonPath);
}
/**
 * Set an instance as the active default.
 *
 * @param name - Name of the instance to set as active
 * @param options - Options
 * @throws Error if instance not found or dw.json doesn't exist
 */
export async function setActiveInstance(name, options = {}) {
    const dwJsonPath = options.path ?? path.join(options.projectDirectory || options.workingDirectory || process.cwd(), 'dw.json');
    let content;
    try {
        content = await fsp.readFile(dwJsonPath, 'utf8');
    }
    catch (error) {
        if (error.code === 'ENOENT') {
            throw new Error('No dw.json file found');
        }
        throw error;
    }
    const existing = JSON.parse(content);
    // Find the target instance
    let found = false;
    // Check root config
    if (existing.name === name) {
        found = true;
        existing.active = true;
    }
    else if (existing.active !== undefined) {
        existing.active = false;
    }
    // Check and update configs array
    if (existing.configs) {
        for (const c of existing.configs) {
            if (c.name === name) {
                found = true;
                c.active = true;
            }
            else if (c.active !== undefined) {
                c.active = false;
            }
        }
    }
    if (!found) {
        throw new Error(`Instance "${name}" not found`);
    }
    await saveDwJson(existing, dwJsonPath);
}
/**
 * Loads configuration from a dw.json file.
 *
 * If an explicit path is provided, uses that file. Otherwise, looks for dw.json
 * in the projectDirectory (or cwd). Does NOT search parent directories.
 *
 * Use `findDwJson()` if you need to search upward through parent directories.
 *
 * @param options - Loading options
 * @returns The parsed config with its path, or undefined if no dw.json found
 *
 * @example
 * // Load from ./dw.json (current directory)
 * const result = loadDwJson();
 * if (result) {
 *   console.log(`Loaded from ${result.path}`);
 *   console.log(result.config.hostname);
 * }
 *
 * // Load from specific directory
 * const result = loadDwJson({ projectDirectory: '/path/to/project' });
 *
 * // Use named instance
 * const result = loadDwJson({ instance: 'staging' });
 *
 * // Explicit path
 * const result = loadDwJson({ path: './config/dw.json' });
 */
export async function loadDwJson(options = {}) {
    const logger = getLogger();
    // If explicit path provided, use it. Otherwise default to ./dw.json (no upward search)
    const dwJsonPath = options.path ?? path.join(options.projectDirectory || options.workingDirectory || process.cwd(), 'dw.json');
    logger.trace({ path: dwJsonPath }, '[DwJsonSource] Checking for config file');
    try {
        await fsp.access(dwJsonPath);
    }
    catch {
        logger.trace({ path: dwJsonPath }, '[DwJsonSource] No config file found');
        return undefined;
    }
    try {
        const content = await fsp.readFile(dwJsonPath, 'utf8');
        const raw = JSON.parse(content);
        // Normalize root-level keys to camelCase
        const normalized = normalizeConfigKeys(raw);
        // Normalize keys in each configs[] item
        if (Array.isArray(normalized.configs)) {
            normalized.configs = normalized.configs.map((item) => normalizeConfigKeys(item));
        }
        const config = selectConfig(normalized, options.instance);
        if (!config) {
            return undefined;
        }
        return {
            config,
            path: dwJsonPath,
        };
    }
    catch (error) {
        // Invalid JSON or read error - log at trace level and re-throw
        // The resolver will catch this and create a SOURCE_ERROR warning
        const message = error instanceof Error ? error.message : String(error);
        logger.trace({ path: dwJsonPath, error: message }, '[DwJsonSource] Failed to parse config file');
        throw error;
    }
}
//# sourceMappingURL=dw-json.js.map