// storage/comments-store.ts — Spec Comments persistence (SPEC-282)
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { withFileLock } from './file-mutex.js';
// ---------------------------------------------------------------------------
// Path helpers
// ---------------------------------------------------------------------------
function getProjectHash(projectPath) {
    // Mirror the hash logic used elsewhere: simple djb2 on the path
    let h = 5381;
    for (let i = 0; i < projectPath.length; i++) {
        h = ((h << 5) + h) ^ projectPath.charCodeAt(i);
        h = h >>> 0;
    }
    return h.toString(16).padStart(16, '0');
}
function getCommentsPath(dataDir, projectPath, specId) {
    const hash = getProjectHash(projectPath);
    return join(dataDir, 'projects', hash, 'comments', `${specId}.json`);
}
function ensureDir(dirPath) {
    if (!existsSync(dirPath)) {
        mkdirSync(dirPath, { recursive: true });
    }
}
// ---------------------------------------------------------------------------
// Low-level I/O
// ---------------------------------------------------------------------------
function readComments(filePath) {
    if (!existsSync(filePath)) {
        return [];
    }
    try {
        const raw = readFileSync(filePath, 'utf-8');
        const parsed = JSON.parse(raw);
        if (!Array.isArray(parsed)) {
            return [];
        }
        return parsed;
    }
    catch {
        return [];
    }
}
function writeComments(filePath, comments) {
    ensureDir(dirname(filePath));
    writeFileSync(filePath, JSON.stringify(comments, null, 2), 'utf-8');
}
// ---------------------------------------------------------------------------
// ID generation
// ---------------------------------------------------------------------------
function generateCommentId(comments) {
    if (comments.length === 0) {
        return 'CMT-001';
    }
    const nums = comments
        .map((c) => {
        const m = /^CMT-(\d+)$/.exec(c.id);
        return m?.[1] !== undefined ? parseInt(m[1], 10) : 0;
    })
        .filter((n) => n > 0);
    const maxNum = nums.length > 0 ? Math.max(...nums) : 0;
    return `CMT-${String(maxNum + 1).padStart(3, '0')}`;
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/**
 * Add a new comment to a spec. Returns the created comment.
 */
export async function addComment(dataDir, projectPath, specId, input) {
    const filePath = getCommentsPath(dataDir, projectPath, specId);
    return withFileLock(filePath, () => {
        const comments = readComments(filePath);
        const id = generateCommentId(comments);
        const newComment = {
            id,
            specId,
            author: input.author,
            text: input.text,
            resolved: false,
            blocking: input.blocking ?? false,
            createdAt: new Date().toISOString(),
            ...(input.acIndex !== undefined ? { acIndex: input.acIndex } : {}),
        };
        comments.push(newComment);
        writeComments(filePath, comments);
        return Promise.resolve(newComment);
    });
}
/**
 * Add a reply to an existing comment (thread).
 * Returns the created reply comment, or null if parentId not found.
 */
export async function replyComment(dataDir, projectPath, specId, input) {
    const filePath = getCommentsPath(dataDir, projectPath, specId);
    return withFileLock(filePath, () => {
        const comments = readComments(filePath);
        const parent = comments.find((c) => c.id === input.parentId);
        if (parent === undefined) {
            return Promise.resolve(null);
        }
        const id = generateCommentId(comments);
        const reply = {
            id,
            specId,
            author: input.author,
            text: input.text,
            parentId: input.parentId,
            resolved: false,
            blocking: false,
            createdAt: new Date().toISOString(),
        };
        comments.push(reply);
        writeComments(filePath, comments);
        return Promise.resolve(reply);
    });
}
/**
 * List comments for a spec, optionally filtering by resolved status.
 */
export function listComments(dataDir, projectPath, specId, filter) {
    const filePath = getCommentsPath(dataDir, projectPath, specId);
    let comments = readComments(filePath);
    if (filter?.resolved !== undefined) {
        comments = comments.filter((c) => c.resolved === filter.resolved);
    }
    return comments.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
}
/**
 * Mark a comment as resolved. Returns the updated comment or null if not found.
 */
export async function resolveComment(dataDir, projectPath, specId, commentId) {
    const filePath = getCommentsPath(dataDir, projectPath, specId);
    return withFileLock(filePath, () => {
        const comments = readComments(filePath);
        const idx = comments.findIndex((c) => c.id === commentId);
        if (idx === -1) {
            return Promise.resolve(null);
        }
        const existing = comments[idx];
        if (existing === undefined) {
            return Promise.resolve(null);
        }
        const updated = {
            ...existing,
            resolved: true,
            resolvedAt: new Date().toISOString(),
        };
        comments[idx] = updated;
        writeComments(filePath, comments);
        return Promise.resolve(updated);
    });
}
/**
 * Count unresolved (and optionally blocking) comments for a spec.
 * Used by planu_status, list_specs, and check_readiness integrations.
 */
export function countUnresolvedComments(dataDir, projectPath, specId) {
    const filePath = getCommentsPath(dataDir, projectPath, specId);
    const comments = readComments(filePath).filter((c) => !c.resolved);
    return {
        total: comments.length,
        blocking: comments.filter((c) => c.blocking).length,
    };
}
//# sourceMappingURL=comments-store.js.map