import * as fs from 'node:fs';
import * as path from 'node:path';
import { getSchemaDir, resolveSchema } from './resolver.js';
import { ArtifactGraph } from './graph.js';
import { detectCompleted } from './state.js';
import { resolveSchemaForChange } from '../../utils/change-metadata.js';
import { readProjectConfig, validateConfigRules } from '../project-config.js';
// Session-level cache for validation warnings (avoid repeating same warnings)
const shownWarnings = new Set();
/**
 * Error thrown when loading a template fails.
 */
export class TemplateLoadError extends Error {
    templatePath;
    constructor(message, templatePath) {
        super(message);
        this.templatePath = templatePath;
        this.name = 'TemplateLoadError';
    }
}
/**
 * Loads a template from a schema's templates directory.
 *
 * @param schemaName - Schema name (e.g., "spec-driven")
 * @param templatePath - Relative path within the templates directory (e.g., "proposal.md")
 * @param projectRoot - Optional project root for project-local schema resolution
 * @returns The template content
 * @throws TemplateLoadError if the template cannot be loaded
 */
export function loadTemplate(schemaName, templatePath, projectRoot) {
    const schemaDir = getSchemaDir(schemaName, projectRoot);
    if (!schemaDir) {
        throw new TemplateLoadError(`Schema '${schemaName}' not found`, templatePath);
    }
    const fullPath = path.join(schemaDir, 'templates', templatePath);
    if (!fs.existsSync(fullPath)) {
        throw new TemplateLoadError(`Template not found: ${fullPath}`, fullPath);
    }
    try {
        return fs.readFileSync(fullPath, 'utf-8');
    }
    catch (err) {
        const ioError = err instanceof Error ? err : new Error(String(err));
        throw new TemplateLoadError(`Failed to read template: ${ioError.message}`, fullPath);
    }
}
/**
 * Loads change context combining graph and completion state.
 *
 * Schema resolution order:
 * 1. Explicit schemaName parameter (if provided)
 * 2. Schema from .openspec.yaml metadata (if exists in change directory)
 * 3. Default 'spec-driven'
 *
 * @param projectRoot - Project root directory
 * @param changeName - Change name
 * @param schemaName - Optional schema name override. If not provided, auto-detected from metadata.
 * @returns Change context with graph, completed set, and metadata
 */
export function loadChangeContext(projectRoot, changeName, schemaName) {
    const changeDir = path.join(projectRoot, 'openspec', 'changes', changeName);
    // Resolve schema: explicit > metadata > default
    const resolvedSchemaName = resolveSchemaForChange(changeDir, schemaName);
    const schema = resolveSchema(resolvedSchemaName, projectRoot);
    const graph = ArtifactGraph.fromSchema(schema);
    const completed = detectCompleted(graph, changeDir);
    return {
        graph,
        completed,
        schemaName: resolvedSchemaName,
        changeName,
        changeDir,
        projectRoot,
    };
}
/**
 * Generates enriched instructions for creating an artifact.
 *
 * Instruction injection order:
 * 1. <context> - Project context from config (if present)
 * 2. <rules> - Artifact-specific rules from config (if present)
 * 3. <template> - Schema's template content
 *
 * @param context - Change context
 * @param artifactId - Artifact ID to generate instructions for
 * @param projectRoot - Project root directory (for reading config)
 * @returns Enriched artifact instructions
 * @throws Error if artifact not found
 */
