/**
 * Pipeline step: Mirror repo-based tasks to the Content Lake.
 *
 * When --repo-tasks-path is provided, this step upserts mirror documents
 * in the Sanity Content Lake for all repo-sourced tasks. This makes
 * repo tasks visible in Studio with full provenance tracking.
 *
 * Two paths reach this step:
 *
 *   - **Dogfood** (`taskSourceType` unset): the framework mirroring its own
 *     `.ailf/tasks/` suite. Best-effort and non-blocking — a write failure
 *     never fails the eval.
 *   - **External registration** (`taskSourceType: "repo"` + `registerTasks`):
 *     a cross-repo consumer opting in to register its submitted suite. Here a
 *     gate refusal is NOT a silent skip — the caller explicitly asked to
 *     register, so a refusal aborts the run with a structured, caller-actionable
 *     error (the same `code` / `message` / `remediation` the gateway pre-flight
 *     returns). This replaces the W0145 blanket skip, which turned the external
 *     path off entirely because it wrote false provenance and had no governance.
 *
 * The step is therefore non-optional (like `ProvenanceGateStep`): the orchestrator
 * only aborts on a *non-optional* step returning `status: "failed"`. It returns
 * `failed` ONLY on a registration refusal; every other failure mode (dogfood
 * mirror errors, transient I/O) is downgraded to a non-blocking `skipped` /
 * `success` so the long-standing "mirror failure does not block the eval"
 * contract holds for the dogfood path.
 *
 * It runs after validate and before fetch-docs so mirror documents exist
 * before evaluation begins — which is also why a registration refusal aborts
 * before any compute is spent.
 *
 * @see packages/eval/src/pipeline/mirror-repo-tasks.ts
 * @see docs/superpowers/specs/2026-06-15-external-task-registration-design.md (§1, §3, §4, §6)
 * @see docs/decisions/D0037-run-classification-and-ownership-taxonomy.md
 */
import { buildRegistrationError, resolveActiveTeam, } from "../../_vendor/ailf-core/index.js";
import { getAilfSanityClient, getSanityClient } from "../../sanity/client.js";
import { detectGitContext, findAreaTeamConflicts, gitContextFromCaller, mirrorRepoTasks, } from "../../pipeline/mirror-repo-tasks.js";
/**
 * Default task loader: read the repo-tasks directory via a fresh
 * `RepoTaskSource` and keep only literacy tasks (the only mode the mirror
 * document shape models). A fresh instance is used rather than the composite
 * task source on the context, so we mirror exactly the submitted repo suite —
 * not Content Lake tasks the composite would also surface.
 */
