/**
 * buildRunContext — the single code path that derives `RunContext` from
 * pipeline inputs.
 *
 * `RunContext` is the 9-field shape shared between `RunManifest.context`
 * (in GCS) and `ReportProvenance` (in Content Lake, which `extends
 * RunContext`). Routing every consumer through this function makes it
 * structurally impossible for the two to disagree: there is no second
 * code path to drift against.
 *
 * Contract test: `packages/eval/src/__tests__/run-context-parity.test.ts`
 *
 * @see docs/decisions/D0032-run-anchored-artifact-store.md (§ Move 5 — Drift Prevention)
 */
import { execSync } from "node:child_process";
import { createRequire } from "node:module";
import * as os from "node:os";
import { PLACEHOLDER_OWNER_TEAM, } from "../_vendor/ailf-core/index.js";
import { isRunClassification, } from "../_vendor/ailf-shared/index.js";
import { ConsoleLogger } from "../adapters/loggers/index.js";
import { tryLoadConfigFile } from "./compiler/config-loader.js";
const requireFromHere = createRequire(import.meta.url);
/**
 * Derive `RunContext` from pipeline inputs. The only construction path.
 *
 * Both `FinalizeRunStep` (via `RunManifest.context`) and
 * `PublishReportStep` (via `ReportProvenance`) call this function — the
 * former directly, the latter transitively through `buildProvenance`.
 */
export function buildRunContext(input) {
    const log = input.logger ?? new ConsoleLogger();
    const models = loadModelsConfig(input.rootDir, log);
    // Cross-repo evaluations: prefer explicit caller git metadata over
    // CI env vars (which always reflect the AILF core repo).
    const git = input.callerGit
        ? {
            branch: input.callerGit.branch ?? "unknown",
            prNumber: input.callerGit.prNumber,
            repo: input.callerGit.repo,
            sha: input.callerGit.sha ?? "unknown",
        }
        : detectGitMetadata();
    const trigger = detectTrigger();
    // D0037: caller envelope (from PipelineRequest) takes precedence over
    // server-env detection — same pattern as `callerGit` for identity
    // preservation across the --remote boundary.
    const envelope = input.callerEnvelope;
    const classification = envelope?.classification ?? detectClassification(log);
    // W0143: a caller-supplied owner whose team is the init-template
    // placeholder is treated as "user hasn't filled this in yet" — drop the
    // envelope owner and fall through to env detection (which performs the
    // same sanitization on AILF_OWNER_TEAM). Avoids persisting the literal
    // placeholder verbatim on the report.
    const sanitizedEnvelopeOwner = envelope?.owner && envelope.owner.team !== PLACEHOLDER_OWNER_TEAM
        ? envelope.owner
        : undefined;
    const owner = sanitizedEnvelopeOwner ?? detectOwner();
    const executor = envelope?.executor ?? detectExecutor();
    // `tool` and `host` are server-environment facts — they always reflect
    // where this pipeline is actually running, never what a caller claimed.
    // Callers cannot override these via the envelope and the wire schema
    // explicitly doesn't carry them (see pipeline-request.ts).
    const tool = detectTool(log);
    const host = detectHost();
    const labels = envelope?.labels ?? detectLabels();
    const purpose = envelope?.purpose ?? (process.env.AILF_PURPOSE?.trim() || undefined);
    // Non-literacy modes (agent-harness, mcp-server, etc.) don't use the
    // config/models.ts model matrix — listing those models would be
    // misleading. Only include them for literacy mode where they're the
    // actual eval targets.
    //
    // When `PipelineRequest.models` pinned a subset, filter here too so
    // `provenance.models` matches what actually ran (W0281). Without this
    // the report would advertise the full cohort even though only the
    // requested subset reached the LLMs.
    const requestedSet = input.requestedModelIds?.length
        ? new Set(input.requestedModelIds)
        : undefined;
    const evaluatedModels = input.mode === "literacy"
        ? models.models
            .filter((m) => !requestedSet || requestedSet.has(m.id))
            .map((m) => ({ id: m.id, label: m.label }))
        : [];
    return {
        areas: input.areas,
        classification,
        evalFingerprint: input.evalFingerprint,
        executor,
        git,
        graderModel: models.grader.id,
        host,
        labels,
        lineage: input.lineage,
        mode: input.mode,
        models: evaluatedModels,
        owner,
        purpose,
        source: {
            baseUrl: input.source.baseUrl,
            dataset: input.source.dataset,
            name: input.source.name,
            perspective: input.source.perspective,
            projectId: input.source.projectId,
        },
        taskIds: input.taskIds,
        tool,
        trigger,
        variant: input.mode === "literacy" ? input.variant : undefined,
    };
}
// ---------------------------------------------------------------------------
// Environment-derived context
// ---------------------------------------------------------------------------
/**
 * Extract git metadata from GitHub Actions environment variables.
 * Returns undefined when not running in CI.
 */
