// Data loader for workflow visualizer overlay — aggregates state + metrics.
import { existsSync, readFileSync, statSync } from 'node:fs';
import { join } from 'node:path';
import { deriveState } from './state.js';
import { parseSummary, loadFile } from './files.js';
import { isDbAvailable, getMilestoneSlices, getSliceTasks } from './nerf-db.js';
import { parseRoadmap, parsePlan } from './parsers-legacy.js';
import { findMilestoneIds } from './milestone-ids.js';
import { resolveMilestoneFile, resolveSliceFile, resolveNerfRootFile, nerfRoot } from './paths.js';
import { getLedger, getProjectTotals, aggregateByPhase, aggregateBySlice, aggregateByModel, aggregateByTier, formatTierSavings, loadLedgerFromDisk, } from './metrics.js';
import { loadAllCaptures, countPendingCaptures } from './captures.js';
import { loadEffectiveNerfPreferences } from './preferences.js';
import { runProviderChecks } from './doctor-providers.js';
import { generateSkillHealthReport } from './skill-health.js';
import { runEnvironmentChecks } from './doctor-environment.js';
import { computeProgressScore } from './progress-score.js';
import { getHealthHistory } from './doctor-proactive.js';
// ─── Critical Path ────────────────────────────────────────────────────────────
export function computeCriticalPath(milestones) {
    const empty = {
        milestonePath: [],
        slicePath: [],
        milestoneSlack: new Map(),
        sliceSlack: new Map(),
    };
    if (milestones.length === 0)
        return empty;
    // Milestone-level critical path (weight = number of incomplete slices)
    const msMap = new Map(milestones.map(m => [m.id, m]));
    const msIds = milestones.map(m => m.id);
    const msAdj = new Map();
    const msWeight = new Map();
    for (const ms of milestones) {
        msAdj.set(ms.id, []);
        const incomplete = ms.slices.filter(s => !s.done).length;
        msWeight.set(ms.id, ms.status === 'complete' ? 0 : Math.max(1, incomplete));
    }
    for (const ms of milestones) {
        for (const dep of ms.dependsOn) {
            if (msMap.has(dep)) {
                const adj = msAdj.get(dep);
                if (adj)
                    adj.push(ms.id);
            }
        }
    }
    // Topological sort (Kahn's algorithm)
    const inDegree = new Map();
    for (const id of msIds)
        inDegree.set(id, 0);
    for (const ms of milestones) {
        for (const dep of ms.dependsOn) {
            if (msMap.has(dep))
                inDegree.set(ms.id, (inDegree.get(ms.id) ?? 0) + 1);
        }
    }
    const queue = [];
    for (const [id, deg] of inDegree) {
        if (deg === 0)
            queue.push(id);
    }
    const topoOrder = [];
    while (queue.length > 0) {
        const node = queue.shift();
        topoOrder.push(node);
        for (const next of (msAdj.get(node) ?? [])) {
            const d = (inDegree.get(next) ?? 1) - 1;
            inDegree.set(next, d);
            if (d === 0)
                queue.push(next);
        }
    }
    // Longest path from each root
    const dist = new Map();
    const prev = new Map();
    for (const id of msIds) {
        dist.set(id, 0);
        prev.set(id, null);
    }
    for (const node of topoOrder) {
        const w = msWeight.get(node) ?? 1;
        const nodeDist = dist.get(node) + w;
        for (const next of (msAdj.get(node) ?? [])) {
            if (nodeDist > dist.get(next)) {
                dist.set(next, nodeDist);
                prev.set(next, node);
            }
        }
    }
    // Find the end of the critical path (node with max dist + own weight)
    let maxDist = 0;
    let endNode = msIds[0];
    for (const id of msIds) {
        const totalDist = dist.get(id) + (msWeight.get(id) ?? 1);
        if (totalDist > maxDist) {
            maxDist = totalDist;
            endNode = id;
        }
    }
    // Trace back
    const milestonePath = [];
    let cur = endNode;
    while (cur !== null) {
        milestonePath.unshift(cur);
        cur = prev.get(cur) ?? null;
    }
    // Compute milestone slack
    const milestoneSlack = new Map();
    const criticalSet = new Set(milestonePath);
    for (const id of msIds) {
        if (criticalSet.has(id)) {
            milestoneSlack.set(id, 0);
        }
        else {
            const nodeTotal = dist.get(id) + (msWeight.get(id) ?? 1);
            milestoneSlack.set(id, Math.max(0, maxDist - nodeTotal));
        }
    }
    // Slice-level critical path within active milestone
    const activeMs = milestones.find(m => m.status === 'active');
    let slicePath = [];
    const sliceSlack = new Map();
    if (activeMs && activeMs.slices.length > 0) {
        const slMap = new Map(activeMs.slices.map(s => [s.id, s]));
        const slAdj = new Map();
        for (const s of activeMs.slices)
            slAdj.set(s.id, []);
        for (const s of activeMs.slices) {
            for (const dep of s.depends) {
                if (slMap.has(dep)) {
                    const adj = slAdj.get(dep);
                    if (adj)
                        adj.push(s.id);
                }
            }
        }
        // Topo sort slices
        const slIn = new Map();
        for (const s of activeMs.slices)
            slIn.set(s.id, 0);
        for (const s of activeMs.slices) {
            for (const dep of s.depends) {
                if (slMap.has(dep))
                    slIn.set(s.id, (slIn.get(s.id) ?? 0) + 1);
            }
        }
        const slQueue = [];
        for (const [id, d] of slIn) {
            if (d === 0)
                slQueue.push(id);
        }
        const slTopo = [];
        while (slQueue.length > 0) {
            const n = slQueue.shift();
            slTopo.push(n);
            for (const next of (slAdj.get(n) ?? [])) {
                const d = (slIn.get(next) ?? 1) - 1;
                slIn.set(next, d);
                if (d === 0)
                    slQueue.push(next);
            }
        }
        const slDist = new Map();
        const slPrev = new Map();
        for (const s of activeMs.slices) {
            const w = s.done ? 0 : 1;
            slDist.set(s.id, 0);
            slPrev.set(s.id, null);
        }
        for (const n of slTopo) {
            const w = (slMap.get(n)?.done ? 0 : 1);
            const nd = slDist.get(n) + w;
            for (const next of (slAdj.get(n) ?? [])) {
                if (nd > slDist.get(next)) {
                    slDist.set(next, nd);
                    slPrev.set(next, n);
                }
            }
        }
        let slMax = 0;
        let slEnd = activeMs.slices[0].id;
        for (const s of activeMs.slices) {
            const totalDist = slDist.get(s.id) + (s.done ? 0 : 1);
            if (totalDist > slMax) {
                slMax = totalDist;
                slEnd = s.id;
            }
        }
        let slCur = slEnd;
        while (slCur !== null) {
            slicePath.unshift(slCur);
            slCur = slPrev.get(slCur) ?? null;
        }
        const slCritSet = new Set(slicePath);
        for (const s of activeMs.slices) {
            if (slCritSet.has(s.id)) {
                sliceSlack.set(s.id, 0);
            }
            else {
                const nodeTotal = slDist.get(s.id) + (s.done ? 0 : 1);
                sliceSlack.set(s.id, Math.max(0, slMax - nodeTotal));
            }
        }
    }
    return { milestonePath, slicePath, milestoneSlack, sliceSlack };
}
// ─── Agent Activity ──────────────────────────────────────────────────────────
function loadAgentActivity(units, milestones) {
    if (units.length === 0)
        return null;
    // Find currently running unit (finishedAt === 0)
    const running = units.find(u => u.finishedAt === 0);
    const now = Date.now();
    const completedUnits = units.filter(u => u.finishedAt > 0).length;
    const totalSlices = milestones.reduce((sum, m) => sum + m.slices.length, 0);
    // Completion rate from finished units
    const finished = units.filter(u => u.finishedAt > 0);
    let completionRate = 0;
    if (finished.length >= 2) {
        const earliest = Math.min(...finished.map(u => u.startedAt));
        const latest = Math.max(...finished.map(u => u.finishedAt));
        const totalHours = (latest - earliest) / 3_600_000;
        completionRate = totalHours > 0 ? finished.length / totalHours : 0;
    }
    const sessionCost = units.reduce((sum, u) => sum + u.cost, 0);
    const sessionTokens = units.reduce((sum, u) => sum + u.tokens.total, 0);
    return {
        currentUnit: running
            ? { type: running.type, id: running.id, startedAt: running.startedAt }
            : null,
        elapsed: running ? now - running.startedAt : 0,
        completedUnits,
        totalSlices,
        completionRate,
        active: !!running,
        sessionCost,
        sessionTokens,
    };
}
// ─── Changelog & Verifications ────────────────────────────────────────────────
const changelogCache = new Map();
async function loadChangelogAndVerifications(basePath, milestones) {
    const entries = [];
    const verifications = [];
    for (const ms of milestones) {
        for (const sl of ms.slices) {
            if (!sl.done)
                continue;
            const summaryFile = resolveSliceFile(basePath, ms.id, sl.id, 'SUMMARY');
            if (!summaryFile)
                continue;
            const cacheKey = `${ms.id}/${sl.id}`;
            const cached = changelogCache.get(cacheKey);
            let mtime = 0;
            try {
                mtime = statSync(summaryFile).mtimeMs;
            }
            catch {
                continue;
            }
            if (cached && cached.mtime === mtime) {
                entries.push(cached.entry);
                verifications.push(cached.verification);
                continue;
            }
            const content = await loadFile(summaryFile);
            if (!content)
                continue;
            const summary = parseSummary(content);
            const entry = {
                milestoneId: ms.id,
                sliceId: sl.id,
                title: sl.title,
                oneLiner: summary.oneLiner,
                filesModified: summary.filesModified.map(f => ({
                    path: f.path,
                    description: f.description,
                })),
                completedAt: String(summary.frontmatter.completed_at ?? ''),
            };
            const verification = {
                milestoneId: ms.id,
                sliceId: sl.id,
                verificationResult: summary.frontmatter.verification_result || '',
                blockerDiscovered: summary.frontmatter.blocker_discovered,
                keyDecisions: summary.frontmatter.key_decisions || [],
                patternsEstablished: summary.frontmatter.patterns_established || [],
                provides: summary.frontmatter.provides || [],
                requires: (summary.frontmatter.requires || []).map(r => ({
                    slice: r.slice,
                    provides: r.provides,
                })),
            };
            changelogCache.set(cacheKey, { mtime, entry, verification });
            entries.push(entry);
            verifications.push(verification);
        }
    }
    entries.sort((a, b) => String(b.completedAt || '').localeCompare(String(a.completedAt || '')));
    return { changelog: { entries }, verifications };
}
// ─── Knowledge Loader ─────────────────────────────────────────────────────────
function loadKnowledge(basePath) {
    const knowledgePath = resolveNerfRootFile(basePath, 'KNOWLEDGE');
    if (!existsSync(knowledgePath)) {
        return { rules: [], patterns: [], lessons: [], exists: false };
    }
    let content;
    try {
        content = readFileSync(knowledgePath, 'utf-8');
    }
    catch {
        return { rules: [], patterns: [], lessons: [], exists: false };
    }
    const rules = [];
    const patterns = [];
    const lessons = [];
    const lines = content.split('\n');
    let currentSection = '';
    for (const line of lines) {
        if (line.startsWith('## Rules')) {
            currentSection = 'rules';
            continue;
        }
        if (line.startsWith('## Patterns')) {
            currentSection = 'patterns';
            continue;
        }
        if (line.startsWith('## Lessons')) {
            currentSection = 'lessons';
            continue;
        }
        if (line.startsWith('## ')) {
            currentSection = '';
            continue;
        }
        if (!line.startsWith('| ') || line.startsWith('| ---') || line.startsWith('| ID'))
            continue;
        const cols = line.split('|').map(c => c.trim()).filter(c => c.length > 0);
        if (cols.length < 2)
            continue;
        if (currentSection === 'rules' && cols.length >= 3) {
            rules.push({ id: cols[0], scope: cols[1], content: cols[2] });
        }
        else if (currentSection === 'patterns' && cols.length >= 2) {
            patterns.push({ id: cols[0], content: cols[1] });
        }
        else if (currentSection === 'lessons' && cols.length >= 2) {
            lessons.push({ id: cols[0], content: cols[1] });
        }
    }
    return { rules, patterns, lessons, exists: true };
}
// ─── Health Loader ────────────────────────────────────────────────────────────
function loadHealth(units, totals, basePath) {
    const prefs = loadEffectiveNerfPreferences();
    const budgetCeiling = prefs?.preferences?.budget_ceiling;
    const tokenProfile = prefs?.preferences?.token_profile ?? 'standard';
    let truncationRate = 0;
    let continueHereRate = 0;
    if (totals && totals.units > 0) {
        truncationRate = (totals.totalTruncationSections / totals.units) * 100;
        continueHereRate = (totals.continueHereFiredCount / totals.units) * 100;
    }
    const tierBreakdown = aggregateByTier(units);
    const tierSavingsLine = formatTierSavings(units);
    // Provider checks — fast (auth.json + env vars only, no network)
    let providers = [];
    try {
        providers = runProviderChecks().map((r) => ({
            name: r.name,
            label: r.label,
            category: r.category,
            ok: r.status === "ok" || r.status === "unconfigured",
            required: r.required,
            message: r.message,
        }));
    }
    catch { /* non-fatal */ }
    // Skill health summary
    let skillSummary = { total: 0, warningCount: 0, criticalCount: 0, topIssue: null };
    try {
        const report = generateSkillHealthReport(basePath);
        const warnings = report.suggestions.filter(s => s.severity === "warning");
        const criticals = report.suggestions.filter(s => s.severity === "critical");
        skillSummary = {
            total: report.skills.length,
            warningCount: warnings.length,
            criticalCount: criticals.length,
            topIssue: report.suggestions[0]?.message ?? null,
        };
    }
    catch { /* non-fatal */ }
    // Environment issues (from doctor-environment.ts, #1221)
    let environmentIssues = [];
    try {
        environmentIssues = runEnvironmentChecks(basePath).filter(r => r.status !== "ok");
    }
    catch { /* non-fatal */ }
    // Doctor run history — persisted across sessions (sync read to keep loadHealth sync)
    let doctorHistory = [];
    try {
        const historyPath = join(nerfRoot(basePath), "doctor-history.jsonl");
        if (existsSync(historyPath)) {
            const lines = readFileSync(historyPath, "utf-8").split("\n").filter(l => l.trim());
            doctorHistory = lines.slice(-20).reverse().map(l => JSON.parse(l));
        }
    }
    catch { /* non-fatal */ }
    // Current progress score — only meaningful when auto-mode has health data
    let progressScore = null;
    try {
        const history = getHealthHistory();
        if (history.length > 0) {
            const score = computeProgressScore();
            progressScore = { level: score.level, summary: score.summary, signals: score.signals };
        }
    }
    catch { /* non-fatal */ }
    return {
        budgetCeiling,
        tokenProfile,
        truncationRate,
        continueHereRate,
        tierBreakdown,
        tierSavingsLine,
        toolCalls: totals?.toolCalls ?? 0,
        assistantMessages: totals?.assistantMessages ?? 0,
        userMessages: totals?.userMessages ?? 0,
        providers,
        skillSummary,
        environmentIssues,
        doctorHistory,
        progressScore,
    };
}
const RECENT_ENTRY_LIMIT = 3;
const FEATURE_PREVIEW_LIMIT = 5;
const UPDATED_WINDOW_MS = 7 * 24 * 60 * 60 * 1000;
function buildVisualizerStats(milestones, entries) {
    const missing = [];
    for (const ms of milestones) {
        for (const sl of ms.slices) {
            if (!sl.done)
                missing.push({ milestoneId: ms.id, sliceId: sl.id, title: sl.title });
        }
    }
    const missingCount = missing.length;
    const missingSlices = missing.slice(0, FEATURE_PREVIEW_LIMIT);
    const now = Date.now();
    const updatedEntries = entries.filter(entry => {
        if (!entry.completedAt)
            return false;
        const parsed = Date.parse(entry.completedAt);
        return !Number.isNaN(parsed) && now - parsed <= UPDATED_WINDOW_MS;
    });
    const updatedCount = updatedEntries.length;
    const updatedSlices = updatedEntries.slice(0, FEATURE_PREVIEW_LIMIT).map(entry => ({
        milestoneId: entry.milestoneId,
        sliceId: entry.sliceId,
        title: entry.title,
        completedAt: entry.completedAt,
    }));
    const recentEntries = entries.slice(0, RECENT_ENTRY_LIMIT);
    return {
        missingCount,
        missingSlices,
        updatedCount,
        updatedSlices,
        recentEntries,
    };
}
function loadDiscussionState(basePath, milestones) {
    const states = [];
    for (const ms of milestones) {
        const contextPath = resolveMilestoneFile(basePath, ms.id, "CONTEXT");
        const draftPath = resolveMilestoneFile(basePath, ms.id, "CONTEXT-DRAFT");
        const state = contextPath
            ? "discussed"
            : draftPath
                ? "draft"
                : "undiscussed";
        let lastUpdated = null;
        const target = contextPath ?? draftPath;
        if (target) {
            try {
                lastUpdated = new Date(statSync(target).mtimeMs).toISOString();
            }
            catch {
                lastUpdated = null;
            }
        }
        states.push({
            milestoneId: ms.id,
            title: ms.title,
            state,
            hasContext: !!contextPath,
            hasDraft: !!draftPath,
            lastUpdated,
        });
    }
    return states;
}
// ─── File Fingerprint Cache ───────────────────────────────────────────────────
/**
 * Mtime-based cache for parsed file contents. Avoids re-reading and re-parsing
 * roadmap/plan files whose mtime hasn't changed since the last load.
 */
