/**
 * pipeline/retrieval-metrics.ts
 *
 * Computes retrieval precision and recall by comparing the doc slugs an agent
 * visited against the canonical doc set defined in task definitions.
 *
 * The canonical (expected) set is keyed on the cross-version **document key**
 * (D0084) — `baseDocumentId(ref.id)` for id refs, the editorial slug/path for
 * slug/path refs — with the slug as the display label. The agent's *visited*
 * docs stay on a slug/URL axis (the agent browses the live site), so the two
 * axes are deliberately asymmetric (design §4 / W-doctype-retrieval-attribution
 * AC#3). This module is dormant — `report.retrievalMetrics` is never produced
 * and there is no production call site — but is kept document-type-agnostic so a
 * future revival counts non-article `_id`/`path` pins by construction.
 *
 * This is a pure computation module — no file I/O beyond reading task files.
 */
import { existsSync } from "fs";
import { isPathRef, isSlugRef, resolveDocRefToDocumentKey, } from "../_vendor/ailf-core/index.js";
import { discoverTsTaskFiles, loadTsTaskFileSync, } from "../adapters/task-sources/task-file-loader.js";
import { resolveVendoredSubdir } from "./compiler/config-loader.js";
/**
 * Empty slug→document-key bridge for the dormant loader.
 *
 * `loadCanonicalDocs` reads task definitions directly, with no run manifest, so
 * there is genuinely no slug→`_id` bridge to resolve slug/path refs through.
 * Passed to `resolveDocRefToDocumentKey` so id refs still resolve to their base
 * `documentId`; slug/path refs return `undefined` and fall back to their
 * editorial handle (see `canonicalDocKeysForRefs`).
 */
const NO_MANIFEST_BRIDGE = new Map();
// --- Types for task definitions (just the canonical_docs part) ---
/**
 * Compute retrieval metrics from agentic behavior data.
 *
 * @param rootDir  Package root (to find tasks/*.yaml)
 * @param behaviors  Agentic behavior data per task
 * @returns RetrievalMetrics or null if no canonical docs or behavior data
 */
export function computeRetrievalMetrics(rootDir, behaviors) {
    const canonicalMap = loadCanonicalDocs(rootDir);
    if (canonicalMap.size === 0)
        return null;
    if (behaviors.length === 0)
        return null;
    // Compute per-task metrics
    const taskMetrics = [];
    for (const behavior of behaviors) {
        const taskId = behavior.taskId;
        if (!taskId)
            continue;
        const canonical = canonicalMap.get(taskId);
        if (!canonical)
            continue;
        taskMetrics.push(computeTaskMetrics(taskId, canonical.area, behavior.docSlugsVisited, canonical.docKeys));
    }
    if (taskMetrics.length === 0)
        return null;
    // Group by area
    const byArea = new Map();
    for (const tm of taskMetrics) {
        if (!byArea.has(tm.area))
            byArea.set(tm.area, []);
        byArea.get(tm.area).push(tm);
    }
    const areas = [...byArea.entries()]
        .sort(([a], [b]) => a.localeCompare(b))
        .map(([area, tasks]) => {
        const avgPrecision = tasks.reduce((s, t) => s + t.precision, 0) / tasks.length;
        const avgRecall = tasks.reduce((s, t) => s + t.recall, 0) / tasks.length;
        const avgF1 = tasks.reduce((s, t) => s + t.f1, 0) / tasks.length;
        // Invisible docs: canonical docs never retrieved across tasks in this area
        const allRetrieved = new Set(tasks.flatMap((t) => t.retrieved));
        const allExpected = new Set(tasks.flatMap((t) => t.expected));
        const invisibleDocs = [...allExpected]
            .filter((s) => !allRetrieved.has(s))
            .sort();
        return {
            area,
            avgF1,
            avgPrecision,
            avgRecall,
            invisibleDocs,
            taskCount: tasks.length,
            tasks,
        };
    });
    // Overall averages
    const overall = {
        avgF1: areas.reduce((s, a) => s + a.avgF1, 0) / (areas.length || 1),
        avgPrecision: areas.reduce((s, a) => s + a.avgPrecision, 0) / (areas.length || 1),
        avgRecall: areas.reduce((s, a) => s + a.avgRecall, 0) / (areas.length || 1),
    };
    // Global invisible docs
    const allRetrieved = new Set(taskMetrics.flatMap((t) => t.retrieved));
    const allExpected = new Set(taskMetrics.flatMap((t) => t.expected));
    const globalInvisibleDocs = [...allExpected]
        .filter((s) => !allRetrieved.has(s))
        .sort();
    return {
        areas,
        globalInvisibleDocs,
        overall,
    };
}
/**
 * Compute retrieval metrics for a single task.
 */