export function generateInstructions(context, artifactId, projectRoot) {
    const artifact = context.graph.getArtifact(artifactId);
    if (!artifact) {
        throw new Error(`Artifact '${artifactId}' not found in schema '${context.schemaName}'`);
    }
    const templateContent = loadTemplate(context.schemaName, artifact.template, context.projectRoot);
    const dependencies = getDependencyInfo(artifact, context.graph, context.completed);
    const unlocks = getUnlockedArtifacts(context.graph, artifactId);
    // Build enriched template with project config injections
    let enrichedTemplate = '';
    let projectConfig = null;
    // Use projectRoot from context if not explicitly provided
    const effectiveProjectRoot = projectRoot ?? context.projectRoot;
    // Try to read project config
    if (effectiveProjectRoot) {
        try {
            projectConfig = readProjectConfig(effectiveProjectRoot);
        }
        catch {
            // If config read fails, continue without config
        }
    }
    // Validate rules artifact IDs if config has rules (only once per session)
    if (projectConfig?.rules) {
        const validArtifactIds = new Set(context.graph.getAllArtifacts().map((a) => a.id));
        const warnings = validateConfigRules(projectConfig.rules, validArtifactIds, context.schemaName);
        // Show each unique warning only once per session
        for (const warning of warnings) {
            if (!shownWarnings.has(warning)) {
                console.warn(warning);
                shownWarnings.add(warning);
            }
        }
    }
    // 1. Add context (all artifacts)
    if (projectConfig?.context) {
        enrichedTemplate += `<context>\n${projectConfig.context}\n</context>\n\n`;
    }
    // 2. Add rules (only for matching artifact)
    const rulesForArtifact = projectConfig?.rules?.[artifactId];
    if (rulesForArtifact && rulesForArtifact.length > 0) {
        enrichedTemplate += `<rules>\n`;
        for (const rule of rulesForArtifact) {
            enrichedTemplate += `- ${rule}\n`;
        }
        enrichedTemplate += `</rules>\n\n`;
    }
    // 3. Add original template (without wrapper - CLI handles XML structure)
    enrichedTemplate += templateContent;
    return {
        changeName: context.changeName,
        artifactId: artifact.id,
        schemaName: context.schemaName,
        changeDir: context.changeDir,
        outputPath: artifact.generates,
        description: artifact.description,
        instruction: artifact.instruction,
        template: enrichedTemplate,
        dependencies,
        unlocks,
    };
}
/**
 * Gets dependency info including paths and descriptions.
 */
function getDependencyInfo(artifact, graph, completed) {
    return artifact.requires.map(id => {
        const depArtifact = graph.getArtifact(id);
        return {
            id,
            done: completed.has(id),
            path: depArtifact?.generates ?? id,
            description: depArtifact?.description ?? '',
        };
    });
}
/**
 * Gets artifacts that become available after completing the given artifact.
 */
function getUnlockedArtifacts(graph, artifactId) {
    const unlocks = [];
    for (const artifact of graph.getAllArtifacts()) {
        if (artifact.requires.includes(artifactId)) {
            unlocks.push(artifact.id);
        }
    }
    return unlocks.sort();
}
/**
 * Formats the status of all artifacts in a change.
 *
 * @param context - Change context
 * @returns Formatted change status
 */
export function formatChangeStatus(context) {
    // Load schema to get apply phase configuration
    const schema = resolveSchema(context.schemaName, context.projectRoot);
    const applyRequires = schema.apply?.requires ?? schema.artifacts.map(a => a.id);
    const artifacts = context.graph.getAllArtifacts();
    const ready = new Set(context.graph.getNextArtifacts(context.completed));
    const blocked = context.graph.getBlocked(context.completed);
    const artifactStatuses = artifacts.map(artifact => {
        if (context.completed.has(artifact.id)) {
            return {
                id: artifact.id,
                outputPath: artifact.generates,
                status: 'done',
            };
        }
        if (ready.has(artifact.id)) {
            return {
                id: artifact.id,
                outputPath: artifact.generates,
                status: 'ready',
            };
        }
        return {
            id: artifact.id,
            outputPath: artifact.generates,
            status: 'blocked',
            missingDeps: blocked[artifact.id] ?? [],
        };
    });
    // Sort by build order for consistent output
    const buildOrder = context.graph.getBuildOrder();
    const orderMap = new Map(buildOrder.map((id, idx) => [id, idx]));
    artifactStatuses.sort((a, b) => (orderMap.get(a.id) ?? 0) - (orderMap.get(b.id) ?? 0));
    return {
        changeName: context.changeName,
        schemaName: context.schemaName,
        isComplete: context.graph.isComplete(context.completed),
        applyRequires,
        artifacts: artifactStatuses,
    };
}
//# sourceMappingURL=instruction-loader.js.map