/**
 * Report storage abstraction.
 * Default implementation: local file system.
 * Can be replaced with database, S3, etc.
 */
import { readdir, readFile, writeFile, unlink, access, mkdir, rename, stat } from 'node:fs/promises';
import { join } from 'node:path';
// Per-id in-memory mutex for safe read-modify-write.
// Uses a queue to avoid the race window between checking and acquiring the lock.
const locks = new Map();
async function withLock(id, fn) {
    // Chain onto any existing lock for this id, so requests are serialized
    const prev = locks.get(id) ?? Promise.resolve();
    let releaseLock;
    const next = new Promise((r) => { releaseLock = r; });
    locks.set(id, next);
    await prev;
    try {
        return await fn();
    }
    finally {
        locks.delete(id);
        releaseLock();
    }
}
/**
 * Create a file-system-based report store.
 */
export function createFileStore(dir) {
    async function ensureDir() {
        try {
            await access(dir);
        }
        catch {
            await mkdir(dir, { recursive: true });
        }
    }
    function normalizeReportDocument(data, fallbackId) {
        if (!data || typeof data !== 'object')
            return null;
        const record = data;
        if (record.kind === 'evaluation') {
            if (!record.meta || !record.summary || !Array.isArray(record.results))
                return null;
            return { ...record, id: typeof record.id === 'string' && record.id ? record.id : fallbackId };
        }
        if (record.kind === 'batch-evaluation') {
            if (!record.meta || !Array.isArray(record.items))
                return null;
            return { ...record, id: typeof record.id === 'string' && record.id ? record.id : fallbackId };
        }
        if (record.kind === undefined
            && record.overview === undefined
            && record.artifacts === undefined
            && record.meta
            && record.summary
            && Array.isArray(record.results)) {
            return {
                ...record,
                kind: 'evaluation',
                id: typeof record.id === 'string' && record.id ? record.id : fallbackId,
            };
        }
        return null;
    }
    function isEvaluationReport(report) {
        return report.kind === 'evaluation';
    }
    // Studio 每个 / 和 /skills/<name> 请求都调 list(),里面对每个 .json 同步 readFile +
    // JSON.parse。报告数上来后这是主性能瓶颈。缓存策略:fingerprint = dir mtime + 文件名
    // 排序串 + 每个文件 mtime;任一变化 invalidate。fingerprint 算 cheap(只 stat),命中后
    // 完全跳过 readFile。
    let cachedFingerprint = '';
    let cachedRuns = null;
    async function computeListFingerprint() {
        try {
            const dirStat = await stat(dir);
            const files = (await readdir(dir)).filter((f) => f.endsWith('.json')).sort();
            const parts = await Promise.all(files.map(async (f) => {
                try {
                    const s = await stat(join(dir, f));
                    return `${f}:${s.mtimeMs}:${s.size}`;
                }
                catch {
                    return `${f}:?`;
                }
            }));
            return `${dirStat.mtimeMs}|${parts.join(',')}`;
        }
        catch {
            return null;
        }
    }
    async function list() {
        try {
            await access(dir);
        }
        catch {
            return [];
        }
        const fp = await computeListFingerprint();
        if (fp != null && fp === cachedFingerprint && cachedRuns)
            return cachedRuns;
        const files = (await readdir(dir))
            .filter((f) => f.endsWith('.json'))
            .sort()
            .reverse();
        const runs = [];
        for (const file of files) {
            try {
                const data = JSON.parse(await readFile(join(dir, file), 'utf-8'));
                const report = normalizeReportDocument(data, file.replace(/\.json$/, ''));
                if (report)
                    runs.push(report);
            }
            catch { /* skip corrupt files */ }
        }
        runs.sort((a, b) => {
            const ta = a.meta?.timestamp || '';
            const tb = b.meta?.timestamp || '';
            return tb.localeCompare(ta);
        });
        if (fp != null) {
            cachedFingerprint = fp;
            cachedRuns = runs;
        }
        return runs;
    }
    async function get(id) {
        try {
            const data = JSON.parse(await readFile(join(dir, `${id}.json`), 'utf-8'));
            return normalizeReportDocument(data, id);
        }
        catch {
            return null;
        }
    }
    async function save(id, report) {
        await ensureDir();
        const tmpPath = join(dir, `${id}.json.tmp.${Date.now()}.${Math.random().toString(36).slice(2)}`);
        await writeFile(tmpPath, JSON.stringify(report, null, 2));
        await rename(tmpPath, join(dir, `${id}.json`));
    }
    /**
     * Atomic read-modify-write with in-memory mutex.
     * Prevents concurrent updates from overwriting each other.
     */
    async function update(id, mutator) {
        return withLock(id, async () => {
            const report = await get(id);
            if (!report)
                return null;
            mutator(report);
            await save(id, report);
            return report;
        });
    }
    async function remove(id) {
        try {
            await unlink(join(dir, `${id}.json`));
            return true;
        }
        catch (err) {
            const fsError = err;
            if (fsError.code === 'ENOENT')
                return false;
            throw err;
        }
    }
    async function exists(id) {
        try {
            await access(join(dir, `${id}.json`));
            return true;
        }
        catch {
            return false;
        }
    }
    async function findByVariant(variantName) {
        const all = await list();
        return all.filter(isEvaluationReport).filter((r) => r.meta?.variants?.includes(variantName));
    }
    async function findByArtifactHash(hash) {
        const all = await list();
        return all.filter(isEvaluationReport).filter((r) => {
            const hashes = r.meta?.artifactHashes || {};
            return Object.values(hashes).includes(hash);
        });
    }
    return { list, get, save, update, remove, exists, findByVariant, findByArtifactHash };
}
export async function queryJobList(jobStore, query = {}) {
    let jobs = await jobStore.list();
    if (query.status) {
        jobs = jobs.filter((job) => job.status === query.status);
    }
    if (query.reportId) {
        jobs = jobs.filter((job) => job.resultReportId === query.reportId);
    }
    if (query.project) {
        jobs = jobs.filter((job) => job.request.project === query.project);
    }
    if (query.owner) {
        jobs = jobs.filter((job) => job.request.owner === query.owner);
    }
    if (query.tag) {
        jobs = jobs.filter((job) => job.request.tags?.includes(query.tag));
    }
    if (typeof query.limit === 'number' && Number.isFinite(query.limit) && query.limit >= 0) {
        jobs = jobs.slice(0, query.limit);
    }
    return jobs;
}
export async function queryJob(jobStore, id) {
    return jobStore.get(id);
}
export async function queryRunList(reportStore) {
    return (await reportStore.list()).map((report) => ({
        id: report.id,
        kind: report.kind,
        meta: report.meta,
        ...(report.kind === 'evaluation' ? { summary: report.summary } : { items: report.items }),
    }));
}
export async function queryRun(reportStore, id) {
    return reportStore.get(id);
}
export async function queryTrend(reportStore, variantName) {
    const runs = await reportStore.findByVariant(variantName);
    const points = runs.map((report) => {
        const summary = report.summary?.[variantName] || {};
        const meta = report.meta;
        return {
            reportId: report.id,
            timestamp: meta.timestamp,
            avgCompositeScore: summary.avgCompositeScore ?? null,
            avgNumTurns: summary.avgNumTurns ?? null,
            avgCostPerSample: summary.avgCostPerSample ?? null,
            artifactHash: meta.artifactHashes?.[variantName] || null,
            gitCommitShort: meta.gitInfo?.commitShort || null,
            gitBranch: meta.gitInfo?.branch || null,
        };
    });
    return {
        variant: variantName,
        points,
        runs,
    };
}
//# sourceMappingURL=report-store.js.map