/**
 * pipeline/checks.ts
 *
 * Pre/postcondition checking for pipeline steps.
 * Each function verifies filesystem or environment state and returns
 * ValidationIssue[] — an empty array means all checks passed.
 */
import { config as loadEnv } from "dotenv";
import { existsSync, readFileSync, statSync } from "fs";
import { join, resolve } from "path";
import { findExplicitDotenvArg } from "../lib/dotenv-resolution.js";
import { configFileForMode } from "./eval-constants.js";
// ---------------------------------------------------------------------------
// Precondition: contexts exist for each feature area
// ---------------------------------------------------------------------------
/**
 * Check that `contexts/canonical/<taskId>.md` exists and is non-empty
 * for every task ID.
 */
export function checkCanonicalContextsExist(rootDir, taskIds) {
    const issues = [];
    const canonicalDir = resolve(rootDir, "contexts", "canonical");
    for (const taskId of taskIds) {
        const filePath = join(canonicalDir, `${taskId}.md`);
        if (!existsSync(filePath)) {
            issues.push({
                message: `Missing canonical context for task "${taskId}". Run \`pnpm fetch-docs\` to generate it.`,
                path: filePath,
                severity: "error",
                source: "checkCanonicalContextsExist",
            });
            continue;
        }
        const stat = statSync(filePath);
        if (stat.size === 0) {
            issues.push({
                message: `Canonical context for task "${taskId}" is empty. Re-run \`pnpm fetch-docs\`.`,
                path: filePath,
                severity: "error",
                source: "checkCanonicalContextsExist",
            });
        }
    }
    return issues;
}
// ---------------------------------------------------------------------------
// Precondition: canonical context files exist per task
// ---------------------------------------------------------------------------
/**
 * Check that `contexts/<area>.md` exists and is non-empty for every area.
 * This is the precondition for the eval step — it cannot run without docs.
 */
export function checkContextsExist(rootDir, areas) {
    const issues = [];
    const contextsDir = resolve(rootDir, "contexts");
    for (const area of areas) {
        const filePath = join(contextsDir, `${area}.md`);
        if (!existsSync(filePath)) {
            issues.push({
                message: `Missing context file for area "${area}". Run \`pnpm fetch-docs\` to generate it.`,
                path: filePath,
                severity: "error",
                source: "checkContextsExist",
            });
            continue;
        }
        const stat = statSync(filePath);
        if (stat.size === 0) {
            issues.push({
                message: `Context file for area "${area}" is empty. Re-run \`pnpm fetch-docs\`.`,
                path: filePath,
                severity: "error",
                source: "checkContextsExist",
            });
        }
    }
    return issues;
}
// ---------------------------------------------------------------------------
// Postcondition: eval results exist and are valid JSON
// ---------------------------------------------------------------------------
/**
 * Check that required environment variables are set.
 *
 * Loads the resolved `.env` file first (with override, matching the dotenv
 * CLI `-o` flag used by other scripts), then checks for required keys. The
 * resolution order mirrors `cli.ts`'s `resolveEnvPath()` so a `--dotenv
 * <path>` argument on the parent CLI invocation isn't silently clobbered
 * here. Without this, a Tier 2 test that uses `--dotenv` to override
 * tenant-pointing vars (e.g. `AILF_GCS_ARTIFACT_BUCKET`,
 * `GOOGLE_APPLICATION_CREDENTIALS`) gets its overrides reverted to the
 * repo `.env` values when this function runs as part of the validate
 * step. (W0138 Slice 2 surface — see `gcs-pipeline-replay-roundtrip.test.ts`.)
 */
export function checkEnvironment(rootDir) {
    const issues = [];
    // Load the active .env so we see the same vars as dotenv -e <path> -o.
    // Resolution: explicit --dotenv arg wins, then the repo-root .env.
    const envPath = findExplicitDotenvArg() ?? resolve(rootDir, "..", "..", ".env");
    if (existsSync(envPath)) {
        loadEnv({ override: true, path: envPath });
    }
    if (!process.env.OPENAI_API_KEY) {
        issues.push({
            message: "OPENAI_API_KEY is not set. Ensure it is defined in the root .env file.",
            severity: "warning",
            source: "checkEnvironment",
        });
    }
    if (!process.env.SANITY_API_TOKEN) {
        issues.push({
            message: "SANITY_API_TOKEN is not set. It is needed for 'turbo fetch-docs'. Ensure it is defined in the root .env file.",
            severity: "warning",
            source: "checkEnvironment",
        });
    }
    return issues;
}
// ---------------------------------------------------------------------------
// Postcondition: score summary is valid
// ---------------------------------------------------------------------------
/**
 * Check that the generated promptfoo config for a given mode exists.
 *
 * When `mode` is provided, checks only for that mode's config file
 * (e.g. `promptfooconfig.agent-harness.yaml` for mode `"agent-harness"`).
 *
 * When `mode` is omitted, falls back to the legacy literacy check:
 * baseline `promptfooconfig.yaml` (required) plus optional observed/agentic.
 */
