/**
 * Adapter: Load task definitions from the Sanity Content Lake.
 *
 * Fetches ailf.task documents via GROQ and maps them to
 * `ContentLakeAuthorableTask` — the subset of `GeneralizedTaskDefinition`
 * authorable in Studio per D0038. Today that subset is exactly the
 * literacy variant.
 *
 * The pipeline never knows which adapter loaded the tasks; the
 * `TaskSource` port widens the return type back to
 * `GeneralizedTaskDefinition[]`.
 *
 * Wired in the composition root as the default task source.
 *
 * @see packages/core/src/ports/task-source.ts — TaskSource port
 * @see docs/decisions/D0038-content-lake-authorable-task-modes.md
 */
import { buildContextDocPath } from "../../_vendor/ailf-shared/index.js";
import { filterByChangedDocs } from "./changed-docs-filter.js";
import { ContentLakeAuthorableTaskSchema } from "./repo-schemas.js";
// ---------------------------------------------------------------------------
// GROQ query — fetches ailf.task documents with resolved references
// ---------------------------------------------------------------------------
/**
 * GROQ query that projects ailf.task documents into a shape suitable for
 * mapping to LiteracyTaskDefinition.
 *
 * Key projections:
 * - area reference → dereferenced areaId string
 * - contextDocs[] → dereferenced article slugs with reason
 * - referenceSolution → title (for identification, not full content)
 *
 * Filter parameters:
 * - $areas: string[] | null — when non-null, only tasks in these areas
 * - $taskIds: string[] | null — when non-null, only these task IDs
 */
const TASKS_QUERY = /* groq */ `
*[_type == "ailf.task"
  && (
    !defined($areas)
    // Current field name
    || area->areaId.current in $areas
    // Legacy field name (pre-schema-rename documents)
    || featureArea->areaId.current in $areas
  )
  && (!defined($taskIds) || id.current in $taskIds)
  && (
    // Status-based filtering (unified — replaces execution.enabled)
    status == "active"
    || !defined(status)
    || ($includeDrafts == true && status == "draft")
    // Explicit --task targeting bypasses draft/paused (but not archived)
    || (defined($taskIds) && status != "archived")
  )
  && (!defined($tags) || count((tags)[@ in $tags]) > 0)
] | order(coalesce(area->areaId.current, featureArea->areaId.current) asc, id.current asc) {
  "taskId": id.current,
  // The coalesce on title preserves back-compat: older documents that used
  // the description field as the task label (before title was required)
  // still read cleanly. New documents have title and description as
  // distinct fields.
  "title": coalesce(title, description),
  description,
  "areaId": coalesce(area->areaId.current, featureArea->areaId.current),
  "promptText": coalesce(promptText, taskPrompt),
  status,
  docCoverage,
  "contextDocs": coalesce(contextDocs, canonicalDocs)[] {
    refType,
    "slug": doc->slug.current,
    "docRefId": doc->_id,
    "sectionSlug": doc->primarySection->slug.current,
    path,
    "docId": docId,
    perspective,
    reason
  },
  "assertions": coalesce(assertions, assert)[] {
    type, template, weight, value, threshold,
    "criteria": criteria[] {
      "id": coalesce(id.current, _key),
      "text": coalesce(text, @)
    }
  },
  rawAssert,
  baseline,
  tags,
  "referenceSolutionTitle": referenceSolution->title
}
`;
// ---------------------------------------------------------------------------
// ContentLakeTaskSource adapter
// ---------------------------------------------------------------------------
export class ContentLakeTaskSource {
    client;
    constructor(client) {
        this.client = client;
    }
    async loadTasks(filter) {
        const params = buildGroqParams(filter);
        const raw = await this.client.fetch(TASKS_QUERY, params);
        if (!Array.isArray(raw)) {
            throw new Error("ContentLakeTaskSource: GROQ query did not return an array");
        }
        const definitions = [];
        for (const entry of raw) {
            const mapped = mapToAuthorableTask(entry);
            if (!mapped)
                continue;
            // Runtime gate (W0073): every mapped task must satisfy the domain
            // schema before it flows into the pipeline. Throws loudly on drift so
            // a Studio document that strays from GeneralizedTaskDefinition
            // surfaces at load time rather than corrupting downstream state.
            const parsed = ContentLakeAuthorableTaskSchema.safeParse(mapped);
            if (!parsed.success) {
                const issues = parsed.error.issues
                    .slice(0, 5)
                    .map((i) => `  [${i.path.join(".")}]: ${i.message}`)
                    .join("\n");
                const more = parsed.error.issues.length > 5
                    ? `\n  …and ${parsed.error.issues.length - 5} more issue(s)`
                    : "";
                throw new Error(`ContentLakeTaskSource: ailf.task "${mapped.id}" failed domain ` +
                    `schema validation:\n${issues}${more}`);
            }
            definitions.push(mapped);
        }
        if (definitions.length === 0 && !filter) {
            console.warn("  ⚠️  ContentLakeTaskSource: no ailf.task documents found in the Content Lake. " +
                "Have you run the migration (Phase 3) or created tasks in Studio?");
        }
        return filterByChangedDocs(definitions, filter?.changedDocs);
    }
}
// ---------------------------------------------------------------------------
// GROQ parameter builder
// ---------------------------------------------------------------------------
function buildGroqParams(filter) {
    return {
        areas: filter?.areas && filter.areas.length > 0
            ? filter.areas.map((a) => a.toLowerCase())
            : null,
        includeDrafts: filter?.includeDrafts ?? false,
        tags: filter?.tags && filter.tags.length > 0 ? filter.tags : null,
        taskIds: filter?.taskIds && filter.taskIds.length > 0 ? filter.taskIds : null,
    };
}
// ---------------------------------------------------------------------------
// Mapping: Content Lake → LiteracyTaskDefinition
// ---------------------------------------------------------------------------
/**
 * Map a Content Lake ailf.task document to a `ContentLakeAuthorableTask`.
 *
 * Returns null if the document is missing required fields (taskId,
 * title, areaId, promptText). These are required by the
 * Studio schema, but defensive coding handles edge cases (drafts,
 * partially-created documents, etc.).
 */
