"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.runEventHookCreateCommand = runEventHookCreateCommand;
exports.runEventHookUpdateCommand = runEventHookUpdateCommand;
const node_crypto_1 = require("node:crypto");
const promises_1 = require("node:fs/promises");
const node_path_1 = __importDefault(require("node:path"));
const control_plane_config_1 = require("../config/control-plane-config");
const config_1 = require("../event-hooks/config");
const path_1 = require("../event-hooks/path");
const storage_1 = require("../event-hooks/storage");
const sample_payload_1 = require("../event-hooks/sample-payload");
const tracked_project_1 = require("../core/tracked-project");
const runtime_1 = require("../worker/runtime");
const draft_file_support_1 = require("./draft-file-support");
const event_hook_draft_file_1 = require("./event-hook-draft-file");
async function runEventHookCreateCommand(cwd, args) {
    if (args.mode === "apply-draft") {
        const draft = await (0, event_hook_draft_file_1.readEventHookDraftFile)(args.draftPath);
        return applyEventHookDraft(draft, "create");
    }
    const target = await resolveAuthoringTarget(cwd, args);
    const hook = buildCreateHookConfig(target, args);
    await assertCreateHookTargetAvailable(target, hook);
    const draft = await generateEventHookDraft(target, {
        operation: "create",
        prompt: args.prompt,
        hook,
    });
    const draftFile = buildEventHookDraftFile({
        command: "create",
        prompt: args.prompt,
        target,
        hook,
        draft,
    });
    if (args.saveDraft) {
        return saveReusableEventHookDraft(target, draftFile);
    }
    return applyEventHookDraft(draftFile);
}
async function runEventHookUpdateCommand(cwd, args) {
    if (args.mode === "apply-draft") {
        const draft = await (0, event_hook_draft_file_1.readEventHookDraftFile)(args.draftPath);
        return applyEventHookDraft(draft, "update");
    }
    const target = await resolveAuthoringTarget(cwd, args);
    const existing = await loadRegisteredHook(target, args.path);
    const currentBody = await (0, promises_1.readFile)(existing.absolutePath, "utf8");
    const baseDigest = digestHookBody(currentBody);
    const draft = await generateEventHookDraft(target, {
        operation: "update",
        prompt: args.prompt,
        hook: existing.hook,
        currentHook: {
            body: currentBody,
            digest: baseDigest,
        },
    });
    const draftFile = buildEventHookDraftFile({
        command: "update",
        prompt: args.prompt,
        target,
        hook: existing.hook,
        draft,
        baseDigest,
    });
    if (args.saveDraft) {
        return saveReusableEventHookDraft(target, draftFile);
    }
    return applyEventHookDraft(draftFile);
}
async function applyEventHookDraft(draft, expectedCommand) {
    if (expectedCommand !== undefined && draft.command !== expectedCommand) {
        throw new Error(`Event hook draft command mismatch: expected ${expectedCommand}, got ${draft.command}.`);
    }
    const target = await resolveTargetFromDraft(draft);
    if (draft.command === "create") {
        return applyCreateDraft(target, draft);
    }
    return applyUpdateDraft(target, draft);
}
async function applyCreateDraft(target, draft) {
    const hook = normalizeDraftHook(draft);
    const hookPath = resolveHookPath(target, hook.path);
    if (await fileExists(hookPath.absolutePath)) {
        throw new Error(`Event hook file already exists: ${hookPath.absolutePath}`);
    }
    await assertHookCanBeRegistered(target, hook);
    await (0, promises_1.mkdir)(node_path_1.default.dirname(hookPath.absolutePath), { recursive: true });
    await writeNewHookFile(hookPath.absolutePath, draft.body);
    try {
        await appendHookToRegistry(target, hook);
    }
    catch (error) {
        await (0, promises_1.rm)(hookPath.absolutePath, { force: true });
        throw error;
    }
    console.log(`Created event hook: ${hook.path}`);
    console.log(hookPath.absolutePath);
    return 0;
}
async function applyUpdateDraft(target, draft) {
    const existing = await loadRegisteredHook(target, draft.hook.path);
    assertUpdateDraftRegistrationMatches(existing.hook, draft);
    const currentBody = await (0, promises_1.readFile)(existing.absolutePath, "utf8");
    if (digestHookBody(currentBody) !== draft.baseDigest) {
        throw new Error("Event hook file changed since the draft was prepared. Re-run event-hook update --prompt.");
    }
    await (0, promises_1.writeFile)(existing.absolutePath, draft.body, "utf8");
    console.log(`Updated event hook: ${existing.hook.path}`);
    console.log(existing.absolutePath);
    return 0;
}
async function generateEventHookDraft(target, input) {
    const runtime = await (0, runtime_1.createConfiguredWorkerRuntime)(target.config);
    if (runtime.runEventHookDraft === undefined) {
        throw new Error("Event hook draft worker runtime is unavailable.");
    }
    const result = await runtime.runEventHookDraft({
        operation: input.operation,
        prompt: input.prompt,
        outputLanguage: target.config.i18n.defaultLanguage,
        hook: {
            path: input.hook.path,
            runner: input.hook.runner ?? (0, config_1.inferEventHookRunner)(input.hook.path),
            on: input.hook.on,
        },
        sampleEvent: (0, sample_payload_1.buildSampleEventHookPayload)(input.hook),
        ...(input.currentHook ? { currentHook: input.currentHook } : {}),
        workspacePath: target.cwd,
        repositoryRoot: target.cwd,
    });
    if (result.kind === "deferred") {
        throw new Error(`Event hook draft worker deferred until ${result.cooldownUntil ?? "later"}.`);
    }
    return result.launchResult.draft;
}
async function resolveAuthoringTarget(commandCwd, args) {
    if (args.scope === "repo") {
        const cwd = resolveSingleTargetCwd(commandCwd, args.cwd);
        const config = await (0, control_plane_config_1.loadConfig)(cwd);
        return {
            scope: "repo",
            cwd,
            config,
            baseDirectory: cwd,
        };
    }
    const userDirectory = (0, storage_1.requireUserEventHookDirectory)();
    const cwd = resolveSingleTargetCwd(commandCwd, args.cwd);
    const config = await (0, control_plane_config_1.loadConfig)(cwd);
    const storageTarget = args.allRepos
        ? { scope: "global" }
        : { scope: "project", config };
    return buildUserAuthoringTarget({
        cwd,
        config,
        storageTarget,
        userDirectory,
    });
}
async function resolveTargetFromDraft(draft) {
    const config = await (0, control_plane_config_1.loadConfig)(draft.target.cwd);
    switch (draft.target.scope) {
        case "repo":
            return {
                scope: "repo",
                cwd: draft.target.cwd,
                config,
                baseDirectory: draft.target.cwd,
            };
        case "user:all":
            return buildUserAuthoringTarget({
                cwd: draft.target.cwd,
                config,
                storageTarget: { scope: "global" },
                userDirectory: (0, storage_1.requireUserEventHookDirectory)(),
            });
        case "user:project":
            return buildUserAuthoringTarget({
                cwd: draft.target.cwd,
                config,
                storageTarget: { scope: "project", config },
                userDirectory: (0, storage_1.requireUserEventHookDirectory)(),
            });
    }
}
function buildUserAuthoringTarget(input) {
    return {
        scope: input.storageTarget.scope === "global" ? "user:all" : "user:project",
        cwd: input.cwd,
        config: input.config,
        baseDirectory: input.userDirectory,
        storageTarget: input.storageTarget,
    };
}
function buildCreateHookConfig(target, args) {
    const runner = resolveCreateRunner(args);
    const hookPath = args.path ?? buildDefaultHookPath(target, args.prompt, runner);
    const hook = {
        path: hookPath,
        runner,
        on: (0, config_1.parseEventHookTriggerList)(args.on),
    };
    const validated = target.scope === "repo"
        ? (0, config_1.validateRepoEventHooks)([hook])[0]
        : (0, config_1.validateUserEventHooks)([hook])[0];
    if (validated === undefined) {
        throw new Error("Event hook validation returned no hook.");
    }
    return validated;
}
async function assertCreateHookTargetAvailable(target, hook) {
    const hookPath = resolveHookPath(target, hook.path);
    if (await fileExists(hookPath.absolutePath)) {
        throw new Error(`Event hook file already exists: ${hookPath.absolutePath}`);
    }
    await assertHookCanBeRegistered(target, hook);
}
function resolveCreateRunner(args) {
    if (args.runner !== undefined) {
        return args.runner;
    }
    return args.path === undefined ? "worker" : (0, config_1.inferEventHookRunner)(args.path);
}
function buildDefaultHookPath(target, prompt, runner) {
    const extension = (0, config_1.getDefaultEventHookExtensionForRunner)(runner);
    if (extension === null) {
        throw new Error("Custom event hook runners require --path.");
    }
    const fileName = `${slugifyHookPath(prompt)}${extension}`;
    return target.scope === "repo"
        ? node_path_1.default.posix.join(".alpaca", "event-hooks", fileName)
        : fileName;
}
async function loadRegisteredHook(target, requestedPath) {
    const hooks = await loadRegistryHooks(target);
    for (const hook of hooks) {
        if (isMatchingHookPath(target, hook.path, requestedPath)) {
            const hookPath = resolveHookPath(target, hook.path);
            return { hook, absolutePath: hookPath.absolutePath };
        }
    }
    throw new Error(`Event hook is not registered: ${requestedPath}`);
}
async function loadRegistryHooks(target) {
    if (target.scope === "repo") {
        return [...(await (0, control_plane_config_1.loadConfig)(target.cwd)).eventHooks];
    }
    if (target.storageTarget === undefined) {
        throw new Error("Missing user event hook storage target.");
    }
    return (0, storage_1.loadUserEventHooks)({ target: target.storageTarget });
}
async function appendHookToRegistry(target, hook) {
    if (target.scope === "repo") {
        const currentConfig = await (0, control_plane_config_1.loadConfig)(target.cwd);
        const nextHooks = (0, config_1.validateRepoEventHooks)([
            ...currentConfig.eventHooks,
            hook,
        ]);
        await (0, promises_1.writeFile)((0, control_plane_config_1.getConfigPath)(target.cwd), (0, control_plane_config_1.formatConfigFile)({ ...currentConfig, eventHooks: nextHooks }), "utf8");
        return;
    }
    if (target.storageTarget === undefined) {
        throw new Error("Missing user event hook storage target.");
    }
    const currentHooks = await (0, storage_1.loadUserEventHooks)({
        target: target.storageTarget,
    });
    const nextHooks = (0, config_1.validateUserEventHooks)([...currentHooks, hook]);
    await (0, storage_1.writeUserEventHooks)({
        target: target.storageTarget,
        eventHooks: nextHooks,
    });
}
async function assertHookCanBeRegistered(target, hook) {
    if (target.scope === "repo") {
        const currentConfig = await (0, control_plane_config_1.loadConfig)(target.cwd);
        (0, config_1.validateRepoEventHooks)([...currentConfig.eventHooks, hook]);
        return;
    }
    if (target.storageTarget === undefined) {
        throw new Error("Missing user event hook storage target.");
    }
    const currentHooks = await (0, storage_1.loadUserEventHooks)({
        target: target.storageTarget,
    });
    (0, config_1.validateUserEventHooks)([...currentHooks, hook]);
}
function buildEventHookDraftFile(input) {
    const hook = {
        path: input.hook.path,
        runner: input.hook.runner ?? (0, config_1.inferEventHookRunner)(input.hook.path),
        on: [...input.hook.on],
    };
    return {
        kind: "alpaca.eventHookDraft",
        schemaVersion: 1,
        command: input.command,
        preparedAt: new Date().toISOString(),
        sourcePrompt: input.prompt,
        target: toDraftTarget(input.target),
        hook,
        hookDigest: (0, event_hook_draft_file_1.buildEventHookDraftHookDigest)({
            command: input.command,
            target: toDraftTarget(input.target),
            hook,
        }),
        body: input.draft.body,
        summary: input.draft.summary,
        ...(input.baseDigest !== undefined ? { baseDigest: input.baseDigest } : {}),
    };
}
async function saveReusableEventHookDraft(target, draftFile) {
    const draftPath = await (0, draft_file_support_1.writeMachineLocalReusableDraft)({
        draftJson: (0, event_hook_draft_file_1.stringifyEventHookDraftFile)(draftFile),
        command: `event-hook-${draftFile.command}`,
        preparedAt: draftFile.preparedAt,
        title: draftFile.hook.path,
        trackedProject: (0, tracked_project_1.toTrackedProjectRef)(target.config),
        cwd: target.cwd,
    });
    console.log(`Saved event hook draft: ${draftPath}`);
    return 0;
}
function toDraftTarget(target) {
    if (target.scope === "repo") {
        return { scope: "repo", cwd: target.cwd };
    }
    if (target.scope === "user:all") {
        return { scope: "user:all", cwd: target.cwd };
    }
    return { scope: "user:project", cwd: target.cwd };
}
function normalizeDraftHook(draft) {
    return {
        path: draft.hook.path,
        runner: draft.hook.runner,
        on: (0, config_1.parseEventHookTriggerList)(draft.hook.on),
    };
}
function assertUpdateDraftRegistrationMatches(registeredHook, draft) {
    const registeredRunner = registeredHook.runner ?? (0, config_1.inferEventHookRunner)(registeredHook.path);
    const draftRunner = draft.hook.runner;
    const registeredTriggers = [...registeredHook.on].sort();
    const draftTriggers = [...draft.hook.on].sort();
    if (registeredHook.path !== draft.hook.path ||
        registeredRunner !== draftRunner ||
        registeredTriggers.length !== draftTriggers.length ||
        registeredTriggers.some((trigger, index) => trigger !== draftTriggers[index])) {
        throw new Error("Event hook registration changed since the draft was prepared. Re-run event-hook update --prompt.");
    }
}
function resolveHookPath(target, hookPath) {
    if (target.scope !== "repo") {
        return (0, storage_1.normalizeUserEventHookFilePath)({
            hookPath,
            baseDirectory: target.baseDirectory,
        });
    }
    return (0, config_1.normalizeEventHookConfigPath)({
        hookPath,
        baseDir: target.baseDirectory,
        allowAbsolutePath: false,
    });
}
function isMatchingHookPath(target, configuredPath, requestedPath) {
    const configured = (0, path_1.normalizeEventHookPathForMatch)({
        baseDir: target.baseDirectory,
        allowAbsolutePath: false,
        hookPath: configuredPath,
    });
    const requested = (0, path_1.normalizeEventHookPathForMatch)({
        baseDir: target.baseDirectory,
        allowAbsolutePath: false,
        hookPath: requestedPath,
    });
    return configured !== null && requested !== null && configured === requested;
}
function digestHookBody(body) {
    return (0, node_crypto_1.createHash)("sha256").update(body).digest("hex");
}
async function fileExists(filePath) {
    try {
        await (0, promises_1.readFile)(filePath, "utf8");
        return true;
    }
    catch (error) {
        if (isFileNotFoundError(error)) {
            return false;
        }
        throw error;
    }
}
async function writeNewHookFile(filePath, body) {
    try {
        await (0, promises_1.writeFile)(filePath, body, { encoding: "utf8", flag: "wx" });
    }
    catch (error) {
        if (hasErrorCode(error, "EEXIST")) {
            throw new Error(`Event hook file already exists: ${filePath}`);
        }
        throw error;
    }
}
function isFileNotFoundError(error) {
    return hasErrorCode(error, "ENOENT");
}
function hasErrorCode(error, code) {
    return (typeof error === "object" &&
        error !== null &&
        "code" in error &&
        error.code === code);
}
function resolveSingleTargetCwd(commandCwd, cwdValue) {
    return cwdValue === undefined
        ? commandCwd
        : node_path_1.default.resolve(commandCwd, cwdValue);
}
function slugifyHookPath(value) {
    const slug = value
        .trim()
        .toLowerCase()
        .replaceAll(/[^0-9a-z]+/gu, "-")
        .replaceAll(/^-|-$/gu, "")
        .slice(0, 48);
    return slug || "event-hook";
}