export function computeTaskMetrics(taskId, area, retrieved, canonical) {
    const retrievedSet = new Set(retrieved);
    const hits = [...retrievedSet].filter((s) => canonical.has(s));
    const missed = [...canonical].filter((s) => !retrievedSet.has(s));
    const unexpected = [...retrievedSet].filter((s) => !canonical.has(s));
    const precision = retrievedSet.size > 0 ? hits.length / retrievedSet.size : 0;
    const recall = canonical.size > 0 ? hits.length / canonical.size : 0;
    const f1 = precision + recall > 0 ? (2 * precision * recall) / (precision + recall) : 0;
    return {
        area,
        expected: [...canonical],
        f1,
        hits,
        missed,
        precision,
        recall,
        retrieved: [...retrievedSet],
        taskId,
        unexpected,
    };
}
/**
 * The cross-version **document keys** (D0084) a task's canonical doc refs
 * resolve to — the canonical (expected) set behind the recall/precision/F1
 * denominators.
 *
 * Every ref is routed through the shared `resolveDocRefToDocumentKey`
 * (`@sanity/ailf-core`), the single ref→document-identity resolver, so the
 * slug-only assumption that previously dropped `_id`/`path`-only pins cannot
 * silently regrow:
 *
 * - An **id ref** contributes `baseDocumentId(ref.id)` directly — type-agnostic,
 *   so a task pinning a non-article doc only by `_id` now counts toward the
 *   canonical set, where the old `d.slug`-only read dropped it. This is the
 *   denominator-growth the fixture test exercises.
 * - A **slug ref** / **path ref** (last segment) contributes its editorial
 *   handle. This dormant loader has no run manifest, hence the empty
 *   {@link NO_MANIFEST_BRIDGE}: the editorial slug is the best available key and
 *   doubles as the display label and the axis the agent's visited slugs/URLs are
 *   compared on (AC#3). A revival with a real manifest would bridge slug→`_id`
 *   exactly as `compute-attribution-step.ts` does.
 * - A **perspective ref** contributes nothing (one-to-many fetch-time
 *   expansion; no single key from the ref alone), matching the live path.
 *
 * The axis asymmetry is deliberate: a non-article `_id` key grows the recall
 * denominator but is never "hit" by a slug-visiting agent — correct, since the
 * doc is expected yet has no slug for the agent to retrieve it by.
 *
 * Exported for unit testing (the dormant `loadCanonicalDocs` reads from disk).
 */
export function canonicalDocKeysForRefs(docs) {
    const keys = new Set();
    for (const ref of docs) {
        const idKey = resolveDocRefToDocumentKey(ref, NO_MANIFEST_BRIDGE);
        if (idKey !== undefined) {
            keys.add(idKey); // id refs (any type) → baseDocumentId(ref.id)
            continue;
        }
        // No manifest bridge → degrade a slug/path ref to its editorial handle.
        // A perspective ref (or a malformed entry) contributes no key.
        if (isSlugRef(ref)) {
            keys.add(ref.slug);
        }
        else if (isPathRef(ref)) {
            const segments = ref.path.split("/");
            keys.add(segments[segments.length - 1]);
        }
    }
    return keys;
}
/**
 * Load canonical docs from *.task.ts files in tasks/literacy/.
 * Returns a map of taskId → { docKeys: Set<string>, area: string }, where each
 * key is the document key (D0084) the ref resolves to (slug for slug/path refs).
 */
export function loadCanonicalDocs(rootDir) {
    const tasksDir = resolveVendoredSubdir(rootDir, "tasks/literacy");
    if (!existsSync(tasksDir))
        return new Map();
    const result = new Map();
    const files = discoverTsTaskFiles(tasksDir);
    for (const file of files) {
        const loaded = loadTsTaskFileSync(file);
        for (const task of loaded.tasks) {
            const t = task;
            const id = typeof t.id === "string" ? t.id : undefined;
            const area = typeof t.area === "string" ? t.area : undefined;
            if (!id || !area)
                continue;
            // Resolve context.docs to document keys (id refs included), not d.slug.
            const context = t.context;
            if (!context?.docs || !Array.isArray(context.docs))
                continue;
            const docs = context.docs.filter((d) => typeof d === "object" && d !== null);
            const docKeys = canonicalDocKeysForRefs(docs);
            if (docKeys.size > 0) {
                result.set(id, { area, docKeys });
            }
        }
    }
    return result;
}
