/**
 * Instructions Command
 *
 * Generates enriched instructions for creating artifacts or applying tasks.
 * Includes both artifact instructions and apply instructions.
 */
import ora from 'ora';
import path from 'path';
import * as fs from 'fs';
import { loadChangeContext, generateInstructions, resolveSchema, resolveArtifactOutputs, } from '../../core/artifact-graph/index.js';
import { getChangeDir, resolveCurrentPlanningHomeSync, } from '../../core/planning-home.js';
import { resolveRootForCommand, withStoreFlag, toPlanningHome, toRootOutput, } from '../../core/root-selection.js';
import { assembleReferenceIndex, renderReferencedStoresBlock, renderReferencedStoresSection, } from '../../core/references.js';
import { readRegistrySnapshot } from '../../core/store/registry.js';
import { readProjectConfig } from '../../core/project-config.js';
import { validateChangeExists, validateSchemaExists, } from './shared.js';
// -----------------------------------------------------------------------------
// Artifact Instructions Command
// -----------------------------------------------------------------------------
/**
 * Reads the resolved root's config once, assembles the referenced-store
 * index when references are declared, and resolves the config path for
 * fix text. Shared by both instruction surfaces.
 */
async function loadRootConfigContext(root) {
    // readProjectConfig never throws: missing/unparseable configs are null.
    const projectConfig = readProjectConfig(root.path);
    // One registry read serves every relationship consumer in this
    // output so it never carries a torn snapshot.
    const snapshot = await readRegistrySnapshot();
    const registryEntries = snapshot.entries;
    const declared = projectConfig?.references ?? [];
    const index = declared.length > 0
        ? await assembleReferenceIndex({ references: declared, resolvedRoot: root, registryEntries })
        : [];
    // Omitted, not empty: an index emptied by self-reference omission must
    // look identical to an undeclared one in JSON.
    return {
        projectConfig,
        references: index.length > 0 ? index : undefined,
    };
}
export async function instructionsCommand(artifactId, options) {
    // Resolve (and banner) before the spinner starts so stderr stays readable.
    const root = await resolveRootForCommand(options, { json: options.json });
    if (!root) {
        return;
    }
    const spinner = options.json ? undefined : ora('Generating instructions...').start();
    try {
        const planningHome = toPlanningHome(root);
        const projectRoot = root.path;
        const changeName = await validateChangeExists(options.change, projectRoot, root.changesDir, { newChangeHint: withStoreFlag(root, 'openspec new change <name>') });
        // Validate schema if explicitly provided
        if (options.schema) {
            validateSchemaExists(options.schema, projectRoot);
        }
        // loadChangeContext will auto-detect schema from metadata if not provided
        const context = loadChangeContext(projectRoot, changeName, options.schema, {
            changeDir: getChangeDir(planningHome, changeName),
            planningHome,
        });
        if (!artifactId) {
            spinner?.stop();
            const validIds = context.graph.getAllArtifacts().map((a) => a.id);
            throw new Error(`Missing required argument <artifact>. Valid artifacts:\n  ${validIds.join('\n  ')}`);
        }
        const artifact = context.graph.getArtifact(artifactId);
        if (!artifact) {
            spinner?.stop();
            const validIds = context.graph.getAllArtifacts().map((a) => a.id);
            throw new Error(`Artifact '${artifactId}' not found in schema '${context.schemaName}'. Valid artifacts:\n  ${validIds.join('\n  ')}`);
        }
        const { projectConfig, references } = await loadRootConfigContext(root);
        const instructions = generateInstructions(context, artifactId, projectRoot, {
            projectConfig,
            references,
        });
        const isBlocked = instructions.dependencies.some((d) => !d.done);
        spinner?.stop();
        if (options.json) {
            console.log(JSON.stringify({ ...instructions, root: toRootOutput(root) }, null, 2));
            return;
        }
        printInstructionsText(instructions, isBlocked);
    }
    catch (error) {
        spinner?.stop();
        throw error;
    }
}
export function printInstructionsText(instructions, isBlocked) {
    const { artifactId, changeName, schemaName, changeDir, resolvedOutputPath, description, instruction, context, rules, template, dependencies, unlocks, } = instructions;
    // Opening tag
    console.log(`<artifact id="${artifactId}" change="${changeName}" schema="${schemaName}">`);
    console.log();
    // Warning for blocked artifacts
    if (isBlocked) {
        const missing = dependencies.filter((d) => !d.done).map((d) => d.id);
        console.log('<warning>');
        console.log('This artifact has unmet dependencies. Complete them first or proceed with caution.');
        console.log(`Missing: ${missing.join(', ')}`);
        console.log('</warning>');
        console.log();
    }
    // Task directive
    console.log('<task>');
    console.log(`Create the ${artifactId} artifact for change "${changeName}".`);
    console.log(description);
    console.log('</task>');
    console.log();
    // Project context (AI constraint - do not include in output)
    if (context) {
        console.log('<project_context>');
        console.log('<!-- This is background information for you. Do NOT include this in your output. -->');
        console.log(context);
        console.log('</project_context>');
        console.log();
    }
    // Referenced-store index (read-only upstream context)
    if (instructions.references && instructions.references.length > 0) {
        console.log(renderReferencedStoresBlock(instructions.references));
        console.log();
    }
    // Rules (AI constraint - do not include in output)
    if (rules && rules.length > 0) {
        console.log('<rules>');
        console.log('<!-- These are constraints for you to follow. Do NOT include this in your output. -->');
        for (const rule of rules) {
            console.log(`- ${rule}`);
        }
        console.log('</rules>');
        console.log();
    }
    // Dependencies (files to read for context)
    if (dependencies.length > 0) {
        console.log('<dependencies>');
        console.log('Read these files for context before creating this artifact:');
        console.log();
        for (const dep of dependencies) {
            const status = dep.done ? 'done' : 'missing';
            const fullPath = path.join(changeDir, dep.path);
            console.log(`<dependency id="${dep.id}" status="${status}">`);
            console.log(`  <path>${fullPath}</path>`);
            console.log(`  <description>${dep.description}</description>`);
            console.log('</dependency>');
        }
        console.log('</dependencies>');
        console.log();
    }
    // Output location
    console.log('<output>');
    console.log(`Write to: ${resolvedOutputPath}`);
    console.log('</output>');
    console.log();
    // Instruction (guidance)
    if (instruction) {
        console.log('<instruction>');
        console.log(instruction.trim());
        console.log('</instruction>');
        console.log();
    }
    // Template
    console.log('<template>');
    console.log('<!-- Use this as the structure for your output file. Fill in the sections. -->');
    console.log(template.trim());
    console.log('</template>');
    console.log();
    // Success criteria placeholder
    console.log('<success_criteria>');
    console.log('<!-- To be defined in schema validation rules -->');
    console.log('</success_criteria>');
    console.log();
    // Unlocks
    if (unlocks.length > 0) {
        console.log('<unlocks>');
        console.log(`Completing this artifact enables: ${unlocks.join(', ')}`);
        console.log('</unlocks>');
        console.log();
    }
    // Closing tag
    console.log('</artifact>');
}
// -----------------------------------------------------------------------------
// Apply Instructions Command
// -----------------------------------------------------------------------------
/**
 * Parses tasks.md content and extracts task items with their completion status.
 */
