import { ExecutionDump } from "./types.mjs";
import { getDebug } from "@midscene/shared/logger";
import { assert, uuid } from "@midscene/shared/utils";
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('task-runner');
const UI_CONTEXT_CACHE_TTL_MS = 300;
class TaskRunner {
    async emitOnTaskUpdate(error) {
        if (!this.onTaskUpdate) return;
        await this.onTaskUpdate(this, error);
    }
    async getUiContext(options) {
        const now = Date.now();
        const shouldReuse = !options?.forceRefresh && this.lastUiContext && now - this.lastUiContext.capturedAt <= UI_CONTEXT_CACHE_TTL_MS;
        if (shouldReuse && this.lastUiContext?.context) {
            debug(`reuse cached uiContext captured ${now - this.lastUiContext.capturedAt}ms ago`);
            return this.lastUiContext?.context;
        }
        try {
            const uiContext = await this.uiContextBuilder();
            if (uiContext) this.lastUiContext = {
                context: uiContext,
                capturedAt: Date.now()
            };
            else this.lastUiContext = void 0;
            return uiContext;
        } catch (error) {
            this.lastUiContext = void 0;
            throw error;
        }
    }
    async captureScreenshot() {
        try {
            const uiContext = await this.getUiContext({
                forceRefresh: true
            });
            return uiContext?.screenshot;
        } catch (error) {
            console.error('error while capturing screenshot', error);
        }
    }
    attachRecorderItem(task, screenshot, phase) {
        if (!phase || !screenshot) return;
        const recorderItem = {
            type: 'screenshot',
            ts: Date.now(),
            screenshot,
            timing: phase
        };
        if (!task.recorder) {
            task.recorder = [
                recorderItem
            ];
            return;
        }
        task.recorder.push(recorderItem);
    }
    markTaskAsPending(task) {
        return {
            taskId: uuid(),
            status: 'pending',
            ...task
        };
    }
    normalizeStatusFromError(options, errorMessage) {
        if ('error' !== this.status) return;
        assert(options?.allowWhenError, errorMessage || `task runner is in error state, cannot proceed\nerror=${this.latestErrorTask()?.error}\n${this.latestErrorTask()?.errorStack}`);
        this.status = this.tasks.length > 0 ? 'pending' : 'init';
    }
    findPreviousNonSubTaskUIContext(currentIndex) {
        for(let i = currentIndex - 1; i >= 0; i--){
            const candidate = this.tasks[i];
            if (candidate && !candidate.subTask) {
                if (candidate.uiContext) return candidate.uiContext;
            }
        }
    }
    async append(task, options) {
        this.normalizeStatusFromError(options, `task runner is in error state, cannot append task\nerror=${this.latestErrorTask()?.error}\n${this.latestErrorTask()?.errorStack}`);
        if (Array.isArray(task)) this.tasks.push(...task.map((item)=>this.markTaskAsPending(item)));
        else this.tasks.push(this.markTaskAsPending(task));
        if ('running' !== this.status) this.status = 'pending';
        await this.emitOnTaskUpdate();
    }
    async appendAndFlush(task, options) {
        await this.append(task, options);
        return this.flush(options);
    }
    async flush(options) {
        if ('init' === this.status && this.tasks.length > 0) console.warn('illegal state for task runner, status is init but tasks are not empty');
        this.normalizeStatusFromError(options, 'task runner is in error state');
        assert('running' !== this.status, 'task runner is already running');
        assert('completed' !== this.status, 'task runner is already completed');
        const nextPendingIndex = this.tasks.findIndex((task)=>'pending' === task.status);
        if (nextPendingIndex < 0) return;
        this.status = 'running';
        await this.emitOnTaskUpdate();
        let taskIndex = nextPendingIndex;
        let successfullyCompleted = true;
        let previousFindOutput;
        while(taskIndex < this.tasks.length){
            const task = this.tasks[taskIndex];
            assert('pending' === task.status, `task status should be pending, but got: ${task.status}`);
            task.timing = {
                start: Date.now()
            };
            try {
                task.status = 'running';
                await this.emitOnTaskUpdate();
                try {
                    if (this.onTaskStart) await this.onTaskStart(task);
                } catch (e) {
                    console.error('error in onTaskStart', e);
                }
                assert([
                    'Insight',
                    'Action Space',
                    'Planning'
                ].indexOf(task.type) >= 0, `unsupported task type: ${task.type}`);
                const { executor, param } = task;
                assert(executor, `executor is required for task type: ${task.type}`);
                let returnValue;
                let uiContext;
                if (task.subTask) {
                    uiContext = this.findPreviousNonSubTaskUIContext(taskIndex);
                    assert(uiContext, 'subTask requires uiContext from previous non-subTask task');
                } else {
                    const forceRefresh = 'Insight' === task.type;
                    uiContext = await this.getUiContext({
                        forceRefresh
                    });
                }
                task.uiContext = uiContext;
                const executorContext = {
                    task,
                    element: previousFindOutput?.element,
                    uiContext
                };
                if ('Insight' === task.type) {
                    assert('Query' === task.subType || 'Assert' === task.subType || 'WaitFor' === task.subType || 'Boolean' === task.subType || 'Number' === task.subType || 'String' === task.subType, `unsupported service subType: ${task.subType}`);
                    returnValue = await task.executor(param, executorContext);
                } else if ('Planning' === task.type) {
                    returnValue = await task.executor(param, executorContext);
                    if ('Locate' === task.subType) previousFindOutput = returnValue?.output;
                } else if ('Action Space' === task.type) returnValue = await task.executor(param, executorContext);
                else {
                    console.warn(`unsupported task type: ${task.type}, will try to execute it directly`);
                    returnValue = await task.executor(param, executorContext);
                }
                const isLastTask = taskIndex === this.tasks.length - 1;
                if (isLastTask) {
                    const screenshot = await this.captureScreenshot();
                    this.attachRecorderItem(task, screenshot, 'after-calling');
                }
                Object.assign(task, returnValue);
                task.status = 'finished';
                task.timing.end = Date.now();
                task.timing.cost = task.timing.end - task.timing.start;
                await this.emitOnTaskUpdate();
                taskIndex++;
            } catch (e) {
                successfullyCompleted = false;
                task.error = e;
                task.errorMessage = e?.message || ('string' == typeof e ? e : 'error-without-message');
                task.errorStack = e.stack;
                task.status = 'failed';
                task.timing.end = Date.now();
                task.timing.cost = task.timing.end - task.timing.start;
                await this.emitOnTaskUpdate();
                break;
            }
        }
        for(let i = taskIndex + 1; i < this.tasks.length; i++)this.tasks[i].status = 'cancelled';
        if (taskIndex + 1 < this.tasks.length) await this.emitOnTaskUpdate();
        let finalizeError;
        if (successfullyCompleted) {
            this.status = 'completed';
            await this.emitOnTaskUpdate();
        } else {
            this.status = 'error';
            const errorTask = this.latestErrorTask();
            const messageBase = errorTask?.errorMessage || (errorTask?.error ? String(errorTask.error) : 'Task execution failed');
            const stack = errorTask?.errorStack;
            const message = stack ? `${messageBase}\n${stack}` : messageBase;
            finalizeError = new TaskExecutionError(message, this, errorTask, {
                cause: errorTask?.error
            });
            await this.emitOnTaskUpdate(finalizeError);
        }
        if (finalizeError) throw finalizeError;
        if (this.tasks.length) {
            const outputIndex = Math.min(taskIndex, this.tasks.length - 1);
            const { thought, output } = this.tasks[outputIndex];
            return {
                thought,
                output
            };
        }
    }
    isInErrorState() {
        return 'error' === this.status;
    }
    latestErrorTask() {
        if ('error' !== this.status) return null;
        for(let i = this.tasks.length - 1; i >= 0; i--)if ('failed' === this.tasks[i].status) return this.tasks[i];
        return null;
    }
    dump() {
        return new ExecutionDump({
            logTime: Date.now(),
            name: this.name,
            tasks: this.tasks
        });
    }
    async appendErrorPlan(errorMsg) {
        const errorTask = {
            type: 'Action Space',
            subType: 'Error',
            param: {
                thought: errorMsg
            },
            thought: errorMsg,
            executor: async ()=>{
                throw new Error(errorMsg || 'error without thought');
            }
        };
        await this.appendAndFlush(errorTask);
        return {
            output: void 0,
            runner: this
        };
    }
    constructor(name, uiContextBuilder, options){
        _define_property(this, "name", void 0);
        _define_property(this, "tasks", void 0);
        _define_property(this, "status", void 0);
        _define_property(this, "onTaskStart", void 0);
        _define_property(this, "uiContextBuilder", void 0);
        _define_property(this, "onTaskUpdate", void 0);
        _define_property(this, "lastUiContext", void 0);
        this.status = options?.tasks && options.tasks.length > 0 ? 'pending' : 'init';
        this.name = name;
        this.tasks = (options?.tasks || []).map((item)=>this.markTaskAsPending(item));
        this.onTaskStart = options?.onTaskStart;
        this.uiContextBuilder = uiContextBuilder;
        this.onTaskUpdate = options?.onTaskUpdate;
    }
}
class TaskExecutionError extends Error {
    constructor(message, runner, errorTask, options){
        super(message, options), _define_property(this, "runner", void 0), _define_property(this, "errorTask", void 0);
        this.runner = runner;
        this.errorTask = errorTask;
    }
}
export { TaskExecutionError, TaskRunner };

//# sourceMappingURL=task-runner.mjs.map