export function mapToAuthorableTask(raw) {
    // Required fields — skip malformed documents
    if (!raw.taskId || !raw.title || !raw.areaId || !raw.promptText) {
        return null;
    }
    const docs = (raw.contextDocs ?? [])
        .map(mapCanonicalDocRef)
        .filter((d) => d !== null);
    const assertions = mapAssertions(raw.assertions ?? []);
    // Append raw pass-through assertions (escape hatch for arbitrary Promptfoo
    // assertion types that aren't in the curated list). These bypass template
    // resolution and flow directly into the expanded Promptfoo test case as
    // value-based assertions.
    const rawAssertions = (raw.rawAssert ?? [])
        .filter((a) => !!a.type)
        .map((a) => ({
        type: a.type,
        ...(a.value !== undefined ? { value: a.value } : {}),
        ...(a.threshold !== undefined ? { threshold: a.threshold } : {}),
    }));
    const allAssertions = [
        ...assertions,
        ...rawAssertions,
    ];
    const baseline = raw.baseline
        ? {
            ...(raw.baseline.enabled !== undefined
                ? { enabled: raw.baseline.enabled }
                : {}),
            ...(raw.baseline.rubric
                ? {
                    rubric: raw.baseline.rubric,
                }
                : {}),
        }
        : undefined;
    return {
        mode: "literacy",
        id: raw.taskId,
        title: raw.title,
        area: raw.areaId,
        prompt: { text: raw.promptText },
        context: { docs },
        assertions: allAssertions,
        docCoverage: raw.docCoverage ?? false,
        // Reference solution path is not meaningful for CL tasks — the solution
        // content lives in the Content Lake document, not on the filesystem.
        referenceSolution: "",
        ...(baseline ? { baseline } : {}),
        ...(raw.tags?.length ? { tags: raw.tags } : {}),
        ...(raw.status ? { status: raw.status } : {}),
        ...(raw.description ? { description: raw.description } : {}),
    };
}
/**
 * Map a Content Lake context doc entry to the polymorphic CanonicalDocRef.
 *
 * Uses `refType` to determine which value field to read. Falls back to
 * slug-based resolution for backward compatibility (documents created
 * before the polymorphic schema migration).
 */
