/**
 * Progress query handlers — milestone progress rendering in JSON format.
 *
 * Ported from get-shit-done/bin/lib/commands.cjs (cmdProgressRender, determinePhaseStatus).
 * Provides progress handler that scans disk for plan/summary counts per phase
 * and determines status via VERIFICATION.md inspection.
 *
 * @example
 * ```typescript
 * import { progressJson } from './progress.js';
 *
 * const result = await progressJson([], '/project');
 * // { data: { milestone_version: 'v3.0', phases: [...], total_plans: 6, percent: 83 } }
 * ```
 */
import { readFile, readdir } from 'node:fs/promises';
import { existsSync, readdirSync, readFileSync, mkdirSync, writeFileSync, unlinkSync } from 'node:fs';
import { join, relative } from 'node:path';
import { GSDError, ErrorClassification } from '../errors.js';
import { comparePhaseNum, normalizePhaseName, planningPaths, toPosixPath } from './helpers.js';
import { getMilestoneInfo, extractCurrentMilestone, roadmapGetPhase } from './roadmap.js';
import { getMilestonePhaseFilter } from './state.js';
import { findPhase } from './phase.js';
// ─── Internal helpers ─────────────────────────────────────────────────────
/**
 * Determine the status of a phase based on plan/summary counts and verification state.
 *
 * Port of determinePhaseStatus from commands.cjs lines 15-36.
 *
 * @param plans - Number of PLAN.md files in the phase directory
 * @param summaries - Number of SUMMARY.md files in the phase directory
 * @param phaseDir - Absolute path to the phase directory
 * @returns Status string: Pending, Planned, In Progress, Executed, Complete, Needs Review
 */
export async function determinePhaseStatus(plans, summaries, phaseDir, defaultWhenNoPlans = 'Pending') {
    if (plans === 0)
        return defaultWhenNoPlans;
    if (summaries < plans && summaries > 0)
        return 'In Progress';
    if (summaries < plans)
        return 'Planned';
    // summaries >= plans — check verification
    try {
        const files = await readdir(phaseDir);
        const verificationFile = files.find(f => f === 'VERIFICATION.md' || f.endsWith('-VERIFICATION.md'));
        if (verificationFile) {
            const content = await readFile(join(phaseDir, verificationFile), 'utf-8');
            if (/status:\s*passed/i.test(content))
                return 'Complete';
            if (/status:\s*human_needed/i.test(content))
                return 'Needs Review';
            if (/status:\s*gaps_found/i.test(content))
                return 'Executed';
            // Verification exists but unrecognized status — treat as executed
            return 'Executed';
        }
    }
    catch { /* directory read failed — fall through */ }
    // No verification file — executed but not verified
    return 'Executed';
}
// ─── Exported handlers ────────────────────────────────────────────────────
/**
 * Query handler for progress / progress.json.
 *
 * Port of cmdProgressRender (JSON format) from commands.cjs lines 535-597.
 * Scans phases directory, counts plans/summaries, determines status per phase.
 *
 * @param args - Unused
 * @param projectDir - Project root directory
 * @returns QueryResult with milestone progress data
 */
export const progressJson = async (_args, projectDir, workstream) => {
    const phasesDir = planningPaths(projectDir, workstream).phases;
    const milestone = await getMilestoneInfo(projectDir, workstream);
    const phases = [];
    let totalPlans = 0;
    let totalSummaries = 0;
    try {
        const entries = await readdir(phasesDir, { withFileTypes: true });
        const dirs = entries
            .filter(e => e.isDirectory())
            .map(e => e.name)
            .sort((a, b) => comparePhaseNum(a, b));
        for (const dir of dirs) {
            const dm = dir.match(/^(\d+(?:\.\d+)*)-?(.*)/);
            const phaseNum = dm ? dm[1] : dir;
            const phaseName = dm && dm[2] ? dm[2].replace(/-/g, ' ') : '';
            const phaseFiles = await readdir(join(phasesDir, dir));
            const plans = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md').length;
            const summaries = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md').length;
            totalPlans += plans;
            totalSummaries += summaries;
            const status = await determinePhaseStatus(plans, summaries, join(phasesDir, dir));
            phases.push({ number: phaseNum, name: phaseName, plans, summaries, status });
        }
    }
    catch { /* intentionally empty */ }
    const percent = totalPlans > 0 ? Math.min(100, Math.round((totalSummaries / totalPlans) * 100)) : 0;
    return {
        data: {
            milestone_version: milestone.version,
            milestone_name: milestone.name,
            phases,
            total_plans: totalPlans,
            total_summaries: totalSummaries,
            percent,
        },
    };
};
// ─── progressBar ─────────────────────────────────────────────────────────
/**
 * Progress bar line — port of `cmdProgressRender` `format === 'bar'` from commands.cjs (lines 588–593).
 * Uses the same plan/summary counts as `progressJson` / CJS (not `roadmap.analyze` percent).
 */