function parseTasksFile(content) {
    const tasks = [];
    const lines = content.split('\n');
    let taskIndex = 0;
    for (const line of lines) {
        // Match checkbox patterns: - [ ] or - [x] or - [X]
        const checkboxMatch = line.match(/^[-*]\s*\[([ xX])\]\s*(.+)\s*$/);
        if (checkboxMatch) {
            taskIndex++;
            const done = checkboxMatch[1].toLowerCase() === 'x';
            const description = checkboxMatch[2].trim();
            tasks.push({
                id: `${taskIndex}`,
                description,
                done,
            });
        }
    }
    return tasks;
}
/**
 * Generates apply instructions for implementing tasks from a change.
 * Schema-aware: reads apply phase configuration from schema to determine
 * required artifacts, tracking file, and instruction.
 */
export async function generateApplyInstructions(projectRoot, changeName, schemaName, options = {}) {
    const planningHome = options.planningHome ?? resolveCurrentPlanningHomeSync({ startPath: projectRoot });
    const references = options.references;
    // loadChangeContext will auto-detect schema from metadata if not provided
    const context = loadChangeContext(projectRoot, changeName, schemaName, {
        changeDir: getChangeDir(planningHome, changeName),
        planningHome,
    });
    const changeDir = context.changeDir;
    // Get the full schema to access the apply phase configuration
    const schema = resolveSchema(context.schemaName, projectRoot);
    const applyConfig = schema.apply;
    // Determine required artifacts and tracking file from schema
    // Fallback: if no apply block, require all artifacts
    const requiredArtifactIds = applyConfig?.requires ?? schema.artifacts.map((a) => a.id);
    const tracksFile = applyConfig?.tracks ?? null;
    const schemaInstruction = applyConfig?.instruction ?? null;
    // Check which required artifacts are missing
    const missingArtifacts = [];
    for (const artifactId of requiredArtifactIds) {
        const artifact = schema.artifacts.find((a) => a.id === artifactId);
        if (artifact && resolveArtifactOutputs(changeDir, artifact.generates).length === 0) {
            missingArtifacts.push(artifactId);
        }
    }
    // Build context files from all existing artifacts in schema
    const contextFiles = {};
    for (const artifact of schema.artifacts) {
        const outputs = resolveArtifactOutputs(changeDir, artifact.generates);
        if (outputs.length > 0) {
            contextFiles[artifact.id] = outputs;
        }
    }
    // Parse tasks if tracking file exists
    let tasks = [];
    let tracksFileExists = false;
    if (tracksFile) {
        const tracksPath = path.join(changeDir, tracksFile);
        tracksFileExists = fs.existsSync(tracksPath);
        if (tracksFileExists) {
            const tasksContent = await fs.promises.readFile(tracksPath, 'utf-8');
            tasks = parseTasksFile(tasksContent);
        }
    }
    // Calculate progress
    const total = tasks.length;
    const complete = tasks.filter((t) => t.done).length;
    const remaining = total - complete;
    // Determine state and instruction
    let state;
    let instruction;
    if (missingArtifacts.length > 0) {
        state = 'blocked';
        instruction = `Cannot apply this change yet. Missing artifacts: ${missingArtifacts.join(', ')}.\nUse the openspec-continue-change skill to create the missing artifacts first.`;
    }
    else if (tracksFile && !tracksFileExists) {
        // Tracking file configured but doesn't exist yet
        const tracksFilename = path.basename(tracksFile);
        state = 'blocked';
        instruction = `The ${tracksFilename} file is missing and must be created.\nUse openspec-continue-change to generate the tracking file.`;
    }
    else if (tracksFile && tracksFileExists && total === 0) {
        // Tracking file exists but contains no tasks
        const tracksFilename = path.basename(tracksFile);
        state = 'blocked';
        instruction = `The ${tracksFilename} file exists but contains no tasks.\nAdd tasks to ${tracksFilename} or regenerate it with openspec-continue-change.`;
    }
    else if (tracksFile && remaining === 0 && total > 0) {
        state = 'all_done';
        instruction = 'All tasks are complete! This change is ready to be archived.\nConsider running tests and reviewing the changes before archiving.';
    }
    else if (!tracksFile) {
        // No tracking file configured in schema - ready to apply
        state = 'ready';
        instruction = schemaInstruction?.trim() ?? 'All required artifacts complete. Proceed with implementation.';
    }
    else {
        state = 'ready';
        instruction = schemaInstruction?.trim() ?? 'Read context files, work through pending tasks, mark complete as you go.\nPause if you hit blockers or need clarification.';
    }
    return {
        changeName,
        changeDir,
        schemaName: context.schemaName,
        contextFiles,
        progress: { total, complete, remaining },
        tasks,
        state,
        missingArtifacts: missingArtifacts.length > 0 ? missingArtifacts : undefined,
        instruction,
        ...(references !== undefined ? { references } : {}),
    };
}
export async function applyInstructionsCommand(options) {
    // Resolve (and banner) before the spinner starts so stderr stays readable.
    const root = await resolveRootForCommand(options, { json: options.json });
    if (!root) {
        return;
    }
    const spinner = options.json ? undefined : ora('Generating apply instructions...').start();
    try {
        const planningHome = toPlanningHome(root);
        const projectRoot = root.path;
        const changeName = await validateChangeExists(options.change, projectRoot, root.changesDir, { newChangeHint: withStoreFlag(root, 'openspec new change <name>') });
        // Validate schema if explicitly provided
        if (options.schema) {
            validateSchemaExists(options.schema, projectRoot);
        }
        // generateApplyInstructions uses loadChangeContext which auto-detects schema
        const { references } = await loadRootConfigContext(root);
        const instructions = await generateApplyInstructions(projectRoot, changeName, options.schema, {
            planningHome,
            references,
        });
        spinner?.stop();
        if (options.json) {
            console.log(JSON.stringify({ ...instructions, root: toRootOutput(root) }, null, 2));
            return;
        }
        printApplyInstructionsText(instructions);
    }
    catch (error) {
        spinner?.stop();
        throw error;
    }
}
export function printApplyInstructionsText(instructions) {
    const { changeName, schemaName, contextFiles, progress, tasks, state, missingArtifacts, instruction } = instructions;
    console.log(`## Apply: ${changeName}`);
    console.log(`Schema: ${schemaName}`);
    console.log();
    if (instructions.references && instructions.references.length > 0) {
        console.log(renderReferencedStoresSection(instructions.references));
        console.log();
    }
    // Warning for blocked state
    if (state === 'blocked' && missingArtifacts) {
        console.log('### ⚠️ Blocked');
        console.log();
        console.log(`Missing artifacts: ${missingArtifacts.join(', ')}`);
        console.log('Use the openspec-continue-change skill to create these first.');
        console.log();
    }
    // Context files (dynamically from schema)
    const contextFileEntries = Object.entries(contextFiles);
    if (contextFileEntries.length > 0) {
        console.log('### Context Files');
        for (const [artifactId, filePaths] of contextFileEntries) {
            for (const filePath of filePaths) {
                console.log(`- ${artifactId}: ${filePath}`);
            }
        }
        console.log();
    }
    // Progress (only show if we have tracking)
    if (progress.total > 0 || tasks.length > 0) {
        console.log('### Progress');
        if (state === 'all_done') {
            console.log(`${progress.complete}/${progress.total} complete ✓`);
        }
        else {
            console.log(`${progress.complete}/${progress.total} complete`);
        }
        console.log();
    }
    // Tasks
    if (tasks.length > 0) {
        console.log('### Tasks');
        for (const task of tasks) {
            const checkbox = task.done ? '[x]' : '[ ]';
            console.log(`- ${checkbox} ${task.description}`);
        }
        console.log();
    }
    // Instruction
    console.log('### Instruction');
    console.log(instruction);
}
//# sourceMappingURL=instructions.js.map