// @crash-shield-ignore-file — config/cache reader for Planu-controlled JSON; writer is this codebase, shape guaranteed by build/seed.
// engine/session-state/writer.ts — SPEC-633: Write session.json universal state file
import { readFile, writeFile, mkdir } from 'node:fs/promises';
import { join } from 'node:path';
import { execSync } from 'node:child_process';
const STATE_DIR = 'state';
const ROOT_SESSION = 'session.json';
function stateDir(projectPath) {
    return join(projectPath, 'planu', STATE_DIR);
}
function specFile(projectPath, specId) {
    return join(stateDir(projectPath), `session-${specId}.json`);
}
function rootFile(projectPath) {
    return join(projectPath, 'planu', ROOT_SESSION);
}
function getModifiedFiles(projectPath) {
    try {
        const out = execSync('git diff --name-only HEAD 2>/dev/null', {
            cwd: projectPath,
            encoding: 'utf-8',
            stdio: ['pipe', 'pipe', 'pipe'],
        });
        return out.trim().split('\n').filter(Boolean).slice(0, 10);
    }
    catch {
        return [];
    }
}
function getBranch(projectPath) {
    try {
        const out = execSync('git rev-parse --abbrev-ref HEAD 2>/dev/null', {
            cwd: projectPath,
            encoding: 'utf-8',
            stdio: ['pipe', 'pipe', 'pipe'],
        });
        return out.trim() || null;
    }
    catch {
        return null;
    }
}
function buildResumePrompt(state) {
    const tests = state.testsStatus === 'unknown' ? '' : ` Tests: ${state.testsStatus}.`;
    const files = state.filesModified.length > 0 ? ` Files: ${state.filesModified.slice(0, 3).join(', ')}.` : '';
    return `Continuing ${state.specId} (${state.specTitle.slice(0, 40)}). Last: ${state.lastAction}.${tests}${files} Ready?`;
}
/**
 * SPEC-633: Write per-spec session state and update root session.json index.
 * Best-effort — never throws.
 */
export async function writeSpecSessionState(projectPath, specId, specTitle, lastAction, testsStatus = 'unknown', contextUsage = 0) {
    try {
        await mkdir(stateDir(projectPath), { recursive: true });
        const branch = getBranch(projectPath);
        const filesModified = getModifiedFiles(projectPath);
        const partial = {
            specId,
            specTitle,
            branch,
            filesModified,
            testsStatus,
            contextUsage,
            lastAction,
        };
        const resumePrompt = buildResumePrompt(partial);
        const state = {
            ...partial,
            resumePrompt: resumePrompt.slice(0, 300),
            updatedAt: new Date().toISOString(),
        };
        await writeFile(specFile(projectPath, specId), JSON.stringify(state, null, 2), 'utf-8');
        // Update root session.json index
        let index = { activeSpecs: [], updatedAt: '' };
        try {
            const raw = await readFile(rootFile(projectPath), 'utf-8');
            index = JSON.parse(raw);
        }
        catch {
            /* start fresh */
        }
        if (!index.activeSpecs.includes(specId)) {
            index.activeSpecs.push(specId);
        }
        index.updatedAt = new Date().toISOString();
        await writeFile(rootFile(projectPath), JSON.stringify(index, null, 2), 'utf-8');
        // Auto-commit planu/ changes so session.json is never left unstaged
        void (async () => {
            try {
                const { planuAutoCommit } = await import('../git/planu-autocommit.js');
                await planuAutoCommit({ projectPath, reason: 'status-update' });
            }
            catch {
                /* best-effort */
            }
        })();
    }
    catch {
        /* best-effort */
    }
}
/** SPEC-656: Compute epicProgress map from all project specs. Tags shared by >=3 specs are epics. */
export function computeEpicProgress(specs) {
    const tagCounts = new Map();
    for (const spec of specs) {
        for (const tag of spec.tags) {
            tagCounts.set(tag, (tagCounts.get(tag) ?? 0) + 1);
        }
    }
    const epicTags = [...tagCounts.entries()].filter(([, count]) => count >= 3).map(([tag]) => tag);
    const progress = {};
    for (const epic of epicTags) {
        const epicSpecs = specs.filter((s) => s.tags.includes(epic));
        const done = epicSpecs.filter((s) => s.status === 'done').length;
        progress[epic] = `${String(done)}/${String(epicSpecs.length)} done`;
    }
    return progress;
}
/**
 * SPEC-656: Update epicProgress in root session.json after any status change.
 * Best-effort — never throws.
 */