function detectGitMetadata() {
    const repo = process.env.GITHUB_REPOSITORY;
    if (!repo)
        return undefined;
    const sha = process.env.GITHUB_SHA ?? "unknown";
    const ref = process.env.GITHUB_REF ?? "";
    // Extract branch name from ref (refs/heads/main → main)
    const branch = ref.startsWith("refs/heads/")
        ? ref.slice("refs/heads/".length)
        : ref.startsWith("refs/pull/")
            ? `pr-${ref.split("/")[2]}`
            : ref;
    // Extract PR number from GITHUB_REF (refs/pull/123/merge)
    const prMatch = ref.match(/^refs\/pull\/(\d+)\//);
    const prNumber = prMatch ? parseInt(prMatch[1], 10) : undefined;
    return { branch, prNumber, repo, sha };
}
/**
 * Infer what triggered this evaluation from environment variables.
 *
 * Detection order:
 * 1. AILF_TRIGGER_TYPE — explicit override (for custom integrations)
 * 2. GITHUB_EVENT_NAME === "schedule" — cron-triggered
 * 3. GITHUB_EVENT_NAME === "repository_dispatch" — cross-repo trigger
 * 4. GITHUB_ACTIONS === "true" — CI-triggered
 * 5. Default: manual
 */
function detectTrigger() {
    const explicit = process.env.AILF_TRIGGER_TYPE;
    if (explicit === "scheduled") {
        return {
            schedule: process.env.AILF_SCHEDULE ?? "unknown",
            type: "scheduled",
        };
    }
    if (explicit === "webhook") {
        return {
            documentId: process.env.AILF_WEBHOOK_DOCUMENT_ID,
            source: process.env.AILF_WEBHOOK_SOURCE ?? "unknown",
            type: "webhook",
        };
    }
    // GitHub Actions context
    const eventName = process.env.GITHUB_EVENT_NAME;
    if (eventName === "schedule") {
        return {
            schedule: process.env.GITHUB_SCHEDULE ?? "unknown",
            type: "scheduled",
        };
    }
    if (eventName === "repository_dispatch") {
        return {
            callerRef: process.env.GITHUB_REF,
            // Note: callerRepo here is a fallback. The accurate caller repo
            // comes from callerGit (injected into the PipelineRequest payload).
            // GITHUB_REPOSITORY_OWNER_ID is just the org ID, not owner/repo.
            callerRepo: process.env.GITHUB_REPOSITORY_OWNER_ID ?? "unknown",
            type: "cross-repo",
        };
    }
    if (process.env.GITHUB_ACTIONS === "true") {
        return {
            runId: process.env.GITHUB_RUN_ID ?? "unknown",
            type: "ci",
            workflow: process.env.GITHUB_WORKFLOW ?? "unknown",
        };
    }
    return { type: "manual" };
}
// ---------------------------------------------------------------------------
// Classification, owner, executor, labels, tool, host detection (D0037)
// ---------------------------------------------------------------------------
/**
 * Resolve `classification` from `AILF_CLASSIFICATION`, validated against
 * the closed enum. Defaults to `"adhoc"` so unannotated runs never leak
 * into the canonical `"official"` series.
 */
export function detectClassification(log) {
    const raw = process.env.AILF_CLASSIFICATION?.trim();
    if (!raw)
        return "adhoc";
    if (isRunClassification(raw))
        return raw;
    log.warn(`AILF_CLASSIFICATION="${raw}" is not a recognized value; defaulting to "adhoc"`);
    return "adhoc";
}
/**
 * Resolve `owner` from `AILF_OWNER_TEAM` (+ optional
 * `AILF_OWNER_INDIVIDUAL`). `team` is free-form; default is `"unknown"`.
 *
 * The init-template placeholder (`PLACEHOLDER_OWNER_TEAM`) is treated as
 * if the env var were unset — consumers that haven't filled it in yet
 * end up with `team: "unknown"` instead of the literal placeholder being
 * persisted onto the report (W0143).
 */
export function detectOwner() {
    const rawTeam = process.env.AILF_OWNER_TEAM?.trim();
    const team = rawTeam && rawTeam !== PLACEHOLDER_OWNER_TEAM ? rawTeam : "unknown";
    const individual = process.env.AILF_OWNER_INDIVIDUAL?.trim() || undefined;
    return individual ? { individual, team } : { team };
}
/**
 * Detect who/what invoked the run.
 *
 * Priority:
 * 1. GitHub Actions context → `{ type: "system", name: "github-actions", ... }`
 * 2. CLI context → `{ type: "user", surface: "cli", ... }` with git-config
 *    or OS username fallback. Email capture gated by
 *    `AILF_CAPTURE_EMAIL` (default on; set `0` to opt out).
 *
 * Every identity field is optional — missing git, containers, or masked
 * env vars must never block a run.
 */
export function detectExecutor() {
    if (process.env.GITHUB_ACTIONS === "true") {
        return {
            name: "github-actions",
            runId: process.env.GITHUB_RUN_ID?.trim() || undefined,
            type: "system",
            workflow: process.env.GITHUB_WORKFLOW?.trim() || undefined,
        };
    }
    const surface = resolveExecutorSurface();
    const githubActor = process.env.GITHUB_ACTOR?.trim() || undefined;
    const name = detectGitConfig("user.name") ?? githubActor ?? osUsername() ?? undefined;
    const email = shouldCaptureEmail() ? detectGitConfig("user.email") : undefined;
    const exec = { surface, type: "user" };
    if (name)
        exec.name = name;
    if (email)
        exec.email = email;
    if (githubActor)
        exec.githubActor = githubActor;
    return exec;
}
function resolveExecutorSurface() {
    const explicit = process.env.AILF_EXECUTOR_SURFACE?.trim();
    if (explicit === "cli" || explicit === "studio" || explicit === "api") {
        return explicit;
    }
    return "cli";
}
function shouldCaptureEmail() {
    const raw = process.env.AILF_CAPTURE_EMAIL?.trim().toLowerCase();
    if (raw === "0" || raw === "false" || raw === "no")
        return false;
    return true;
}
function detectGitConfig(key) {
    try {
        const value = execSync(`git config --get ${key}`, {
            encoding: "utf8",
            stdio: ["ignore", "pipe", "ignore"],
            timeout: 500,
        }).trim();
        return value || undefined;
    }
    catch {
        return undefined;
    }
}
function osUsername() {
    try {
        return os.userInfo().username || undefined;
    }
    catch {
        return undefined;
    }
}
/**
 * Parse `AILF_LABELS` (comma-separated) into a trimmed unique list.
 * Returns undefined when empty so absent labels stay absent.
 */
function detectLabels() {
    const raw = process.env.AILF_LABELS;
    if (!raw)
        return undefined;
    const labels = raw
        .split(",")
        .map((label) => label.trim())
        .filter(Boolean);
    if (labels.length === 0)
        return undefined;
    return Array.from(new Set(labels));
}
/**
 * Resolve `tool` — which AILF/Node ran the eval. Captured on every new
 * run so cross-version trend comparisons can isolate framework changes
 * from doc changes.
 */
export function detectTool(log) {
    const nodeVersion = process.version;
    let ailfVersion;
    try {
        const pkg = requireFromHere("../../package.json");
        ailfVersion = pkg.version;
    }
    catch (err) {
        log.warn(`Could not read @sanity/ailf package.json for tool.ailfVersion: ${err instanceof Error ? err.message : String(err)}`);
    }
    return { ailfVersion: ailfVersion ?? "unknown", nodeVersion };
}
/**
 * Resolve `host` — platform + arch + CI provider. Hostname is
 * intentionally excluded (leaks identity without filtering benefit).
 */
export function detectHost() {
    const ci = process.env.GITHUB_ACTIONS === "true" ? "github-actions" : undefined;
    const host = { arch: os.arch(), platform: os.platform() };
    if (ci)
        host.ci = ci;
    return host;
}
// ---------------------------------------------------------------------------
// Model config loading
// ---------------------------------------------------------------------------
/**
 * Load config/models to extract model list and grader info.
 * Falls back to a minimal config if the file can't be read.
 */
function loadModelsConfig(rootDir, log) {
    const result = tryLoadConfigFile("models", rootDir);
    if (result)
        return result.data;
    log.warn("Could not read config/models for run context");
    return {
        defaults: {},
        grader: { id: "unknown" },
        models: [],
    };
}
