import { existsSync, mkdirSync, writeFileSync } from "node:fs";
import { basename, dirname, join, resolve as external_node_path_resolve } from "node:path";
import { assert, ifInBrowser, ifInWorker } from "@midscene/shared/utils";
import { z } from "zod";
import { getMidsceneRunSubDir } from "@midscene/shared/common";
import { getDebug } from "@midscene/shared/logger";
import { buildDetailedLocateParam, buildDetailedLocateParamAndRestParams } from "./utils.mjs";
function _define_property(obj, key, value) {
    if (key in obj) Object.defineProperty(obj, key, {
        value: value,
        enumerable: true,
        configurable: true,
        writable: true
    });
    else obj[key] = value;
    return obj;
}
const debug = getDebug('yaml-player');
const VARIABLE_FULL_MATCH_RE = /^\$([a-zA-Z_][a-zA-Z0-9_]*)$/;
const VARIABLE_EMBEDDED_RE = /\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g;
function deepTransform(value, transform) {
    if (Array.isArray(value)) return value.map((item)=>deepTransform(item, transform));
    if (null !== value && 'object' == typeof value) {
        const result = {};
        for (const [key, val] of Object.entries(value))result[key] = deepTransform(val, transform);
        return result;
    }
    return transform(value);
}
const aiTaskHandlerMap = {
    aiQuery: 'aiQuery',
    aiNumber: 'aiNumber',
    aiString: 'aiString',
    aiBoolean: 'aiBoolean',
    aiAsk: 'aiAsk',
    aiLocate: 'aiLocate'
};
const isStringParamSchema = (schema)=>{
    if (!schema) return false;
    const schemaDef = schema?._def;
    if (!schemaDef?.typeName) return false;
    switch(schemaDef.typeName){
        case z.ZodFirstPartyTypeKind.ZodString:
        case z.ZodFirstPartyTypeKind.ZodEnum:
        case z.ZodFirstPartyTypeKind.ZodNativeEnum:
            return true;
        case z.ZodFirstPartyTypeKind.ZodLiteral:
            return 'string' == typeof schemaDef.value;
        case z.ZodFirstPartyTypeKind.ZodOptional:
        case z.ZodFirstPartyTypeKind.ZodNullable:
        case z.ZodFirstPartyTypeKind.ZodDefault:
            return isStringParamSchema(schemaDef.innerType);
        case z.ZodFirstPartyTypeKind.ZodEffects:
            return isStringParamSchema(schemaDef.schema);
        case z.ZodFirstPartyTypeKind.ZodPipeline:
            return isStringParamSchema(schemaDef.out);
        case z.ZodFirstPartyTypeKind.ZodUnion:
            {
                const options = schemaDef.options;
                return Array.isArray(options) ? options.every((option)=>isStringParamSchema(option)) : false;
            }
        default:
            return false;
    }
};
const buildShortcutActionParam = (actionName, interfaceAlias, value)=>{
    if ('Launch' === actionName || 'launch' === interfaceAlias) return {
        uri: value
    };
    if ('Terminate' === actionName || 'terminate' === interfaceAlias) return {
        uri: value
    };
    if ('RunAdbShell' === actionName || 'runAdbShell' === interfaceAlias) return {
        command: value
    };
};
class ScriptPlayer {
    setResult(key, value) {
        const keyToUse = key || this.unnamedResultIndex++;
        if (this.result[keyToUse]) console.warn(`result key ${keyToUse} already exists, will overwrite`);
        this.result[keyToUse] = value;
        return this.flushResult();
    }
    setPlayerStatus(status, error) {
        this.status = status;
        this.errorInSetup = error;
    }
    notifyCurrentTaskStatusChange(taskIndex) {
        const taskIndexToNotify = 'number' == typeof taskIndex ? taskIndex : this.currentTaskIndex;
        if ('number' != typeof taskIndexToNotify) return;
        const taskStatus = this.taskStatusList[taskIndexToNotify];
        if (this.onTaskStatusChange) this.onTaskStatusChange(taskStatus);
    }
    async setTaskStatus(index, statusValue, error) {
        this.taskStatusList[index].status = statusValue;
        if (error) this.taskStatusList[index].error = error;
        this.notifyCurrentTaskStatusChange(index);
    }
    setTaskIndex(taskIndex) {
        this.currentTaskIndex = taskIndex;
    }
    flushResult() {
        if (this.output) {
            const output = external_node_path_resolve(process.cwd(), this.output);
            const outputDir = dirname(output);
            if (!existsSync(outputDir)) mkdirSync(outputDir, {
                recursive: true
            });
            writeFileSync(output, JSON.stringify(this.result || {}, void 0, 2));
        }
    }
    flushUnstableLogContent() {
        if (this.unstableLogContent) {
            const content = this.interfaceAgent?._unstableLogContent();
            const filePath = external_node_path_resolve(process.cwd(), this.unstableLogContent);
            const outputDir = dirname(filePath);
            if (!existsSync(outputDir)) mkdirSync(outputDir, {
                recursive: true
            });
            writeFileSync(filePath, JSON.stringify(content, null, 2));
        }
    }
    resolveVariables(value) {
        return deepTransform(value, (val)=>{
            if ('string' != typeof val) return val;
            const fullMatch = val.match(VARIABLE_FULL_MATCH_RE);
            if (fullMatch) {
                const varName = fullMatch[1];
                if (!(varName in this.result)) throw new Error(`Variable "${varName}" is not defined`);
                return this.result[varName];
            }
            return val.replace(VARIABLE_EMBEDDED_RE, (_, varName)=>{
                if (!(varName in this.result)) throw new Error(`Variable "${varName}" is not defined`);
                const replacement = this.result[varName];
                return 'string' == typeof replacement ? replacement : JSON.stringify(replacement);
            });
        });
    }
    async playTask(taskStatus, agent) {
        const { flow } = taskStatus;
        assert(flow, 'missing flow in task');
        for(const flowItemIndex in flow){
            const currentStep = Number.parseInt(flowItemIndex, 10);
            taskStatus.currentStep = currentStep;
            const flowItem = this.resolveVariables(flow[flowItemIndex]);
            const flowItemRecord = flowItem;
            if ('Finalize' in flowItemRecord) continue;
            debug(`playing step ${flowItemIndex}, flowItem=${JSON.stringify(flowItem)}`);
            const simpleAIKey = Object.keys(aiTaskHandlerMap).find((key)=>Object.prototype.hasOwnProperty.call(flowItemRecord, key));
            if ('aiAct' in flowItem || 'aiAction' in flowItem || 'ai' in flowItem) {
                const actionTask = flowItem;
                const { aiAct, aiAction, ai, ...actionOptions } = actionTask;
                const prompt = aiAct || aiAction || ai;
                assert(prompt, 'missing prompt for ai (aiAct)');
                await agent.aiAct(prompt, actionOptions);
            } else if ('aiAssert' in flowItem) {
                const assertTask = flowItem;
                const { aiAssert: prompt, errorMessage: msg, name, ...restOpts } = assertTask;
                assert(prompt, 'missing prompt for aiAssert');
                const { pass, thought, message } = await agent.aiAssert(prompt, msg, {
                    ...restOpts,
                    keepRawResponse: true
                }) || {};
                this.setResult(name, {
                    pass,
                    thought,
                    message
                });
                if (!pass) throw new Error(message);
            } else if (simpleAIKey) {
                const { [simpleAIKey]: prompt, name, ...options } = flowItem;
                assert(prompt, `missing prompt for ${simpleAIKey}`);
                const agentMethod = agent[aiTaskHandlerMap[simpleAIKey]];
                assert('function' == typeof agentMethod, `missing agent method for ${simpleAIKey}`);
                const aiResult = await agentMethod.call(agent, prompt, options);
                this.setResult(name, aiResult);
            } else if ('aiWaitFor' in flowItem) {
                const waitForTask = flowItem;
                const { aiWaitFor, timeout, ...restWaitForOpts } = waitForTask;
                const prompt = aiWaitFor;
                assert(prompt, 'missing prompt for aiWaitFor');
                const waitForOptions = {
                    ...restWaitForOpts,
                    ...void 0 !== timeout ? {
                        timeout,
                        timeoutMs: timeout
                    } : {}
                };
                await agent.aiWaitFor(prompt, waitForOptions);
            } else if ('sleep' in flowItem) {
                const sleepTask = flowItem;
                const ms = sleepTask.sleep;
                let msNumber = ms;
                if ('string' == typeof ms) msNumber = Number.parseInt(ms, 10);
                assert(msNumber && msNumber > 0, `ms for sleep must be greater than 0, but got ${ms}`);
                await new Promise((resolve)=>setTimeout(resolve, msNumber));
            } else if ("javascript" in flowItem) {
                const evaluateJavaScriptTask = flowItem;
                const result = await agent.evaluateJavaScript(evaluateJavaScriptTask.javascript);
                this.setResult(evaluateJavaScriptTask.name, result);
            } else if ('logScreenshot' in flowItem || 'recordToReport' in flowItem) {
                const recordTask = flowItem;
                const title = recordTask.recordToReport ?? recordTask.logScreenshot ?? 'untitled';
                const content = recordTask.content || '';
                await agent.recordToReport(title, {
                    content
                });
            } else if ('aiInput' in flowItem) {
                const { aiInput, value: rawValue, ...inputTask } = flowItem;
                let locatePrompt;
                let value;
                if (inputTask.locate) {
                    value = aiInput || rawValue;
                    locatePrompt = inputTask.locate;
                } else {
                    locatePrompt = aiInput || '';
                    value = rawValue;
                }
                await agent.callActionInActionSpace('Input', {
                    ...inputTask,
                    ...void 0 !== value ? {
                        value: String(value)
                    } : {},
                    ...locatePrompt ? {
                        locate: buildDetailedLocateParam(locatePrompt, inputTask)
                    } : {}
                });
            } else if ('aiKeyboardPress' in flowItem) {
                const { aiKeyboardPress, ...keyboardPressTask } = flowItem;
                let locatePrompt;
                let keyName;
                if (keyboardPressTask.locate) {
                    keyName = aiKeyboardPress;
                    locatePrompt = keyboardPressTask.locate;
                } else if (keyboardPressTask.keyName) {
                    keyName = keyboardPressTask.keyName;
                    locatePrompt = aiKeyboardPress;
                } else keyName = aiKeyboardPress;
                await agent.callActionInActionSpace('KeyboardPress', {
                    ...keyboardPressTask,
                    ...keyName ? {
                        keyName
                    } : {},
                    ...locatePrompt ? {
                        locate: buildDetailedLocateParam(locatePrompt, keyboardPressTask)
                    } : {}
                });
            } else if ('aiScroll' in flowItem) {
                const { aiScroll, ...scrollTask } = flowItem;
                const { locate, ...scrollOptions } = scrollTask;
                const locatePrompt = locate ?? aiScroll ?? void 0;
                await agent.aiScroll(locatePrompt, scrollOptions);
            } else if ('aiTap' in flowItem) {
                const { aiTap, prompt, locate, ...tapOptions } = flowItem;
                let locatePrompt;
                let opts = tapOptions;
                const locateObj = locate ?? ('object' == typeof aiTap && null !== aiTap ? aiTap.locate : void 0);
                if ('string' == typeof aiTap && aiTap) locatePrompt = aiTap;
                else if ('object' == typeof locateObj && locateObj?.prompt) {
                    const { prompt: lp, ...locateOpts } = locateObj;
                    locatePrompt = lp;
                    opts = {
                        ...locateOpts,
                        ...tapOptions
                    };
                } else locatePrompt = aiTap?.prompt || prompt || locateObj;
                assert(locatePrompt, 'missing prompt for aiTap');
                await agent.aiTap(locatePrompt, opts);
            } else {
                const actionSpace = this.actionSpace;
                let locatePromptShortcut;
                let actionParamForMatchedAction;
                const matchedAction = actionSpace.find((action)=>{
                    const actionInterfaceAlias = action.interfaceAlias;
                    if (actionInterfaceAlias && Object.prototype.hasOwnProperty.call(flowItem, actionInterfaceAlias)) {
                        actionParamForMatchedAction = flowItem[actionInterfaceAlias];
                        if ('string' == typeof actionParamForMatchedAction) locatePromptShortcut = actionParamForMatchedAction;
                        return true;
                    }
                    const keyOfActionInActionSpace = action.name;
                    if (Object.prototype.hasOwnProperty.call(flowItem, keyOfActionInActionSpace)) {
                        actionParamForMatchedAction = flowItem[keyOfActionInActionSpace];
                        if ('string' == typeof actionParamForMatchedAction) locatePromptShortcut = actionParamForMatchedAction;
                        return true;
                    }
                    return false;
                });
                assert(matchedAction, `unknown flowItem in yaml: ${JSON.stringify(flowItem)}`);
                const schemaIsStringParam = isStringParamSchema(matchedAction.paramSchema);
                let stringParamToCall;
                const resultName = flowItem.name;
                const timeout = flowItem.timeout;
                const hasRunAdbShellAlias = Object.prototype.hasOwnProperty.call(flowItem, 'runAdbShell');
                if (hasRunAdbShellAlias && 'string' == typeof actionParamForMatchedAction && 'number' == typeof timeout && 'function' == typeof agent.runAdbShell) {
                    const result = await agent.runAdbShell(actionParamForMatchedAction, {
                        timeout
                    });
                    if (void 0 !== result) this.setResult(resultName, result);
                    continue;
                }
                const specialActionParamToCall = 'string' == typeof actionParamForMatchedAction ? buildShortcutActionParam(matchedAction.name, matchedAction.interfaceAlias, actionParamForMatchedAction) : void 0;
                if (specialActionParamToCall) {
                    debug(`matchedAction: ${matchedAction.name}`, `flowParams: ${JSON.stringify(specialActionParamToCall)}`);
                    const result = await agent.callActionInActionSpace(matchedAction.name, specialActionParamToCall);
                    if (void 0 !== result) this.setResult(resultName, result);
                } else if ('string' == typeof actionParamForMatchedAction && schemaIsStringParam) {
                    if (matchedAction.paramSchema) {
                        const parseResult = matchedAction.paramSchema.safeParse(actionParamForMatchedAction);
                        if (parseResult.success && 'string' == typeof parseResult.data) stringParamToCall = parseResult.data;
                        else if (!parseResult.success) {
                            debug(`parse failed for action ${matchedAction.name} with string param`, parseResult.error);
                            stringParamToCall = actionParamForMatchedAction;
                        }
                    } else stringParamToCall = actionParamForMatchedAction;
                    if (void 0 !== stringParamToCall) {
                        debug(`matchedAction: ${matchedAction.name}`, `flowParams: ${JSON.stringify(stringParamToCall)}`);
                        const result = await agent.callActionInActionSpace(matchedAction.name, stringParamToCall);
                        const resultName = flowItem.name;
                        if (void 0 !== result) this.setResult(resultName, result);
                    }
                } else {
                    const sourceForParams = locatePromptShortcut && 'string' == typeof actionParamForMatchedAction ? {
                        ...flowItem,
                        prompt: locatePromptShortcut
                    } : 'object' == typeof actionParamForMatchedAction && null !== actionParamForMatchedAction ? actionParamForMatchedAction : flowItem;
                    const { locateParam, restParams } = buildDetailedLocateParamAndRestParams(locatePromptShortcut || '', sourceForParams, [
                        matchedAction.name,
                        matchedAction.interfaceAlias || '_never_mind_'
                    ]);
                    const flowParams = {
                        ...restParams,
                        locate: locateParam
                    };
                    debug(`matchedAction: ${matchedAction.name}`, `flowParams: ${JSON.stringify(flowParams, null, 2)}`);
                    const result = await agent.callActionInActionSpace(matchedAction.name, flowParams);
                    const resultName = flowItem.name;
                    if (void 0 !== result) this.setResult(resultName, result);
                }
            }
        }
        this.reportFile = agent.reportFile;
        await this.flushUnstableLogContent();
    }
    async run() {
        const { target, web, android, ios, computer, tasks } = this.script;
        const webEnv = web || target;
        const androidEnv = android;
        const iosEnv = ios;
        const computerEnv = computer;
        const platform = webEnv || androidEnv || iosEnv || computerEnv;
        this.setPlayerStatus('running');
        let agent = null;
        let freeFn = [];
        try {
            const { agent: newAgent, freeFn: newFreeFn } = await this.setupAgent(platform);
            this.actionSpace = await newAgent.getActionSpace();
            agent = newAgent;
            const originalOnTaskStartTip = agent.onTaskStartTip;
            agent.onTaskStartTip = (tip)=>{
                if ('running' === this.status) this.agentStatusTip = tip;
                originalOnTaskStartTip?.(tip);
            };
            freeFn = [
                ...newFreeFn || [],
                {
                    name: 'restore-agent-onTaskStartTip',
                    fn: ()=>{
                        if (agent) agent.onTaskStartTip = originalOnTaskStartTip;
                    }
                }
            ];
        } catch (e) {
            this.setPlayerStatus('error', e);
            return;
        }
        this.interfaceAgent = agent;
        let taskIndex = 0;
        this.setPlayerStatus('running');
        let errorFlag = false;
        while(taskIndex < tasks.length){
            const taskStatus = this.taskStatusList[taskIndex];
            this.setTaskStatus(taskIndex, 'running');
            this.setTaskIndex(taskIndex);
            try {
                await this.playTask(taskStatus, this.interfaceAgent);
                this.setTaskStatus(taskIndex, 'done');
            } catch (e) {
                this.setTaskStatus(taskIndex, 'error', e);
                if (taskStatus.continueOnError) ;
                else {
                    this.reportFile = agent.reportFile;
                    errorFlag = true;
                    break;
                }
            }
            this.reportFile = agent?.reportFile;
            taskIndex++;
        }
        if (errorFlag) this.setPlayerStatus('error');
        else this.setPlayerStatus('done');
        this.agentStatusTip = '';
        for (const fn of freeFn)try {
            await fn.fn();
        } catch (e) {}
    }
    constructor(script, setupAgent, onTaskStatusChange, scriptPath){
        _define_property(this, "script", void 0);
        _define_property(this, "setupAgent", void 0);
        _define_property(this, "onTaskStatusChange", void 0);
        _define_property(this, "currentTaskIndex", void 0);
        _define_property(this, "taskStatusList", void 0);
        _define_property(this, "status", void 0);
        _define_property(this, "reportFile", void 0);
        _define_property(this, "result", void 0);
        _define_property(this, "unnamedResultIndex", void 0);
        _define_property(this, "output", void 0);
        _define_property(this, "unstableLogContent", void 0);
        _define_property(this, "errorInSetup", void 0);
        _define_property(this, "interfaceAgent", void 0);
        _define_property(this, "agentStatusTip", void 0);
        _define_property(this, "target", void 0);
        _define_property(this, "actionSpace", void 0);
        _define_property(this, "scriptPath", void 0);
        this.script = script;
        this.setupAgent = setupAgent;
        this.onTaskStatusChange = onTaskStatusChange;
        this.taskStatusList = [];
        this.status = 'init';
        this.unnamedResultIndex = 0;
        this.interfaceAgent = null;
        this.actionSpace = [];
        this.scriptPath = scriptPath;
        this.result = {};
        const resolvedAiActContext = script.agent?.aiActContext ?? script.agent?.aiActionContext;
        if (void 0 !== resolvedAiActContext && script.agent) {
            if (void 0 === script.agent.aiActContext && void 0 !== script.agent.aiActionContext) console.warn('agent.aiActionContext is deprecated, please use agent.aiActContext instead. The legacy name is still accepted for backward compatibility.');
            script.agent.aiActContext = resolvedAiActContext;
        }
        this.target = script.target || script.web || script.android || script.ios || script.computer || script.config;
        if (ifInBrowser || ifInWorker) {
            this.output = void 0;
            debug('output is undefined in browser or worker');
        } else if (this.target?.output) {
            this.output = external_node_path_resolve(process.cwd(), this.target.output);
            debug('setting output by config.output', this.output);
        } else {
            const scriptName = this.scriptPath ? basename(this.scriptPath, '.yaml').replace(/\.(ya?ml)$/i, '') : "script";
            this.output = join(getMidsceneRunSubDir('output'), `${scriptName}-${Date.now()}.json`);
            debug("setting output by script path", this.output);
        }
        if (ifInBrowser || ifInWorker) this.unstableLogContent = void 0;
        else if ('string' == typeof this.target?.unstableLogContent) this.unstableLogContent = external_node_path_resolve(process.cwd(), this.target.unstableLogContent);
        else if (this.target?.unstableLogContent === true) this.unstableLogContent = join(getMidsceneRunSubDir('output'), 'unstableLogContent.json');
        this.taskStatusList = (script.tasks || []).map((task, taskIndex)=>({
                ...task,
                index: taskIndex,
                status: 'init',
                totalSteps: task.flow?.length || 0
            }));
    }
}
export { ScriptPlayer };

//# sourceMappingURL=player.mjs.map