export async function updateEpicProgressInSession(projectPath, specs) {
    try {
        const epicProgress = computeEpicProgress(specs);
        if (Object.keys(epicProgress).length === 0) {
            return;
        }
        const filePath = rootFile(projectPath);
        let index = { activeSpecs: [], updatedAt: '' };
        try {
            const raw = await readFile(filePath, 'utf-8');
            index = JSON.parse(raw);
        }
        catch {
            /* start fresh */
        }
        const terminalSpecIds = new Set(specs
            .filter((spec) => spec.status === 'done' || spec.status === 'discarded')
            .map((spec) => spec.id));
        index.activeSpecs = index.activeSpecs.filter((specId) => !terminalSpecIds.has(specId));
        index.epicProgress = epicProgress;
        index.updatedAt = new Date().toISOString();
        await writeFile(filePath, JSON.stringify(index, null, 2), 'utf-8');
        // Auto-commit planu/ changes so session.json is never left unstaged
        void (async () => {
            try {
                const { planuAutoCommit } = await import('../git/planu-autocommit.js');
                await planuAutoCommit({ projectPath, reason: 'status-update' });
            }
            catch {
                /* best-effort */
            }
        })();
    }
    catch {
        /* best-effort */
    }
}
/**
 * SPEC-686: Patch planu/session.json with the given partial fields.
 * Reads current state, merges, and writes back — preserves all other fields.
 * Best-effort — never throws. Re-exported from session-writer for convenience.
 */
export async function patchSessionJson(projectPath, patch) {
    try {
        const filePath = rootFile(projectPath);
        await mkdir(stateDir(projectPath), { recursive: true });
        let index = { activeSpecs: [], updatedAt: '' };
        try {
            const raw = await readFile(filePath, 'utf-8');
            index = JSON.parse(raw);
        }
        catch {
            /* start fresh */
        }
        const updated = {
            ...index,
            ...patch,
            updatedAt: new Date().toISOString(),
        };
        await writeFile(filePath, JSON.stringify(updated, null, 2), 'utf-8');
        // Auto-commit planu/ changes so session.json is never left unstaged
        void (async () => {
            try {
                const { planuAutoCommit } = await import('../git/planu-autocommit.js');
                await planuAutoCommit({ projectPath, reason: 'status-update' });
            }
            catch {
                /* best-effort */
            }
        })();
    }
    catch {
        /* best-effort */
    }
}
/**
 * SPEC-633: Remove spec from root session.json when it moves to done/discarded.
 */
export async function removeSpecFromSession(projectPath, specId) {
    try {
        const raw = await readFile(rootFile(projectPath), 'utf-8');
        const index = JSON.parse(raw);
        index.activeSpecs = index.activeSpecs.filter((id) => id !== specId);
        index.updatedAt = new Date().toISOString();
        await writeFile(rootFile(projectPath), JSON.stringify(index, null, 2), 'utf-8');
        // Auto-commit planu/ changes so session.json is never left unstaged
        void (async () => {
            try {
                const { planuAutoCommit } = await import('../git/planu-autocommit.js');
                await planuAutoCommit({ projectPath, reason: 'status-update' });
            }
            catch {
                /* best-effort */
            }
        })();
    }
    catch {
        /* best-effort */
    }
}
//# sourceMappingURL=writer.js.map