import { readFile, stat } from "node:fs/promises";
import { extname } from "node:path";
import { supportsSpawnMode } from "@poe-code/agent-spawn/configs";
import { SPAWN_MODES } from "@poe-code/agent-spawn/types";
import { hasOwnErrorCode } from "../error-codes.js";
import { extractBlock } from "../loader/extract-block.js";
import { splitFrontmatter } from "../loader/frontmatter.js";
import { lint } from "../lint.js";
import { createLintModulesFromRuntimeRegistry } from "../lint/runtime-modules.js";
import { parseModule } from "../parse/parser.js";
import { run } from "../run.js";
function getOwnEntry(record, key) {
    return Object.prototype.hasOwnProperty.call(record, key) ? record[key] : undefined;
}
function createHarnessMeta(filepath, frontmatter) {
    return {
        filepath,
        kind: getOwnEntry(frontmatter, "kind"),
        version: getOwnEntry(frontmatter, "version")
    };
}
function assertFrontmatterAgentModes(frontmatter) {
    const agents = getOwnEntry(frontmatter, "agents");
    if (typeof agents !== "object" || agents === null || Array.isArray(agents)) {
        return;
    }
    for (const [name, definition] of Object.entries(agents)) {
        if (typeof definition !== "object" || definition === null || Array.isArray(definition)) {
            continue;
        }
        const record = definition;
        const agent = getOwnEntry(record, "agent");
        const mode = getOwnEntry(record, "mode");
        if (typeof agent !== "string" || typeof mode !== "string") {
            continue;
        }
        // Malformed mode strings are reported by the agent module's own validation.
        if (!SPAWN_MODES.includes(mode)) {
            continue;
        }
        if (!supportsSpawnMode(agent, mode)) {
            throw new Error(`Harness agent "${name}": agent "${agent}" does not support mode "${mode}".`);
        }
    }
}
export class LintError extends Error {
    diagnostics;
    constructor(diagnostics) {
        super(formatLintErrorMessage(diagnostics));
        this.name = "LintError";
        this.diagnostics = diagnostics;
    }
}
export async function runHarness(filepath, options) {
    const rawSource = stripByteOrderMark(await readHarnessFile(filepath));
    const { executableSource, frontmatter, isRawScript } = loadExecutableSource(filepath, rawSource);
    assertFrontmatterAgentModes(frontmatter);
    const meta = createHarnessMeta(filepath, frontmatter);
    const modules = excludeHarnessModule(options.modulesFor(frontmatter, meta), isRawScript);
    const diagnostics = lint(executableSource, {
        filename: filepath,
        modules: createLintModulesFromRuntimeRegistry(modules)
    });
    const lintErrors = diagnostics.filter((diagnostic) => diagnostic.severity === "error");
    if (lintErrors.length > 0) {
        throw new LintError(lintErrors);
    }
    return runHarnessSource(executableSource, {
        entryPointArgs: hasDefaultExport(executableSource, filepath) ? [] : undefined,
        filename: filepath,
        modules,
        otelSink: options.otelSink,
        signal: options.signal,
        snapshotBackend: options.snapshotBackend,
        snapshotIntervalMs: options.snapshotIntervalMs,
        snapshotPath: options.snapshotPath
    });
}
export async function runHarnessPair(filepath, options) {
    const pair = resolveHarnessPair(filepath);
    const [rawMarkdown, rawScript] = await Promise.all([
        readHarnessFile(pair.markdownPath),
        readHarnessFile(pair.scriptPath)
    ]);
    const { frontmatter, body } = splitFrontmatter(stripByteOrderMark(rawMarkdown));
    assertFrontmatterAgentModes(frontmatter);
    const executableSource = stripByteOrderMark(rawScript);
    const meta = createHarnessMeta(pair.markdownPath, frontmatter);
    const modules = options.modulesFor(frontmatter, meta);
    const diagnostics = lint(executableSource, {
        allowedExportNames: ["schema"],
        defaultExport: {
            parameters: ["frontmatter"],
            required: true
        },
        filename: pair.scriptPath,
        frontmatterFields: Object.keys(frontmatter),
        modules: createLintModulesFromRuntimeRegistry(modules)
    });
    const lintErrors = diagnostics.filter((diagnostic) => diagnostic.severity === "error");
    if (lintErrors.length > 0) {
        throw new LintError(lintErrors);
    }
    return runHarnessSource(executableSource, {
        entryPointArgs: [frontmatter],
        filename: pair.scriptPath,
        importMeta: {
            body,
            filepath: pair.markdownPath,
            kind: meta.kind,
            version: meta.version
        },
        modules,
        otelSink: options.otelSink,
        signal: options.signal,
        snapshotBackend: options.snapshotBackend,
        snapshotIntervalMs: options.snapshotIntervalMs,
        snapshotPath: options.snapshotPath
    });
}
async function readHarnessFile(filepath) {
    try {
        const stats = await stat(filepath);
        if (!stats.isFile()) {
            throw new Error(`Harness path must point to a file: ${filepath}`);
        }
        return await readFile(filepath, "utf8");
    }
    catch (error) {
        if (hasOwnErrorCode(error, "ENOENT") || hasOwnErrorCode(error, "ENOTDIR")) {
            throw new Error(`Harness file not found: ${filepath}`);
        }
        throw error;
    }
}
function loadExecutableSource(filepath, source) {
    if (isRawScriptPath(filepath)) {
        if (source.length === 0) {
            throw new Error(`No code block found in empty harness file: ${filepath}`);
        }
        return {
            executableSource: source,
            frontmatter: {},
            isRawScript: true
        };
    }
    const { frontmatter, body } = splitFrontmatter(source);
    if (source.length === 0 || body.trim().length === 0) {
        throw new Error(`No code block found in empty harness file: ${filepath}`);
    }
    const { source: executableBlock, lineOffset } = extractBlock(body);
    const absoluteLineOffset = countLineBreaks(source.slice(0, source.length - body.length)) + lineOffset;
    return {
        executableSource: createLineOffsetSource(stripHashbang(executableBlock), absoluteLineOffset),
        frontmatter,
        isRawScript: false
    };
}
function resolveHarnessPair(filepath) {
    const extension = extname(filepath);
    const basename = extension.length === 0 ? filepath : filepath.slice(0, -extension.length);
    if (isRawScriptExtension(extension)) {
        return {
            markdownPath: `${basename}.md`,
            scriptPath: filepath
        };
    }
    return {
        markdownPath: filepath,
        scriptPath: `${basename}.ajs`
    };
}
function isRawScriptPath(filepath) {
    return isRawScriptExtension(extname(filepath));
}
function isRawScriptExtension(extension) {
    return extension === ".safejs" || extension === ".ajs";
}
async function runHarnessSource(executableSource, options) {
    try {
        return await run(executableSource, options);
    }
    catch (error) {
        if (isSetupError(error)) {
            throw error;
        }
        return {
            ...(isAbortError(error) ? { aborted: true } : {}),
            error,
            ok: false
        };
    }
}
function isSetupError(error) {
    return error instanceof Error && error.name === "ParseError";
}
function isAbortError(error) {
    if (error instanceof Error) {
        return error.name === "AbortError";
    }
    return (typeof error === "object" &&
        error !== null &&
        Object.prototype.hasOwnProperty.call(error, "name") &&
        error.name === "AbortError");
}
function hasDefaultExport(source, filename) {
    return parseModule(source, filename).body.some((statement) => statement.type === "ExportDefaultDeclaration");
}
function excludeHarnessModule(modules, isRawScript) {
    if (!isRawScript) {
        return modules;
    }
    if (modules instanceof Map) {
        const rawModules = new Map(modules);
        rawModules.delete("harness");
        return rawModules;
    }
    const rawModules = {};
    for (const [moduleName, moduleExports] of Object.entries(modules)) {
        if (moduleName !== "harness") {
            rawModules[moduleName] = moduleExports;
        }
    }
    return rawModules;
}
function stripByteOrderMark(source) {
    return source.startsWith("\uFEFF") ? source.slice(1) : source;
}
function stripHashbang(source) {
    if (!source.startsWith("#!")) {
        return source;
    }
    const lineBreakIndex = source.indexOf("\n");
    if (lineBreakIndex === -1) {
        return "";
    }
    return source.slice(lineBreakIndex + 1);
}
function countLineBreaks(source) {
    let count = 0;
    for (const character of source) {
        if (character === "\n") {
            count += 1;
        }
    }
    return count;
}
function createLineOffsetSource(source, lineOffset) {
    return `${"\n".repeat(Math.max(lineOffset, 0))}${source}`;
}
function formatLintErrorMessage(diagnostics) {
    if (diagnostics.length === 0) {
        return "Script lint failed.";
    }
    return diagnostics
        .map((diagnostic) => `${diagnostic.filename}:${diagnostic.line}:${diagnostic.column} ${diagnostic.code} ${diagnostic.message}`)
        .join("\n");
}
