/**
 * GitHub US Auto-Closer — Closes GitHub issues when all ACs are complete
 *
 * For each affected user story, checks if ALL acceptance criteria are
 * marked complete in spec.md. If so, posts a completion comment and
 * closes the associated GitHub issue.
 *
 * @module github-us-auto-closer
 */
import { readFile } from 'fs/promises';
import { existsSync } from 'fs';
import * as path from 'path';
import { execFileNoThrow } from '../../../../../src/utils/execFileNoThrow.js';
/**
 * Auto-close GitHub issues for user stories with all ACs complete.
 *
 * For each affected US:
 * 1. Parse AC states from spec.md
 * 2. Skip if not all ACs complete
 * 3. Look up GitHub issue link from frontmatter
 * 4. Check if issue already closed (idempotent)
 * 5. Post completion comment
 * 6. Close issue via `gh issue close`
 *
 * Never throws — all errors captured in result.errors.
 */
export async function autoCloseCompletedUserStories(incrementId, affectedUSIds, specPath, options) {
    const result = { closed: [], skipped: [], errors: [] };
    if (affectedUSIds.length === 0) {
        return result;
    }
    let content;
    try {
        content = await readFile(specPath, 'utf-8');
    }
    catch (err) {
        result.errors.push({
            usId: affectedUSIds[0],
            error: err instanceof Error ? err.message : String(err),
        });
        return result;
    }
    const issueLinks = await parseIssueLinks(specPath);
    const repoSlug = `${options.owner}/${options.repo}`;
    const env = options.token ? { GH_TOKEN: options.token } : undefined;
    const execOpts = env ? { env } : {};
    for (const usId of affectedUSIds) {
        const acStates = parseACStatesForUS(content, usId);
        // Skip if not all ACs complete
        if (acStates.length === 0 || acStates.some(ac => !ac.completed)) {
            result.skipped.push({ usId, reason: 'incomplete-acs' });
            continue;
        }
        // Skip if no issue link
        const link = issueLinks[usId];
        if (!link) {
            result.skipped.push({ usId, reason: 'no-issue-link' });
            continue;
        }
        const issueNum = String(link.issueNumber);
        // Check if already closed (idempotent)
        const viewResult = await execFileNoThrow('gh', ['issue', 'view', issueNum, '--json', 'state', '-R', repoSlug], execOpts);
        if (viewResult.success) {
            try {
                const issueState = JSON.parse(viewResult.stdout);
                if (issueState.state === 'CLOSED') {
                    result.skipped.push({ usId, reason: 'already-closed' });
                    continue;
                }
            }
            catch {
                // Parse error — proceed with close attempt
            }
        }
        // Post completion comment
        const commentBody = buildCompletionComment(incrementId, usId, acStates);
        await execFileNoThrow('gh', ['issue', 'comment', issueNum, '--body', commentBody, '-R', repoSlug], execOpts);
        // Close the issue
        const closeResult = await execFileNoThrow('gh', ['issue', 'close', issueNum, '-R', repoSlug], execOpts);
        if (closeResult.success) {
            // Ensure required labels exist before applying them
            await ensureLabelExists('status:completed', repoSlug, execOpts, {
                color: '6f42c1',
                description: 'Completed work item',
            });
            // Update labels: remove status:active, add status:completed
            await execFileNoThrow('gh', ['issue', 'edit', issueNum, '--remove-label', 'status:active', '--add-label', 'status:completed', '-R', repoSlug], execOpts);
            result.closed.push({ usId, issueNumber: link.issueNumber });
        }
        else {
            result.errors.push({
                usId,
                error: closeResult.stderr || 'Unknown error closing issue',
            });
        }
    }
    return result;
}
/**
 * Build a completion comment for a fully-complete user story.
 */
function buildCompletionComment(incrementId, usId, acStates) {
    const total = acStates.length;
    let comment = `**All acceptance criteria completed** — ${usId} (Increment ${incrementId})\n\n`;
    comment += `**Status**: ${total}/${total} ACs complete (100%)\n\n`;
    comment += `**Completed**:\n`;
    for (const ac of acStates) {
        comment += `- [x] **${ac.id}**: ${ac.description}\n`;
    }
    comment += '\n';
    comment += `---\n`;
    comment += `Auto-closed by SpecWeave | ${new Date().toISOString().split('T')[0]}\n`;
    return comment;
}
/**
 * Parse issue links from metadata.json (sibling of spec.md).
 *
 * Supports TWO formats:
 * - OLD: metadata.github.issues[] with { userStory, number, url }
 * - NEW: metadata.externalLinks.github.issues with { [US-XXX]: { issueNumber, issueUrl } }
 *
 * Falls back to empty if metadata.json is missing or invalid.
 */
