/**
 * Pipeline step: Fetch documentation from Sanity CMS.
 *
 * Uses ctx.docFetcher (the DocFetcher port) for all fetching. The adapter
 * handles GROQ queries, perspective diffing, document overlays, and URL
 * fetching. This step orchestrates the call and writes metadata files.
 *
 * When a source-level perspective is active and release impact data is
 * available, this step computes which tasks are affected by the release
 * and stores a `releaseAutoScope` entry in PipelineState. Downstream
 * steps (GenerateConfigsStep, RunEvalStep) use this to narrow scope.
 */
import { existsSync, mkdirSync, writeFileSync } from "fs";
import { join } from "path";
import { assoc, isIdRef, isPathRef, isSlugRef, } from "../../_vendor/ailf-core/index.js";
import { emitFileContents } from "../../artifact-capture/emit-file.js";
import { getStepInputPaths } from "../../pipeline/cache.js";
import { buildCacheContext } from "../cache-context.js";
import { checkCanonicalContextsExist } from "../../pipeline/checks.js";
import { loadSource } from "../../sources.js";
import { configToSourceOverrides } from "../config-to-source-overrides.js";
export class FetchDocsStep {
    name = "fetch-docs";
    check() {
        return [];
    }
    async execute(ctx, state) {
        if (ctx.config.skipFetch) {
            return { status: "skipped", reason: "--no-fetch" };
        }
        const start = Date.now();
        // Load tasks via ctx.taskSource — the composition root wires the
        // right adapter for every taskSourceType (W0146). FetchDocsStep and
        // GenerateConfigsStep MUST go through the same adapter so configs
        // reference context files that were actually fetched.
        const filter = {
            ...(ctx.config.areas?.length ? { areas: ctx.config.areas } : {}),
            ...(ctx.config.tasks?.length ? { taskIds: ctx.config.tasks } : {}),
            ...(ctx.config.tags?.length ? { tags: ctx.config.tags } : {}),
            ...(ctx.config.changedDocs?.length
                ? { changedDocs: ctx.config.changedDocs }
                : {}),
        };
        const allTasks = await ctx.taskSource.loadTasks(Object.keys(filter).length > 0 ? filter : undefined);
        // Bridge: narrow to literacy tasks for canonical doc access
        const literacyTasks = allTasks.filter((t) => t.mode === "literacy");
        const tasksWithDocs = literacyTasks.filter((t) => (t.context?.docs?.length ?? 0) > 0);
        if (tasksWithDocs.length === 0) {
            return {
                status: "skipped",
                reason: "No literacy tasks with canonical_docs — nothing to fetch",
            };
        }
        // Resolve source once with typed overrides
        const overrides = configToSourceOverrides(ctx.config);
        const resolvedSource = loadSource(ctx.config.source, overrides);
        // Log source info
        console.log(`  Source: ${resolvedSource.name}`);
        console.log(`  Base URL: ${resolvedSource.baseUrl}`);
        if (resolvedSource.perspective) {
            console.log(`  Perspective: ${resolvedSource.perspective}`);
        }
        if (resolvedSource.documentIds && resolvedSource.documentIds.length > 0) {
            console.log(`  Documents: ${resolvedSource.documentIds.length} document ID(s)`);
        }
        if (resolvedSource.urls.length > 0) {
            console.log(`  URLs:      ${resolvedSource.urls.length} direct URL(s)`);
        }
        // Precondition: docFetcher must be available
        if (!ctx.docFetcher) {
            return {
                durationMs: Date.now() - start,
                error: "DocFetcher port not available. Ensure composition root wires ctx.docFetcher.",
                status: "failed",
            };
        }
        // Execute the fetch via the DocFetcher port
        let releaseImpact;
        try {
            const result = await ctx.docFetcher.fetch(tasksWithDocs, resolvedSource);
            // Capture release impact for auto-scoping
            releaseImpact = result.metadata?.releaseImpact;
            // Write metadata files for downstream pipeline consumption
            if (result.metadata) {
                writeMetadataFiles(ctx.config.rootDir, result.metadata);
            }
            // W0050 — documentManifest is run-scoped bulk JSON. The
            // release-impact/document-overlay/url-fetch captures had no
            // registered descriptors (they were extras-only); dropped per Q3
            // ("producers always call emit; registered types only").
            const documentManifestPath = join(ctx.config.rootDir, "contexts", "document-manifest.json");
            if (existsSync(documentManifestPath)) {
                await emitFileContents(ctx.artifactWriter, "documentManifest", assoc(ctx), documentManifestPath);
            }
        }
        catch (err) {
            return {
                durationMs: Date.now() - start,
                error: `fetch-docs failed: ${err instanceof Error ? err.message : String(err)}`,
                status: "failed",
            };
        }
        // Postcondition: canonical context files exist for all tasks
        const taskIds = tasksWithDocs.map((t) => t.id);
        const contextIssues = checkCanonicalContextsExist(ctx.config.rootDir, taskIds);
        const contextErrors = contextIssues.filter((i) => i.severity === "error");
        if (contextErrors.length > 0) {
            return {
                durationMs: Date.now() - start,
                error: `Postcondition failed: ${contextErrors.map((e) => e.message).join("; ")}`,
                status: "failed",
            };
        }
        // -----------------------------------------------------------------
        // Release auto-scope: compute which tasks are affected by the
        // content release. This only activates when:
        //   1. A source-level perspective is active
        //   2. Release impact data was computed (docs were fetched)
        //   3. Auto-scoping was not explicitly disabled (--no-auto-scope)
        //   4. No explicit area/task filters are set (those take precedence)
        // -----------------------------------------------------------------
        if (resolvedSource.perspective &&
            releaseImpact &&
            !ctx.config.noAutoScope) {
            const autoScope = computeAutoScope(literacyTasks, releaseImpact, resolvedSource.perspective);
            if (autoScope) {
                state.releaseAutoScope = autoScope;
                logAutoScope(autoScope);
            }
        }
        const durationMs = Date.now() - start;
        return {
            durationMs,
            status: "success",
            summary: `Fetched canonical contexts for ${taskIds.length} tasks`,
        };
    }
    cacheInputs(ctx) {
        return getStepInputPaths(ctx.config.rootDir, "fetch-docs");
    }
    cacheContext(ctx) {
        return buildCacheContext(ctx.config);
    }
}
/**
 * Write metadata files returned by DocFetcher to the contexts/ directory.
 *
 * These files are consumed by downstream pipeline steps (scoring,
 * comparison, reporting) for traceability and impact analysis.
 */
