/*
 * Copyright 2026, Jaime Terrats.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
import { join, relative } from 'node:path';
import { PLUGIN_VERSION } from '../../version.js';
import { computeFileHash, getChangedFiles } from './delta-engine.js';
import { resolveDependencies } from './dependency-resolver.js';
import { getFilteredParsers, registerParser } from './parser-registry.js';
import { apexParser } from './parsers/apex-parser.js';
import { fieldParser } from './parsers/field-parser.js';
import { flowParser } from './parsers/flow-parser.js';
import { labelParser } from './parsers/label-parser.js';
import { lwcParser } from './parsers/lwc-parser.js';
import { objectParser } from './parsers/object-parser.js';
import { permParser } from './parsers/perm-parser.js';
import { triggerParser } from './parsers/trigger-parser.js';
import { renderJson } from './renderers/json-renderer.js';
import { renderMarkdown } from './renderers/markdown-renderer.js';
// Register all parsers
registerParser(apexParser);
registerParser(triggerParser);
registerParser(objectParser);
registerParser(fieldParser);
registerParser(lwcParser);
registerParser(flowParser);
registerParser(permParser);
registerParser(labelParser);
/**
 * Builds the metadata index by scanning the source directory, parsing all files,
 * resolving dependencies, and writing the output.
 */
export function buildMetadataIndex(cwd, options, warn) {
    const { sourceDir, mode, include, exclude, format } = options;
    const fullSourceDir = join(cwd, sourceDir);
    if (!existsSync(fullSourceDir)) {
        throw new Error(`Source directory not found: ${fullSourceDir}`);
    }
    const parsers = getFilteredParsers(include, exclude);
    if (parsers.length === 0) {
        throw new Error('No parsers available after applying include/exclude filters.');
    }
    // Determine delta
    const changedFiles = getChangedFiles(cwd, sourceDir, mode);
    const effectiveMode = changedFiles === null ? 'full' : mode;
    // Collect and filter files
    const filesToProcess = resolveFilesToProcess(cwd, fullSourceDir, changedFiles);
    // Parse files and extract dependencies
    const { entries, fileContents } = parseFiles(cwd, filesToProcess, parsers, warn);
    extractAllDependencies(entries, fileContents, parsers, warn);
    // Resolve dependencies (forward + reverse)
    const { entries: resolved, edges } = resolveDependencies(entries);
    // Build the index object
    const index = assembleIndex(resolved, edges, effectiveMode, sourceDir);
    // Render output
    const outputPath = format === 'md' ? renderMarkdown(cwd, index) : renderJson(cwd, index);
    return { index, outputPath };
}
/**
 * Loads an existing metadata index from disk.
 */
export function loadMetadataIndex(cwd) {
    const indexPath = join(cwd, '.setup-agents', 'sf-metadata-index.json');
    if (!existsSync(indexPath))
        return null;
    try {
        return JSON.parse(readFileSync(indexPath, 'utf8'));
    }
    catch {
        return null;
    }
}
/**
 * Queries the dependency graph for a given API name.
 * Returns direct dependencies and dependents.
 */
