"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.runEventHooksForPublications = runEventHooksForPublications;
exports.loadResolvedEventHooks = loadResolvedEventHooks;
exports.runSingleEventHook = runSingleEventHook;
const node_crypto_1 = require("node:crypto");
const promises_1 = require("node:fs/promises");
const node_path_1 = __importDefault(require("node:path"));
const command_1 = require("../core/command");
const tracked_project_1 = require("../core/tracked-project");
const issue_logger_1 = require("../run/issue-logger");
const workflow_labels_1 = require("../platform/ticket-tracker/workflow-labels");
const runtime_1 = require("../worker/runtime");
const publisher_events_1 = require("../workflows/publisher-events");
const event_hook_contract_1 = require("../worker/event-hook-contract");
const config_1 = require("./config");
const ledger_1 = require("./ledger");
const storage_1 = require("./storage");
const EVENT_HOOK_TIMEOUT_MS = 60_000;
async function runEventHooksForPublications(input) {
    const appliedPublications = input.publications.filter((record) => record.result.publishStatus === "applied");
    if (appliedPublications.length === 0) {
        return;
    }
    let hooks;
    try {
        hooks = await loadResolvedEventHooks(input);
    }
    catch (error) {
        await logHookSetupFailure({
            ...input,
            publications: appliedPublications,
            summary: `Event hook setup failed: ${formatError(error)}`,
        });
        return;
    }
    if (hooks.length === 0) {
        return;
    }
    let worker = input.worker;
    for (const record of appliedPublications) {
        const recordLogger = resolvePublicationLogger({
            logger: input.logger,
            config: input.config,
            record,
            homeDirectory: input.homeDirectory,
        });
        let tokens;
        try {
            tokens = buildEventHookTriggerTokens(input.config, record);
        }
        catch (error) {
            await appendHookLog(recordLogger, [
                "status: failed",
                `summary: Event hook trigger matching failed: ${formatError(error)}`,
            ]);
            continue;
        }
        for (const hook of hooks) {
            try {
                const matchedTriggers = hook.on.filter((trigger) => tokens.has(trigger));
                if (matchedTriggers.length === 0) {
                    continue;
                }
                const payload = buildEventHookPayload({
                    config: input.config,
                    hook,
                    matchedTriggers,
                    publication: record,
                    occurredAt: input.now ?? new Date(),
                });
                const logger = resolveEventHookLogger({
                    logger: input.logger,
                    config: input.config,
                    payload,
                    homeDirectory: input.homeDirectory,
                });
                let alreadyHandled = false;
                try {
                    alreadyHandled = await (0, ledger_1.hasEventHookLedgerEntry)({
                        target: ledgerTargetForHook(hook, input.config),
                        eventId: payload.eventId,
                        hookPath: ledgerHookPath(hook),
                        ...optionalHomeDirectory(input.homeDirectory),
                    });
                }
                catch (error) {
                    await appendHookLog(logger, [
                        `eventId: ${payload.eventId}`,
                        `hook: ${hook.path}`,
                        "status: failed",
                        `summary: Ledger read failed before hook execution: ${formatError(error)}`,
                    ]);
                    continue;
                }
                if (alreadyHandled) {
                    await appendHookLog(logger, [
                        `eventId: ${payload.eventId}`,
                        `hook: ${hook.path}`,
                        "status: skipped_duplicate",
                    ]);
                    continue;
                }
                await executeResolvedEventHook({
                    ...input,
                    hook,
                    payload,
                    logger,
                    getWorker: async () => {
                        worker ??= await (0, runtime_1.createConfiguredWorkerRuntime)(input.config, {
                            ...optionalHomeDirectory(input.homeDirectory),
                        });
                        return worker;
                    },
                });
            }
            catch (error) {
                await appendHookLog(recordLogger, [
                    `hook: ${hook.path}`,
                    "status: failed",
                    `summary: Event hook failed before execution: ${formatError(error)}`,
                ]);
            }
        }
    }
}
async function loadResolvedEventHooks(input) {
    const repoHooks = input.config.eventHooks.map((hook) => resolveEventHookConfig({
        hook,
        source: "repo",
        baseDir: input.repoRoot,
        allowAbsolutePath: false,
        ...optionalHomeDirectory(input.homeDirectory),
    }));
    const user = (await (0, storage_1.loadUserEventHookEntries)(input)).map((entry) => resolveEventHookConfig({
        hook: entry.hook,
        source: entry.source,
        baseDir: entry.baseDirectory,
        allowAbsolutePath: false,
        ...optionalHomeDirectory(input.homeDirectory),
    }));
    const all = [...repoHooks, ...user];
    const seen = new Set();
    for (const hook of all) {
        const duplicateKey = node_path_1.default.normalize(hook.absolutePath);
        if (seen.has(duplicateKey)) {
            throw new Error(`Duplicate event hook path: ${hook.path}`);
        }
        seen.add(duplicateKey);
    }
    return all;
}
async function runSingleEventHook(input) {
    const resolved = resolveEventHookConfig({
        hook: input.hook,
        source: input.source,
        baseDir: input.baseDir,
        allowAbsolutePath: input.allowAbsolutePath,
        ...optionalHomeDirectory(input.homeDirectory),
    });
    const execution = await runEventHook({
        repoRoot: input.repoRoot,
        config: input.config,
        hook: resolved,
        payload: input.payload,
        getWorker: async () => input.worker ??
            (await (0, runtime_1.createConfiguredWorkerRuntime)(input.config, {
                ...optionalHomeDirectory(input.homeDirectory),
            })),
    });
    return execution.result;
}
async function executeResolvedEventHook(input) {
    try {
        await (0, ledger_1.writeEventHookLedgerEntry)({
            target: ledgerTargetForHook(input.hook, input.config),
            eventId: input.payload.eventId,
            hookPath: ledgerHookPath(input.hook),
            status: "started",
            ...optionalHomeDirectory(input.homeDirectory),
        });
    }
    catch (error) {
        await appendHookLog(input.logger, [
            `eventId: ${input.payload.eventId}`,
            `hook: ${input.hook.path}`,
            "status: failed",
            `summary: ${formatError(error)}`,
        ]);
        return;
    }
    let execution;
    try {
        execution = await runEventHook(input);
    }
    catch (error) {
        execution = {
            result: {
                status: "failed",
                summary: formatError(error),
            },
        };
    }
    try {
        await (0, ledger_1.writeEventHookLedgerEntry)({
            target: ledgerTargetForHook(input.hook, input.config),
            eventId: input.payload.eventId,
            hookPath: ledgerHookPath(input.hook),
            status: execution.result.status === "failed" ? "failed" : "completed",
            summary: execution.result.summary,
            ...optionalHomeDirectory(input.homeDirectory),
        });
    }
    catch (error) {
        execution = {
            ...execution,
            result: {
                status: "failed",
                summary: `Ledger write failed after hook execution: ${formatError(error)}`,
            },
        };
    }
    await appendHookLog(input.logger, [
        `eventId: ${input.payload.eventId}`,
        `hook: ${input.hook.path}`,
        `runner: ${input.hook.runner}`,
        `status: ${execution.result.status}`,
        `summary: ${execution.result.summary}`,
        ...(execution.stderr !== undefined ? ["stderr:", execution.stderr] : []),
    ]);
}
async function runEventHook(input) {
    if (input.hook.runner === "worker") {
        return runWorkerEventHook(input);
    }
    return runProgramEventHook(input);
}
async function runWorkerEventHook(input) {
    const hookMarkdown = await (0, promises_1.readFile)(input.hook.absolutePath, "utf8");
    const worker = await input.getWorker();
    if (worker.runEventHook === undefined) {
        throw new Error("Worker event hook runtime is unavailable.");
    }
    const result = await worker.runEventHook({
        hookMarkdown,
        event: input.payload,
        workspacePath: input.repoRoot,
        repositoryRoot: input.repoRoot,
        outputLanguage: input.config.i18n.defaultLanguage,
    });
    if (result.kind === "deferred") {
        return {
            result: {
                status: "failed",
                summary: `Worker hook deferred: ${result.reason}`,
            },
        };
    }
    return { result: result.launchResult.result };
}
async function runProgramEventHook(input) {
    const command = input.hook.runner === "node" ? process.execPath : input.hook.runner;
    let commandResult;
    try {
        commandResult = await (0, command_1.runStreamingCommand)({
            command,
            args: [input.hook.absolutePath],
            cwd: input.repoRoot,
            input: `${JSON.stringify(input.payload)}\n`,
            timeoutMs: EVENT_HOOK_TIMEOUT_MS,
            allowNonZeroExit: true,
        });
    }
    catch (error) {
        throw new Error(`Program hook failed to start: ${formatError(error)}`);
    }
    if (commandResult.exitCode !== 0) {
        const detail = commandResult.stderr.trim() || commandResult.stdout.trim();
        throw new Error(`Program hook exited with ${commandResult.exitCode}${detail ? `: ${detail}` : ""}`);
    }
    const stderr = commandResult.stderr.trim();
    let result;
    try {
        result = parseProgramHookResult(commandResult.stdout);
    }
    catch (error) {
        throw new Error(`Program hook stdout result was invalid: ${formatError(error)}${stderr ? `; stderr: ${stderr}` : ""}`);
    }
    return {
        result,
        ...(stderr.length > 0 ? { stderr } : {}),
    };
}
function parseProgramHookResult(stdout) {
    const text = stdout.trim();
    if (text.length === 0) {
        throw new Error("Program hook stdout was empty.");
    }
    return event_hook_contract_1.workerEventHookResultSchema.parse(JSON.parse(text));
}
function resolveEventHookConfig(input) {
    const resolvedPath = input.source === "user:all" || input.source === "user:project"
        ? (0, storage_1.normalizeUserEventHookFilePath)({
            hookPath: input.hook.path,
            baseDirectory: input.baseDir,
            ...optionalHomeDirectory(input.homeDirectory),
        })
        : (0, config_1.normalizeEventHookConfigPath)({
            hookPath: input.hook.path,
            baseDir: input.baseDir,
            allowAbsolutePath: input.allowAbsolutePath,
        });
    return {
        ...input.hook,
        source: input.source,
        runner: input.hook.runner ?? (0, config_1.inferEventHookRunner)(input.hook.path),
        absolutePath: resolvedPath.absolutePath,
        normalizedPath: resolvedPath.normalizedPath,
    };
}
function ledgerTargetForHook(hook, config) {
    return hook.source === "user:all"
        ? { scope: "global" }
        : { scope: "project", config };
}
function ledgerHookPath(hook) {
    return `${hook.source}:${node_path_1.default.normalize(hook.absolutePath)}`;
}
function buildEventHookTriggerTokens(config, record) {
    const tokens = new Set(["all"]);
    if (record.request.target.kind === "new_issue") {
        tokens.add("issueCreated");
    }
    if (record.origin === "worker_error_handoff") {
        tokens.add("workerFailed");
    }
    if (record.origin === "control_plane_error_handoff") {
        tokens.add("systemFailed");
    }
    if (record.request.visibleUpdate?.kind === "workflow_update" &&
        record.request.visibleUpdate.event ===
            publisher_events_1.WORKFLOW_VISIBLE_EVENTS.workflowCompleted) {
        tokens.add("completed");
    }
    if (record.request.terminalAction?.kind === "close" &&
        record.result.targetClosed) {
        tokens.add("completed");
    }
    for (const trigger of resolveAddedWorkflowLabelTriggers(config, record)) {
        tokens.add(trigger);
    }
    return tokens;
}
function resolveAddedWorkflowLabelTriggers(config, record) {
    const projection = record.request.workflowProjection;
    if (projection === undefined) {
        if (record.request.target.kind === "new_issue") {
            return workflowLabelKeysForLabelNames(config, record.result.targetIssue.labelNames ?? []);
        }
        return [];
    }
    const previous = workflowLabelKeysForProjection(config, {
        snapshot: projection.previous,
        auxiliaryLabels: projection.auxiliaryManagedLabels?.previous ?? [],
        ...(projection.labelProjection !== undefined
            ? { labelProjection: projection.labelProjection }
            : {}),
    });
    const next = workflowLabelKeysForProjection(config, {
        snapshot: projection.next,
        auxiliaryLabels: projection.auxiliaryManagedLabels?.next ?? [],
        ...(projection.labelProjection !== undefined
            ? { labelProjection: projection.labelProjection }
            : {}),
    });
    const added = [...next].filter((trigger) => !previous.has(trigger));
    return added.includes("aiCoolingDown")
        ? added.filter((trigger) => trigger !== "aiScheduled")
        : added;
}
function workflowLabelKeysForProjection(config, input) {
    const resolvedLabels = (0, workflow_labels_1.resolveWorkflowLabels)(config.ticketTracker.workflowLabels);
    return new Set([
        ...(0, workflow_labels_1.projectWorkflowLabelNames)(config.ticketTracker.workflowLabels, input.snapshot, input.labelProjection),
        ...input.auxiliaryLabels,
    ].flatMap((label) => {
        const key = workflowLabelKeyForLabel(resolvedLabels, label);
        return key === null ? [] : [key];
    }));
}
function workflowLabelKeysForLabelNames(config, labelNames) {
    const resolvedLabels = (0, workflow_labels_1.resolveWorkflowLabels)(config.ticketTracker.workflowLabels);
    return labelNames.flatMap((label) => {
        const key = workflowLabelKeyForLabel(resolvedLabels, label);
        return key === null ? [] : [key];
    });
}
function workflowLabelKeyForLabel(resolvedLabels, label) {
    const entry = Object.entries(resolvedLabels).find(([, labelName]) => labelName === label);
    if (entry === undefined) {
        return null;
    }
    const key = entry[0];
    return (0, config_1.isWorkflowLabelTrigger)(key) ? key : null;
}
function buildEventHookPayload(input) {
    const { request, result } = input.publication;
    const hookResult = buildEventHookResultPayload(input.publication);
    const previousStatus = request.workflowProjection?.previous.status ?? null;
    const nextStatus = request.workflowProjection?.next.status ??
        result.targetWorkflowProjection?.status ??
        null;
    const pullRequestUrl = request.target.kind === "pull_request_thread"
        ? result.targetIssue.url
        : null;
    const payloadBase = {
        publishTarget: {
            kind: request.target.kind,
            ...(request.target.kind === "new_issue"
                ? { source: request.target.source }
                : {}),
        },
        visibleEvent: request.visibleUpdate?.kind === "workflow_update"
            ? request.visibleUpdate.event
            : null,
        previousStatus,
        nextStatus,
        visibleCommentId: result.visibleUpdateComment?.id ?? null,
        workflowStateCommentId: result.workflowStateComment?.id ?? null,
        targetClosed: result.targetClosed,
    };
    const eventId = (0, node_crypto_1.createHash)("sha256")
        .update(JSON.stringify({
        project: {
            repositoryHost: {
                kind: input.config.repositoryHost.kind,
                projectRef: input.config.repositoryHost.projectRef,
            },
            ticketTracker: {
                kind: input.config.ticketTracker.kind,
                projectRef: input.config.ticketTracker.projectRef,
            },
        },
        issueId: result.targetIssue.id,
        ...payloadBase,
    }))
        .digest("hex");
    return {
        schemaVersion: 1,
        eventId,
        occurredAt: input.occurredAt.toISOString(),
        hook: {
            path: input.hook.path,
            runner: input.hook.runner,
        },
        matchedTriggers: [...input.matchedTriggers],
        repository: {
            host: input.config.repositoryHost.kind,
            projectRef: input.config.repositoryHost.projectRef,
            baseBranch: input.config.repositoryHost.baseBranch,
        },
        issue: {
            id: result.targetIssue.id,
            displayId: result.targetIssue.displayId,
            title: result.targetIssue.title,
            url: result.targetIssue.url,
            kind: result.targetIssue.kind,
            isOpen: result.targetClosed ? false : result.targetIssue.isOpen,
            labelNames: resolvePublishedLabelNames(input.config, input.publication),
        },
        transition: {
            previousStatus,
            nextStatus,
            previousLabel: request.workflowProjection?.previous.status === undefined
                ? null
                : (0, workflow_labels_1.getWorkflowLabelForStatus)(input.config.ticketTracker.workflowLabels, request.workflowProjection.previous.status),
            nextLabel: nextStatus === null
                ? null
                : (0, workflow_labels_1.getWorkflowLabelForStatus)(input.config.ticketTracker.workflowLabels, nextStatus),
        },
        publish: {
            status: "applied",
            visibleEvent: payloadBase.visibleEvent,
            targetClosed: result.targetClosed,
            commentUrl: result.visibleUpdateComment?.url ?? null,
            workflowStateCommentUrl: result.workflowStateComment?.url ?? null,
        },
        pullRequest: {
            url: pullRequestUrl,
            threadId: request.target.kind === "pull_request_thread"
                ? request.target.pullRequestThreadId
                : null,
        },
        result: {
            ...hookResult,
        },
    };
}
function buildEventHookResultPayload(record) {
    const visibleUpdate = record.request.visibleUpdate?.kind === "workflow_update"
        ? record.request.visibleUpdate
        : null;
    const isFailureHandoff = record.origin === "worker_error_handoff" ||
        record.origin === "control_plane_error_handoff";
    const useVisiblePublicText = visibleUpdate !== null && isFailureHandoff;
    return {
        runMode: record.run?.runMode ?? null,
        workerOutcome: useVisiblePublicText
            ? (visibleUpdate.workerOutcome ?? record.run?.workerOutcome ?? null)
            : (record.run?.workerOutcome ?? visibleUpdate?.workerOutcome ?? null),
        needsHumanReason: useVisiblePublicText
            ? (visibleUpdate.needsHumanReason ?? record.run?.needsHumanReason ?? null)
            : (record.run?.needsHumanReason ??
                visibleUpdate?.needsHumanReason ??
                null),
        summary: useVisiblePublicText
            ? visibleUpdate.summary
            : isFailureHandoff
                ? null
                : (record.run?.summary ?? visibleUpdate?.summary ?? null),
        nextStep: useVisiblePublicText
            ? visibleUpdate.next
            : isFailureHandoff
                ? null
                : (record.run?.nextStep ?? visibleUpdate?.next ?? null),
        workflowTemplateName: record.run?.workflowTemplateName ??
            visibleUpdate?.workflowTemplateLabel ??
            null,
    };
}
function resolvePublishedLabelNames(config, record) {
    const projection = record.request.workflowProjection;
    if (projection === undefined) {
        return record.result.targetIssue.labelNames ?? [];
    }
    return (0, workflow_labels_1.resolveWorkflowStateSyncLabelNames)({
        workflowLabels: config.ticketTracker.workflowLabels,
        currentLabelNames: projection.currentLabelNames ??
            record.result.targetIssue.labelNames ??
            [],
        next: projection.next,
        ...(projection.labelProjection !== undefined
            ? { labelProjection: projection.labelProjection }
            : {}),
        ...(projection.auxiliaryManagedLabels !== undefined
            ? { auxiliaryManagedLabels: projection.auxiliaryManagedLabels }
            : {}),
    });
}
function resolveEventHookLogger(input) {
    return (input.logger ??
        new issue_logger_1.IssueLogger((0, tracked_project_1.toTrackedProjectRef)(input.config), input.payload.issue.id, {
            ...optionalHomeDirectory(input.homeDirectory),
            locale: input.config.i18n.defaultLanguage,
        }));
}
function resolvePublicationLogger(input) {
    return (input.logger ??
        new issue_logger_1.IssueLogger((0, tracked_project_1.toTrackedProjectRef)(input.config), input.record.result.targetIssue.id, {
            ...optionalHomeDirectory(input.homeDirectory),
            locale: input.config.i18n.defaultLanguage,
        }));
}
async function logHookSetupFailure(input) {
    for (const record of input.publications) {
        const logger = resolvePublicationLogger({
            logger: input.logger,
            config: input.config,
            record,
            homeDirectory: input.homeDirectory,
        });
        await appendHookLog(logger, [
            `issue: ${record.result.targetIssue.displayId}`,
            "status: failed",
            `summary: ${input.summary}`,
        ]);
    }
}
async function appendHookLog(logger, lines) {
    try {
        await logger.appendSection("eventHook", lines.join("\n"));
    }
    catch {
        // Hook logging must not change already-published workflow state.
    }
}
function formatError(error) {
    if (error instanceof command_1.CommandError) {
        return error.stderr.trim() || error.stdout.trim() || error.message;
    }
    return error instanceof Error ? error.message : String(error);
}
function optionalHomeDirectory(homeDirectory) {
    return homeDirectory === undefined ? {} : { homeDirectory };
}
