// storage/feedback-store.ts — Feedback Hub persistence (SPEC-188)
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { withFileLock } from './file-mutex.js';
import { sendFeedbackRemote } from './feedback-remote.js';
// ---------------------------------------------------------------------------
// Path helpers
// ---------------------------------------------------------------------------
function getFeedbackPath(dataDir) {
    return join(dataDir, 'global', 'feedback', 'feedback-ledger.json');
}
function ensureDir(dirPath) {
    if (!existsSync(dirPath)) {
        mkdirSync(dirPath, { recursive: true });
    }
}
// ---------------------------------------------------------------------------
// Low-level I/O
// ---------------------------------------------------------------------------
function readLedger(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 writeLedger(filePath, entries) {
    ensureDir(dirname(filePath));
    writeFileSync(filePath, JSON.stringify(entries, null, 2), 'utf-8');
}
// ---------------------------------------------------------------------------
// ID generation
// ---------------------------------------------------------------------------
function generateId(entries) {
    if (entries.length === 0) {
        return 'FB-001';
    }
    const nums = entries
        .map((e) => {
        const m = /^FB-(\d+)$/.exec(e.id);
        return m?.[1] !== undefined ? parseInt(m[1], 10) : 0;
    })
        .filter((n) => n > 0);
    const maxNum = nums.length > 0 ? Math.max(...nums) : 0;
    return `FB-${String(maxNum + 1).padStart(3, '0')}`;
}
// ---------------------------------------------------------------------------
// Version helper
// ---------------------------------------------------------------------------
function readPlanuVersion(dataDir) {
    // Walk up from dataDir to find package.json
    const projectRoot = dirname(dataDir.endsWith('/data') ? dataDir : dataDir);
    const candidates = [
        join(dataDir, '..', 'package.json'),
        join(dataDir, '../../package.json'),
        join(process.cwd(), 'package.json'),
    ];
    for (const candidate of candidates) {
        if (existsSync(candidate)) {
            try {
                const raw = readFileSync(candidate, 'utf-8');
                const pkg = JSON.parse(raw);
                if (typeof pkg.version === 'string') {
                    return pkg.version;
                }
            }
            catch {
                // continue
            }
        }
    }
    void projectRoot;
    return 'unknown';
}
// ---------------------------------------------------------------------------
// Similarity helper (word overlap)
// ---------------------------------------------------------------------------
const STOP_WORDS = new Set([
    'the',
    'a',
    'an',
    'is',
    'are',
    'was',
    'were',
    'it',
    'its',
    'in',
    'on',
    'at',
    'to',
    'for',
    'of',
    'and',
    'or',
    'but',
    'not',
    'with',
    'this',
    'that',
    'i',
    'we',
    'my',
    'our',
]);
function tokenize(text) {
    return new Set(text
        .toLowerCase()
        .replace(/[^a-z0-9\s]/g, ' ')
        .split(/\s+/)
        .filter((w) => w.length > 1 && !STOP_WORDS.has(w)));
}
function jaccardScore(a, b) {
    if (a.size === 0 && b.size === 0) {
        return 1;
    }
    const intersection = [...a].filter((w) => b.has(w)).length;
    const union = new Set([...a, ...b]).size;
    return union === 0 ? 0 : intersection / union;
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/**
 * Submit a new feedback entry. Auto-generates ID, timestamp, and version.
 */
export async function submitFeedback(dataDir, entry) {
    const filePath = getFeedbackPath(dataDir);
    const version = readPlanuVersion(dataDir);
    return withFileLock(filePath, () => {
        const entries = readLedger(filePath);
        const id = generateId(entries);
        const newEntry = {
            ...entry,
            id,
            createdAt: new Date().toISOString(),
            planuVersion: version,
        };
        entries.push(newEntry);
        writeLedger(filePath, entries);
        void sendFeedbackRemote({
            type: newEntry.type,
            title: newEntry.title,
            description: newEntry.description,
            planuVersion: newEntry.planuVersion,
            stack: newEntry.context?.stack,
            tags: newEntry.tags,
        }).catch((err) => {
            console.error('[planu] sendFeedbackRemote failed:', err);
        });
        return Promise.resolve(newEntry);
    });
}
/**
 * Load and filter feedback entries.
 */
export function getFeedback(dataDir, filter) {
    const filePath = getFeedbackPath(dataDir);
    let entries = readLedger(filePath);
    if (filter?.type !== undefined) {
        entries = entries.filter((e) => e.type === filter.type);
    }
    if (filter?.status !== undefined) {
        entries = entries.filter((e) => e.status === filter.status);
    }
    if (filter?.specId !== undefined) {
        entries = entries.filter((e) => e.specId === filter.specId);
    }
    return entries;
}
/**
 * Update an existing feedback entry by ID.
 */
export async function updateFeedback(dataDir, id, update) {
    const filePath = getFeedbackPath(dataDir);
    return withFileLock(filePath, () => {
        const entries = readLedger(filePath);
        const idx = entries.findIndex((e) => e.id === id);
        if (idx === -1) {
            return Promise.resolve(null);
        }
        const existing = entries[idx];
        if (existing === undefined) {
            return Promise.resolve(null);
        }
        const merged = { ...existing, ...update, id };
        entries[idx] = merged;
        writeLedger(filePath, entries);
        return Promise.resolve(merged);
    });
}
/**
 * Mark feedback as resolved, optionally linking spec and PR.
 */
export async function resolveFeedback(dataDir, id, specId, prUrl, notes) {
    return updateFeedback(dataDir, id, {
        status: 'resolved',
        resolvedAt: new Date().toISOString(),
        ...(specId !== undefined ? { specId } : {}),
        ...(prUrl !== undefined ? { prUrl } : {}),
        ...(notes !== undefined ? { resolvedNotes: notes } : {}),
    });
}
/**
 * Compute summary statistics across all feedback.
 */
export function getSummary(dataDir) {
    const entries = readLedger(getFeedbackPath(dataDir));
    const byType = {
        bug: 0,
        feature: 0,
        suggestion: 0,
        dx: 0,
    };
    const byStatus = {
        pending: 0,
        triaged: 0,
        spec_created: 0,
        implementing: 0,
        resolved: 0,
        dismissed: 0,
    };
    let totalResolutionMs = 0;
    let resolvedCount = 0;
    for (const e of entries) {
        byType[e.type] = byType[e.type] + 1;
        byStatus[e.status] = byStatus[e.status] + 1;
        if (e.status === 'resolved' && e.resolvedAt) {
            const created = new Date(e.createdAt).getTime();
            const resolved = new Date(e.resolvedAt).getTime();
            if (!isNaN(created) && !isNaN(resolved)) {
                totalResolutionMs += resolved - created;
                resolvedCount++;
            }
        }
    }
    const avgResolutionDays = resolvedCount > 0 ? totalResolutionMs / resolvedCount / (1000 * 60 * 60 * 24) : 0;
    const topRequests = [...entries]
        .filter((e) => (e.votes ?? 0) > 0)
        .sort((a, b) => (b.votes ?? 0) - (a.votes ?? 0))
        .slice(0, 5)
        .map((e) => ({ title: e.title, votes: e.votes ?? 0, id: e.id }));
    return {
        total: entries.length,
        byType,
        byStatus,
        pending: byStatus.pending,
        resolved: byStatus.resolved,
        avgResolutionDays,
        topRequests,
    };
}
/**
 * Find feedback entries with title similar to the given string.
 * Uses Jaccard word overlap. Threshold defaults to 0.3.
 */
export function findSimilar(dataDir, title, threshold = 0.3) {
    const entries = readLedger(getFeedbackPath(dataDir));
    const queryTokens = tokenize(title);
    return entries.filter((e) => {
        const entryTokens = tokenize(e.title);
        return jaccardScore(queryTokens, entryTokens) >= threshold;
    });
}
//# sourceMappingURL=feedback-store.js.map