export function checkGeneratedConfigsExist(rootDir, mode) {
    const issues = [];
    if (mode) {
        const configName = configFileForMode(mode);
        const configPath = resolve(rootDir, configName);
        if (!existsSync(configPath)) {
            issues.push({
                message: `Config '${configName}' not found for mode '${mode}'. Run the pipeline to generate it.`,
                path: configPath,
                severity: "error",
                source: "checkGeneratedConfigsExist",
            });
        }
        return issues;
    }
    // Legacy literacy check: baseline required, observed/agentic optional
    const baselinePath = resolve(rootDir, "promptfooconfig.yaml");
    if (!existsSync(baselinePath)) {
        issues.push({
            message: "Baseline config 'promptfooconfig.yaml' not found. Run the pipeline to generate it.",
            path: baselinePath,
            severity: "error",
            source: "checkGeneratedConfigsExist",
        });
    }
    const optionalConfigs = [
        "promptfooconfig.observed.yaml",
        "promptfooconfig.agentic.yaml",
    ];
    for (const name of optionalConfigs) {
        const configPath = resolve(rootDir, name);
        if (!existsSync(configPath)) {
            issues.push({
                message: `Optional config \`${name}\` not found. Run the pipeline to generate it.`,
                path: configPath,
                severity: "warning",
                source: "checkGeneratedConfigsExist",
            });
        }
    }
    return issues;
}
// ---------------------------------------------------------------------------
// Precondition: generated promptfoo configs exist
// ---------------------------------------------------------------------------
/**
 * Check that the eval results JSON file exists, is valid JSON, and contains
 * a `results` array.
 */
export function checkResultsExist(rootDir, resultsPath) {
    const issues = [];
    const filePath = resolve(rootDir, resultsPath ?? join("results", "latest", "eval-results.json"));
    if (!existsSync(filePath)) {
        issues.push({
            message: "Eval results file not found. Run 'pnpm eval' first.",
            path: filePath,
            severity: "error",
            source: "checkResultsExist",
        });
        return issues;
    }
    let parsed;
    try {
        const raw = readFileSync(filePath, "utf-8");
        parsed = JSON.parse(raw);
    }
    catch (err) {
        issues.push({
            message: `Eval results file is not valid JSON: ${err instanceof Error ? err.message : String(err)}`,
            path: filePath,
            severity: "error",
            source: "checkResultsExist",
        });
        return issues;
    }
    if (typeof parsed !== "object" || parsed === null || !("results" in parsed)) {
        issues.push({
            message: 'Eval results file is missing the "results" key.',
            path: filePath,
            severity: "error",
            source: "checkResultsExist",
        });
        return issues;
    }
    // Promptfoo wraps results in: { results: { results: [...], stats: {...} } }
    const outer = parsed.results;
    const resultsArray = Array.isArray(outer)
        ? outer
        : typeof outer === "object" &&
            outer !== null &&
            "results" in outer &&
            Array.isArray(outer.results)
            ? outer.results
            : null;
    if (!resultsArray) {
        issues.push({
            message: 'The "results" key in eval results is not an array (or results.results).',
            path: filePath,
            severity: "error",
            source: "checkResultsExist",
        });
    }
    return issues;
}
// ---------------------------------------------------------------------------
// Environment variable checks
// ---------------------------------------------------------------------------
/**
 * Check that `results/latest/score-summary.json` exists, parses as JSON,
 * contains feature area scores, and no scores are NaN or out of 0–100.
 */
export function checkScoreSummaryValid(rootDir) {
    const issues = [];
    const filePath = resolve(rootDir, "results", "latest", "score-summary.json");
    if (!existsSync(filePath)) {
        issues.push({
            message: "Score summary not found. Run 'pnpm report' to generate it.",
            path: filePath,
            severity: "error",
            source: "checkScoreSummaryValid",
        });
        return issues;
    }
    let parsed;
    try {
        const raw = readFileSync(filePath, "utf-8");
        parsed = JSON.parse(raw);
    }
    catch (err) {
        issues.push({
            message: `Score summary is not valid JSON: ${err instanceof Error ? err.message : String(err)}`,
            path: filePath,
            severity: "error",
            source: "checkScoreSummaryValid",
        });
        return issues;
    }
    if (typeof parsed !== "object" || parsed === null) {
        issues.push({
            message: "Score summary is not a JSON object.",
            path: filePath,
            severity: "error",
            source: "checkScoreSummaryValid",
        });
        return issues;
    }
    const record = parsed;
    const scoreKeys = Object.keys(record);
    if (scoreKeys.length === 0) {
        issues.push({
            message: "Score summary contains no feature area scores.",
            path: filePath,
            severity: "error",
            source: "checkScoreSummaryValid",
        });
        return issues;
    }
    for (const key of scoreKeys) {
        const value = record[key];
        if (typeof value === "number") {
            if (Number.isNaN(value)) {
                issues.push({
                    message: `Score for "${key}" is NaN.`,
                    path: filePath,
                    severity: "error",
                    source: "checkScoreSummaryValid",
                });
            }
            else if (value < 0 || value > 100) {
                issues.push({
                    message: `Score for "${key}" is ${value}, which is outside the 0–100 range.`,
                    path: filePath,
                    severity: "warning",
                    source: "checkScoreSummaryValid",
                });
            }
        }
    }
    return issues;
}
