/**
 * Fixture provisioner — five-stage pipeline for preparing sandbox state.
 *
 * Pipeline stages:
 *   Resolve → Fetch → Cache → Transform → Inject
 *
 * Handles three URI schemes for v1:
 * - file:// — local filesystem path (relative to task)
 * - template:// — built-in project templates
 * - sanity:// — Content Lake document by ID or query
 *
 * @see docs/design-docs/architecture-overhaul/fixtures-artifacts.md
 * @see docs/archive/exec-plans/architecture-overhaul/phase-4-agent-harness.md
 */
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
import { createHash } from "crypto";
import { basename, dirname, resolve } from "path";
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/**
 * Run the five-stage fixture provisioning pipeline.
 *
 * @param refs - Fixture references from the task definition
 * @param options - Provisioning configuration
 * @returns Provisioned fixtures and injection metadata
 */
export async function provisionFixtures(refs, options) {
    const fixtures = [];
    const vars = {};
    const warnings = [];
    const manifest = {};
    for (const ref of refs) {
        try {
            // Stage 1: Resolve — parse URI, determine backend
            const resolved = resolveFixtureURI(ref.uri, options.rootDir);
            // Stage 2: Fetch — read content from source
            const content = await fetchFixtureContent(resolved, warnings);
            if (content === null)
                continue;
            // Stage 3: Cache — store in content-addressable cache
            const contentHash = hashContent(content);
            if (options.cacheDir) {
                cacheFixture(options.cacheDir, contentHash, content);
            }
            manifest[ref.uri] = contentHash;
            // Stage 4: Transform — apply preprocessing
            const transformed = applyTransform(content, ref.transform);
            // Stage 5: Inject — place into target
            const provisioned = {
                uri: ref.uri,
                content: transformed,
                contentHash,
                inject: ref.inject,
                key: ref.key,
            };
            fixtures.push(provisioned);
            // Handle injection targets
            switch (ref.inject) {
                case "vars":
                    if (ref.key) {
                        vars[ref.key] = transformed;
                    }
                    break;
                case "working_dir":
                    if (options.sandbox) {
                        injectIntoWorkingDir(options.sandbox.workingDir, ref.key ?? basename(ref.uri), transformed);
                    }
                    break;
                case "system_prompt":
                case "provider_config":
                    // These are handled by the caller when assembling the Promptfoo config
                    break;
            }
        }
        catch (err) {
            const msg = err instanceof Error ? err.message : String(err);
            warnings.push(`Fixture "${ref.uri}": ${msg}`);
        }
    }
    return { fixtures, vars, warnings, manifest };
}
function assertPathContained(absolutePath, rootDir) {
    const normalizedBase = resolve(rootDir) + "/";
    if (!absolutePath.startsWith(normalizedBase) &&
        absolutePath !== resolve(rootDir)) {
        throw new Error(`Path traversal detected: "${absolutePath}" resolves outside rootDir "${rootDir}"`);
    }
}
function resolveFixtureURI(uri, rootDir) {
    if (uri.startsWith("file://")) {
        const relativePath = uri.slice(7);
        const absolutePath = resolve(rootDir, relativePath);
        assertPathContained(absolutePath, rootDir);
        return { scheme: "file", path: relativePath, absolutePath };
    }
    if (uri.startsWith("template://")) {
        return { scheme: "template", path: uri.slice(11) };
    }
    if (uri.startsWith("sanity://")) {
        return { scheme: "sanity", path: uri.slice(9) };
    }
    // Bare path — treat as file
    const absolutePath = resolve(rootDir, uri);
    assertPathContained(absolutePath, rootDir);
    return { scheme: "file", path: uri, absolutePath };
}
// ---------------------------------------------------------------------------
// Stage 2: Fetch
// ---------------------------------------------------------------------------
async function fetchFixtureContent(resolved, warnings) {
    switch (resolved.scheme) {
        case "file": {
            if (!resolved.absolutePath || !existsSync(resolved.absolutePath)) {
                warnings.push(`Fixture file not found: ${resolved.absolutePath ?? resolved.path}`);
                return null;
            }
            return readFileSync(resolved.absolutePath, "utf-8");
        }
        case "template": {
            // Template fixtures are resolved from built-in templates
            // For v1, return a placeholder — template registry is a future enhancement
            warnings.push(`Template fixture "${resolved.path}" — template registry not yet implemented, ` +
                "returning placeholder content");
            return `<!-- Template: ${resolved.path} -->\n`;
        }
        case "sanity": {
            // Sanity fixtures require the DocFetcher port (injected at eval time)
            // At compile time, return a deferred marker
            warnings.push(`Sanity fixture "${resolved.path}" — deferred to eval time ` +
                "(requires DocFetcher port)");
            return `<!-- Sanity document: ${resolved.path} -->\n`;
        }
        default:
            warnings.push(`Unknown fixture scheme: ${resolved.scheme}`);
            return null;
    }
}
// ---------------------------------------------------------------------------
// Stage 3: Cache
// ---------------------------------------------------------------------------
function hashContent(content) {
    return createHash("sha256").update(content).digest("hex");
}
function cacheFixture(cacheDir, hash, content) {
    const cachePath = resolve(cacheDir, hash);
    if (!existsSync(cachePath)) {
        mkdirSync(dirname(cachePath), { recursive: true });
        writeFileSync(cachePath, content);
    }
}
// ---------------------------------------------------------------------------
// Stage 4: Transform
// ---------------------------------------------------------------------------
function applyTransform(content, transform) {
    if (!transform || transform === "none")
        return content;
    switch (transform) {
        case "strip-html":
            return content.replace(/<[^>]*>/g, "").trim();
        case "extract-text":
            // Remove all markup, normalize whitespace
            return content
                .replace(/<[^>]*>/g, " ")
                .replace(/\s+/g, " ")
                .trim();
        case "truncate":
            // Default truncation: 10,000 characters
            return content.length > 10_000
                ? content.slice(0, 10_000) + "\n... (truncated)"
                : content;
        default:
            return content;
    }
}
// ---------------------------------------------------------------------------
// Stage 5: Inject
// ---------------------------------------------------------------------------
function injectIntoWorkingDir(workingDir, targetPath, content) {
    const fullPath = resolve(workingDir, targetPath);
    const normalizedBase = resolve(workingDir) + "/";
    // Path containment: prevent targetPath like "../../etc/cron.d/evil"
    if (!fullPath.startsWith(normalizedBase) &&
        fullPath !== resolve(workingDir)) {
        throw new Error(`Path traversal detected: "${targetPath}" resolves outside sandbox "${workingDir}"`);
    }
    mkdirSync(dirname(fullPath), { recursive: true });
    writeFileSync(fullPath, content);
}
