// @crash-shield-ignore-file — config/cache reader for Planu-controlled JSON; writer is this codebase, shape guaranteed by build/seed.
// engine/config-loader.ts — Canonical 3-layer config loader (SPEC-205)
//
// THE PATTERN:
//   Layer 1 (lowest precedence): src/config/{area}.json — system defaults, always present
//   Layer 2: data/global/{area}-custom.json — user-level global overrides
//   Layer 3 (highest precedence): data/projects/{hash}/config/{area}-overrides.json — project overrides
//
// Items are merged by `id` field: an override with the same `id` replaces the system default.
// Items with new `id` values are appended.
//
// USAGE FOR FUTURE SPEC AUTHORS:
//   1. Create src/config/{area}.json with your default items (each must have an `id` field)
//   2. Define a Zod schema for a single item, then wrap with z.array()
//   3. Instantiate: new ConfigLoader<YourType>('your-area', yourItemArraySchema)
//   4. Call loader.load(projectPath?) to get merged items
//
// This module only imports from types/ — respects the dependency rule.
import { readFileSync } from 'node:fs';
import { readFile } from 'node:fs/promises';
import { createRequire } from 'node:module';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const DATA_DIR = join(__dirname, '..', '..', 'data');
/**
 * Canonical 3-layer configuration loader.
 *
 * Generalizes the pattern from catalog-loader.ts (SPEC-054) into a reusable
 * class that any module can use to make its configuration extensible.
 *
 * @typeParam T - The type of a single configuration item (must have `id: string`)
 */
export class ConfigLoader {
    area;
    schema;
    require;
    constructor(area, schema) {
        this.area = area;
        this.schema = schema;
        this.require = createRequire(import.meta.url);
    }
    /**
     * Synchronously load and merge all 3 config layers.
     * Safe for module-level initialization.
     */
    load(projectHash) {
        const system = this.loadSystem();
        const global = this.loadGlobalSync();
        const project = projectHash ? this.loadProjectSync(projectHash) : [];
        return this.merge(system, global, project);
    }
    /**
     * Async version of load — preferred when called from async contexts.
     */
    async loadAsync(projectHash) {
        const system = this.loadSystem();
        const global = await this.loadGlobalAsync();
        const project = projectHash ? await this.loadProjectAsync(projectHash) : [];
        return this.merge(system, global, project);
    }
    /**
     * Validate raw data against the schema. Returns parsed items or throws
     * a descriptive error with the exact path of the invalid field.
     */
    validate(data) {
        const result = this.schema.safeParse(data);
        if (result.success) {
            return result.data;
        }
        const details = formatZodError(result.error);
        throw new Error(`Invalid ${this.area} config: ${details}`);
    }
    /**
     * Load only system defaults (src/config/{area}.json).
     */
    loadSystem() {
        try {
            const raw = this.require(`../config/${this.area}.json`);
            return this.validate(raw);
        }
        catch (err) {
            if (err instanceof Error && err.message.includes('Invalid')) {
                throw err;
            }
            throw new Error(`System config not found: src/config/${this.area}.json`, { cause: err });
        }
    }
    // -----------------------------------------------------------------------
    // Private helpers
    // -----------------------------------------------------------------------
    loadGlobalSync() {
        return this.loadFileSync(join(DATA_DIR, 'global', `${this.area}-custom.json`));
    }
    loadProjectSync(projectHash) {
        return this.loadFileSync(join(DATA_DIR, 'projects', projectHash, 'config', `${this.area}-overrides.json`));
    }
    async loadGlobalAsync() {
        return this.loadFileAsync(join(DATA_DIR, 'global', `${this.area}-custom.json`));
    }
    async loadProjectAsync(projectHash) {
        return this.loadFileAsync(join(DATA_DIR, 'projects', projectHash, 'config', `${this.area}-overrides.json`));
    }
    loadFileSync(filePath) {
        try {
            const content = readFileSync(filePath, 'utf-8');
            const parsed = JSON.parse(content);
            return this.validate(parsed);
        }
        catch (err) {
            if (err instanceof Error && err.message.includes('Invalid')) {
                throw err;
            }
            // File does not exist or is unreadable — return empty (optional layer)
            return [];
        }
    }
    async loadFileAsync(filePath) {
        try {
            const content = await readFile(filePath, 'utf-8');
            const parsed = JSON.parse(content);
            return this.validate(parsed);
        }
        catch (err) {
            if (err instanceof Error && err.message.includes('Invalid')) {
                throw err;
            }
            return [];
        }
    }
    /**
     * Merge items from 3 layers by `id`. Higher-precedence layers override
     * lower-precedence items with the same `id`. New ids are appended.
     */
    merge(system, global, project) {
        const map = new Map();
        for (const item of system) {
            map.set(item.id, item);
        }
        for (const item of global) {
            map.set(item.id, item);
        }
        for (const item of project) {
            map.set(item.id, item);
        }
        return [...map.values()];
    }
}
/**
 * 3-layer configuration loader for structured (object) configs.
 *
 * Similar to ConfigLoader but works with a single object instead of an array.
 * Override layers are deep-merged (top-level keys only) with the system defaults.
 *
 * @typeParam T - The config object type
 */
