import { existsSync, readFileSync } from 'fs';
import path from 'path';
import { parse as parseYaml } from 'yaml';
import { z } from 'zod';
/**
 * Zod schema for project configuration.
 *
 * Purpose:
 * 1. Documentation - clearly defines the config file structure
 * 2. Type safety - TypeScript infers ProjectConfig type from schema
 * 3. Runtime validation - uses safeParse() for resilient field-by-field validation
 *
 * Why Zod over manual validation:
 * - Helps understand OpenSpec's data interfaces at a glance
 * - Single source of truth for type and validation
 * - Consistent with other OpenSpec schemas
 */
export const ProjectConfigSchema = z.object({
    // Required: which schema to use (e.g., "spec-driven", or project-local schema name)
    schema: z
        .string()
        .min(1)
        .describe('The workflow schema to use (e.g., "spec-driven")'),
    // Optional: project context (injected into all artifact instructions)
    // Max size: 50KB (enforced during parsing)
    context: z
        .string()
        .optional()
        .describe('Project context injected into all artifact instructions'),
    // Optional: per-artifact rules (additive to schema's built-in guidance)
    rules: z
        .record(z.string(), // artifact ID
    z.array(z.string()) // list of rules
    )
        .optional()
        .describe('Per-artifact rules, keyed by artifact ID'),
});
const MAX_CONTEXT_SIZE = 50 * 1024; // 50KB hard limit
/**
 * Read and parse openspec/config.yaml from project root.
 * Uses resilient parsing - validates each field independently using Zod safeParse.
 * Returns null if file doesn't exist.
 * Returns partial config if some fields are invalid (with warnings).
 *
 * Performance note (Jan 2025):
 * Benchmarks showed direct file reads are fast enough without caching:
 * - Typical config (1KB): ~0.5ms per read
 * - Large config (50KB): ~1.6ms per read
 * - Missing config: ~0.01ms per read
 * Config is read 1-2 times per command (schema resolution + instruction loading),
 * adding ~1-3ms total overhead. Caching would add complexity (mtime checks,
 * invalidation logic) for negligible benefit. Direct reads also ensure config
 * changes are reflected immediately without stale cache issues.
 *
 * @param projectRoot - The root directory of the project (where `openspec/` lives)
 * @returns Parsed config or null if file doesn't exist
 */
export function readProjectConfig(projectRoot) {
    // Try both .yaml and .yml, prefer .yaml
    let configPath = path.join(projectRoot, 'openspec', 'config.yaml');
    if (!existsSync(configPath)) {
        configPath = path.join(projectRoot, 'openspec', 'config.yml');
        if (!existsSync(configPath)) {
            return null; // No config is OK
        }
    }
    try {
        const content = readFileSync(configPath, 'utf-8');
        const raw = parseYaml(content);
        if (!raw || typeof raw !== 'object') {
            console.warn(`openspec/config.yaml is not a valid YAML object`);
            return null;
        }
        const config = {};
        // Parse schema field using Zod
        const schemaField = z.string().min(1);
        const schemaResult = schemaField.safeParse(raw.schema);
        if (schemaResult.success) {
            config.schema = schemaResult.data;
        }
        else if (raw.schema !== undefined) {
            console.warn(`Invalid 'schema' field in config (must be non-empty string)`);
        }
        // Parse context field with size limit
        if (raw.context !== undefined) {
            const contextField = z.string();
            const contextResult = contextField.safeParse(raw.context);
            if (contextResult.success) {
                const contextSize = Buffer.byteLength(contextResult.data, 'utf-8');
                if (contextSize > MAX_CONTEXT_SIZE) {
                    console.warn(`Context too large (${(contextSize / 1024).toFixed(1)}KB, limit: ${MAX_CONTEXT_SIZE / 1024}KB)`);
                    console.warn(`Ignoring context field`);
                }
                else {
                    config.context = contextResult.data;
                }
            }
            else {
                console.warn(`Invalid 'context' field in config (must be string)`);
            }
        }
        // Parse rules field using Zod
        if (raw.rules !== undefined) {
            const rulesField = z.record(z.string(), z.array(z.string()));
            // First check if it's an object structure (guard against null since typeof null === 'object')
            if (typeof raw.rules === 'object' && raw.rules !== null && !Array.isArray(raw.rules)) {
                const parsedRules = {};
                let hasValidRules = false;
                for (const [artifactId, rules] of Object.entries(raw.rules)) {
                    const rulesArrayResult = z.array(z.string()).safeParse(rules);
                    if (rulesArrayResult.success) {
                        // Filter out empty strings
                        const validRules = rulesArrayResult.data.filter((r) => r.length > 0);
                        if (validRules.length > 0) {
                            parsedRules[artifactId] = validRules;
                            hasValidRules = true;
                        }
                        if (validRules.length < rulesArrayResult.data.length) {
                            console.warn(`Some rules for '${artifactId}' are empty strings, ignoring them`);
                        }
                    }
                    else {
                        console.warn(`Rules for '${artifactId}' must be an array of strings, ignoring this artifact's rules`);
                    }
                }
                if (hasValidRules) {
                    config.rules = parsedRules;
                }
            }
            else {
                console.warn(`Invalid 'rules' field in config (must be object)`);
            }
        }
        // Return partial config even if some fields failed
        return Object.keys(config).length > 0 ? config : null;
    }
    catch (error) {
        console.warn(`Failed to parse openspec/config.yaml:`, error);
        return null;
    }
}
/**
 * Validate artifact IDs in rules against a schema's artifacts.
 * Called during instruction loading (when schema is known).
 * Returns warnings for unknown artifact IDs.
 *
 * @param rules - The rules object from config
 * @param validArtifactIds - Set of valid artifact IDs from the schema
 * @param schemaName - Name of the schema for error messages
 * @returns Array of warning messages for unknown artifact IDs
 */
