"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ActCache = void 0;
const crypto_1 = require("crypto");
const utils_js_1 = require("./utils.js");
const sdkErrors_js_1 = require("../types/public/sdkErrors.js");
class ActCache {
    storage;
    logger;
    getActHandler;
    getDefaultLlmClient;
    domSettleTimeoutMs;
    constructor({ storage, logger, getActHandler, getDefaultLlmClient, domSettleTimeoutMs, }) {
        this.storage = storage;
        this.logger = logger;
        this.getActHandler = getActHandler;
        this.getDefaultLlmClient = getDefaultLlmClient;
        this.domSettleTimeoutMs = domSettleTimeoutMs;
    }
    get enabled() {
        return this.storage.enabled;
    }
    async prepareContext(instruction, page, variables) {
        if (!this.enabled)
            return null;
        const sanitizedInstruction = instruction.trim();
        const sanitizedVariables = variables ? { ...variables } : undefined;
        const variableKeys = sanitizedVariables
            ? Object.keys(sanitizedVariables).sort()
            : [];
        const pageUrl = await (0, utils_js_1.safeGetPageUrl)(page);
        const cacheKey = this.buildActCacheKey(sanitizedInstruction, pageUrl, variableKeys);
        return {
            instruction: sanitizedInstruction,
            cacheKey,
            pageUrl,
            variableKeys,
            variables: sanitizedVariables,
        };
    }
    async tryReplay(context, page, timeout, llmClientOverride) {
        if (!this.enabled)
            return null;
        const { value: entry, error, path, } = await this.storage.readJson(`${context.cacheKey}.json`);
        if (error && path) {
            this.logger({
                category: "cache",
                message: `failed to read act cache entry: ${path}`,
                level: 2,
                auxiliary: {
                    error: { value: String(error), type: "string" },
                },
            });
            return null;
        }
        if (!entry)
            return null;
        if (entry.version !== 1)
            return null;
        if (!Array.isArray(entry.actions) || entry.actions.length === 0) {
            return null;
        }
        const entryVariableKeys = Array.isArray(entry.variableKeys)
            ? [...entry.variableKeys].sort()
            : [];
        const contextVariableKeys = [...context.variableKeys];
        if (!this.doVariableKeysMatch(entryVariableKeys, contextVariableKeys)) {
            return null;
        }
        if (contextVariableKeys.length > 0 &&
            (!context.variables ||
                !this.hasAllVariableValues(contextVariableKeys, context.variables))) {
            this.logger({
                category: "cache",
                message: "act cache miss: missing variables for replay",
                level: 2,
                auxiliary: {
                    instruction: { value: context.instruction, type: "string" },
                },
            });
            return null;
        }
        this.logger({
            category: "cache",
            message: "act cache hit",
            level: 1,
            auxiliary: {
                instruction: { value: context.instruction, type: "string" },
                url: {
                    value: entry.url ?? context.pageUrl,
                    type: "string",
                },
            },
        });
        return await this.replayCachedActions(context, entry, page, timeout, llmClientOverride);
    }
    async store(context, result) {
        if (!this.enabled)
            return;
        const entry = {
            version: 1,
            instruction: context.instruction,
            url: context.pageUrl,
            variableKeys: context.variableKeys,
            actions: result.actions ?? [],
            actionDescription: result.actionDescription,
            message: result.message,
        };
        const { error, path } = await this.storage.writeJson(`${context.cacheKey}.json`, entry);
        if (error && path) {
            this.logger({
                category: "cache",
                message: "failed to write act cache entry",
                level: 1,
                auxiliary: {
                    error: { value: String(error), type: "string" },
                },
            });
            return;
        }
        this.logger({
            category: "cache",
            message: "act cache stored",
            level: 2,
            auxiliary: {
                instruction: { value: context.instruction, type: "string" },
                url: { value: context.pageUrl, type: "string" },
            },
        });
    }
    buildActCacheKey(instruction, url, variableKeys) {
        const payload = JSON.stringify({
            instruction,
            url,
            variableKeys,
        });
        return (0, crypto_1.createHash)("sha256").update(payload).digest("hex");
    }
    async replayCachedActions(context, entry, page, timeout, llmClientOverride) {
        const handler = this.getActHandler();
        if (!handler) {
            throw new sdkErrors_js_1.StagehandNotInitializedError("act()");
        }
        const effectiveClient = llmClientOverride ?? this.getDefaultLlmClient();
        const execute = async () => {
            const actionResults = [];
            for (const action of entry.actions) {
                await (0, utils_js_1.waitForCachedSelector)({
                    page,
                    selector: action.selector,
                    timeout: this.domSettleTimeoutMs,
                    logger: this.logger,
                    context: "act",
                });
                const result = await handler.takeDeterministicAction(action, page, this.domSettleTimeoutMs, effectiveClient, undefined, context.variables);
                actionResults.push(result);
                if (!result.success) {
                    break;
                }
            }
            if (actionResults.length === 0) {
                return {
                    success: false,
                    message: "Failed to perform act: cached entry has no actions",
                    actionDescription: entry.actionDescription ?? entry.instruction,
                    actions: [],
                };
            }
            const success = actionResults.every((r) => r.success);
            const actions = actionResults.flatMap((r) => r.actions ?? []);
            const message = actionResults
                .map((r) => r.message)
                .filter((m) => m && m.trim().length > 0)
                .join(" → ") ||
                entry.message ||
                `Replayed ${entry.actions.length} cached action${entry.actions.length === 1 ? "" : "s"}.`;
            const actionDescription = entry.actionDescription ||
                actionResults[actionResults.length - 1]?.actionDescription ||
                entry.actions[entry.actions.length - 1]?.description ||
                entry.instruction;
            if (success &&
                actions.length > 0 &&
                this.haveActionsChanged(entry.actions, actions)) {
                await this.refreshCacheEntry(context, {
                    ...entry,
                    actions,
                    message,
                    actionDescription,
                });
            }
            return {
                success,
                message,
                actionDescription,
                actions,
            };
        };
        return await this.runWithTimeout(execute, timeout);
    }
    haveActionsChanged(original, updated) {
        if (original.length !== updated.length) {
            return true;
        }
        for (let i = 0; i < original.length; i += 1) {
            const orig = original[i];
            const next = updated[i];
            if (!next) {
                return true;
            }
            if (orig.selector !== next.selector) {
                return true;
            }
            if (orig.description !== next.description) {
                return true;
            }
            if ((orig.method ?? "") !== (next.method ?? "")) {
                return true;
            }
            const origArgs = orig.arguments ?? [];
            const nextArgs = next.arguments ?? [];
            if (origArgs.length !== nextArgs.length) {
                return true;
            }
            for (let j = 0; j < origArgs.length; j += 1) {
                if (origArgs[j] !== nextArgs[j]) {
                    return true;
                }
            }
        }
        return false;
    }
    async refreshCacheEntry(context, entry) {
        const { error, path } = await this.storage.writeJson(`${context.cacheKey}.json`, {
            ...entry,
            variableKeys: context.variableKeys,
        });
        if (error && path) {
            this.logger({
                category: "cache",
                message: "failed to update act cache entry after self-heal",
                level: 0,
                auxiliary: {
                    error: { value: String(error), type: "string" },
                },
            });
            return;
        }
        this.logger({
            category: "cache",
            message: "act cache entry updated after self-heal",
            level: 2,
            auxiliary: {
                instruction: { value: context.instruction, type: "string" },
                url: { value: context.pageUrl, type: "string" },
            },
        });
    }
    doVariableKeysMatch(entryKeys, contextKeys) {
        if (entryKeys.length !== contextKeys.length) {
            return false;
        }
        for (let i = 0; i < entryKeys.length; i += 1) {
            if (entryKeys[i] !== contextKeys[i]) {
                return false;
            }
        }
        return true;
    }
    hasAllVariableValues(variableKeys, variables) {
        for (const key of variableKeys) {
            if (!(key in variables)) {
                return false;
            }
        }
        return true;
    }
    async runWithTimeout(run, timeout) {
        if (!timeout) {
            return await run();
        }
        return await new Promise((resolve, reject) => {
            const timer = setTimeout(() => {
                reject(new Error(`act() timed out after ${timeout}ms`));
            }, timeout);
            void run().then((value) => {
                clearTimeout(timer);
                resolve(value);
            }, (err) => {
                clearTimeout(timer);
                reject(err);
            });
        });
    }
}
exports.ActCache = ActCache;
//# sourceMappingURL=ActCache.js.map