/**
 * V2 Dependency Resolver — Phase 3 (Round 1)
 *
 * 4-step lookup chain for `import[].from` references:
 *   1. Local component in same spec
 *   2. Local npm package (node_modules/<name>/package.json)
 *   3. Registry library (Library.type: component) — HTTP stubbed for Round 1
 *   4. Registry factory (Library.type: factory) — HTTP stubbed for Round 1
 *
 * Decision points applied:
 *   D1: version defaults to ^latest (per-realize-run cache)
 *   Offline mode: skips steps 3 + 4, fails loudly if unresolvable
 *   Cache: <cacheDir>/registry-cache/ (default .specverse/registry-cache/)
 *   Fail-loud: throws DependencyResolutionError when nothing resolves
 *
 * @see docs/proposals/2026-05-08-COMPONENT-DEPENDENCIES-V2.md — "Realize bridge — Resolver"
 */
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
import { join, resolve } from 'path';
import { RegistryClient } from '../../registry/client/registry-client.js';
// ============================================================================
// Error
// ============================================================================
export class DependencyResolutionError extends Error {
    from;
    triedSteps;
    constructor(message, from, triedSteps) {
        super(message);
        this.from = from;
        this.triedSteps = triedSteps;
        this.name = 'DependencyResolutionError';
    }
}
/**
 * Map a spec-author version range to what the registry content endpoint
 * understands: an exact version or the literal `latest`.  Round 1 collapses
 * any range (`^`, `~`, `>=`, `*`, `x`) to `latest`; exact pins pass through.
 * Proper semver-range selection against available versions is a follow-up.
 */
function toRegistryVersion(version) {
    if (!version || version === 'latest' || version === '^latest')
        return 'latest';
    if (/^[\^~><=]/.test(version) || version.includes('*') || /\bx\b/.test(version))
        return 'latest';
    return version;
}
function warnRegistryMiss(kind, key, err) {
    const msg = err instanceof Error ? err.message : String(err);
    // eslint-disable-next-line no-console
    console.warn(`[resolver] registry ${kind} lookup for "${key}" failed (treating as miss): ${msg}`);
}
/**
 * Default `RegistryGateway` backed by the real `RegistryClient`.
 */
class RegistryClientGateway {
    client;
    constructor(client) {
        this.client = client ?? new RegistryClient();
    }
    async getComponentLibrary(name, version) {
        try {
            const content = await this.client.getLibraryContent(name, toRegistryVersion(version));
            if (!content)
                return null;
            return { name, version: content.version, specContent: content.content };
        }
        catch (err) {
            warnRegistryMiss('component', name, err);
            return null;
        }
    }
    async findFactoryByCapability(capability) {
        try {
            const resp = await this.client.fetchFactories({ capability });
            const first = resp.results[0];
            if (!first)
                return null;
            return {
                name: first.name,
                version: first.source?.version ?? 'latest',
                metadata: {
                    category: first.category,
                    capabilities: first.capabilities,
                    framework: first.framework,
                },
            };
        }
        catch (err) {
            warnRegistryMiss('factory', capability, err);
            return null;
        }
    }
}
// ============================================================================
// Resolver implementation
// ============================================================================
/**
 * Default semver range when the spec author omits `version:` (D1).
 */
const DEFAULT_VERSION = '^latest';
/**
 * Resolve an effective cache directory from options + environment.
 */
function resolveCacheDir(options) {
    if (options.cacheDir)
        return options.cacheDir;
    const root = options.workspaceRoot ?? process.cwd();
    return join(root, '.specverse', 'registry-cache');
}
/**
 * Determine whether the registry cache should be refreshed.
 * Checks:
 *   1. `options.refreshRegistry` flag
 *   2. `SPECVERSE_REFRESH_REGISTRY` environment variable
 */
function shouldRefreshRegistry(options) {
    if (options.refreshRegistry)
        return true;
    return process.env['SPECVERSE_REFRESH_REGISTRY'] === '1';
}
/**
 * Read a cache entry if it exists (and we are not refreshing).
 */
function readCache(cacheDir, key, refresh) {
    if (refresh)
        return null;
    const filePath = join(cacheDir, `${key}.json`);
    if (!existsSync(filePath))
        return null;
    try {
        const raw = readFileSync(filePath, 'utf-8');
        return JSON.parse(raw);
    }
    catch {
        return null;
    }
}
/**
 * Write a cache entry.  Creates the cache directory if it doesn't exist.
 */
