/**
 * Spec Content Synchronization
 *
 * Syncs spec CONTENT (title, description, user stories) to external tools.
 * Does NOT sync STATUS - that's managed by external tool.
 *
 * Sync Direction:
 * - Title/Description/User Stories: SpecWeave → External Tool (we update)
 * - Status/State: External Tool → SpecWeave (we read)
 */
import fs from 'fs/promises';
import path from 'path';
import matter from 'gray-matter';
import { detectSpecIdentifier } from './spec-identifier-detector.js';
import { consoleLogger } from '../../utils/logger.js';
/**
 * Parse individual User Story file to extract ACs and tasks
 */
async function parseUserStoryFile(usFilePath, usId, logger = consoleLogger) {
    try {
        const content = await fs.readFile(usFilePath, 'utf-8');
        const { data: frontmatter } = matter(content);
        const usNumberMatch = usId.match(/US-(\d+)/);
        if (!usNumberMatch)
            return null;
        const title = extractUSTitle(content, frontmatter);
        const usNumberPattern = usNumberMatch[1].replace(/^0+/, '');
        const acceptanceCriteria = extractACsFromSection(content, usNumberPattern, frontmatter.priority);
        return { id: usId, title, acceptanceCriteria };
    }
    catch (error) {
        logger.error(`Failed to parse US file ${usFilePath}:`, error);
        return null;
    }
}
/**
 * Extract user story title from content or frontmatter
 */