async function loadRepoTasksFromSource(repoTasksPath) {
    const { RepoTaskSource } = await import("../../adapters/task-sources/repo-task-source.js");
    const repoSource = new RepoTaskSource(repoTasksPath);
    return (await repoSource.loadTasks()).filter((t) => t.mode === "literacy");
}
export class MirrorRepoTasksStep {
    name = "mirror-repo-tasks";
    // Non-optional by design (same rationale as ProvenanceGateStep): an explicit
    // registration request that the gate refuses must abort the run, and the
    // orchestrator only aborts on a *non-optional* failed step. Dogfood and
    // transient failures are downgraded internally so they never fail the eval.
    optional = false;
    loadRepoTasks;
    resolveTeam;
    findConflicts;
    runMirror;
    createWriteClient;
    createEditorialClient;
    constructor(deps = {}) {
        this.loadRepoTasks = deps.loadRepoTasks ?? loadRepoTasksFromSource;
        this.resolveTeam = deps.resolveTeam ?? resolveActiveTeam;
        this.findConflicts = deps.findConflicts ?? findAreaTeamConflicts;
        this.runMirror = deps.runMirror ?? mirrorRepoTasks;
        this.createWriteClient =
            deps.createWriteClient ?? ((token) => getAilfSanityClient({ token }));
        this.createEditorialClient =
            deps.createEditorialClient ?? (() => getSanityClient());
    }
    check(_ctx) {
        // No hard preconditions. Whether to mirror, skip, or refuse is decided in
        // execute() (it returns "skipped" when there is nothing to mirror) — the
        // step is non-optional only so a registration refusal can abort the run.
        return [];
    }
    async execute(ctx) {
        const start = Date.now();
        // Skip if no repo tasks configured
        if (!ctx.config.repoTasksPath) {
            return { status: "skipped", reason: "No --repo-tasks-path provided" };
        }
        const registering = ctx.config.registerTasks === true;
        const isExternalRepoPath = ctx.config.taskSourceType === "repo";
        // Gate (replaces the W0145 blanket skip): the external inline path
        // (taskSourceType="repo") mirrors ONLY when registration was explicitly
        // requested. Without opt-in, a consumer's ephemeral inline tasks are still
        // not written to AILF's Content Lake. The dogfood path (taskSourceType
        // unset) is unaffected and proceeds unconditionally below.
        if (isExternalRepoPath && !registering) {
            return {
                status: "skipped",
                reason: 'taskSourceType="repo" without registerTasks — inline tasks are not registered',
            };
        }
        // A Content Lake write token is required either way. When registration was
        // requested, its absence is a server misconfiguration (not the caller's
        // fault) and must abort — the caller asked to register and deserves to know
        // it did not happen, rather than getting a silent skip.
        const token = process.env.AILF_REPORT_SANITY_API_TOKEN ?? process.env.SANITY_API_TOKEN;
        if (!token) {
            if (registering) {
                return {
                    status: "failed",
                    durationMs: Date.now() - start,
                    error: "registerTasks was requested but no Content Lake write token is configured (AILF_REPORT_SANITY_API_TOKEN / SANITY_API_TOKEN). This is a server misconfiguration, not a problem with your request.",
                };
            }
            return {
                status: "skipped",
                reason: "No write token available for Content Lake mirroring",
            };
        }
        // Client construction can throw on bad Sanity config. The step is
        // non-optional, so an unguarded throw here would abort even a dogfood eval
        // (runStep turns it into a failed result) — funnel it through `obstacle` so
        // the dogfood path stays best-effort.
        let client;
        try {
            client = this.createWriteClient(token);
        }
        catch (err) {
            const msg = err instanceof Error ? err.message : String(err);
            return this.obstacle(registering, start, ctx, `registerTasks was requested but the Content Lake client could not be created (${msg}). This is a server-side configuration issue, not a problem with your request.`, `Could not create Content Lake client: ${msg}`);
        }
        // Load the FULL submitted suite (everything in repoTasksPath). The
        // registered set is the whole submitted suite, never the run's scoped
        // subset — registration means "make this team's suite available", not
        // "snapshot what this run scored".
        let repoTasks;
        try {
            repoTasks = await this.loadRepoTasks(ctx.config.repoTasksPath);
        }
        catch (err) {
            const msg = err instanceof Error ? err.message : String(err);
            return this.obstacle(registering, start, ctx, `registerTasks was requested but the submitted tasks could not be loaded: ${msg}`, `Could not load repo tasks: ${msg}`);
        }
        const teamSlug = ctx.config.callerEnvelope?.owner?.team;
        // Resolve the gate and the owner-team linkage. `ownerTeam` is the resolved
        // active team whose ref newly created areas will carry.
        let ownerTeam;
        if (registering) {
            // registerTasks is a contract: every refusal below is a structured,
            // caller-actionable abort (built from the shared `buildRegistrationError`,
            // so it matches the gateway pre-flight), never a silent skip.
            if (repoTasks.length === 0) {
                return this.refuse(start, buildRegistrationError({
                    code: "registration_no_tasks",
                    team: teamSlug ?? "unknown",
                }));
            }
            // The gate's two lookups (team resolution + area-team conflicts) are I/O.
            // A genuine refusal `return`s from inside the try (not caught); only a
            // failed *query* falls to the catch, which aborts with an internal/
            // transient message rather than blaming the caller for a server issue.
            try {
                const resolved = await this.resolveTeam(teamSlug ?? "", client);
                if (resolved.status !== "active") {
                    return this.refuse(start, buildRegistrationError({
                        code: "registration_team_not_found",
                        team: teamSlug ?? "unknown",
                    }));
                }
                ownerTeam = { teamId: resolved.teamId, slug: resolved.slug };
                // Refuse rather than mis-attribute: if any in-scope area already belongs
                // to a different team, registering would silently re-own it.
                const areas = uniqueAreas(repoTasks);
                const conflicts = await this.findConflicts(client, areas, ownerTeam.teamId);
                if (conflicts.length > 0) {
                    const conflict = conflicts[0];
                    return this.refuse(start, buildRegistrationError({
                        code: "registration_area_team_conflict",
                        team: ownerTeam.slug,
                        area: conflict.area,
                        existingTeam: conflict.existingTeam,
                    }));
                }
            }
            catch (err) {
                const msg = err instanceof Error ? err.message : String(err);
                return {
                    status: "failed",
                    durationMs: Date.now() - start,
                    error: `registerTasks was requested but the AILF team registry could not be queried (${msg}). This is a transient or server-side issue, not a problem with your request; retry shortly.`,
                };
            }
        }
        else {
            // Dogfood path: mirror unconditionally, with best-effort team linkage.
            if (repoTasks.length === 0) {
                return {
                    durationMs: Date.now() - start,
                    status: "success",
                    summary: "No repo tasks to mirror",
                };
            }
            // If owner.team happens to resolve to an active team, newly created areas
            // get its ref; otherwise areas are created teamless exactly as before. No
            // team is ever required here, and an existing area is never re-teamed.
            if (teamSlug) {
                try {
                    const resolved = await this.resolveTeam(teamSlug, client);
                    if (resolved.status === "active") {
                        ownerTeam = { teamId: resolved.teamId, slug: resolved.slug };
                    }
                }
                catch (err) {
                    // Best-effort on the dogfood path — a team-lookup failure must never
                    // fail the eval. Proceed with no team ref (areas created teamless).
                    ctx.logger.warn(`Owner-team lookup failed (non-blocking; areas created teamless): ${err instanceof Error ? err.message : String(err)}`);
                }
            }
        }
        // Provenance (D0037): prefer the caller's git metadata over the runner
        // environment. The external eval runs inside the framework checkout, so
        // detectGitContext() would stamp the framework repo; callerGit yields the
        // correct origin.repo and a correctly namespaced mirror id. Fall back to
        // detectGitContext() only when callerGit is absent (the dogfood path).
        const git = ctx.config.callerGit
            ? gitContextFromCaller(ctx.config.callerGit, ctx.config.callerEnvelope)
            : await detectGitContext(ctx.config.repoTasksPath);
        ctx.logger.info(`  Mirroring ${repoTasks.length} repo task(s) from ${git.repo}@${git.branch}` +
            (ownerTeam ? ` (team: ${ownerTeam.slug})` : ""));
        try {
            const editorialClient = this.createEditorialClient();
            const result = await this.runMirror({
                client,
                editorialClient,
                git,
                ownerTeam,
                logger: ctx.logger,
                tasks: repoTasks,
            });
            // Log results
            if (result.areasCreated.length > 0) {
                ctx.logger.info(`  Auto-created feature areas: ${result.areasCreated.join(", ")}`);
            }
            if (result.unresolvedSlugs.length > 0) {
                ctx.logger.warn(`  Unresolved canonical doc slugs: ${result.unresolvedSlugs.join(", ")}`);
            }
            for (const err of result.errors) {
                ctx.logger.warn(`  Mirror error: ${err}`);
            }
            return {
                durationMs: Date.now() - start,
                status: "success",
                summary: `Mirrored ${result.upserted} task(s), skipped ${result.skipped} unchanged` +
                    (result.errors.length > 0
                        ? ` (${result.errors.length} error(s))`
                        : ""),
            };
        }
        catch (err) {
            // The write itself is best-effort even for a registration run: the gate
            // (the caller-controlled part) has already passed, and per-task write
            // failures are collected non-fatally inside mirrorRepoTasks. A thrown
            // systemic error here is logged and not allowed to fail the eval —
            // preserving the "mirror failure does not block" contract.
            const msg = err instanceof Error ? err.message : String(err);
            ctx.logger.warn(`Mirror step failed (non-blocking): ${msg}`);
            return {
                durationMs: Date.now() - start,
                status: "success",
                summary: `Mirror failed (non-blocking): ${msg}`,
            };
        }
    }
    /**
     * Turn a refused registration into a failed StepResult. The orchestrator
     * lifts a failed non-optional step into the run's
     * `failureReason { type: "step-failed", step, message }`, which
     * external-eval.yml's safety-net writes to the job `error` field and the
     * HMAC callback returns. The single string channel carries the same `code`,
     * `message`, and `remediation` the gateway pre-flight surfaces (both compose
     * the error via the shared `buildRegistrationError`).
     */
    refuse(start, error) {
        return {
            status: "failed",
            durationMs: Date.now() - start,
            error: `${error.message} ${error.remediation} [${error.code}]`,
        };
    }
    /**
     * Resolve a non-fatal mirror obstacle (couldn't build the client, couldn't
     * load tasks). On the dogfood path it is a non-blocking `skipped`; when
     * registering it aborts (`failed`) so the caller learns their explicit
     * registration request did not happen. The single place the
     * "dogfood never fails" invariant for setup failures lives.
     */
    obstacle(registering, start, ctx, failError, skipReason) {
        if (registering) {
            return {
                status: "failed",
                durationMs: Date.now() - start,
                error: failError,
            };
        }
        ctx.logger.warn(`Mirror step skipped (non-blocking): ${skipReason}`);
        return { status: "skipped", reason: skipReason };
    }
}
/**
 * The distinct feature areas a task suite references (mirrors the mirror's own
 * grouping). Empty/whitespace areas are dropped so a task with no `area` can't
 * inject a `""` into the conflict query and surface a spurious conflict against
 * a degenerate empty-areaId feature area.
 */
function uniqueAreas(tasks) {
    return [
        ...new Set(tasks.map((t) => (t.area ?? "").trim()).filter((a) => a !== "")),
    ];
}