export const progressBar = async (_args, projectDir, workstream) => {
    const json = await progressJson([], projectDir, workstream);
    const d = json.data;
    const totalPlans = d.total_plans;
    const totalSummaries = d.total_summaries;
    const percent = d.percent;
    const barWidth = 20;
    const filled = Math.round((percent / 100) * barWidth);
    const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(barWidth - filled);
    const text = `[${bar}] ${totalSummaries}/${totalPlans} plans (${percent}%)`;
    return { data: { bar: text, percent, completed: totalSummaries, total: totalPlans } };
};
/**
 * Markdown progress table — port of `cmdProgressRender` `format === 'table'` from commands.cjs (lines 575–587).
 */
export const progressTable = async (_args, projectDir, workstream) => {
    const json = await progressJson([], projectDir, workstream);
    const d = json.data;
    const { milestone_version, milestone_name, phases, total_plans, total_summaries, percent } = d;
    const barWidth = 10;
    const filled = Math.round((percent / 100) * barWidth);
    const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(barWidth - filled);
    let out = `# ${milestone_version} ${milestone_name}\n\n`;
    out += `**Progress:** [${bar}] ${total_summaries}/${total_plans} plans (${percent}%)\n\n`;
    out += '| Phase | Name | Plans | Status |\n';
    out += '|-------|------|-------|--------|\n';
    for (const p of phases) {
        out += `| ${p.number} | ${p.name} | ${p.summaries}/${p.plans} | ${p.status} |\n`;
    }
    return { data: { rendered: out } };
};
// ─── statsJson ───────────────────────────────────────────────────────────
/**
 * Statistics aggregate — port of `cmdStats` JSON/table output from commands.cjs lines 816–971.
 */