export class ObjectConfigLoader {
    area;
    schema;
    require;
    constructor(area, schema) {
        this.area = area;
        this.schema = schema;
        this.require = createRequire(import.meta.url);
    }
    /** Synchronously load and merge all 3 config layers. */
    load(projectHash) {
        const system = this.loadSystem();
        const global = this.loadFileSync(join(DATA_DIR, 'global', `${this.area}-custom.json`));
        const project = projectHash
            ? this.loadFileSync(join(DATA_DIR, 'projects', projectHash, 'config', `${this.area}-overrides.json`))
            : null;
        return this.merge(system, global, project);
    }
    /** Async version of load. */
    async loadAsync(projectHash) {
        const system = this.loadSystem();
        const global = await this.loadFileAsync(join(DATA_DIR, 'global', `${this.area}-custom.json`));
        const project = projectHash
            ? await this.loadFileAsync(join(DATA_DIR, 'projects', projectHash, 'config', `${this.area}-overrides.json`))
            : null;
        return this.merge(system, global, project);
    }
    /** Validate raw data against the schema. */
    validate(data) {
        const result = this.schema.safeParse(data);
        if (result.success) {
            return result.data;
        }
        const details = formatZodError(result.error);
        throw new Error(`Invalid ${this.area} config: ${details}`);
    }
    /** Load system defaults. */
    loadSystem() {
        try {
            const raw = this.require(`../config/${this.area}.json`);
            return this.validate(raw);
        }
        catch (err) {
            if (err instanceof Error && err.message.includes('Invalid')) {
                throw err;
            }
            throw new Error(`System config not found: src/config/${this.area}.json`, { cause: err });
        }
    }
    loadFileSync(filePath) {
        try {
            const content = readFileSync(filePath, 'utf-8');
            return JSON.parse(content);
        }
        catch {
            return null;
        }
    }
    async loadFileAsync(filePath) {
        try {
            const content = await readFile(filePath, 'utf-8');
            return JSON.parse(content);
        }
        catch {
            return null;
        }
    }
    /** Shallow merge: system <- global <- project (top-level keys). */
    merge(system, global, project) {
        let result = { ...system };
        if (global) {
            result = { ...result, ...global };
        }
        if (project) {
            result = { ...result, ...project };
        }
        return this.validate(result);
    }
}
/**
 * Format a ZodError into a human-readable string with field paths.
 */
function formatZodError(error) {
    return error.issues
        .map((issue) => {
        const path = issue.path.length > 0 ? issue.path.join('.') : '(root)';
        return `${path}: ${issue.message}`;
    })
        .join('; ');
}
//# sourceMappingURL=config-loader.js.map