export function validateConfigRules(rules, validArtifactIds, schemaName) {
    const warnings = [];
    for (const artifactId of Object.keys(rules)) {
        if (!validArtifactIds.has(artifactId)) {
            const validIds = Array.from(validArtifactIds).sort().join(', ');
            warnings.push(`Unknown artifact ID in rules: "${artifactId}". ` +
                `Valid IDs for schema "${schemaName}": ${validIds}`);
        }
    }
    return warnings;
}
/**
 * Suggest valid schema names when user provides invalid schema.
 * Uses fuzzy matching to find similar names.
 *
 * @param invalidSchemaName - The invalid schema name from config
 * @param availableSchemas - List of available schemas with their type (built-in or project-local)
 * @returns Error message with suggestions and available schemas
 */
export function suggestSchemas(invalidSchemaName, availableSchemas) {
    // Simple fuzzy match: Levenshtein distance
    function levenshtein(a, b) {
        const matrix = [];
        for (let i = 0; i <= b.length; i++) {
            matrix[i] = [i];
        }
        for (let j = 0; j <= a.length; j++) {
            matrix[0][j] = j;
        }
        for (let i = 1; i <= b.length; i++) {
            for (let j = 1; j <= a.length; j++) {
                if (b.charAt(i - 1) === a.charAt(j - 1)) {
                    matrix[i][j] = matrix[i - 1][j - 1];
                }
                else {
                    matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1);
                }
            }
        }
        return matrix[b.length][a.length];
    }
    // Find closest matches (distance <= 3)
    const suggestions = availableSchemas
        .map((s) => ({ ...s, distance: levenshtein(invalidSchemaName, s.name) }))
        .filter((s) => s.distance <= 3)
        .sort((a, b) => a.distance - b.distance)
        .slice(0, 3);
    const builtIn = availableSchemas.filter((s) => s.isBuiltIn).map((s) => s.name);
    const projectLocal = availableSchemas.filter((s) => !s.isBuiltIn).map((s) => s.name);
    let message = `Schema '${invalidSchemaName}' not found in openspec/config.yaml\n\n`;
    if (suggestions.length > 0) {
        message += `Did you mean one of these?\n`;
        suggestions.forEach((s) => {
            const type = s.isBuiltIn ? 'built-in' : 'project-local';
            message += `  - ${s.name} (${type})\n`;
        });
        message += '\n';
    }
    message += `Available schemas:\n`;
    if (builtIn.length > 0) {
        message += `  Built-in: ${builtIn.join(', ')}\n`;
    }
    if (projectLocal.length > 0) {
        message += `  Project-local: ${projectLocal.join(', ')}\n`;
    }
    else {
        message += `  Project-local: (none found)\n`;
    }
    message += `\nFix: Edit openspec/config.yaml and change 'schema: ${invalidSchemaName}' to a valid schema name`;
    return message;
}
//# sourceMappingURL=project-config.js.map