function mapCanonicalDocRef(raw) {
    const reason = raw.reason ?? "";
    const refType = raw.refType ?? "slug"; // default for legacy documents (pre-id-default)
    switch (refType) {
        case "slug":
            return raw.slug ? { slug: raw.slug, reason } : null;
        case "path": {
            const path = buildContextDocPath({
                path: raw.path,
                sectionSlug: raw.sectionSlug,
                slug: raw.slug,
            });
            return path ? { path, reason } : null;
        }
        case "id": {
            // Prefer explicit docId field; fall back to doc reference _id
            const id = raw.docId || raw.docRefId || null;
            if (!id)
                return null;
            // Carry slug and derived path as optional DX annotations — single
            // source of truth in `buildContextDocPath` (@sanity/ailf-shared).
            const derivedPath = buildContextDocPath({
                sectionSlug: raw.sectionSlug,
                slug: raw.slug,
            });
            return {
                id,
                reason,
                ...(raw.slug ? { slug: raw.slug } : {}),
                ...(derivedPath ? { path: derivedPath } : {}),
            };
        }
        case "perspective":
            return raw.perspective ? { perspective: raw.perspective, reason } : null;
        default:
            // Unknown refType — try slug as fallback
            return raw.slug ? { slug: raw.slug, reason } : null;
    }
}
/**
 * Map Content Lake assertion objects to domain AssertionDefinition[].
 *
 * Content Lake assertions use the same field names as the domain type
 * (type, template, criteria, value, threshold, weight) but all fields
 * are optional in the raw response. This function normalizes them.
 */
function mapAssertions(raw) {
    return raw
        .filter((a) => !!a.type)
        .map((a) => {
        if (a.type === "llm-rubric" && a.template && a.criteria) {
            // Tighten the runtime contract: the GROQ projection's
            // `coalesce(text, @)` falls through to the entire criterion
            // element when `text` is missing, so a partial legacy criterion
            // like `{_key: "abc"}` arrives here as `{ id: "abc", text: {...} }`
            // — `text` set to the whole `@` object. Explicit type checks
            // drop those with a diagnostic, instead of letting the non-string
            // `text` propagate until the outer ContentLakeAuthorableTaskSchema
            // parse fails deep inside the assertions array (noisy diagnostic).
            return {
                criteria: a.criteria
                    .filter((c) => {
                    if (!c)
                        return false;
                    const idOk = typeof c.id === "string" && c.id.length > 0;
                    const textOk = typeof c.text === "string" && c.text.length > 0;
                    if (!idOk || !textOk) {
                        console.warn(`[ContentLakeTaskSource] dropping malformed criterion: ${JSON.stringify(c).slice(0, 100)}`);
                        return false;
                    }
                    return true;
                })
                    .map((c) => ({ id: c.id, text: c.text })),
                template: a.template,
                type: "llm-rubric",
                // Use `!= null` (loose) so we drop both `undefined` AND `null`.
                // GROQ projects missing scalar fields as `null`, but the domain
                // schema's `z.number().optional()` accepts `T | undefined`, not
                // `T | null` — a strict `!== undefined` check would forward
                // `weight: null` and trigger Zod's "Invalid input" on assertions.
                ...(a.weight != null ? { weight: a.weight } : {}),
            };
        }
        // Value-based assertion — same null-vs-undefined hazard as above.
        const result = { type: a.type };
        if (a.value != null)
            result.value = a.value;
        if (a.threshold != null)
            result.threshold = a.threshold;
        if (a.weight != null)
            result.weight = a.weight;
        return result;
    });
}
