// @crash-shield-ignore-file — config/cache reader for Planu-controlled JSON; writer is this codebase, shape guaranteed by build/seed.
// Planu — Version Resolver (SPEC-321)
// Resolves framework versions: in-memory cache → persistent cache → registry → fallback.
// Never throws — always returns a VersionResolutionResult.
import { readFile, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
// -- Constants ---------------------------------------------------------------
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
const CACHE_FILE = join(tmpdir(), 'planu-version-cache.json');
// -- In-memory cache ---------------------------------------------------------
const memoryCache = new Map();
function cacheKey(packageName, registry) {
    return `${registry}:${packageName}`;
}
function isExpired(entry) {
    const age = Date.now() - new Date(entry.fetchedAt).getTime();
    return age > CACHE_TTL_MS;
}
// -- Persistent cache --------------------------------------------------------
async function loadPersistentCache() {
    try {
        const raw = await readFile(CACHE_FILE, 'utf-8');
        return JSON.parse(raw);
    }
    catch {
        return { entries: {} };
    }
}
async function savePersistentCache(store) {
    try {
        await writeFile(CACHE_FILE, JSON.stringify(store, null, 2), 'utf-8');
    }
    catch {
        // Ignore write errors — cache is best-effort
    }
}
async function getFromPersistentCache(key) {
    const store = await loadPersistentCache();
    return store.entries[key] ?? null;
}
async function setInPersistentCache(key, entry) {
    const store = await loadPersistentCache();
    store.entries[key] = entry;
    await savePersistentCache(store);
}
// -- Registry fetch ----------------------------------------------------------
async function fetchFromRegistry(packageName, registry) {
    try {
        const { fetchLatestVersion } = await import('./package-registry.js');
        const result = await fetchLatestVersion(packageName, registry);
        if (!result) {
            return null;
        }
        return { version: result.version, isDeprecated: result.deprecated };
    }
    catch {
        return null;
    }
}
// -- Fallback versions -------------------------------------------------------
const FALLBACK_VERSIONS = {
    'npm:next': '14.0.0',
    'npm:react': '18.0.0',
    'npm:hono': '4.0.0',
    'npm:expo': '50.0.0',
    'npm:commander': '12.0.0',
    'npm:tsup': '8.0.0',
    'npm:astro': '4.0.0',
    'npm:remix': '2.0.0',
    'npm:@remix-run/node': '2.0.0',
    'pypi:fastapi': '0.100.0',
    'pypi:django': '5.0.0',
    'go:gin': '1.9.0',
    'pub:flutter': '3.0.0',
};
function getFallbackVersion(packageName, registry) {
    const key = `${registry}:${packageName}`;
    return FALLBACK_VERSIONS[key] ?? '1.0.0';
}
// -- Public API --------------------------------------------------------------
/**
 * Resolve the latest version of a framework package.
 * Order: memory cache → persistent cache → registry → fallback.
 * Never throws — always returns a result.
 */
export async function resolveFrameworkVersion(packageName, registry, cachePolicy = 'flexible') {
    const key = cacheKey(packageName, registry);
    // Offline mode: use cache only, fall back if not found
    if (cachePolicy === 'offline') {
        const memEntry = memoryCache.get(key);
        if (memEntry) {
            return { ...memEntry, source: 'cache' };
        }
        const persEntry = await getFromPersistentCache(key);
        if (persEntry) {
            memoryCache.set(key, persEntry);
            return { ...persEntry, source: 'cache' };
        }
        return {
            version: getFallbackVersion(packageName, registry),
            source: 'fallback',
            fetchedAt: new Date().toISOString(),
            isDeprecated: false,
        };
    }
    // Check memory cache
    const memEntry = memoryCache.get(key);
    if (memEntry && !isExpired(memEntry)) {
        return { ...memEntry, source: 'cache' };
    }
    // Check persistent cache (flexible mode accepts non-expired entries)
    if (cachePolicy === 'flexible') {
        const persEntry = await getFromPersistentCache(key);
        if (persEntry && !isExpired(persEntry)) {
            memoryCache.set(key, persEntry);
            return { ...persEntry, source: 'cache' };
        }
    }
    // Fetch from registry
    const fetched = await fetchFromRegistry(packageName, registry);
    if (fetched) {
        const entry = {
            version: fetched.version,
            isDeprecated: fetched.isDeprecated,
            fetchedAt: new Date().toISOString(),
        };
        memoryCache.set(key, entry);
        await setInPersistentCache(key, entry);
        return { ...entry, source: 'registry' };
    }
    // Fallback: registry unreachable
    const fallbackVersion = getFallbackVersion(packageName, registry);
    return {
        version: fallbackVersion,
        source: 'fallback',
        fetchedAt: new Date().toISOString(),
        isDeprecated: false,
    };
}
/** Clear the in-memory cache (useful for testing). */
export function clearVersionCache() {
    memoryCache.clear();
}
// ---------------------------------------------------------------------------
// SPEC-664: Extract package mentions from spec text + batch resolution
// ---------------------------------------------------------------------------
const SCOPED_NPM_RE = /@[a-z][a-z0-9._-]*\/[a-z][a-z0-9._-]*/g;
const INSTALL_KEYWORD_RE = /(?:pnpm add|npm install|yarn add|bun add)\s+((?:@[a-z][a-z0-9._-]*\/)?[a-z][a-z0-9._-]*)/g;
const IMPORT_FROM_RE = /from\s+['"]([^'"./][^'"]*)['"]/g;
const REQUIRE_RE = /require\(\s*['"]([^'"./][^'"]*)['"]\s*\)/g;
function dedupePackages(names) {
    return [...new Set(names)];
}
function isValidNpmName(name) {
    return name.length >= 2 && name.length <= 214 && !name.startsWith('.') && !name.startsWith('-');
}
/** Extract npm package names mentioned in spec text. */
export function extractNpmPackages(text) {
    const found = [];
    for (const m of text.matchAll(SCOPED_NPM_RE)) {
        found.push(m[0]);
    }
    for (const m of text.matchAll(INSTALL_KEYWORD_RE)) {
        if (m[1]) {
            found.push(m[1]);
        }
    }
    for (const m of text.matchAll(IMPORT_FROM_RE)) {
        if (m[1]) {
            found.push(m[1]);
        }
    }
    for (const m of text.matchAll(REQUIRE_RE)) {
        if (m[1]) {
            found.push(m[1]);
        }
    }
    return dedupePackages(found).filter(isValidNpmName);
}
/** Extract pip package names after "pip install" keywords in spec text. */
export function extractPipPackages(text) {
    const found = [];
    const pipRe = /pip install\s+([\w-]+(?:\[[\w,]+\])?)/g;
    for (const m of text.matchAll(pipRe)) {
        if (m[1]) {
            found.push(m[1]);
        }
    }
    return dedupePackages(found).filter(isValidNpmName);
}
const SPEC_RESOLVE_LIMIT = 5;
const SPEC_RESOLVE_TIMEOUT_MS = 4_000;
/**
 * Resolves npm package versions mentioned in spec text.
 * Returns strings like ["next@15.2.0", "@supabase/supabase-js@2.39.0"], or null.
 * Results are cached per project with 7-day TTL.
 */
export async function resolveSpecVersions(specText, projectId) {
    const packages = extractNpmPackages(specText).slice(0, SPEC_RESOLVE_LIMIT);
    if (packages.length === 0) {
        return null;
    }
    const { getCachedVersions, saveCachedVersions, isExpired: isCacheExpired, } = await import('../storage/version-cache-store.js');
    const cache = await getCachedVersions(projectId);
    const resolved = [];
    const updatedCache = { ...cache };
    await Promise.allSettled(packages.map(async (name) => {
        const cached = cache[name];
        if (cached && !isCacheExpired(cached)) {
            resolved.push(`${name}@${cached.version}`);
            return;
        }
        try {
            const { fetchLatestVersion: fetchVersion } = await import('./package-registry.js');
            const result = await Promise.race([
                fetchVersion(name, 'npm'),
                new Promise((resolve) => {
                    /* c8 ignore next */
                    setTimeout(() => {
                        resolve(null);
                    }, SPEC_RESOLVE_TIMEOUT_MS);
                }),
            ]);
            if (result) {
                updatedCache[name] = {
                    version: result.version,
                    resolvedAt: new Date().toISOString(),
                    registry: 'npm',
                };
                resolved.push(`${name}@${result.version}`);
            }
        }
        catch {
            /* best-effort — skip unresolvable packages */
        }
    }));
    if (Object.keys(updatedCache).length > Object.keys(cache).length) {
        await saveCachedVersions(projectId, updatedCache).catch(() => {
            /* best-effort */
        });
    }
    return resolved.length > 0 ? resolved : null;
}
//# sourceMappingURL=version-resolver.js.map