async function parseIssueLinks(specPath) {
    const links = {};
    try {
        const metadataPath = path.join(path.dirname(specPath), 'metadata.json');
        if (!existsSync(metadataPath))
            return links;
        const raw = await readFile(metadataPath, 'utf-8');
        const metadata = JSON.parse(raw);
        // OLD format: metadata.github.issues[] array
        if (metadata.github?.issues && Array.isArray(metadata.github.issues)) {
            for (const entry of metadata.github.issues) {
                if (entry.userStory && entry.number) {
                    links[entry.userStory] = {
                        issueNumber: entry.number,
                        issueUrl: entry.url || '',
                    };
                }
            }
        }
        // NEW format: metadata.externalLinks.github.issues object
        if (metadata.externalLinks?.github?.issues) {
            const issues = metadata.externalLinks.github.issues;
            for (const [usId, data] of Object.entries(issues)) {
                const issueData = data;
                if (issueData.issueNumber) {
                    links[usId] = {
                        issueNumber: issueData.issueNumber,
                        issueUrl: issueData.issueUrl || '',
                    };
                }
            }
        }
    }
    catch {
        // Graceful fallback: return empty if metadata.json is missing or invalid
    }
    return links;
}
/**
 * Ensure a label exists in the repo before applying it.
 * Creates the label if missing; logs a warning and continues on permission errors.
 */
async function ensureLabelExists(labelName, repoSlug, execOpts, defaults) {
    // Check if label exists
    const checkResult = await execFileNoThrow('gh', ['label', 'list', '--repo', repoSlug, '--search', labelName, '--json', 'name', '--jq', '.[].name'], execOpts);
    if (checkResult.success) {
        const existing = (checkResult.stdout || '').trim().split('\n').filter(Boolean);
        if (existing.some(name => name === labelName)) {
            return; // Label already exists
        }
    }
    // Create the label
    const createResult = await execFileNoThrow('gh', ['label', 'create', labelName, '--repo', repoSlug, '--color', defaults.color, '--description', defaults.description, '--force'], execOpts);
    if (!createResult.success) {
        // Permission error or other failure — log warning and continue
        console.warn(`⚠️  Could not create label "${labelName}": ${createResult.stderr || 'unknown error'} (continuing without label)`);
    }
}
/**
 * Build the AC ID prefix for a given US ID.
 * US-001 → "AC-US1-", US-SPE-001 → "AC-SPE-US1-"
 */
function buildACPrefix(usId) {
    const compoundMatch = usId.match(/^US-([A-Z]+)-(\d+)$/);
    if (compoundMatch) {
        return `AC-${compoundMatch[1]}-US${parseInt(compoundMatch[2], 10)}-`;
    }
    const simpleMatch = usId.match(/^US-(\d+)$/);
    if (simpleMatch) {
        return `AC-US${parseInt(simpleMatch[1], 10)}-`;
    }
    const fallback = usId.match(/(\d+)$/);
    const num = fallback ? parseInt(fallback[1], 10) : 0;
    return `AC-US${num}-`;
}
/**
 * Extract AC states for a specific user story from spec.md content.
 * Supports both simple (AC-US1-01) and compound (AC-SPE-US1-01) AC ID formats.
 */
function parseACStatesForUS(content, usId) {
    const states = [];
    const acPrefix = buildACPrefix(usId);
    const escapedPrefix = acPrefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    // Support both bold (**AC-...**:) and plain (AC-...:) formats
    const acPattern = new RegExp(`- \\[([ x])\\] (?:\\*\\*)?${escapedPrefix}(\\d+)(?:\\*\\*)?:\\s*(.+)`, 'g');
    let match;
    while ((match = acPattern.exec(content)) !== null) {
        states.push({
            id: `${acPrefix}${match[2]}`,
            description: match[3].trim(),
            completed: match[1] === 'x',
        });
    }
    return states;
}
//# sourceMappingURL=github-us-auto-closer.js.map