/**
 * Spec-to-Increment Mapper
 *
 * Maps permanent living docs specs to specific increment tasks.
 * Enables traceability: "Which increment implemented US-001?"
 */
import fs from 'fs/promises';
import path from 'path';
import matter from 'gray-matter';
export class SpecIncrementMapper {
    constructor(rootDir = process.cwd()) {
        this.rootDir = rootDir;
    }
    /**
     * Map a spec to all increments that implement it
     */
    async mapSpecToIncrements(specId) {
        const increments = await this.findIncrementsForSpec(specId);
        const userStoryMappings = {};
        // Build user story to task mappings
        for (const increment of increments) {
            for (const task of increment.tasks) {
                for (const userStoryId of task.userStories) {
                    if (!userStoryMappings[userStoryId]) {
                        userStoryMappings[userStoryId] = [];
                    }
                    userStoryMappings[userStoryId].push(task);
                }
            }
        }
        return {
            specId,
            increments,
            userStoryMappings
        };
    }
    /**
     * Find all increments that implement a specific user story
     */
    async findIncrementsByUserStory(userStoryId) {
        const incrementsDir = path.join(this.rootDir, '.specweave/increments');
        try {
            const entries = await fs.readdir(incrementsDir, { withFileTypes: true });
            const incrementDirs = entries
                .filter(e => e.isDirectory() && /^\d{3,4}E?-/.test(e.name))
                .map(e => e.name);
            const results = [];
            for (const incrementId of incrementDirs) {
                const tasks = await this.getTasksFromIncrement(incrementId);
                const relevantTasks = tasks.filter(t => t.userStories.includes(userStoryId));
                if (relevantTasks.length > 0) {
                    results.push({
                        id: incrementId,
                        tasks: relevantTasks
                    });
                }
            }
            return results;
        }
        catch (error) {
            return [];
        }
    }
    /**
     * Get all tasks that implement a specific user story
     */
    async getTasksForUserStory(userStoryId) {
        const increments = await this.findIncrementsByUserStory(userStoryId);
        return increments.flatMap((inc) => inc.tasks);
    }
    /**
     * Update spec with increment links in frontmatter (Forward link)
     */
    async updateSpecWithIncrementLinks(specId, incrementId) {
        const specPath = await this.findSpecPath(specId);
        if (!specPath)
            return false;
        try {
            const content = await fs.readFile(specPath, 'utf-8');
            const { data: frontmatter, content: markdownContent } = matter(content);
            frontmatter.linked_increments = frontmatter.linked_increments ?? [];
            if (frontmatter.linked_increments.includes(incrementId))
                return false;
            frontmatter.linked_increments.push(incrementId);
            await fs.writeFile(specPath, matter.stringify(markdownContent, frontmatter));
            return true;
        }
        catch {
            return false;
        }
    }
    /**
     * Update increment with spec link in frontmatter (Backward link)
     */
    async updateIncrementWithSpecLink(incrementId, specId) {
        const incrementSpecPath = path.join(this.rootDir, '.specweave/increments', incrementId, 'spec.md');
        try {
            await fs.access(incrementSpecPath);
            const content = await fs.readFile(incrementSpecPath, 'utf-8');
            const { data: frontmatter, content: markdownContent } = matter(content);
            if (frontmatter.spec_id === specId || frontmatter.source_spec === specId)
                return false;
            frontmatter.source_spec = specId;
            await fs.writeFile(incrementSpecPath, matter.stringify(markdownContent, frontmatter));
            return true;
        }
        catch {
            return false;
        }
    }
    /**
     * Create bidirectional link between spec and increment (atomic operation)
     */
    async createBidirectionalLink(specId, incrementId) {
        const forwardLink = await this.updateSpecWithIncrementLinks(specId, incrementId);
        const backwardLink = await this.updateIncrementWithSpecLink(incrementId, specId);
        return {
            success: forwardLink || backwardLink,
            forwardLink,
            backwardLink
        };
    }
    /**
     * Get spec for a given increment (reverse lookup)
     */
    async getSpecForIncrement(incrementId) {
        const incrementSpecPath = path.join(this.rootDir, '.specweave/increments', incrementId, 'spec.md');
        try {
            const { data: frontmatter } = matter(await fs.readFile(incrementSpecPath, 'utf-8'));
            return frontmatter.source_spec ?? frontmatter.spec_id ?? null;
        }
        catch {
            return null;
        }
    }
    /**
     * Validate bidirectional links and detect broken references
     */
    async validateLinks() {
        const brokenForwardLinks = [];
        const brokenBackwardLinks = [];
        const orphanedIncrements = [];
        const incrementsDir = path.join(this.rootDir, '.specweave/increments');
        try {
            const entries = await fs.readdir(incrementsDir, { withFileTypes: true });
            const incrementDirs = entries
                .filter(e => e.isDirectory() && /^\d{3,4}E?-/.test(e.name))
                .map(e => e.name);
            for (const incrementId of incrementDirs) {
                // Check backward link
                const specId = await this.getSpecForIncrement(incrementId);
                if (!specId) {
                    orphanedIncrements.push(incrementId);
                    continue;
                }
                // Validate forward link exists
                const specPath = await this.findSpecPath(specId);
                if (specPath) {
                    const content = await fs.readFile(specPath, 'utf-8');
                    const { data: frontmatter } = matter(content);
                    if (!frontmatter.linked_increments?.includes(incrementId)) {
                        brokenBackwardLinks.push({ incrementId, specId });
                    }
                }
                else {
                    brokenBackwardLinks.push({ incrementId, specId });
                }
            }
            // Check specs for broken forward links
            const specsDir = path.join(this.rootDir, '.specweave/docs/internal/specs');
            const projects = await fs.readdir(specsDir, { withFileTypes: true });
            for (const project of projects.filter(e => e.isDirectory())) {
                const specs = await fs.readdir(path.join(specsDir, project.name));
                for (const specFile of specs.filter(f => f.endsWith('.md'))) {
                    const specPath = path.join(specsDir, project.name, specFile);
                    const content = await fs.readFile(specPath, 'utf-8');
                    const { data: frontmatter } = matter(content);
                    if (frontmatter.linked_increments) {
                        for (const incrementId of frontmatter.linked_increments) {
                            const incrementPath = path.join(incrementsDir, incrementId, 'spec.md');
                            try {
                                await fs.access(incrementPath);
                            }
                            catch {
                                brokenForwardLinks.push({
                                    specId: specFile.replace('.md', ''),
                                    incrementId
                                });
                            }
                        }
                    }
                }
            }
        }
        catch (error) {
            // Directory doesn't exist or other error
        }
        return {
            valid: brokenForwardLinks.length === 0 &&
                brokenBackwardLinks.length === 0 &&
                orphanedIncrements.length === 0,
            brokenForwardLinks,
            brokenBackwardLinks,
            orphanedIncrements
        };
    }
    /**
     * Build complete traceability report for a spec
     */
    async buildTraceabilityReport(specId) {
        const mapping = await this.mapSpecToIncrements(specId);
        const userStories = await this.getUserStoriesFromSpec(specId);
        const totalUserStories = userStories.length;
        const totalIncrements = mapping.increments.length;
        const totalTasks = mapping.increments.reduce((sum, inc) => sum + inc.tasks.length, 0);
        const mappedUserStories = Object.keys(mapping.userStoryMappings).filter(us => userStories.includes(us));
        const unmappedUserStories = userStories.filter(us => !mappedUserStories.includes(us));
        const coverage = totalUserStories > 0
            ? Math.min(100, (mappedUserStories.length / totalUserStories) * 100)
            : 0;
        // Build detailed mapping
        const mappingDetails = {};
        for (const [userStoryId, tasks] of Object.entries(mapping.userStoryMappings)) {
            const incrementGroups = {};
            for (const task of tasks) {
                if (!incrementGroups[task.incrementId]) {
                    incrementGroups[task.incrementId] = [];
                }
                incrementGroups[task.incrementId].push(task.id);
            }
            mappingDetails[userStoryId] = Object.entries(incrementGroups).map(([incrementId, taskIds]) => ({
                incrementId,
                taskIds
            }));
        }
        return {
            specId,
            totalUserStories,
            totalIncrements,
            totalTasks,
            coverage,
            unmappedUserStories,
            mappingDetails
        };
    }
    // Private helper methods
    async findIncrementsForSpec(specId) {
        const incrementsDir = path.join(this.rootDir, '.specweave/increments');
        try {
            const entries = await fs.readdir(incrementsDir, { withFileTypes: true });
            const incrementDirs = entries
                .filter(e => e.isDirectory() && /^\d{3,4}E?-/.test(e.name))
                .map(e => e.name);
            const results = [];
            for (const incrementId of incrementDirs) {
                const specPath = path.join(incrementsDir, incrementId, 'spec.md');
                try {
                    const content = await fs.readFile(specPath, 'utf-8');
                    const { data: frontmatter } = matter(content);
                    // Check if this increment references our spec
                    if (frontmatter.spec_id === specId || content.includes(`spec-${specId}`)) {
                        const tasks = await this.getTasksFromIncrement(incrementId);
                        results.push({
                            id: incrementId,
                            tasks,
                            specId
                        });
                    }
                }
                catch {
                    // Skip if spec.md doesn't exist
                }
            }
            return results;
        }
        catch (error) {
            return [];
        }
    }
    async getTasksFromIncrement(incrementId) {
        const tasksPath = path.join(this.rootDir, '.specweave/increments', incrementId, 'tasks.md');
        try {
            const content = await fs.readFile(tasksPath, 'utf-8');
            const { data: frontmatter } = matter(content);
            const tasks = [];
            // Parse tasks from markdown (tasks use ### heading level)
            const taskRegex = /^###\s+(T-\d+):\s+(.+)$/gm;
            let match;
            while ((match = taskRegex.exec(content)) !== null) {
                const taskId = match[1];
                const title = match[2];
                // Extract user stories for this task
                const taskSection = this.extractTaskSection(content, taskId);
                const userStories = this.extractUserStoriesFromTask(taskSection);
                const githubIssue = this.extractGithubIssue(taskSection);
                const status = this.extractTaskStatus(taskSection);
                tasks.push({
                    id: taskId,
                    title,
                    incrementId,
                    userStories,
                    githubIssue,
                    status
                });
            }
            return tasks;
        }
        catch (error) {
            return [];
        }
    }
    extractTaskSection(content, taskId) {
        const startRegex = new RegExp(`^###\\s+${taskId}:`, 'm');
        const startMatch = content.match(startRegex);
        if (!startMatch)
            return '';
        const startIndex = startMatch.index;
        const nextTaskMatch = content.slice(startIndex + 1).match(/^###\s+T-\d+:/m);
        const endIndex = nextTaskMatch
            ? startIndex + 1 + nextTaskMatch.index
            : content.length;
        return content.slice(startIndex, endIndex);
    }
    extractUserStoriesFromTask(taskSection) {
        // Look for AC references: AC-US1-01, AC-US2-01, etc.
        const acRegex = /AC-US(\d+)-\d+/g;
        const matches = [...taskSection.matchAll(acRegex)];
        const userStoryNumbers = new Set(matches.map(m => m[1]));
        // Also look for explicit "Implements: US-001, US-002"
        const implementsMatch = taskSection.match(/Implements:\s*([^\n]+)/);
        if (implementsMatch) {
            const usMatches = implementsMatch[1].matchAll(/US-(\d+)/g);
            for (const match of usMatches) {
                userStoryNumbers.add(match[1]);
            }
        }
        return Array.from(userStoryNumbers).map(num => `US-${num}`);
    }
    extractGithubIssue(taskSection) {
        const match = taskSection.match(/github.*#(\d+)/i);
        return match ? parseInt(match[1], 10) : undefined;
    }
    extractTaskStatus(taskSection) {
        if (taskSection.includes('[x] Completed'))
            return 'completed';
        if (taskSection.includes('[ ] In Progress'))
            return 'in-progress';
        return 'pending';
    }
    async findSpecPath(specId) {
        const specsDir = path.join(this.rootDir, '.specweave/docs/internal/specs');
        try {
            const defaultPath = path.join(specsDir, 'default', `${specId}.md`);
            try {
                await fs.access(defaultPath);
                return defaultPath;
            }
            catch {
                // Not in default, search all projects
            }
            const entries = await fs.readdir(specsDir, { withFileTypes: true });
            for (const entry of entries.filter((e) => e.isDirectory())) {
                const specPath = path.join(specsDir, entry.name, `${specId}.md`);
                try {
                    await fs.access(specPath);
                    return specPath;
                }
                catch {
                    // Continue to next project
                }
            }
            return null;
        }
        catch {
            return null;
        }
    }
    async getUserStoriesFromSpec(specId) {
        const specPath = await this.findSpecPath(specId);
        if (!specPath)
            return [];
        try {
            const content = await fs.readFile(specPath, 'utf-8');
            return [...content.matchAll(/\*\*(US-\d+)\*\*:/g)].map((m) => m[1]);
        }
        catch {
            return [];
        }
    }
}
//# sourceMappingURL=spec-increment-mapper.js.map