export const statsJson = async (args, projectDir, workstream) => {
    const format = args[0] || 'json';
    const phasesDir = planningPaths(projectDir, workstream).phases;
    const roadmapPath = planningPaths(projectDir, workstream).roadmap;
    const reqPath = planningPaths(projectDir, workstream).requirements;
    const statePath = planningPaths(projectDir, workstream).state;
    const milestone = await getMilestoneInfo(projectDir, workstream);
    const isDirInMilestone = await getMilestonePhaseFilter(projectDir, workstream);
    const phasesByNumber = new Map();
    let totalPlans = 0;
    let totalSummaries = 0;
    try {
        const roadmapContent = await extractCurrentMilestone(await readFile(roadmapPath, 'utf-8'), projectDir, workstream);
        const headingPattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:\s*([^\n]+)/gi;
        let match;
        while ((match = headingPattern.exec(roadmapContent)) !== null) {
            const key = normalizePhaseName(match[1]);
            phasesByNumber.set(key, {
                number: key,
                name: match[2].replace(/\(INSERTED\)/i, '').trim(),
                plans: 0,
                summaries: 0,
                status: 'Not Started',
            });
        }
    }
    catch { /* intentionally empty */ }
    try {
        const entries = readdirSync(phasesDir, { withFileTypes: true });
        const dirs = entries
            .filter(e => e.isDirectory())
            .map(e => e.name)
            .filter(isDirInMilestone)
            .sort((a, b) => comparePhaseNum(a, b));
        for (const dir of dirs) {
            const dm = dir.match(/^(\d+[A-Z]?(?:\.\d+)*)-?(.*)/i);
            const phaseNum = dm ? dm[1] : dir;
            const phaseName = dm && dm[2] ? dm[2].replace(/-/g, ' ') : '';
            const phaseFiles = readdirSync(join(phasesDir, dir));
            const plans = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md').length;
            const summaries = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md').length;
            totalPlans += plans;
            totalSummaries += summaries;
            const status = await determinePhaseStatus(plans, summaries, join(phasesDir, dir), 'Not Started');
            const normalizedNum = normalizePhaseName(phaseNum);
            const existing = phasesByNumber.get(normalizedNum);
            phasesByNumber.set(normalizedNum, {
                number: normalizedNum,
                name: existing?.name || phaseName,
                plans: (existing?.plans || 0) + plans,
                summaries: (existing?.summaries || 0) + summaries,
                status,
            });
        }
    }
    catch { /* intentionally empty */ }
    const phases = [...phasesByNumber.values()].sort((a, b) => comparePhaseNum(a.number, b.number));
    const completedPhases = phases.filter(p => p.status === 'Complete').length;
    const planPercent = totalPlans > 0 ? Math.min(100, Math.round((totalSummaries / totalPlans) * 100)) : 0;
    const percent = phases.length > 0 ? Math.min(100, Math.round((completedPhases / phases.length) * 100)) : 0;
    let requirementsTotal = 0;
    let requirementsComplete = 0;
    try {
        if (existsSync(reqPath)) {
            const reqContent = readFileSync(reqPath, 'utf-8');
            const checked = reqContent.match(/^- \[x\] \*\*/gm);
            const unchecked = reqContent.match(/^- \[ \] \*\*/gm);
            requirementsComplete = checked ? checked.length : 0;
            requirementsTotal = requirementsComplete + (unchecked ? unchecked.length : 0);
        }
    }
    catch { /* intentionally empty */ }
    let lastActivity = null;
    try {
        if (existsSync(statePath)) {
            const stateContent = readFileSync(statePath, 'utf-8');
            const activityMatch = stateContent.match(/^last_activity:\s*(.+)$/im)
                || stateContent.match(/\*\*Last Activity:\*\*\s*(.+)/i)
                || stateContent.match(/^Last Activity:\s*(.+)$/im)
                || stateContent.match(/^Last activity:\s*(.+)$/im);
            if (activityMatch)
                lastActivity = activityMatch[1].trim();
        }
    }
    catch { /* intentionally empty */ }
    const { execGit } = await import('./commit.js');
    let gitCommits = 0;
    let gitFirstCommitDate = null;
    const commitCount = execGit(projectDir, ['rev-list', '--count', 'HEAD']);
    if (commitCount.exitCode === 0) {
        gitCommits = parseInt(commitCount.stdout, 10) || 0;
    }
    const rootHash = execGit(projectDir, ['rev-list', '--max-parents=0', 'HEAD']);
    if (rootHash.exitCode === 0 && rootHash.stdout) {
        const firstCommit = rootHash.stdout.split('\n')[0].trim();
        const firstDate = execGit(projectDir, ['show', '-s', '--format=%as', firstCommit]);
        if (firstDate.exitCode === 0) {
            gitFirstCommitDate = firstDate.stdout.trim() || null;
        }
    }
    const result = {
        milestone_version: milestone.version,
        milestone_name: milestone.name,
        phases,
        phases_completed: completedPhases,
        phases_total: phases.length,
        total_plans: totalPlans,
        total_summaries: totalSummaries,
        percent,
        plan_percent: planPercent,
        requirements_total: requirementsTotal,
        requirements_complete: requirementsComplete,
        git_commits: gitCommits,
        git_first_commit_date: gitFirstCommitDate,
        last_activity: lastActivity,
    };
    if (format === 'table') {
        const barWidth = 10;
        const filled = Math.round((percent / 100) * barWidth);
        const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(barWidth - filled);
        let out = `# ${milestone.version} ${milestone.name} \u2014 Statistics\n\n`;
        out += `**Progress:** [${bar}] ${completedPhases}/${phases.length} phases (${percent}%)\n`;
        if (totalPlans > 0) {
            out += `**Plans:** ${totalSummaries}/${totalPlans} complete (${planPercent}%)\n`;
        }
        out += `**Phases:** ${completedPhases}/${phases.length} complete\n`;
        if (requirementsTotal > 0) {
            out += `**Requirements:** ${requirementsComplete}/${requirementsTotal} complete\n`;
        }
        out += '\n';
        out += '| Phase | Name | Plans | Completed | Status |\n';
        out += '|-------|------|-------|-----------|--------|\n';
        for (const p of phases) {
            out += `| ${p.number} | ${p.name} | ${p.plans} | ${p.summaries} | ${p.status} |\n`;
        }
        if (gitCommits > 0) {
            out += `\n**Git:** ${gitCommits} commits`;
            if (gitFirstCommitDate)
                out += ` (since ${gitFirstCommitDate})`;
            out += '\n';
        }
        if (lastActivity)
            out += `**Last activity:** ${lastActivity}\n`;
        return { data: { rendered: out } };
    }
    return { data: result };
};
/**
 * Markdown statistics table — port of `cmdStats` `format === 'table'` from commands.cjs (lines 942–967).
 * Delegates to `statsJson` with `['table']` (same `rendered` string as CJS).
 */