function extractUSTitle(content, frontmatter) {
    let title = frontmatter.title || '';
    if (!title) {
        const titleMatch = content.match(/^#\s+US-\d+:\s*(.+)$/m);
        title = titleMatch ? titleMatch[1].trim() : '';
    }
    return title.replace(/\s*\(Priority:.*?\)\s*$/, '').trim();
}
/**
 * Extract acceptance criteria from AC section
 */
function extractACsFromSection(content, usNumberPattern, priority) {
    const acSectionMatch = content.match(/##\s+Acceptance Criteria\s*\n([\s\S]*?)(?=\n##|\n---|\z)/);
    if (!acSectionMatch)
        return [];
    // 0708 wrap-up: tolerate a parenthetical annotation between the closing
    // bold and the colon, e.g. `**AC-US1-01** (P0): …`. Without the optional
    // `\\([^)]*\\)?` branch the sync hook misses parenthetical-format ACs and
    // logs false-positive desync warnings.
    const acRegex = new RegExp(`- \\[([ x])\\] \\*\\*AC-US0*${usNumberPattern}-(\\d+)\\*\\*\\s*(?:\\([^)]*\\))?\\s*:\\s*([^\\n]+)`, 'g');
    return [...acSectionMatch[1].matchAll(acRegex)].map(acMatch => ({
        id: `AC-US${usNumberPattern}-${acMatch[2]}`,
        description: acMatch[3].trim(),
        completed: acMatch[1] === 'x',
        priority,
        testable: true
    }));
}
/**
 * Parse spec.md to extract content
 */
export async function parseSpecContent(specPath, options = {}) {
    const logger = options.logger ?? consoleLogger;
    try {
        const content = await fs.readFile(specPath, 'utf-8');
        const { data: frontmatter } = matter(content);
        // Extract title (first heading)
        const titleMatch = content.match(/^#\s+(.+)$/m);
        let title = titleMatch ? titleMatch[1].replace(/^SPEC-\d+:\s*/, '') : 'Untitled';
        // Remove ID prefix from title if present (supports [FS-043], FS-043:, FS-043 formats)
        title = title.replace(/^\[?[A-Z]{2,4}-[A-Z0-9-]+\]?:?\s*/, '');
        // Extract project from path
        // .specweave/docs/internal/specs/{project}/{spec-id}.md
        const pathParts = specPath.split(path.sep);
        const specsIndex = pathParts.indexOf('specs');
        const project = specsIndex >= 0 && pathParts.length > specsIndex + 1
            ? pathParts[specsIndex + 1]
            : 'default';
        // Detect flexible identifier
        const specContentInput = {
            content,
            frontmatter,
            title,
            project,
            path: specPath
        };
        const identifier = detectSpecIdentifier(specContentInput);
        // Extract description (text between title and first ## heading)
        // Match content between the main heading and the first ## heading or ** pattern
        const descMatch = content.match(/^#[^\n]+\n\n(.+?)(?=\n##|\n\*\*[A-Z])/ms);
        const description = descMatch ? descMatch[1].trim() : '';
        // Extract metadata from frontmatter or body
        const metadata = {};
        // Priority from frontmatter first, then body
        if (frontmatter.priority) {
            metadata.priority = frontmatter.priority;
        }
        else {
            const priorityMatch = content.match(/\*\*Priority\*\*:\s*(P\d)/);
            if (priorityMatch)
                metadata.priority = priorityMatch[1];
        }
        // GitHub Project
        const githubMatch = content.match(/\*\*GitHub Project\*\*:\s*(.+)/);
        if (githubMatch)
            metadata.githubProject = githubMatch[1].trim();
        // JIRA Epic
        const jiraMatch = content.match(/\*\*JIRA Epic\*\*:\s*(.+)/);
        if (jiraMatch)
            metadata.jiraEpic = jiraMatch[1].trim();
        // ADO Feature
        const adoMatch = content.match(/\*\*ADO Feature\*\*:\s*(.+)/);
        if (adoMatch)
            metadata.adoFeature = adoMatch[1].trim();
        // Parse user stories
        const userStories = [];
        // Flexible AC pattern for all strategies
        // Supports: AC-US1-01, AC-API-US2-01, AC-US-FE-001-01, etc.
        const extractACsForUserStory = (usId, searchContent) => {
            const acceptanceCriteria = [];
            // Build a flexible pattern that matches this US's ACs
            // Extract the core part: US-001 → "001" or "1", US-API-001 → "API-001" or "API-1"
            const usIdPattern = usId.replace('US-', '');
            // Try multiple AC patterns for this user story
            // Pattern 1: AC-US{number}-{seq} (legacy)
            // Pattern 2: AC-{prefix}-US{number}-{seq} (e.g., AC-API-US2-01)
            // Pattern 3: AC-US-{prefix}-{number}-{seq} (e.g., AC-US-FE-001-01)
            const patterns = [
                // Legacy: AC-US1-01, AC-US001-01 → matches US-001
                new RegExp(`- \\[([ x])\\] \\*\\*AC-US0*(${usIdPattern.replace(/^0+/, '')})-(\\d+)\\*\\*\\s*(?:\\([^)]*\\))?\\s*:\\s*([^(\\n]+)(?:\\(([^)]+)\\))?`, 'g'),
                // Prefixed: AC-API-US2-01 → matches US-API-002
                new RegExp(`- \\[([ x])\\] \\*\\*AC-([A-Z]+)-US0*(${usIdPattern.replace(/^[A-Z]+-0*/, '')})-(\\d+)\\*\\*\\s*(?:\\([^)]*\\))?\\s*:\\s*([^(\\n]+)(?:\\(([^)]+)\\))?`, 'g'),
                // Full match for flexible IDs
                new RegExp(`- \\[([ x])\\] \\*\\*AC-(?:[A-Z]+-)?US(?:[A-Z]*)?-?0*${usIdPattern.replace(/^[A-Z]+-0*/, '')}-(\\d+)\\*\\*\\s*(?:\\([^)]*\\))?\\s*:\\s*([^(\\n]+)(?:\\(([^)]+)\\))?`, 'g'),
            ];
            for (const acRegex of patterns) {
                const acMatches = [...searchContent.matchAll(acRegex)];
                for (const acMatch of acMatches) {
                    const completed = acMatch[1] === 'x';
                    // Different capture groups depending on pattern
                    const acNumber = acMatch[3] || acMatch[2];
                    const acDescription = (acMatch[5] || acMatch[4] || acMatch[3] || '').trim();
                    const acMetadata = acMatch[6] || acMatch[5] || acMatch[4] || '';
                    // Skip if this is actually capturing the description
                    if (!acNumber || acNumber.length > 4)
                        continue;
                    const acId = `AC-${usId.replace('US-', 'US')}-${acNumber.padStart(2, '0')}`;
                    // Avoid duplicates
                    if (!acceptanceCriteria.some(ac => ac.id === acId)) {
                        acceptanceCriteria.push({
                            id: acId,
                            description: acDescription,
                            completed,
                            priority: acMetadata.includes('P1') ? 'P1' : acMetadata.includes('P2') ? 'P2' : undefined,
                            testable: acMetadata.includes('testable'),
                        });
                    }
                }
            }
            return acceptanceCriteria;
        };
        // STRATEGY 1: Try inline format first (backward compatibility)
        // Match user story patterns: **US-001**: Title or **US-API-001**: Title
        // Flexible: supports US-001, US-API-001, US-FE-001, etc.
        const userStoryRegex = /\*\*(US-(?:[A-Z]+-)?(?:\d+))\*\*:\s*(.+)/g;
        const usMatches = [...content.matchAll(userStoryRegex)];
        for (const usMatch of usMatches) {
            const usId = usMatch[1];
            const usTitle = usMatch[2].trim();
            const acceptanceCriteria = extractACsForUserStory(usId, content);
            userStories.push({
                id: usId,
                title: usTitle,
                acceptanceCriteria,
            });
        }
        // STRATEGY 2: If no inline USs found, look for linked US files
        // Match: - [US-001: Title](path/to/file.md) or - [US-API-001: Title](path/to/file.md)
        if (userStories.length === 0) {
            const linkedUsRegex = /- \[(US-(?:[A-Z]+-)?(?:\d+)):\s*([^\]]+)\]\(([^)]+)\)/g;
            const linkedUsMatches = [...content.matchAll(linkedUsRegex)];
            for (const linkedMatch of linkedUsMatches) {
                const usId = linkedMatch[1];
                const usTitle = linkedMatch[2].trim();
                const relativePath = linkedMatch[3];
                // Resolve relative path from spec file location
                const specDir = path.dirname(specPath);
                const usFilePath = path.resolve(specDir, relativePath);
                // Check if file exists
                try {
                    await fs.access(usFilePath);
                    // Parse the linked US file
                    const parsedUs = await parseUserStoryFile(usFilePath, usId, logger);
                    if (parsedUs) {
                        // Use title from link if US file doesn't have one
                        if (!parsedUs.title) {
                            parsedUs.title = usTitle;
                        }
                        userStories.push(parsedUs);
                    }
                    else {
                        // Fallback: create minimal US without ACs
                        userStories.push({
                            id: usId,
                            title: usTitle,
                            acceptanceCriteria: [],
                        });
                    }
                }
                catch (error) {
                    // File doesn't exist or can't be read, create minimal US
                    userStories.push({
                        id: usId,
                        title: usTitle,
                        acceptanceCriteria: [],
                    });
                }
            }
        }
        // STRATEGY 3: If still no USs found, look for heading format: ### US-001: Title or ### US-API-001: Title
        if (userStories.length === 0) {
            const headingUsRegex = /^###\s+(US-(?:[A-Z]+-)?(?:\d+)):\s*(.+)$/gm;
            const headingUsMatches = [...content.matchAll(headingUsRegex)];
            for (const headingMatch of headingUsMatches) {
                const usId = headingMatch[1];
                const usTitle = headingMatch[2].trim();
                const acceptanceCriteria = extractACsForUserStory(usId, content);
                userStories.push({
                    id: usId,
                    title: usTitle,
                    acceptanceCriteria,
                });
            }
        }
        return {
            identifier,
            id: identifier.display, // Legacy field for backward compatibility
            title,
            description,
            userStories,
            metadata,
            project
        };
    }
    catch (error) {
        logger.error('Failed to parse spec:', error);
        return null;
    }
}
/**
 * Detect changes between local spec and external tool
 */
export function detectContentChanges(localSpec, externalContent) {
    const changes = [];
    const normalizeDesc = (str) => str.replace(/\s+/g, ' ').trim();
    if (localSpec.title !== externalContent.title) {
        changes.push(`title: "${externalContent.title}" → "${localSpec.title}"`);
    }
    if (normalizeDesc(localSpec.description) !== normalizeDesc(externalContent.description)) {
        changes.push('description updated');
    }
    if (localSpec.userStories.length !== externalContent.userStoryCount) {
        changes.push(`user stories: ${externalContent.userStoryCount} → ${localSpec.userStories.length}`);
    }
    return { hasChanges: changes.length > 0, changes };
}
/**
 * Build external tool description from spec content
 */
export function buildExternalDescription(spec) {
    let description = '';
    // Add spec description
    if (spec.description) {
        description += spec.description + '\n\n';
    }
    // Add user stories as checklist
    if (spec.userStories.length > 0) {
        description += '## User Stories\n\n';
        for (const us of spec.userStories) {
            description += `### ${us.id}: ${us.title}\n\n`;
            if (us.acceptanceCriteria.length > 0) {
                description += '**Acceptance Criteria:**\n';
                for (const ac of us.acceptanceCriteria) {
                    const checkbox = ac.completed ? '[x]' : '[ ]';
                    description += `- ${checkbox} ${ac.id}: ${ac.description}\n`;
                }
                description += '\n';
            }
        }
    }
    // Add metadata
    if (spec.metadata.priority) {
        description += `\n**Priority:** ${spec.metadata.priority}\n`;
    }
    return description;
}
/**
 * Check if spec has external tool link
 */
export async function hasExternalLink(specPath, provider) {
    try {
        const content = await fs.readFile(specPath, 'utf-8');
        const patterns = {
            github: /\*\*GitHub Project\*\*:\s*(.+)/,
            jira: /\*\*JIRA Epic\*\*:\s*([A-Z]+-\d+)/,
            ado: /\*\*ADO Feature\*\*:\s*#?(\d+)/
        };
        const match = content.match(patterns[provider]);
        return match ? match[1].trim() : null;
    }
    catch {
        return null;
    }
}
/**
 * Update spec.md with external tool link
 */
export async function updateSpecWithExternalLink(specPath, provider, externalId, externalUrl) {
    try {
        let content = await fs.readFile(specPath, 'utf-8');
        const linkTexts = {
            github: `**GitHub Project**: ${externalUrl}`,
            jira: `**JIRA Epic**: ${externalId}`,
            ado: `**ADO Feature**: #${externalId}`
        };
        const linkText = linkTexts[provider];
        const linkPrefix = linkText.split(':')[0];
        if (content.includes(linkPrefix))
            return;
        const titleMatch = content.match(/^(#\s+.+\n)/m);
        if (titleMatch) {
            const insertPos = titleMatch.index + titleMatch[0].length;
            content = content.slice(0, insertPos) + '\n' + linkText + '\n' + content.slice(insertPos);
        }
        else {
            content = linkText + '\n\n' + content;
        }
        await fs.writeFile(specPath, content);
    }
    catch (error) {
        console.error(`Failed to update spec with external link: ${error.message}`);
    }
}
/**
 * Get spec modification time
 */
export async function getSpecModificationTime(specPath) {
    const stats = await fs.stat(specPath);
    return stats.mtime;
}
/**
 * Check if spec was modified since last sync
 */
export async function wasSpecModifiedSinceSync(specPath, lastSyncTime) {
    if (!lastSyncTime)
        return true;
    const modTime = await getSpecModificationTime(specPath);
    return modTime > lastSyncTime;
}
//# sourceMappingURL=spec-content-sync.js.map