/**
 * Project Detector for Intelligent Living Docs Sync
 *
 * Detects which project an increment belongs to using keyword matching and metadata.
 *
 * @module living-docs/project-detector
 */
import * as fs from '../../utils/fs-native.js';
import path from 'path';
import { execSync } from 'child_process';
import { getProjectRoot } from '../../utils/find-project-root.js';
/**
 * Project Detector class
 */
export class ProjectDetector {
    constructor(options = {}) {
        // Use repo name as fallback instead of "default" for GitHub integration
        const detectedFallback = options.fallbackProject || this.detectRepoName() || 'default';
        // Uses getProjectRoot() instead of process.cwd() to prevent
        // accessing wrong .specweave folder when CWD != project root.
        this.options = {
            configPath: options.configPath || path.join(getProjectRoot(), '.specweave', 'config.json'),
            fallbackProject: detectedFallback,
            confidenceThreshold: options.confidenceThreshold || 0.7,
        };
        this.projects = new Map();
        this.loadProjects();
    }
    /**
     * Create and register a default fallback project
     */
    createFallbackProject(id) {
        const project = {
            id,
            name: 'Default Project',
            keywords: [],
            techStack: [],
        };
        this.projects.set(id, project);
        return project;
    }
    /**
     * Detect repository name from git remote
     * Returns repo name (e.g., "specweave" from "anton-abyzov/specweave")
     * or null if not a git repository
     */
    detectRepoName() {
        try {
            // Get git remote URL
            const remoteUrl = execSync('git config --get remote.origin.url', {
                cwd: getProjectRoot(),
                encoding: 'utf-8',
                stdio: ['pipe', 'pipe', 'ignore'] // Suppress stderr
            }).trim();
            if (!remoteUrl) {
                return null;
            }
            // Extract repo name from different URL formats:
            // - https://github.com/owner/repo.git
            // - git@github.com:owner/repo.git
            // - https://github.com/owner/repo
            const match = remoteUrl.match(/[:/]([^/]+)\/([^/.]+)(\.git)?$/);
            if (match) {
                return match[2]; // Return repo name (e.g., "specweave")
            }
            return null;
        }
        catch (error) {
            // Not a git repo or git command failed
            return null;
        }
    }
    /**
     * Load projects from config.json
     */
    loadProjects() {
        try {
            if (fs.existsSync(this.options.configPath)) {
                const config = JSON.parse(fs.readFileSync(this.options.configPath, 'utf-8'));
                // Load projects from multiProject config (v0.8.0+)
                if (config.multiProject?.projects) {
                    for (const [id, project] of Object.entries(config.multiProject.projects)) {
                        this.projects.set(id, {
                            id,
                            name: project.name || id,
                            description: project.description,
                            keywords: project.keywords || [],
                            team: project.team,
                            techStack: project.techStack || [],
                            specsFolder: project.specsFolder,
                        });
                    }
                }
                // If no projects configured, create default
                if (this.projects.size === 0) {
                    this.createFallbackProject(this.options.fallbackProject);
                }
            }
            else {
                // No config found - create default project
                this.createFallbackProject(this.options.fallbackProject);
            }
        }
        catch (error) {
            console.warn('Failed to load projects from config:', error);
            this.createFallbackProject(this.options.fallbackProject);
        }
    }
    /**
     * Detect project for an increment
     */
    detectProject(incrementId, spec) {
        const scores = new Map();
        // Initialize scores for all projects
        for (const projectId of this.projects.keys()) {
            scores.set(projectId, { score: 0, reasoning: [] });
        }
        // 1. Check increment name for project keywords
        for (const [projectId, project] of this.projects.entries()) {
            const scoreData = scores.get(projectId);
            // Project name match (exact)
            if (incrementId.toLowerCase().includes(project.id.toLowerCase())) {
                scoreData.score += 10;
                scoreData.reasoning.push(`Increment name contains project ID "${project.id}"`);
            }
            // Team name match
            if (project.team && incrementId.toLowerCase().includes(project.team.toLowerCase())) {
                scoreData.score += 5;
                scoreData.reasoning.push(`Increment name contains team "${project.team}"`);
            }
            // Keyword matches
            if (project.keywords) {
                for (const keyword of project.keywords) {
                    const keywordLower = keyword.toLowerCase();
                    if (incrementId.toLowerCase().includes(keywordLower) ||
                        spec.frontmatter.title?.toLowerCase().includes(keywordLower)) {
                        scoreData.score += 3;
                        scoreData.reasoning.push(`Keyword match: "${keyword}"`);
                    }
                }
            }
        }
        // 2. Check per-US **Project**: fields (v0.34.1+ - replaces frontmatter.project)
        // Priority: Per-US fields > frontmatter.project (deprecated)
        const perUSProjects = this.extractPerUSProjects(spec);
        if (perUSProjects.length > 0) {
            // Use the first per-US project as primary
            const primaryProject = perUSProjects[0];
            if (this.projects.has(primaryProject)) {
                const scoreData = scores.get(primaryProject);
                scoreData.score += 25; // Highest confidence from per-US field
                scoreData.reasoning.push(`Per-US **Project**: field: "${primaryProject}"`);
            }
        }
        else if (spec.frontmatter.project) {
            // Fallback to frontmatter.project (deprecated, for backward compatibility)
            const projectId = spec.frontmatter.project;
            if (this.projects.has(projectId)) {
                const scoreData = scores.get(projectId);
                scoreData.score += 20; // High confidence from explicit metadata
                scoreData.reasoning.push(`Explicit project metadata: "${projectId}" (deprecated - use per-US fields)`);
            }
        }
        // 3. Check tech stack from spec content
        for (const [projectId, project] of this.projects.entries()) {
            if (project.techStack && project.techStack.length > 0) {
                const scoreData = scores.get(projectId);
                const specContent = JSON.stringify(spec);
                for (const tech of project.techStack) {
                    if (specContent.toLowerCase().includes(tech.toLowerCase())) {
                        scoreData.score += 2;
                        scoreData.reasoning.push(`Tech stack match: "${tech}"`);
                    }
                }
            }
        }
        // Find project with highest score
        let maxScore = 0;
        let bestProjectId = this.options.fallbackProject;
        let bestReasoning = [];
        for (const [projectId, data] of scores.entries()) {
            if (data.score > maxScore) {
                maxScore = data.score;
                bestProjectId = projectId;
                bestReasoning = data.reasoning;
            }
        }
        // If no matches, use fallback
        if (maxScore === 0) {
            bestReasoning.push('No project indicators found - using fallback project');
        }
        // Normalize confidence (max score of ~30 = 1.0 confidence)
        const confidence = Math.min(maxScore / 30, 1.0);
        // Ensure fallback project exists in map
        let project = this.projects.get(bestProjectId);
        if (!project) {
            // Fallback project not found in map - create it on-demand
            project = this.createFallbackProject(bestProjectId);
            bestReasoning.push(`Created fallback project "${bestProjectId}"`);
        }
        return {
            id: project.id,
            name: project.name,
            confidence,
            reasoning: bestReasoning,
            metadata: {
                totalProjects: this.projects.size,
                scores: Object.fromEntries(Array.from(scores.entries()).map(([id, data]) => [id, data.score])),
            },
        };
    }
    /**
     * Get all configured projects
     */
    getProjects() {
        return Array.from(this.projects.values());
    }
    /**
     * Get project by ID
     */
    getProject(projectId) {
        return this.projects.get(projectId) || null;
    }
    /**
     * Get specs folder path for a project
     */
    getSpecsFolder(projectId) {
        const project = this.projects.get(projectId);
        if (project?.specsFolder) {
            return project.specsFolder;
        }
        // Default: .specweave/docs/internal/specs/{projectId}
        return path.join(getProjectRoot(), '.specweave', 'docs', 'internal', 'specs', projectId);
    }
    /**
     * Check if confidence meets threshold for auto-selection
     */
    shouldAutoSelect(context) {
        return context.confidence >= this.options.confidenceThreshold;
    }
    /**
     * Create project context from string ID (fallback when no detection needed)
     */
    createContext(projectId) {
        let project = this.projects.get(projectId);
        if (!project) {
            // Unknown project - use fallback
            let fallbackProject = this.projects.get(this.options.fallbackProject);
            if (!fallbackProject) {
                // Fallback project doesn't exist - create it on-demand
                fallbackProject = this.createFallbackProject(this.options.fallbackProject);
            }
            return {
                id: fallbackProject.id,
                name: fallbackProject.name,
                confidence: 1.0,
                reasoning: [`Fallback to "${fallbackProject.id}" (project "${projectId}" not found)`],
            };
        }
        return {
            id: project.id,
            name: project.name,
            confidence: 1.0,
            reasoning: ['Explicitly selected project'],
            metadata: {
                description: project.description,
                team: project.team,
                techStack: project.techStack,
            },
        };
    }
    /**
     * Ensure project specs folder exists
     */
    async ensureSpecsFolder(projectId) {
        const specsFolder = this.getSpecsFolder(projectId);
        await fs.ensureDir(specsFolder);
        return specsFolder;
    }
    /**
     * Get folder path for a category
     */
    getCategoryFolder(category, projectId) {
        const basePath = path.join(getProjectRoot(), '.specweave', 'docs', 'internal');
        // Categories that don't use project folders
        const globalCategories = ['architecture', 'operations', 'delivery', 'strategy', 'governance'];
        if (globalCategories.includes(category)) {
            return path.join(basePath, category);
        }
        // Project-specific categories (specs, modules, team, etc.)
        if (projectId) {
            return path.join(basePath, category, projectId);
        }
        // Fallback to default project
        return path.join(basePath, category, this.options.fallbackProject);
    }
    /**
     * Extract project ID from increment ID if present
     * Format: 0016-{project}-feature-name OR 0016-feature-name
     */
    extractProjectFromIncrementId(incrementId) {
        // Check if increment ID contains a known project ID
        for (const projectId of this.projects.keys()) {
            // Pattern: 0016-backend-auth OR backend-0016-auth
            const regex = new RegExp(`\\b${projectId}\\b`, 'i');
            if (regex.test(incrementId)) {
                return projectId;
            }
        }
        return null;
    }
    /**
     * Validate project structure exists
     */
    async validateProjectStructure(projectId) {
        const basePath = path.join(getProjectRoot(), '.specweave', 'docs', 'internal');
        const requiredFolders = [
            path.join(basePath, 'specs', projectId),
        ];
        const missing = [];
        for (const folder of requiredFolders) {
            if (!fs.existsSync(folder)) {
                missing.push(folder);
            }
        }
        return {
            valid: missing.length === 0,
            missing,
        };
    }
    /**
     * Create project structure if missing
     */
    async createProjectStructure(projectId) {
        const basePath = path.join(getProjectRoot(), '.specweave', 'docs', 'internal');
        const folders = [
            path.join(basePath, 'specs', projectId),
            // Add more project-specific folders as needed
        ];
        for (const folder of folders) {
            await fs.ensureDir(folder);
        }
        // Create README in specs folder
        const readmePath = path.join(basePath, 'specs', projectId, 'README.md');
        if (!fs.existsSync(readmePath)) {
            const project = this.projects.get(projectId);
            const content = `# ${project?.name || projectId} Specifications

**Project**: ${project?.name || projectId}
**Description**: ${project?.description || 'No description'}

## Specifications

*Specifications will appear here after living docs sync.*

---

Generated by SpecWeave
`;
            await fs.writeFile(readmePath, content, 'utf-8');
        }
    }
    /**
     * Extract project IDs from per-US **Project**: fields in spec content
     *
     * v0.34.1: New method to support per-US project targeting (ADR-0140)
     *
     * @param spec - Parsed spec with user stories
     * @returns Array of unique project IDs found in user stories
     */
    extractPerUSProjects(spec) {
        const projects = new Set();
        // Check if spec has content property (raw markdown)
        const content = spec.content || spec.bodyContent || '';
        if (typeof content !== 'string') {
            return [];
        }
        // Extract **Project**: fields from markdown content
        const projectPattern = /\*\*Project\*\*:\s*([a-zA-Z0-9-]+)/gi;
        let match;
        while ((match = projectPattern.exec(content)) !== null) {
            projects.add(match[1].toLowerCase());
        }
        return Array.from(projects);
    }
}
/**
 * Factory function to create detector
 */
export function createProjectDetector(options) {
    return new ProjectDetector(options);
}
//# sourceMappingURL=project-detector.js.map