// engine/dashboard/data-loader.ts — Reads project/spec data from data/ directory
// for the dashboard. All I/O is synchronous-style via fs promises.
import { readFile, readdir, stat } from 'node:fs/promises';
import { join } from 'node:path';
import { computeDashboardMetrics, computeHealthScore } from './kanban-logic.js';
import { extractSectionBody } from '../spec-format/read-technical-section.js';
/** Root data directory for all project data */
const DATA_DIR = join(process.cwd(), 'data');
/** Safely read and parse a JSON file; returns null on any error */
async function readJsonFile(filePath) {
    try {
        const content = await readFile(filePath, 'utf-8');
        return JSON.parse(content);
    }
    catch {
        return null;
    }
}
/** Safely read a text file; returns null on any error */
async function readTextFile(filePath) {
    try {
        return await readFile(filePath, 'utf-8');
    }
    catch {
        return null;
    }
}
/** Parse criteria counts from PROGRESS.md content */
export function parseCriteriaFromProgress(content) {
    const lines = content.split('\n');
    let total = 0;
    let implemented = 0;
    for (const line of lines) {
        // Match table rows like: | AC-01 | description | Hecho | or | Pendiente |
        const match = /^\|\s*AC-\d+\s*\|/.exec(line);
        if (match) {
            total++;
            if (/\|\s*Hecho\s*\|/i.test(line) || /\|\s*Done\s*\|/i.test(line)) {
                implemented++;
            }
        }
    }
    return { total, implemented };
}
/** Detect the stack label from a raw project record */
function detectStack(raw) {
    if (raw.stack) {
        return raw.stack;
    }
    const parts = [];
    if (raw.language) {
        parts.push(raw.language);
    }
    if (raw.framework) {
        parts.push(raw.framework);
    }
    return parts.length > 0 ? parts.join(' / ') : 'Unknown';
}
/** Load spec cards for a project from its specs/ subdirectory */
async function loadSpecCards(projectDir) {
    const specsDir = join(projectDir, 'specs');
    let entries;
    try {
        entries = await readdir(specsDir);
    }
    catch {
        return [];
    }
    const cards = [];
    const validStatuses = [
        'draft',
        'review',
        'approved',
        'implementing',
        'done',
        'discarded',
    ];
    for (const entry of entries) {
        const specDir = join(specsDir, entry);
        const specFile = join(specDir, 'spec.json');
        const raw = await readJsonFile(specFile);
        if (!raw) {
            continue;
        }
        const specId = raw.id ?? entry;
        const title = raw.title ?? specId;
        const rawStatus = raw.status ?? 'draft';
        const status = validStatuses.includes(rawStatus)
            ? rawStatus
            : 'draft';
        // Try to load PROGRESS.md for criteria
        const progressPath = join(specDir, 'PROGRESS.md');
        const progressContent = await readTextFile(progressPath);
        const { total, implemented } = progressContent !== null
            ? parseCriteriaFromProgress(progressContent)
            : { total: 0, implemented: 0 };
        const progressPercent = total > 0 ? Math.round((implemented / total) * 100) : 0;
        const card = {
            id: specId,
            title,
            status,
            criteriaTotal: total,
            criteriaImplemented: implemented,
            progressPercent,
        };
        if (raw.updatedAt) {
            card.updatedAt = raw.updatedAt;
        }
        const validDifficulties = [1, 2, 3, 4, 5];
        if (raw.difficulty !== undefined && validDifficulties.includes(raw.difficulty)) {
            card.difficulty = raw.difficulty;
        }
        const validRisks = ['low', 'medium', 'high', 'critical'];
        if (raw.risk !== undefined && validRisks.includes(raw.risk)) {
            card.risk = raw.risk;
        }
        if (raw.tags && Array.isArray(raw.tags)) {
            card.tags = raw.tags;
        }
        cards.push(card);
    }
    // Sort by spec ID ascending
    cards.sort((a, b) => a.id.localeCompare(b.id));
    return cards;
}
/** Load all projects from data/projects/ */
export async function loadAllProjects() {
    const projectsDir = join(DATA_DIR, 'projects');
    let entries;
    try {
        entries = await readdir(projectsDir);
    }
    catch {
        return [];
    }
    const summaries = [];
    for (const entry of entries) {
        const projectDir = join(projectsDir, entry);
        // Skip non-directories
        try {
            const s = await stat(projectDir);
            if (!s.isDirectory()) {
                continue;
            }
        }
        catch {
            continue;
        }
        const projectFile = join(projectDir, 'project.json');
        const raw = await readJsonFile(projectFile);
        if (!raw) {
            continue;
        }
        const specs = await loadSpecCards(projectDir);
        // Exclude discarded specs from active counts
        const activeSpecs = specs.filter((s) => s.status !== 'discarded');
        const doneCount = activeSpecs.filter((s) => s.status === 'done').length;
        const lastActivity = raw.updatedAt ?? raw.createdAt ?? 'Unknown';
        summaries.push({
            id: entry,
            name: raw.name ?? entry,
            path: raw.path ?? '',
            stack: detectStack(raw),
            lastActivity,
            specCount: activeSpecs.length,
            doneCount,
            specs,
        });
    }
    return summaries;
}
/** Load a single project by its ID */
export async function loadProject(projectId) {
    const projects = await loadAllProjects();
    return projects.find((p) => p.id === projectId) ?? null;
}
/** Compute health metrics for a project */
export function computeHealthMetrics(project) {
    const totalSpecs = project.specCount;
    const donePercent = totalSpecs > 0 ? Math.round((project.doneCount / totalSpecs) * 100) : 0;
    const pendingApproval = project.specs.filter((s) => s.status === 'draft' || s.status === 'review').length;
    const activeImplementation = project.specs.filter((s) => s.status === 'implementing').length;
    return {
        totalSpecs,
        donePercent,
        pendingApproval,
        activeImplementation,
        totalEstimation: null,
    };
}
/** Read spec markdown files for the detail view */
export async function loadSpecDetail(projectId, specId) {
    const specDir = join(DATA_DIR, 'projects', projectId, 'specs', specId);
    const specMd = await readTextFile(join(specDir, 'spec.md'));
    const hu = specMd;
    const ficha = specMd !== null ? extractSectionBody(specMd, 'Technical') || null : null;
    const progress = specMd !== null ? extractSectionBody(specMd, 'Progress') || null : null;
    return { hu, ficha, progress };
}
/** Extract code links (file references) from a technical markdown */
export function extractCodeLinks(technicalMd) {
    const links = [];
    // Match patterns like `src/path/file.ts` or paths ending with common extensions
    const regex = /`((?:src|tests|lib|packages)\/[\w/.-]+\.\w+)`/g;
    let match;
    while ((match = regex.exec(technicalMd)) !== null) {
        if (match[1]) {
            links.push(match[1]);
        }
    }
    return links;
}
/** Parse YAML-like frontmatter from spec.md content */
export function parseFrontmatter(content) {
    const result = {};
    const fmMatch = /^---\s*\n([\s\S]*?)\n---/.exec(content);
    if (!fmMatch?.[1]) {
        return result;
    }
    const lines = fmMatch[1].split('\n');
    for (const line of lines) {
        const kvMatch = /^(\w[\w-]*):\s*(.*)$/.exec(line);
        if (!kvMatch?.[1]) {
            continue;
        }
        const key = kvMatch[1];
        const rawVal = kvMatch[2]?.trim() ?? '';
        if (rawVal === '') {
            result[key] = null;
        }
        else if (rawVal.startsWith('[') && rawVal.endsWith(']')) {
            // Simple array: [a, b, c]
            result[key] = rawVal
                .slice(1, -1)
                .split(',')
                .map((s) => s.trim().replace(/^['"]|['"]$/g, ''))
                .filter((s) => s.length > 0);
        }
        else if (rawVal === 'true') {
            result[key] = true;
        }
        else if (rawVal === 'false') {
            result[key] = false;
        }
        else if (rawVal === 'null') {
            result[key] = null;
        }
        else if (/^\d+$/.test(rawVal)) {
            result[key] = parseInt(rawVal, 10);
        }
        else {
            result[key] = rawVal.replace(/^['"]|['"]$/g, '');
        }
    }
    return result;
}
/** Load enhanced spec detail with code links and frontmatter */
export async function loadSpecDetailEnhanced(projectId, specId) {
    const specDir = join(DATA_DIR, 'projects', projectId, 'specs', specId);
    const specMd = await readTextFile(join(specDir, 'spec.md'));
    const hu = specMd;
    const ficha = specMd !== null ? extractSectionBody(specMd, 'Technical') || null : null;
    const progress = specMd !== null ? extractSectionBody(specMd, 'Progress') || null : null;
    const codeLinks = ficha ? extractCodeLinks(ficha) : [];
    const frontmatter = specMd ? parseFrontmatter(specMd) : {};
    return { hu, ficha, progress, codeLinks, frontmatter };
}
/** Load the drift score from constitution-drift.json */
export async function loadDriftScore(projectId) {
    const driftFile = join(DATA_DIR, 'projects', projectId, 'constitution-drift.json');
    const raw = await readJsonFile(driftFile);
    return raw?.score ?? 0;
}
/** Load aggregated project metrics */
export async function loadProjectMetrics(projectId) {
    const project = await loadProject(projectId);
    if (!project) {
        return { completionRate: 0, criteriaCoverage: 0, velocity: 0, driftScore: 0 };
    }
    const drift = await loadDriftScore(projectId);
    return computeDashboardMetrics(project.specs, drift);
}
/** Load project health score */
export async function loadProjectHealthScore(projectId) {
    const metrics = await loadProjectMetrics(projectId);
    return computeHealthScore(metrics);
}
/** Load events for a project since a given timestamp.
 *  Scans spec.json files for updatedAt fields newer than `since`. */
export async function loadProjectEvents(projectId, since) {
    const sinceMs = new Date(since).getTime();
    const events = [];
    const specsDir = join(DATA_DIR, 'projects', projectId, 'specs');
    let entries;
    try {
        entries = await readdir(specsDir);
    }
    catch {
        return { events: [], timestamp: new Date().toISOString() };
    }
    for (const entry of entries) {
        const specFile = join(specsDir, entry, 'spec.json');
        const raw = await readJsonFile(specFile);
        if (!raw?.updatedAt) {
            continue;
        }
        const updatedMs = new Date(raw.updatedAt).getTime();
        if (!isNaN(updatedMs) && updatedMs > sinceMs) {
            events.push({
                type: 'spec_updated',
                specId: raw.id ?? entry,
                timestamp: raw.updatedAt,
                data: { status: raw.status, title: raw.title },
            });
        }
    }
    events.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
    return { events, timestamp: new Date().toISOString() };
}
/** Read usage stats from data/global/usage.json (written by SPEC-064) */
export async function loadUsageStats() {
    const usageFile = join(DATA_DIR, 'global', 'usage.json');
    const raw = await readJsonFile(usageFile);
    if (!raw) {
        return {
            tier: 'unknown',
            callsToday: 0,
            dailyLimit: null,
            trialDaysLeft: null,
            isAvailable: false,
        };
    }
    return {
        tier: raw.tier ?? 'free',
        callsToday: raw.callsToday ?? 0,
        dailyLimit: raw.dailyLimit ?? null,
        trialDaysLeft: raw.trialDaysLeft ?? null,
        isAvailable: true,
    };
}
//# sourceMappingURL=data-loader.js.map