export function queryDependencies(index, apiName) {
    const entry = index.entries.find((e) => e.apiName === apiName) ?? null;
    if (!entry)
        return { entry: null, dependencies: [], dependents: [] };
    return {
        entry,
        dependencies: entry.dependencies,
        dependents: entry.dependents ?? [],
    };
}
// ─── Internal helpers ──────────────────────────────────────────────────────
function resolveFilesToProcess(cwd, fullSourceDir, changedFiles) {
    const allFiles = collectFiles(fullSourceDir);
    if (!changedFiles)
        return allFiles;
    return allFiles.filter((f) => {
        const rel = relative(cwd, f);
        return changedFiles.includes(rel);
    });
}
function parseFiles(cwd, filesToProcess, parsers, warn) {
    const entries = [];
    const fileContents = new Map();
    for (const filePath of filesToProcess) {
        const relPath = relative(cwd, filePath);
        parseOneFile(relPath, filePath, parsers, entries, fileContents, warn);
    }
    return { entries, fileContents };
}
function parseOneFile(relPath, filePath, parsers, entries, fileContents, warn) {
    const parser = findMatchingParser(relPath, parsers);
    if (!parser)
        return;
    try {
        const content = readFileSync(filePath, 'utf8');
        fileContents.set(relPath, content);
        collectParsedEntries(parser, relPath, content, filePath, entries);
    }
    catch (err) {
        const message = err instanceof Error ? err.message : String(err);
        if (warn)
            warn(`Skipping ${relPath}: ${message}`);
    }
}
function findMatchingParser(relPath, parsers) {
    return parsers.find((parser) => matchesGlob(relPath, parser.glob));
}
function collectParsedEntries(parser, relPath, content, filePath, entries) {
    const result = parser.parse(relPath, content);
    if (result === null)
        return;
    const parsed = Array.isArray(result) ? result : [result];
    for (const entry of parsed) {
        entry.hash = computeFileHash(filePath);
        entries.push(entry);
    }
}
function extractAllDependencies(entries, fileContents, parsers, warn) {
    for (const entry of entries) {
        const content = fileContents.get(entry.path);
        if (!content)
            continue;
        extractEntryDependencies(entry, content, parsers, warn);
    }
}
function extractEntryDependencies(entry, content, parsers, warn) {
    const parser = parsers.find((p) => p.type === entry.type);
    if (!parser)
        return;
    try {
        entry.dependencies = parser.extractDependencies(entry, content);
    }
    catch (err) {
        const message = err instanceof Error ? err.message : String(err);
        if (warn)
            warn(`Error extracting dependencies for ${entry.apiName}: ${message}`);
    }
}
function assembleIndex(resolved, edges, effectiveMode, sourceDir) {
    const byType = buildTypeStats(resolved);
    const totalDependencies = edges.length;
    const orphanEntries = resolved.filter((e) => e.dependencies.length === 0 && (!e.dependents || e.dependents.length === 0)).length;
    return {
        version: PLUGIN_VERSION,
        generatedAt: new Date().toISOString(),
        generatedBy: '@jterrats/setup-agents',
        mode: effectiveMode,
        sourceRoot: sourceDir,
        stats: {
            totalEntries: resolved.length,
            byType,
            totalDependencies,
            orphanEntries,
        },
        entries: resolved,
        dependencyGraph: { edges },
    };
}
function collectFiles(dir) {
    const results = [];
    const stack = [dir];
    while (stack.length > 0) {
        const current = stack.pop();
        let items;
        try {
            items = readdirSync(current);
        }
        catch {
            continue;
        }
        for (const item of items) {
            const fullPath = join(current, item);
            try {
                const stat = statSync(fullPath);
                if (stat.isDirectory()) {
                    stack.push(fullPath);
                }
                else if (stat.isFile()) {
                    results.push(fullPath);
                }
            }
            catch {
                continue;
            }
        }
    }
    return results;
}
/**
 * Simple glob matching supporting ** and * patterns.
 * Avoids complex regex; uses string splitting for safety.
 */
function matchesGlob(filePath, glob) {
    // Normalize separators
    const normalizedPath = filePath.replace(/\\/g, '/');
    // Split glob into segments
    const globParts = glob.split('/');
    const pathParts = normalizedPath.split('/');
    return matchSegments(pathParts, 0, globParts, 0);
}
function matchSegments(pathParts, pi, globParts, gi) {
    // Base case: both exhausted
    if (gi >= globParts.length && pi >= pathParts.length)
        return true;
    if (gi >= globParts.length)
        return false;
    const globSegment = globParts[gi];
    // ** matches zero or more path segments
    if (globSegment === '**') {
        // Try matching ** as zero segments, one segment, two segments, etc.
        for (let skip = 0; skip <= pathParts.length - pi; skip++) {
            if (matchSegments(pathParts, pi + skip, globParts, gi + 1))
                return true;
        }
        return false;
    }
    // If path is exhausted but glob is not
    if (pi >= pathParts.length)
        return false;
    // Match single segment with * wildcard
    if (matchWildcard(pathParts[pi], globSegment)) {
        return matchSegments(pathParts, pi + 1, globParts, gi + 1);
    }
    return false;
}
function matchWildcard(text, pattern) {
    if (pattern === '*')
        return true;
    if (!pattern.includes('*'))
        return text === pattern;
    const parts = pattern.split('*');
    return matchWildcardParts(text, parts);
}
function matchWildcardParts(text, parts) {
    let pos = 0;
    for (let i = 0; i < parts.length; i++) {
        const part = parts[i];
        if (part === '')
            continue;
        if (i === 0) {
            if (!text.startsWith(part))
                return false;
            pos = part.length;
        }
        else if (i === parts.length - 1) {
            if (!text.endsWith(part))
                return false;
        }
        else {
            const idx = text.indexOf(part, pos);
            if (idx === -1)
                return false;
            pos = idx + part.length;
        }
    }
    return true;
}
function buildTypeStats(entries) {
    const stats = {
        ApexClass: 0,
        ApexTrigger: 0,
        LightningComponentBundle: 0,
        CustomObject: 0,
        CustomField: 0,
        Flow: 0,
        PermissionSet: 0,
        CustomLabel: 0,
    };
    for (const entry of entries) {
        stats[entry.type]++;
    }
    return stats;
}
//# sourceMappingURL=index.js.map