function writeMetadataFiles(rootDir, metadata) {
    const contextsDir = join(rootDir, "contexts");
    mkdirSync(contextsDir, { recursive: true });
    if (metadata.manifest) {
        const path = join(contextsDir, "document-manifest.json");
        writeFileSync(path, JSON.stringify(metadata.manifest, null, 2));
        console.log(`  📋 Document manifest: ${metadata.manifest.length} docs → contexts/document-manifest.json`);
    }
    if (metadata.releaseImpact) {
        const path = join(contextsDir, "release-impact.json");
        writeFileSync(path, JSON.stringify(metadata.releaseImpact, null, 2));
        console.log("  📄 Release impact written to contexts/release-impact.json");
    }
    if (metadata.documentOverlay) {
        const path = join(contextsDir, "document-overlay.json");
        writeFileSync(path, JSON.stringify(metadata.documentOverlay, null, 2));
        console.log("  📄 Document overlay written to contexts/document-overlay.json");
    }
    if (metadata.urlFetch) {
        const path = join(contextsDir, "url-fetch.json");
        writeFileSync(path, JSON.stringify(metadata.urlFetch, null, 2));
        console.log("  📄 URL fetch metadata written to contexts/url-fetch.json");
    }
}
// ---------------------------------------------------------------------------
// Release auto-scope computation
// ---------------------------------------------------------------------------
/**
 * Extract display slugs from a task's canonical doc refs.
 *
 * Uses slug, path (last segment), or id-based slug annotation.
 * Perspective refs are excluded — they resolve at fetch time, not here.
 */