function writeCache(cacheDir, key, entry) {
    try {
        if (!existsSync(cacheDir)) {
            mkdirSync(cacheDir, { recursive: true });
        }
        const filePath = join(cacheDir, `${key}.json`);
        writeFileSync(filePath, JSON.stringify(entry, null, 2), 'utf-8');
    }
    catch {
        // Cache write failure is non-fatal — resolution can continue without it.
    }
}
/**
 * Sanitise a `from:` value so it's safe as a filesystem key.
 * Converts `@scope/pkg` → `scope__pkg`, slashes → `__`.
 */
function cacheKey(from) {
    return from.replace(/^@/, '').replace(/\//g, '__');
}
class DependencyResolverImpl {
    registry;
    constructor(registry) {
        this.registry = registry;
    }
    async resolve(from, version, options = { spec: { components: [], deployments: [], manifests: [] } }) {
        const effectiveVersion = version ?? DEFAULT_VERSION;
        const triedSteps = [];
        // ── Step 1: Local component in same spec ────────────────────────────────
        // Components can land as either an array or a name-keyed object
        // depending on which point in the analyse/inference pipeline assembled
        // the spec. Pre-fix this site did `(options.spec?.components ?? []).find`
        // which only handled the array+undefined cases — object-shaped
        // components crashed with `((intermediate value) ?? []).find is not a
        // function` (idle-meta surfaced 7 of these per run).
        //
        // When the spec keys components by name (`components: { Services: {...} }`)
        // the values usually omit `.name` because the key IS the name. Match
        // by key first; fall back to the inner `.name` only for array-shaped
        // components (which always carry their name as a field).
        triedSteps.push('local-component');
        const componentsAny = options.spec?.components;
        let localMatch;
        if (Array.isArray(componentsAny)) {
            localMatch = componentsAny.find((c) => c?.name === from);
        }
        else if (componentsAny && typeof componentsAny === 'object') {
            const keyed = componentsAny;
            const value = keyed[from];
            if (value) {
                localMatch = { name: from, version: value.version };
            }
        }
        if (localMatch) {
            return {
                fromName: from,
                source: 'local',
                spec: { name: localMatch.name, version: localMatch.version, components: [localMatch] },
                resolvedName: localMatch.name,
                resolvedVersion: localMatch.version,
            };
        }
        // ── Step 2: Local npm package ────────────────────────────────────────────
        triedSteps.push('npm-package');
        const workspaceRoot = resolve(options.workspaceRoot ?? process.cwd());
        const pkgJsonPath = join(workspaceRoot, 'node_modules', from, 'package.json');
        if (existsSync(pkgJsonPath)) {
            try {
                const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
                const pkgName = pkgJson['name'] ?? from;
                const pkgVersion = pkgJson['version'] ?? effectiveVersion;
                return {
                    fromName: from,
                    source: 'npm',
                    spec: { name: pkgName, version: pkgVersion },
                    resolvedName: pkgName,
                    resolvedVersion: pkgVersion,
                    packageSpecifier: pkgName,
                };
            }
            catch {
                // Malformed package.json — fall through to registry steps.
            }
        }
        // ── Offline fence ────────────────────────────────────────────────────────
        if (options.offline) {
            throw new DependencyResolutionError(`Cannot resolve "${from}": not found as a local component or npm package, ` +
                `and offline mode prevents registry lookup.\n` +
                `  Tried: ${triedSteps.join(', ')}`, from, triedSteps);
        }
        const cacheDir = resolveCacheDir(options);
        const refresh = shouldRefreshRegistry(options);
        const key = cacheKey(from);
        // ── Step 3: Registry library (Library.type: component) ───────────────────
        triedSteps.push('registry-component');
        const cachedComponent = readCache(cacheDir, key, refresh);
        if (cachedComponent && cachedComponent.source === 'registry-component') {
            return {
                fromName: from,
                source: 'registry-component',
                spec: { name: cachedComponent.name, version: cachedComponent.version },
                resolvedName: cachedComponent.name,
                resolvedVersion: cachedComponent.version,
                packageSpecifier: cachedComponent.name,
                registryRef: { name: cachedComponent.name, version: cachedComponent.version },
                specContent: cachedComponent.data?.specContent,
            };
        }
        const registryComponentResult = await this.registry.getComponentLibrary(from, effectiveVersion);
        if (registryComponentResult) {
            const entry = {
                cachedAt: new Date().toISOString(),
                source: 'registry-component',
                name: registryComponentResult.name,
                version: registryComponentResult.version,
                data: registryComponentResult,
            };
            writeCache(cacheDir, key, entry);
            return {
                fromName: from,
                source: 'registry-component',
                spec: { name: registryComponentResult.name, version: registryComponentResult.version },
                resolvedName: registryComponentResult.name,
                resolvedVersion: registryComponentResult.version,
                packageSpecifier: registryComponentResult.name,
                registryRef: { name: registryComponentResult.name, version: registryComponentResult.version },
                specContent: registryComponentResult.specContent,
            };
        }
        // ── Step 4: Registry factory (Library.type: factory) ─────────────────────
        triedSteps.push('registry-factory');
        const factoryKey = `factory__${key}`;
        const cachedFactory = readCache(cacheDir, factoryKey, refresh);
        if (cachedFactory && cachedFactory.source === 'registry-factory') {
            return {
                fromName: from,
                source: 'registry-factory',
                spec: { name: cachedFactory.name, version: cachedFactory.version },
                resolvedName: cachedFactory.name,
                resolvedVersion: cachedFactory.version,
                packageSpecifier: cachedFactory.name,
                registryRef: { name: cachedFactory.name, version: cachedFactory.version },
            };
        }
        const registryFactoryResult = await this.registry.findFactoryByCapability(from);
        if (registryFactoryResult) {
            const entry = {
                cachedAt: new Date().toISOString(),
                source: 'registry-factory',
                name: registryFactoryResult.name,
                version: registryFactoryResult.version,
                data: registryFactoryResult,
            };
            writeCache(cacheDir, factoryKey, entry);
            return {
                fromName: from,
                source: 'registry-factory',
                spec: { name: registryFactoryResult.name, version: registryFactoryResult.version },
                resolvedName: registryFactoryResult.name,
                resolvedVersion: registryFactoryResult.version,
                packageSpecifier: registryFactoryResult.name,
                registryRef: { name: registryFactoryResult.name, version: registryFactoryResult.version },
            };
        }
        // ── Nothing resolved ─────────────────────────────────────────────────────
        throw new DependencyResolutionError(`Cannot resolve "${from}" (version: "${effectiveVersion}"): ` +
            `exhausted all 4 lookup steps with no match.\n` +
            `  Tried: ${triedSteps.join(' → ')}\n` +
            `  Hint: check the component name, npm package name, or registry entry for "${from}".`, from, triedSteps);
    }
    async resolveCapability(capability, options = { spec: { components: [], deployments: [], manifests: [] } }) {
        if (options.offline) {
            // In offline mode we cannot query the registry for capability matches.
            return null;
        }
        const cacheDir = resolveCacheDir(options);
        const refresh = shouldRefreshRegistry(options);
        const key = `capability__${cacheKey(capability)}`;
        // Check cache first.
        const cached = readCache(cacheDir, key, refresh);
        if (cached && cached.source === 'registry-factory') {
            return {
                name: cached.name,
                version: cached.version,
                metadata: cached.data,
            };
        }
        const result = await this.registry.findFactoryByCapability(capability);
        if (result) {
            const entry = {
                cachedAt: new Date().toISOString(),
                source: 'registry-factory',
                name: result.name,
                version: result.version,
                data: result,
            };
            writeCache(cacheDir, key, entry);
            return { name: result.name, version: result.version, metadata: result.metadata };
        }
        return null;
    }
}
// ============================================================================
// Factory function (public API)
// ============================================================================
/**
 * Create a `DependencyResolver` instance.
 *
 * The resolver is stateless between realize runs.  A new instance should be
 * created per-run (or per test) to avoid cross-run cache pollution in memory.
 * On-disk cache in `<cacheDir>/registry-cache/` persists across runs and is
 * shared across instances that use the same directory.
 */
export function createDependencyResolver(deps = {}) {
    return new DependencyResolverImpl(deps.registry ?? new RegistryClientGateway());
}
//# sourceMappingURL=index.js.map