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 { resolveArtifactOutputs } from './outputs.js';
import { readChangeMetadata, resolveSchemaForChange } from '../../utils/change-metadata.js';
import { FileSystemUtils } from '../../utils/file-system.js';
import { buildActionContext, buildNextSteps, summarizeAffectedAreas, summarizePlanningHome, } from '../change-status-policy.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 templatePathOnDisk = path.join(schemaDir, 'templates', templatePath);
    if (!fs.existsSync(templatePathOnDisk)) {
        throw new TemplateLoadError(`Template not found: ${templatePathOnDisk}`, templatePathOnDisk);
    }
    const fullPath = FileSystemUtils.canonicalizeExistingPath(templatePathOnDisk);
    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, options = {}) {
    const changeDir = FileSystemUtils.canonicalizeExistingPath(options.changeDir ?? path.join(projectRoot, 'openspec', 'changes', changeName));
    const metadata = readChangeMetadata(changeDir, projectRoot) ?? undefined;
    const resolvedSchemaName = resolveSchemaForChange(changeDir, schemaName, projectRoot, {
        metadata: metadata ?? null,
    });
    const schema = resolveSchema(resolvedSchemaName, projectRoot);
    const graph = ArtifactGraph.fromSchema(schema);
    const completed = detectCompleted(graph, changeDir);
    return {
        graph,
        completed,
        schemaName: resolvedSchemaName,
        changeName,
        changeDir,
        projectRoot,
        ...(options.planningHome ? { planningHome: options.planningHome } : {}),
        ...(metadata ? { metadata } : {}),
        ...(metadata?.initiative ? { initiative: metadata.initiative } : {}),
    };
}
/**
 * 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);
    // Use projectRoot from context if not explicitly provided
    const effectiveProjectRoot = projectRoot ?? context.projectRoot;
    // Try to read project config for context and rules
    let projectConfig = null;
    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);
            }
        }
    }
    // Extract context and rules as separate fields (not prepended to template)
    const configContext = projectConfig?.context?.trim() || undefined;
    const rulesForArtifact = projectConfig?.rules?.[artifactId];
    const configRules = rulesForArtifact && rulesForArtifact.length > 0 ? rulesForArtifact : undefined;
    return {
        changeName: context.changeName,
        artifactId: artifact.id,
        schemaName: context.schemaName,
        changeDir: context.changeDir,
        planningHome: summarizePlanningHome(context.planningHome),
        ...(context.initiative ? { initiative: context.initiative } : {}),
        outputPath: artifact.generates,
        resolvedOutputPath: path.join(context.changeDir, artifact.generates),
        existingOutputPaths: resolveArtifactOutputs(context.changeDir, artifact.generates),
        description: artifact.description,
        instruction: artifact.instruction,
        context: configContext,
        rules: configRules,
        template: templateContent,
        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 artifactPaths = {};
    const artifactStatuses = artifacts.map(artifact => {
        artifactPaths[artifact.id] = {
            outputPath: artifact.generates,
            resolvedOutputPath: path.join(context.changeDir, artifact.generates),
            existingOutputPaths: resolveArtifactOutputs(context.changeDir, artifact.generates),
        };
        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));
    const affectedAreas = summarizeAffectedAreas({
        planningHome: context.planningHome,
        metadata: context.metadata,
    });
    const isComplete = context.graph.isComplete(context.completed);
    const artifactIds = artifactStatuses.map((artifact) => artifact.id);
    return {
        changeName: context.changeName,
        schemaName: context.schemaName,
        planningHome: summarizePlanningHome(context.planningHome),
        ...(context.initiative ? { initiative: context.initiative } : {}),
        changeRoot: context.changeDir,
        artifactPaths,
        affectedAreas,
        isComplete,
        applyRequires,
        nextSteps: buildNextSteps({
            changeName: context.changeName,
            planningHome: context.planningHome,
            artifactStatuses,
            affectedAreas,
            allArtifactsComplete: isComplete,
        }),
        actionContext: buildActionContext({
            planningHome: context.planningHome,
            projectRoot: context.projectRoot,
            artifactIds,
        }),
        artifacts: artifactStatuses,
    };
}
//# sourceMappingURL=instruction-loader.js.map