function extractSlugsFromTask(task) {
    const slugs = [];
    for (const ref of task.context?.docs ?? []) {
        if (isSlugRef(ref)) {
            slugs.push(ref.slug);
        }
        else if (isPathRef(ref)) {
            // Path refs use the last segment as slug (e.g., "groq/groq-introduction" → "groq-introduction")
            const segments = ref.path.split("/");
            slugs.push(segments[segments.length - 1]);
        }
        else if (isIdRef(ref) && ref.slug) {
            // IdDocRef may carry an optional slug annotation for display
            slugs.push(ref.slug);
        }
        // PerspectiveDocRef — one-to-many, slugs unknown at this point; skip
    }
    return slugs;
}
/**
 * Compute which tasks are affected by a content release.
 *
 * Builds a reverse mapping (slug → task IDs) from all tasks' canonical
 * doc refs, then intersects with the release impact's added/modified slugs.
 *
 * Returns null if auto-scoping should not apply (e.g., all tasks affected).
 */
function computeAutoScope(tasks, releaseImpact, perspective) {
    // Build reverse mapping: slug → task IDs
    const slugToTaskIds = new Map();
    for (const task of tasks) {
        for (const slug of extractSlugsFromTask(task)) {
            let taskIds = slugToTaskIds.get(slug);
            if (!taskIds) {
                taskIds = new Set();
                slugToTaskIds.set(slug, taskIds);
            }
            taskIds.add(task.id);
        }
    }
    // Compute affected slugs (added + modified — these have changed content)
    const affectedSlugs = new Set([
        ...releaseImpact.added,
        ...releaseImpact.modified,
    ]);
    // Find tasks affected by the release
    const affectedTaskIds = new Set();
    for (const slug of affectedSlugs) {
        const taskIds = slugToTaskIds.get(slug);
        if (taskIds) {
            for (const id of taskIds) {
                affectedTaskIds.add(id);
            }
        }
    }
    // If all tasks are affected, auto-scoping adds no value
    if (affectedTaskIds.size >= tasks.length)
        return null;
    // If no tasks are affected, still return the scope info so downstream
    // steps know nothing needs evaluation (avoids wasting eval budget)
    const allTaskIds = new Set(tasks.map((t) => t.id));
    const skippedTaskIds = [...allTaskIds].filter((id) => !affectedTaskIds.has(id));
    return {
        affectedTaskIds: [...affectedTaskIds],
        skippedTaskIds,
        perspective,
        impact: {
            added: releaseImpact.added,
            modified: releaseImpact.modified,
            removed: releaseImpact.removed,
            unchanged: releaseImpact.unchanged,
        },
    };
}
/**
 * Log auto-scope results to the console.
 */
function logAutoScope(autoScope) {
    const { affectedTaskIds, skippedTaskIds, impact } = autoScope;
    const totalTasks = affectedTaskIds.length + skippedTaskIds.length;
    const affectedDocs = impact.added.length + impact.modified.length;
    console.log(`  🎯 Release auto-scope: ${affectedTaskIds.length} of ${totalTasks} tasks affected`);
    if (affectedDocs > 0) {
        console.log(`     ${affectedDocs} doc(s) changed → ${affectedTaskIds.length} task(s) to evaluate`);
    }
    if (affectedTaskIds.length > 0) {
        for (const id of affectedTaskIds) {
            console.log(`     ✓ ${id}`);
        }
    }
    if (skippedTaskIds.length > 0) {
        console.log(`     ⏭ ${skippedTaskIds.length} task(s) skipped (no docs affected by release)`);
    }
}