const fileContentCache = new Map();
function readFileCached(filePath) {
    try {
        const mtime = statSync(filePath).mtimeMs;
        const cached = fileContentCache.get(filePath);
        if (cached && cached.mtime === mtime) {
            return cached.content;
        }
        const content = readFileSync(filePath, 'utf-8');
        fileContentCache.set(filePath, { mtime, content });
        return content;
    }
    catch {
        return null;
    }
}
// ─── Loader ───────────────────────────────────────────────────────────────────
export async function loadVisualizerData(basePath) {
    const state = await deriveState(basePath);
    const milestoneIds = findMilestoneIds(basePath);
    const milestones = [];
    for (const mid of milestoneIds) {
        const entry = state.registry.find(r => r.id === mid);
        const status = entry?.status ?? 'pending';
        const dependsOn = entry?.dependsOn ?? [];
        const slices = [];
        const roadmapFile = resolveMilestoneFile(basePath, mid, 'ROADMAP');
        const roadmapContent = roadmapFile ? readFileCached(roadmapFile) : null;
        if (roadmapContent || isDbAvailable()) {
            let normSlices = null;
            if (isDbAvailable()) {
                const dbSlices = getMilestoneSlices(mid);
                if (dbSlices.length > 0) {
                    normSlices = dbSlices.map(s => ({ id: s.id, done: s.status === 'complete', title: s.title, risk: s.risk || 'medium', depends: s.depends, demo: s.demo }));
                }
            }
            if (!normSlices && roadmapContent) {
                // File-based fallback: parse roadmap for slice entries
                const parsed = parseRoadmap(roadmapContent);
                normSlices = parsed.slices.map(s => ({ id: s.id, done: s.done, title: s.title, risk: s.risk || 'medium', depends: s.depends, demo: '' }));
            }
            if (!normSlices)
                normSlices = [];
            for (const s of normSlices) {
                const isActiveSlice = state.activeMilestone?.id === mid &&
                    state.activeSlice?.id === s.id;
                const tasks = [];
                if (isActiveSlice) {
                    // Normalize tasks from DB, fall back to file parsing when DB has no data
                    let usedDbTasks = false;
                    if (isDbAvailable()) {
                        const dbTasks = getSliceTasks(mid, s.id);
                        if (dbTasks.length > 0) {
                            usedDbTasks = true;
                            for (const t of dbTasks) {
                                tasks.push({
                                    id: t.id,
                                    title: t.title,
                                    done: t.status === 'complete' || t.status === 'done',
                                    active: state.activeTask?.id === t.id,
                                    estimate: t.estimate || undefined,
                                });
                            }
                        }
                    }
                    if (!usedDbTasks) {
                        // File-based fallback: parse slice plan for task entries
                        const slicePlanFile = resolveSliceFile(basePath, mid, s.id, 'PLAN');
                        if (slicePlanFile) {
                            const planContent = readFileCached(slicePlanFile);
                            if (planContent) {
                                const parsed = parsePlan(planContent);
                                for (const t of parsed.tasks) {
                                    tasks.push({
                                        id: t.id,
                                        title: t.title,
                                        done: t.done,
                                        active: state.activeTask?.id === t.id,
                                        estimate: t.estimate || undefined,
                                    });
                                }
                            }
                        }
                    }
                }
                slices.push({
                    id: s.id,
                    title: s.title,
                    done: s.done,
                    active: isActiveSlice,
                    risk: s.risk,
                    depends: s.depends,
                    tasks,
                });
            }
        }
        milestones.push({
            id: mid,
            title: entry?.title ?? mid,
            status,
            dependsOn,
            slices,
        });
    }
    // Metrics
    let totals = null;
    let byPhase = [];
    let bySlice = [];
    let byModel = [];
    let byTier = [];
    let tierSavingsLine = '';
    let units = [];
    const ledger = getLedger() ?? loadLedgerFromDisk(basePath);
    if (ledger && ledger.units.length > 0) {
        units = [...ledger.units].sort((a, b) => a.startedAt - b.startedAt);
        totals = getProjectTotals(units);
        byPhase = aggregateByPhase(units);
        bySlice = aggregateBySlice(units);
        byModel = aggregateByModel(units);
        byTier = aggregateByTier(units);
        tierSavingsLine = formatTierSavings(units);
    }
    // Compute new fields
    const criticalPath = computeCriticalPath(milestones);
    let remainingSliceCount = 0;
    for (const ms of milestones) {
        for (const sl of ms.slices) {
            if (!sl.done)
                remainingSliceCount++;
        }
    }
    const agentActivity = loadAgentActivity(units, milestones);
    const { changelog, verifications: sliceVerifications } = await loadChangelogAndVerifications(basePath, milestones);
    const knowledge = loadKnowledge(basePath);
    const allCaptures = loadAllCaptures(basePath);
    const pendingCount = countPendingCaptures(basePath);
    const captures = {
        entries: allCaptures,
        pendingCount,
        totalCount: allCaptures.length,
    };
    const health = loadHealth(units, totals, basePath);
    const stats = buildVisualizerStats(milestones, changelog.entries);
    const discussion = loadDiscussionState(basePath, milestones);
    return {
        milestones,
        phase: state.phase,
        totals,
        byPhase,
        bySlice,
        byModel,
        byTier,
        tierSavingsLine,
        units,
        criticalPath,
        remainingSliceCount,
        agentActivity,
        changelog,
        sliceVerifications,
        knowledge,
        captures,
        health,
        discussion,
        stats,
    };
}