export const statsTable = async (_args, projectDir, workstream) => {
    return statsJson(['table'], projectDir, workstream);
};
// ─── todoMatchPhase ──────────────────────────────────────────────────────
/**
 * Match pending todos against a phase — port of `cmdTodoMatchPhase` from commands.cjs lines 612–729.
 */
export const todoMatchPhase = async (args, projectDir) => {
    const phase = args[0];
    if (!phase) {
        throw new GSDError('phase required for todo match-phase', ErrorClassification.Validation);
    }
    const pendingDir = join(projectDir, '.planning', 'todos', 'pending');
    const todos = [];
    try {
        const files = readdirSync(pendingDir).filter(f => f.endsWith('.md'));
        for (const file of files) {
            try {
                const content = readFileSync(join(pendingDir, file), 'utf-8');
                const titleMatch = content.match(/^title:\s*(.+)$/m);
                const areaMatch = content.match(/^area:\s*(.+)$/m);
                const filesMatch = content.match(/^files:\s*(.+)$/m);
                const body = content.replace(/^(title|area|files|created|priority):.*$/gm, '').trim();
                todos.push({
                    file,
                    title: titleMatch ? titleMatch[1].trim() : 'Untitled',
                    area: areaMatch ? areaMatch[1].trim() : 'general',
                    files: filesMatch ? filesMatch[1].trim().split(/[,\s]+/).filter(Boolean) : [],
                    body: body.slice(0, 200),
                });
            }
            catch { /* skip */ }
        }
    }
    catch { /* skip */ }
    if (todos.length === 0) {
        return { data: { phase, matches: [], todo_count: 0 } };
    }
    const rp = await roadmapGetPhase([phase], projectDir);
    const pd = rp.data;
    let phaseName = '';
    let phaseGoal = '';
    let phaseSection = '';
    if (pd && pd.found === true) {
        phaseName = String(pd.phase_name || '');
        phaseGoal = pd.goal != null ? String(pd.goal) : '';
        phaseSection = String(pd.section || '');
    }
    const phaseText = `${phaseName} ${phaseGoal} ${phaseSection}`.toLowerCase();
    const stopWords = new Set([
        'the', 'and', 'for', 'with', 'from', 'that', 'this', 'will', 'are', 'was', 'has', 'have',
        'been', 'not', 'but', 'all', 'can', 'into', 'each', 'when', 'any', 'use', 'new',
    ]);
    const phaseKeywords = new Set(phaseText
        .split(/[\s\-_/.,;:()\[\]{}|]+/)
        .map(w => w.replace(/[^a-z0-9]/g, ''))
        .filter(w => w.length > 2 && !stopWords.has(w)));
    const fp = await findPhase([phase], projectDir);
    const phaseInfoDisk = fp.data;
    const phasePlans = [];
    if (phaseInfoDisk && phaseInfoDisk.found) {
        try {
            const phaseDir = join(projectDir, phaseInfoDisk.directory);
            const planFiles = readdirSync(phaseDir).filter(f => f.endsWith('-PLAN.md'));
            for (const pf of planFiles) {
                try {
                    const planContent = readFileSync(join(phaseDir, pf), 'utf-8');
                    const fmFiles = planContent.match(/files_modified:\s*\[([^\]]*)\]/);
                    if (fmFiles) {
                        phasePlans.push(...fmFiles[1].split(',').map(s => s.trim().replace(/['"]/g, '')).filter(Boolean));
                    }
                }
                catch { /* skip */ }
            }
        }
        catch { /* skip */ }
    }
    const matches = [];
    for (const todo of todos) {
        let score = 0;
        const reasons = [];
        const todoWords = `${todo.title} ${todo.body}`
            .toLowerCase()
            .split(/[\s\-_/.,;:()\[\]{}|]+/)
            .map(w => w.replace(/[^a-z0-9]/g, ''))
            .filter(w => w.length > 2 && !stopWords.has(w));
        const matchedKeywords = todoWords.filter(w => phaseKeywords.has(w));
        if (matchedKeywords.length > 0) {
            score += Math.min(matchedKeywords.length * 0.2, 0.6);
            reasons.push(`keywords: ${[...new Set(matchedKeywords)].slice(0, 5).join(', ')}`);
        }
        if (todo.area !== 'general' && phaseText.includes(todo.area.toLowerCase())) {
            score += 0.3;
            reasons.push(`area: ${todo.area}`);
        }
        if (todo.files.length > 0 && phasePlans.length > 0) {
            const fileOverlap = todo.files.filter(f => phasePlans.some(pf => pf.includes(f) || f.includes(pf)));
            if (fileOverlap.length > 0) {
                score += 0.4;
                reasons.push(`files: ${fileOverlap.slice(0, 3).join(', ')}`);
            }
        }
        if (score > 0) {
            matches.push({
                file: todo.file,
                title: todo.title,
                area: todo.area,
                score: Math.round(score * 100) / 100,
                reasons,
            });
        }
    }
    matches.sort((a, b) => b.score - a.score);
    return { data: { phase, matches, todo_count: todos.length } };
};
// ─── listTodos ──────────────────────────────────────────────────────────
/**
 * List pending todos from .planning/todos/pending/, optionally filtered by area.
 *
 * Port of `cmdListTodos` from commands.cjs lines 74-109.
 *
 * @param args - args[0]: optional area filter
 */
export const listTodos = async (args, projectDir) => {
    const area = args[0] || null;
    const pendingDir = join(projectDir, '.planning', 'todos', 'pending');
    const todos = [];
    try {
        const files = readdirSync(pendingDir).filter(f => f.endsWith('.md'));
        for (const file of files) {
            try {
                const content = readFileSync(join(pendingDir, file), 'utf-8');
                const createdMatch = content.match(/^created:\s*(.+)$/m);
                const titleMatch = content.match(/^title:\s*(.+)$/m);
                const areaMatch = content.match(/^area:\s*(.+)$/m);
                const todoArea = areaMatch ? areaMatch[1].trim() : 'general';
                if (area && todoArea !== area)
                    continue;
                todos.push({
                    file,
                    created: createdMatch ? createdMatch[1].trim() : 'unknown',
                    title: titleMatch ? titleMatch[1].trim() : 'Untitled',
                    area: todoArea,
                    path: toPosixPath(relative(projectDir, join(pendingDir, file))),
                });
            }
            catch { /* skip */ }
        }
    }
    catch { /* skip */ }
    return { data: { count: todos.length, todos } };
};
// ─── todoComplete ───────────────────────────────────────────────────────
/**
 * Move a todo from pending to completed, adding a completion timestamp.
 *
 * Port of `cmdTodoComplete` from commands.cjs lines 724-749.
 *
 * @param args - args[0]: filename (required)
 */
export const todoComplete = async (args, projectDir) => {
    const filename = args[0];
    if (!filename) {
        throw new GSDError('filename required for todo complete', ErrorClassification.Validation);
    }
    const pendingDir = join(projectDir, '.planning', 'todos', 'pending');
    const completedDir = join(projectDir, '.planning', 'todos', 'completed');
    const sourcePath = join(pendingDir, filename);
    if (!existsSync(sourcePath)) {
        throw new GSDError(`Todo not found: ${filename}`, ErrorClassification.Validation);
    }
    mkdirSync(completedDir, { recursive: true });
    let content = readFileSync(sourcePath, 'utf-8');
    const today = new Date().toISOString().split('T')[0];
    content = `completed: ${today}\n` + content;
    writeFileSync(join(completedDir, filename), content, 'utf-8');
    unlinkSync(sourcePath);
    return { data: { completed: true, file: filename, date: today } };
};
//# sourceMappingURL=progress.js.map