/**
 * Duplicate Detector
 *
 * Detects duplicate external items by scanning living docs for existing external IDs.
 * Prevents re-importing items that have already been imported.
 */
import * as fs from '../utils/fs-native.js';
import path from 'path';
import matter from 'gray-matter';
import { glob } from 'glob';
/**
 * Duplicate Detector
 *
 * Scans living docs US files for external_id metadata and provides
 * duplicate detection functionality.
 */
export class DuplicateDetector {
    constructor(options) {
        this.cache = null;
        this.options = {
            enableCache: true,
            ...options,
        };
    }
    /**
     * Check if an external ID already exists in living docs
     *
     * @param externalId - External ID to check (e.g., "GH-#638", "JIRA-PROJ-123")
     * @returns True if external ID exists (duplicate), false otherwise
     */
    async checkExistingExternalId(externalId) {
        const reference = await this.findExternalIdReference(externalId);
        return reference !== null;
    }
    /**
     * Find the User Story that contains a specific external ID
     *
     * @param externalId - External ID to search for
     * @returns External ID reference if found, null otherwise
     */
    async findExternalIdReference(externalId) {
        // Build or use cache
        const externalIdMap = await this.buildExternalIdMap();
        // Normalize external ID for lookup
        const normalizedId = this.normalizeExternalId(externalId);
        return externalIdMap.get(normalizedId) || null;
    }
    /**
     * Get all external IDs currently in living docs
     *
     * @returns Map of external ID to reference
     */
    async getAllExternalIds() {
        return this.buildExternalIdMap();
    }
    /**
     * Build map of external IDs to User Story references
     */
    async buildExternalIdMap() {
        // Return cached map if available
        if (this.options.enableCache && this.cache) {
            return this.cache;
        }
        const externalIdMap = new Map();
        // Find all US markdown files in specs directory
        const usFiles = await this.findUserStoryFiles();
        // Scan each file for external_id metadata
        for (const filePath of usFiles) {
            const reference = await this.extractExternalIdFromFile(filePath);
            if (reference) {
                const normalizedId = this.normalizeExternalId(reference.externalId);
                externalIdMap.set(normalizedId, reference);
            }
        }
        // Cache the map if enabled
        if (this.options.enableCache) {
            this.cache = externalIdMap;
        }
        return externalIdMap;
    }
    /**
     * Find all User Story markdown files in specs directory
     *
     * Searches recursively in all subdirectories including:
     * - Root level (legacy)
     * - Feature folders (FS-XXX)
     * - Project folders
     * - Archive folders
     */
    async findUserStoryFiles() {
        if (!fs.existsSync(this.options.specsDir)) {
            return [];
        }
        // Match us-*.md files recursively in all subdirectories
        const pattern = path.join(this.options.specsDir, '**/us-*.md');
        const files = await glob(pattern, { nodir: true });
        return files;
    }
    /**
     * Extract external_id and parent tracking info from a User Story file
     *
     * @param filePath - Path to User Story markdown file
     * @returns External ID reference with parent info if found, null otherwise
     */
    async extractExternalIdFromFile(filePath) {
        let content;
        try {
            content = await fs.readFile(filePath, 'utf-8');
        }
        catch {
            return null; // Skip files that can't be read
        }
        const parsed = matter(content);
        const usId = this.extractUsIdFromFile(filePath, content);
        // Check for external_id in frontmatter
        if (parsed.data.external_id) {
            return {
                usId,
                filePath,
                externalId: String(parsed.data.external_id),
                platform: parsed.data.external_platform,
                featureId: parsed.data.feature_id,
                parentId: parsed.data.parent_id,
                isOrphan: parsed.data.is_orphan,
                adoWorkItemType: parsed.data.ado_work_item_type,
            };
        }
        // Fallback: Parse external metadata section in content
        const externalIdMatch = content.match(/^-\s+\*\*External ID\*\*:\s+(.+)$/m);
        if (!externalIdMatch) {
            return null;
        }
        return {
            usId,
            filePath,
            externalId: externalIdMatch[1].trim(),
            platform: content.match(/^-\s+\*\*Platform\*\*:\s+(.+)$/m)?.[1]?.trim()?.toLowerCase(),
            featureId: content.match(/^-\s+\*\*Feature ID\*\*:\s+(.+)$/m)?.[1]?.trim(),
            parentId: content.match(/^-\s+\*\*Parent ID\*\*:\s+(.+)$/m)?.[1]?.trim(),
            isOrphan: !!content.match(/^-\s+\*\*Orphan\*\*:\s+true/m),
            adoWorkItemType: content.match(/^-\s+\*\*ADO Work Item Type\*\*:\s+(.+)$/m)?.[1]?.trim(),
        };
    }
    /**
     * Extract US-ID from file name or content
     */
    extractUsIdFromFile(filePath, content) {
        // Try filename: us-001e-title.md → US-001E
        const fileNameMatch = path.basename(filePath).match(/^us-(\d{3}e)/i);
        if (fileNameMatch) {
            return `US-${fileNameMatch[1].toUpperCase()}`;
        }
        // Try content: # US-001E: Title
        const contentMatch = content.match(/^#\s+(US-\d{3}E):/m);
        if (contentMatch) {
            return contentMatch[1];
        }
        // Fallback: use filename
        return path.basename(filePath, '.md');
    }
    /**
     * Normalize external ID for consistent comparison
     *
     * Examples:
     * - "github#owner/repo#638" → "gh:owner/repo#638" (v0.29+ format with repo)
     * - "GH-#638" → "gh-638" (legacy)
     * - "GITHUB-638" → "gh-638" (legacy)
     * - "JIRA-PROJ-123" → "jira-proj-123"
     *
     * CRITICAL: v0.29+ format includes repo name to prevent cross-repo collisions
     */
    normalizeExternalId(externalId) {
        return externalId
            .toLowerCase()
            // v0.29+ format: github#owner/repo#123 → gh:owner/repo#123
            .replace(/^github#([^#]+)#(\d+)$/i, 'gh:$1#$2')
            // Legacy formats
            .replace(/^github-/i, 'gh-')
            .replace(/^gh-#/, 'gh-')
            .replace(/^jira-/i, 'jira-')
            .replace(/^ado-/i, 'ado-');
    }
    /**
     * Clear the cache (useful for testing or after imports)
     */
    clearCache() {
        this.cache = null;
    }
    /**
     * Get cache statistics
     */
    getCacheStats() {
        return {
            enabled: this.options.enableCache ?? true,
            size: this.cache?.size ?? 0,
        };
    }
}
/**
 * Check if an external ID exists (convenience function)
 *
 * @param externalId - External ID to check
 * @param specsDir - Living docs specs directory
 * @returns True if duplicate exists
 */
export async function checkExistingExternalId(externalId, specsDir) {
    const detector = new DuplicateDetector({ specsDir });
    return detector.checkExistingExternalId(externalId);
}
//# sourceMappingURL=duplicate-detector.js.map