/**
 * GitHub AC Comment Poster — Posts progress comments to GitHub issues
 * when acceptance criteria are completed in spec.md.
 *
 * Triggered by github-ac-sync-handler.sh after task-ac-sync-guard
 * updates spec.md ACs.
 *
 * @module github-ac-comment-poster
 */
import { readFile } from 'fs/promises';
import { existsSync } from 'fs';
import * as path from 'path';
import { execFileNoThrow } from '../../../../../src/utils/execFileNoThrow.js';
import { buildFingerprint, extractFingerprint } from './github-body-utils.js';
/**
 * Post AC progress comments to GitHub issues for affected user stories.
 *
 * For each affected US:
 * 1. Look up GitHub issue link from spec.md frontmatter
 * 2. Extract AC states for that US
 * 3. Build progress comment
 * 4. Post via `gh issue comment`
 *
 * Never throws — all errors are captured in result.errors.
 */
export async function postACProgressComments(incrementId, affectedUSIds, specPath, options) {
    const result = { posted: [], 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;
    for (const usId of affectedUSIds) {
        const link = issueLinks[usId];
        if (!link) {
            continue;
        }
        const acStates = parseACStatesForUS(content, usId);
        // Skip progress comment when all ACs are complete — the auto-closer will
        // post its own "All acceptance criteria completed" comment instead,
        // preventing duplicate comments on the same issue.
        const allComplete = acStates.length > 0 && acStates.every(ac => ac.completed);
        if (!allComplete) {
            const total = acStates.length;
            const completed = acStates.filter(ac => ac.completed).length;
            const currentFingerprint = buildFingerprint(completed, total);
            // Dedup: fetch last comment and check fingerprint before posting
            let shouldPost = true;
            try {
                const commentsResult = await execFileNoThrow('gh', [
                    'api',
                    `repos/${repoSlug}/issues/${link.issueNumber}/comments`,
                    '--jq', '.[-1].body',
                ], env ? { env } : {});
                if (commentsResult.success && commentsResult.stdout.trim()) {
                    const lastFingerprint = extractFingerprint(commentsResult.stdout);
                    if (lastFingerprint === `${completed}/${total}`) {
                        shouldPost = false;
                    }
                }
            }
            catch {
                // If comment fetch fails, proceed with posting
            }
            if (shouldPost) {
                const commentBody = buildProgressCommentForUS(incrementId, usId, acStates)
                    + currentFingerprint + '\n';
                const execResult = await execFileNoThrow('gh', ['issue', 'comment', String(link.issueNumber), '--body', commentBody, '-R', repoSlug], env ? { env } : {});
                if (execResult.success) {
                    result.posted.push({ usId, issueNumber: link.issueNumber });
                }
                else {
                    result.errors.push({
                        usId,
                        error: execResult.stderr || 'Unknown error posting comment',
                    });
                }
            }
        }
        // Patch AC checkboxes directly in the issue body using the known issue number
        if (acStates.length > 0) {
            try {
                await patchIssueBodyCheckboxes(link.issueNumber, acStates, repoSlug, env);
            }
            catch (patchErr) {
                // Body patch failure is non-blocking but should be visible
                result.errors.push({
                    usId,
                    error: `body-patch: ${patchErr instanceof Error ? patchErr.message : String(patchErr)}`,
                });
            }
        }
    }
    return result;
}
/**
 * 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) {
                    // NEW format entries override OLD format for the same US
                    links[usId] = {
                        issueNumber: issueData.issueNumber,
                        issueUrl: issueData.issueUrl || '',
                    };
                }
            }
        }
    }
    catch {
        // Graceful fallback: return empty if metadata.json is missing or invalid
    }
    return links;
}
/**
 * Parse a US ID into parts for AC matching.
 * US-001 → { prefix: undefined, num: 1 }
 * US-SPE-001 → { prefix: 'SPE', num: 1 }
 */
function parseUSIdParts(usId) {
    const compoundMatch = usId.match(/^US-([A-Z]+)-(\d+)$/);
    if (compoundMatch) {
        return { prefix: compoundMatch[1], num: parseInt(compoundMatch[2], 10) };
    }
    const simpleMatch = usId.match(/^US-(\d+)$/);
    if (simpleMatch) {
        return { num: parseInt(simpleMatch[1], 10) };
    }
    const fallback = usId.match(/(\d+)$/);
    return { num: fallback ? parseInt(fallback[1], 10) : 0 };
}
/**
 * Build the AC ID prefix for a given US ID.
 * US-001 → "AC-US1-", US-SPE-001 → "AC-SPE-US1-"
 */
function buildACPrefix(usId) {
    const { prefix, num } = parseUSIdParts(usId);
    return prefix ? `AC-${prefix}-US${num}-` : `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;
}
/**
 * Build a progress comment for a specific user story.
 * Follows ProgressCommentBuilder format: percentage, AC checkboxes, timestamp.
 */
function buildProgressCommentForUS(incrementId, usId, acStates) {
    const total = acStates.length;
    const completed = acStates.filter(ac => ac.completed).length;
    const percentage = total > 0 ? Math.round((completed / total) * 100) : 0;
    let comment = `**Progress Update** — ${usId} (Increment ${incrementId})\n\n`;
    comment += `**Status**: ${completed}/${total} ACs complete (${percentage}%)\n\n`;
    if (completed > 0) {
        comment += `**Completed**:\n`;
        for (const ac of acStates.filter(a => a.completed)) {
            comment += `- [x] **${ac.id}**: ${ac.description}\n`;
        }
        comment += '\n';
    }
    if (completed < total) {
        comment += `**Remaining**:\n`;
        for (const ac of acStates.filter(a => !a.completed)) {
            comment += `- [ ] **${ac.id}**: ${ac.description}\n`;
        }
        comment += '\n';
    }
    comment += `---\n`;
    comment += `Auto-synced by SpecWeave | ${new Date().toISOString().split('T')[0]}\n`;
    return comment;
}
/**
 * Patch AC checkboxes in the GitHub issue body in-place.
 * Fetches the current body, flips checkboxes to match local AC states,
 * and updates the issue. Only modifies lines matching AC-USXX-YY patterns.
 */
async function patchIssueBodyCheckboxes(issueNumber, acStates, repoSlug, env) {
    // Build a map of AC ID → completed
    const acMap = new Map(acStates.map(ac => [ac.id, ac.completed]));
    // Fetch current issue body
    const fetchResult = await execFileNoThrow('gh', ['issue', 'view', String(issueNumber), '--json', 'body', '-q', '.body', '-R', repoSlug], env ? { env } : {});
    if (!fetchResult.success || !fetchResult.stdout)
        return;
    const body = fetchResult.stdout;
    // Patch AC checkboxes: match both bold and plain AC-ID patterns
    const patchedBody = body.replace(/^(- \[)[ x](\] (?:\*\*)?)(AC-[A-Z0-9]+-\d+)((?:\*\*)?: .+)$/gm, (_match, prefix, mid, acId, suffix) => {
        const completed = acMap.get(acId);
        if (completed !== undefined) {
            return `${prefix}${completed ? 'x' : ' '}${mid}${acId}${suffix}`;
        }
        return _match;
    });
    if (patchedBody === body)
        return; // No changes needed
    const editResult = await execFileNoThrow('gh', ['issue', 'edit', String(issueNumber), '--body', patchedBody, '-R', repoSlug], env ? { env } : {});
    if (!editResult.success) {
        throw new Error(editResult.stderr || 'Failed to update issue body');
    }
}
//# sourceMappingURL=github-ac-comment-poster.js.map