// @crash-shield-ignore-file — config/cache reader for Planu-controlled JSON; writer is this codebase, shape guaranteed by build/seed.
// engine/skill-registry/ttl-refresh.ts — SPEC-668: Skills TTL Refresh (30-day auto-update)
import { readFile, writeFile, mkdir, appendFile } from 'node:fs/promises';
import { join } from 'node:path';
import { fetchAnthropicSkillContent } from './anthropic-adapter.js';
const TTL_MS = 30 * 24 * 60 * 60 * 1000;
const SKILLS_DIR = '.claude/skills';
const MANIFEST_FILE = 'manifest.json';
// ---------------------------------------------------------------------------
// Manifest helpers
// ---------------------------------------------------------------------------
function manifestPath(projectPath) {
    return join(projectPath, SKILLS_DIR, MANIFEST_FILE);
}
async function readManifest(projectPath) {
    try {
        const raw = await readFile(manifestPath(projectPath), 'utf-8');
        return JSON.parse(raw);
    }
    catch {
        return { installedAt: new Date().toISOString(), skills: [] };
    }
}
async function writeManifest(projectPath, manifest) {
    await mkdir(join(projectPath, SKILLS_DIR), { recursive: true });
    await writeFile(manifestPath(projectPath), JSON.stringify(manifest, null, 2), 'utf-8');
}
// ---------------------------------------------------------------------------
// Staleness check
// ---------------------------------------------------------------------------
function isStale(entry) {
    const timestamp = entry.adaptedAt ?? entry.installedAt;
    const age = Date.now() - new Date(timestamp).getTime();
    return age > TTL_MS;
}
// ---------------------------------------------------------------------------
// Backup
// ---------------------------------------------------------------------------
async function backupSkill(projectPath, skillName, content) {
    const cacheDir = join(projectPath, '.planu', 'skill-cache');
    await mkdir(cacheDir, { recursive: true });
    const ts = new Date().toISOString().replace(/[:.]/g, '-');
    const fileName = `${skillName}-${ts}.md`;
    await writeFile(join(cacheDir, fileName), content, 'utf-8').catch(() => {
        /* best-effort */
    });
}
// ---------------------------------------------------------------------------
// Read current SKILL.md content (for diff)
// ---------------------------------------------------------------------------
async function readCurrentSkillContent(projectPath, skillName) {
    try {
        return await readFile(join(projectPath, SKILLS_DIR, skillName, 'SKILL.md'), 'utf-8');
    }
    catch {
        return null;
    }
}
// ---------------------------------------------------------------------------
// Write refreshed SKILL.md
// ---------------------------------------------------------------------------
async function writeSkillContent(projectPath, skillName, content) {
    const dir = join(projectPath, SKILLS_DIR, skillName);
    await mkdir(dir, { recursive: true });
    await writeFile(join(dir, 'SKILL.md'), content, 'utf-8');
}
// ---------------------------------------------------------------------------
// Evolution log
// ---------------------------------------------------------------------------
async function appendEvolutionLog(projectPath, entry) {
    const logPath = join(projectPath, 'planu', 'evolution-log.md');
    const line = `- ${new Date().toISOString().substring(0, 10)}: ${entry} (TTL refresh)\n`;
    await appendFile(logPath, line, 'utf-8').catch(() => {
        /* best-effort if planu/ not created yet */
    });
}
// ---------------------------------------------------------------------------
// Per-source fetcher (only anthropic supported with content; others flagged)
// ---------------------------------------------------------------------------
async function tryFetchContent(source, skillName) {
    if (source === 'anthropic') {
        try {
            const result = await fetchAnthropicSkillContent(skillName);
            return result?.skillMd ?? null;
        }
        catch {
            return null;
        }
    }
    // builtin / external sources: no re-fetch API available — skip staleness flagging
    return null;
}
// ---------------------------------------------------------------------------
// Main refresh function — AC1, AC2, AC3, AC4
// ---------------------------------------------------------------------------
/**
 * SPEC-668 AC1: Re-fetch and re-adapt skills whose adaptedAt is older than 30 days.
 * AC2: Back up old content to .planu/skill-cache/ when content changes.
 * AC3: Flag skills whose source is unreachable as 'source-unavailable'.
 * AC4: Append to planu/evolution-log.md when refresh completes with changes.
 * Returns a summary of the operation.
 */
export async function refreshStaleSkills(projectPath) {
    const result = {
        checked: 0,
        refreshed: [],
        upToDate: [],
        sourceUnavailable: [],
        skipped: [],
    };
    const manifest = await readManifest(projectPath);
    const updatedSkills = [];
    const refreshedNames = [];
    for (const entry of manifest.skills) {
        result.checked++;
        if (!isStale(entry)) {
            result.skipped.push(entry.name);
            updatedSkills.push(entry);
            continue;
        }
        // Skip builtin — no remote content to refresh
        if (entry.source === 'builtin') {
            result.skipped.push(entry.name);
            updatedSkills.push({ ...entry, adaptedAt: new Date().toISOString() });
            continue;
        }
        const newContent = await tryFetchContent(entry.source, entry.name);
        if (newContent === null) {
            // AC3: source unavailable
            result.sourceUnavailable.push(entry.name);
            updatedSkills.push({ ...entry, sourceStatus: 'source-unavailable' });
            continue;
        }
        const currentContent = await readCurrentSkillContent(projectPath, entry.name);
        const hasChanged = currentContent !== newContent;
        if (hasChanged && currentContent !== null) {
            // AC2: back up old version
            await backupSkill(projectPath, entry.name, currentContent);
        }
        if (hasChanged) {
            await writeSkillContent(projectPath, entry.name, newContent);
            result.refreshed.push(entry.name);
            refreshedNames.push(entry.name);
        }
        else {
            result.upToDate.push(entry.name);
        }
        updatedSkills.push({
            ...entry,
            adaptedAt: new Date().toISOString(),
            sourceStatus: 'active',
        });
    }
    // Persist updated manifest
    await writeManifest(projectPath, { ...manifest, skills: updatedSkills });
    // AC4: evolution log when there were refreshes
    if (refreshedNames.length > 0) {
        const summary = `Refreshed ${refreshedNames.length} skill(s): ${refreshedNames.join(', ')}`;
        await appendEvolutionLog(projectPath, summary);
    }
    return result;
}
// ---------------------------------------------------------------------------
// Fire-and-forget wrapper — called from init_project (AC1)
// ---------------------------------------------------------------------------
/**
 * SPEC-668: Trigger skills TTL refresh in background when init_project is called.
 * Fire-and-forget — never throws or blocks.
 */
export function triggerSkillTTLRefresh(projectPath) {
    void refreshStaleSkills(projectPath).catch(() => {
        /* silent failure */
    });
}
//# sourceMappingURL=ttl-refresh.js.map