"use strict";
/**
 * Reference Resolver
 * Resolves $ref fields in YAML configurations to support file splitting
 */
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.ReferenceResolver = void 0;
const yaml = __importStar(require("js-yaml"));
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
class ReferenceResolver {
    static DEFAULT_MAX_DEPTH = 10;
    /**
     * Resolve all $ref fields in a YAML object
     */
    async resolve(obj, options = {}) {
        const basePath = options.basePath || process.cwd();
        const cache = options.cache || new Map();
        const maxDepth = options.maxDepth || ReferenceResolver.DEFAULT_MAX_DEPTH;
        const context = {
            currentFile: basePath,
            resolutionStack: [],
            cache,
            maxDepth,
            depth: 0,
        };
        return this.resolveObject(obj, context);
    }
    /**
     * Resolve references in an object recursively
     */
    async resolveObject(obj, context) {
        // Handle null/undefined
        if (obj === null || obj === undefined) {
            return obj;
        }
        // Handle primitives
        if (typeof obj !== 'object') {
            return obj;
        }
        // Check depth limit - only for $ref resolution chains, not object nesting
        if (context.depth > context.maxDepth) {
            throw new Error(`Maximum reference depth (${context.maxDepth}) exceeded. Possible circular reference?`);
        }
        // Handle arrays
        if (Array.isArray(obj)) {
            return Promise.all(obj.map(item => this.resolveObject(item, context)));
        }
        // Handle $ref
        if (obj.$ref && typeof obj.$ref === 'string') {
            return this.resolveReference(obj.$ref, context);
        }
        // Handle regular objects - resolve all properties
        // Don't increment depth for normal object traversal - only for $ref chains
        const resolved = {};
        for (const [key, value] of Object.entries(obj)) {
            resolved[key] = await this.resolveObject(value, context);
        }
        return resolved;
    }
    /**
     * Resolve a $ref string
     */
    async resolveReference(ref, context) {
        // Parse the reference
        const { filePath, fragment } = this.parseReference(ref, context.currentFile);
        // Check for circular references
        if (context.resolutionStack.includes(filePath)) {
            const cycle = [...context.resolutionStack, filePath].join(' -> ');
            throw new Error(`Circular reference detected: ${cycle}`);
        }
        // Load the file (with caching)
        let fileContent = context.cache.get(filePath);
        if (!fileContent) {
            fileContent = await this.loadFile(filePath);
            context.cache.set(filePath, fileContent);
        }
        // Resolve fragment if specified
        const resolved = fragment ? this.resolveFragment(fileContent, fragment) : fileContent;
        // Create new context for nested resolution
        const nestedContext = {
            currentFile: filePath,
            resolutionStack: [...context.resolutionStack, filePath],
            cache: context.cache,
            maxDepth: context.maxDepth,
            depth: context.depth + 1,
        };
        // Recursively resolve any $refs in the resolved content
        return this.resolveObject(resolved, nestedContext);
    }
    /**
     * Parse a reference string into file path and optional fragment
     */
    parseReference(ref, currentFile) {
        const [refPath, fragment] = ref.split('#');
        if (!refPath) {
            throw new Error(`Invalid reference format: ${ref} (file path cannot be empty)`);
        }
        // Resolve relative path
        const baseDir = path.dirname(currentFile);
        const filePath = path.resolve(baseDir, refPath);
        return {
            filePath,
            fragment: fragment || undefined,
        };
    }
    /**
     * Load and parse a YAML file
     */
    async loadFile(filePath) {
        if (!fs.existsSync(filePath)) {
            throw new Error(`Referenced file not found: ${filePath}`);
        }
        try {
            const content = fs.readFileSync(filePath, 'utf-8');
            const parsed = yaml.load(content);
            if (parsed === null || parsed === undefined) {
                throw new Error(`Referenced file is empty: ${filePath}`);
            }
            return parsed;
        }
        catch (error) {
            if (error instanceof yaml.YAMLException) {
                throw new Error(`YAML parse error in ${filePath}: ${error.message}`);
            }
            throw error;
        }
    }
    /**
     * Resolve a fragment (JSONPath) within an object
     * Fragment format: /path/to/nested (similar to JSON Pointer)
     */
    resolveFragment(obj, fragment) {
        // Remove leading slash if present
        const normalizedFragment = fragment.startsWith('/') ? fragment.slice(1) : fragment;
        if (!normalizedFragment) {
            return obj;
        }
        // Split path and traverse
        const parts = normalizedFragment.split('/');
        let current = obj;
        for (const part of parts) {
            // Decode URI-encoded characters (e.g., ~0 = ~, ~1 = /)
            const decodedPart = this.decodeFragmentPart(part);
            if (current === null || current === undefined) {
                throw new Error(`Cannot resolve fragment "${fragment}": path not found at "${decodedPart}"`);
            }
            // Handle array indices
            if (Array.isArray(current)) {
                const index = parseInt(decodedPart, 10);
                if (isNaN(index) || index < 0 || index >= current.length) {
                    throw new Error(`Cannot resolve fragment "${fragment}": invalid array index "${decodedPart}"`);
                }
                current = current[index];
            }
            else if (typeof current === 'object') {
                if (!(decodedPart in current)) {
                    throw new Error(`Cannot resolve fragment "${fragment}": key "${decodedPart}" not found`);
                }
                current = current[decodedPart];
            }
            else {
                throw new Error(`Cannot resolve fragment "${fragment}": expected object or array at "${decodedPart}"`);
            }
        }
        return current;
    }
    /**
     * Decode fragment part (JSON Pointer escaping)
     * ~0 represents ~
     * ~1 represents /
     */
    decodeFragmentPart(part) {
        return part.replace(/~1/g, '/').replace(/~0/g, '~');
    }
    /**
     * Get all referenced files from a resolved object
     * Useful for tracking dependencies
     */
    getAllReferencedFiles(obj, basePath = process.cwd()) {
        const files = new Set();
        this.collectReferencedFiles(obj, basePath, files);
        return Array.from(files);
    }
    /**
     * Recursively collect all referenced files
     */
    collectReferencedFiles(obj, currentFile, files) {
        if (obj === null || obj === undefined || typeof obj !== 'object') {
            return;
        }
        // Handle $ref
        if (obj.$ref && typeof obj.$ref === 'string') {
            const { filePath } = this.parseReference(obj.$ref, currentFile);
            files.add(filePath);
            // Load and recursively check the referenced file
            if (fs.existsSync(filePath)) {
                try {
                    const content = fs.readFileSync(filePath, 'utf-8');
                    const parsed = yaml.load(content);
                    this.collectReferencedFiles(parsed, filePath, files);
                }
                catch (_error) {
                    // Ignore errors during collection
                }
            }
        }
        // Recurse into object properties
        if (Array.isArray(obj)) {
            for (const item of obj) {
                this.collectReferencedFiles(item, currentFile, files);
            }
        }
        else {
            for (const value of Object.values(obj)) {
                this.collectReferencedFiles(value, currentFile, files);
            }
        }
    }
}
exports.ReferenceResolver = ReferenceResolver;
//# sourceMappingURL=reference-resolver.js.map