import { existsSync, readFileSync, writeFileSync } from "node:fs";
import node_http from "node:http";
import { dirname, join, resolve as external_node_path_resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { ReportActionDump, runConnectivityTest } from "@midscene/core";
import { getTmpDir } from "@midscene/core/utils";
import { PLAYGROUND_SERVER_PORT } from "@midscene/shared/constants";
import { globalModelConfigManager, overrideAIConfig } from "@midscene/shared/env";
import { generateElementByPoint } from "@midscene/shared/extractor";
import { getDebug } from "@midscene/shared/logger";
import { uuid as utils_uuid } from "@midscene/shared/utils";
import express from "express";
import { executeAction, formatErrorMessage } from "./common.mjs";
import { createInterfaceMjpegHub, writeMjpegFrame } from "./mjpeg-hub.mjs";
import { buildRuntimeInfo } from "./runtime-metadata.mjs";
import "dotenv/config";
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 defaultPort = PLAYGROUND_SERVER_PORT;
function serializeAiConfigSignature(aiConfig) {
    return JSON.stringify(Object.entries(aiConfig).sort(([leftKey], [rightKey])=>leftKey.localeCompare(rightKey)));
}
function serializeZodField(field) {
    if (!field || 'object' != typeof field) return field;
    const def = field._def;
    if (!def || 'object' != typeof def) return field;
    const typeName = def.typeName;
    const result = {
        _def: {
            typeName
        }
    };
    if (def.description) result._def.description = def.description;
    if (def.innerType) result._def.innerType = serializeZodField(def.innerType);
    if ('ZodDefault' === typeName && 'function' == typeof def.defaultValue) try {
        result._def._serializedDefaultValue = def.defaultValue();
    } catch  {}
    if ('ZodEnum' === typeName && Array.isArray(def.values)) result._def.values = def.values;
    if ('ZodObject' === typeName) {
        const rawShape = 'function' == typeof def.shape ? def.shape() : def.shape;
        if (rawShape && 'object' == typeof rawShape) {
            const serializedShape = {};
            for (const [k, v] of Object.entries(rawShape))serializedShape[k] = serializeZodField(v);
            result._def.shape = serializedShape;
            result.shape = serializedShape;
        }
    }
    if (field.description) result.description = field.description;
    return result;
}
const server_filename = fileURLToPath(import.meta.url);
const server_dirname = dirname(server_filename);
const STATIC_PATH = join(server_dirname, '..', '..', 'static');
const debugScreenshot = getDebug('playground:screenshot', {
    console: true
});
const debugMjpeg = getDebug('playground:mjpeg', {
    console: true
});
class InteractParamsValidationError extends Error {
    constructor(message){
        super(message);
        this.name = 'InteractParamsValidationError';
    }
}
function requireNumber(value, field) {
    if ('number' != typeof value || Number.isNaN(value)) throw new InteractParamsValidationError(`${field} must be a number for this action`);
    return value;
}
function locateFromPoint(x, y, fieldX, fieldY, description) {
    return generateElementByPoint([
        Math.round(requireNumber(x, fieldX)),
        Math.round(requireNumber(y, fieldY))
    ], description);
}
const buildLocateActionParams = (body, actionType)=>{
    const params = {
        locate: locateFromPoint(body.x, body.y, 'x', 'y', `manual ${actionType}`)
    };
    if ('number' == typeof body.duration) params.duration = body.duration;
    return params;
};
const buildSwipeParams = (body)=>{
    const params = {
        start: locateFromPoint(body.x, body.y, 'x', 'y', 'manual swipe start'),
        end: locateFromPoint(body.endX, body.endY, 'endX', 'endY', 'manual swipe end')
    };
    if ('number' == typeof body.duration) params.duration = body.duration;
    if ('number' == typeof body.repeat) params.repeat = body.repeat;
    return params;
};
const buildDragAndDropParams = (body)=>({
        from: locateFromPoint(body.x, body.y, 'x', 'y', 'manual drag from'),
        to: locateFromPoint(body.endX, body.endY, 'endX', 'endY', 'manual drag to')
    });
const buildKeyboardPressParams = (body)=>{
    if ('string' != typeof body.keyName) throw new InteractParamsValidationError('keyName is required for KeyboardPress');
    const params = {
        keyName: body.keyName
    };
    if ('number' == typeof body.x && 'number' == typeof body.y) params.locate = locateFromPoint(body.x, body.y, 'x', 'y', 'manual keyboard press');
    return params;
};
const buildInputParams = (body)=>{
    if ('string' != typeof body.value) throw new InteractParamsValidationError('value is required for Input');
    const params = {
        value: body.value
    };
    if ('number' == typeof body.x && 'number' == typeof body.y) params.locate = locateFromPoint(body.x, body.y, 'x', 'y', 'manual input');
    if ('string' == typeof body.mode) params.mode = body.mode;
    if ('boolean' == typeof body.autoDismissKeyboard) params.autoDismissKeyboard = body.autoDismissKeyboard;
    return params;
};
function getManualInteractParamBuilder(actionType) {
    switch(actionType){
        case 'Tap':
        case 'DoubleClick':
        case 'RightClick':
        case 'Hover':
        case 'LongPress':
            return buildLocateActionParams;
        case 'Swipe':
            return buildSwipeParams;
        case 'DragAndDrop':
            return buildDragAndDropParams;
        case 'KeyboardPress':
            return buildKeyboardPressParams;
        case 'Input':
            return buildInputParams;
        default:
            return;
    }
}
function buildInteractParams(actionType, body) {
    const builder = getManualInteractParamBuilder(actionType);
    if (builder) return builder(body, actionType);
    const { actionType: _omit, ...passthrough } = body;
    return passthrough;
}
function createManualExecutorContext(actionType, param) {
    const task = {
        type: 'Action Space',
        subType: actionType,
        param,
        executor: async ()=>void 0,
        taskId: `manual-${utils_uuid()}`,
        status: 'running'
    };
    return {
        task
    };
}
const errorHandler = (err, req, res, next)=>{
    console.error(err);
    const errorMessage = err instanceof Error ? err.message : 'Internal server error';
    res.status(500).json({
        error: errorMessage
    });
};
const RECOVERABLE_PAGE_SESSION_ERROR_PATTERN = /Session closed|page has been closed|target closed|browser has been closed|Target page, context or browser has been closed/i;
function isRecoverablePageSessionError(error) {
    const message = error instanceof Error ? error.message : String(error);
    return RECOVERABLE_PAGE_SESSION_ERROR_PATTERN.test(message);
}
class PlaygroundServer {
    get agent() {
        return this._activeConnection.agent;
    }
    assertNoActiveSessionForBaseStateUpdate(methodName) {
        if (this._activeConnection.session) throw new Error(`${methodName} cannot update prepared state while a session is active`);
    }
    buildBaseRuntimeState() {
        if (!this._baseRuntimeState) return;
        return {
            ...this._baseRuntimeState,
            metadata: this.buildSessionMetadata()
        };
    }
    resetConnectionToBaseState() {
        this._activeConnection = {
            session: null,
            agent: this._activeConnection.agent,
            agentFactory: this._activeConnection.agentFactory,
            runtime: this.buildBaseRuntimeState(),
            executionHooks: this._baseExecutionHooks,
            sidecars: this._baseSidecars
        };
    }
    syncRuntimeState() {
        this._baseRuntimeState = {
            ...this._baseRuntimeState || {},
            metadata: this.buildSessionMetadata()
        };
        if (this._activeConnection.session) {
            this._activeConnection = {
                ...this._activeConnection,
                runtime: this._activeConnection.runtime ? {
                    ...this._activeConnection.runtime,
                    metadata: this.buildSessionMetadata()
                } : this.buildBaseRuntimeState()
            };
            return;
        }
        this.resetConnectionToBaseState();
    }
    restoreBaseSessionState() {
        this.taskExecutionDumps = {};
        this.currentTaskId = null;
        this.sessionSetupState = 'blocked' === this.sessionSetupState ? 'blocked' : 'required';
        this._activeConnection = {
            session: null,
            agent: null,
            agentFactory: null,
            runtime: this.buildBaseRuntimeState(),
            executionHooks: this._baseExecutionHooks,
            sidecars: this._baseSidecars
        };
        this.syncRuntimeState();
    }
    setPreparedPlatform(prepared) {
        if (this._activeConnection.session && this._activeConnection.agentFactory) this._activeConnection.session = null;
        this.assertNoActiveSessionForBaseStateUpdate('setPreparedPlatform');
        this.sessionManager = prepared.sessionManager;
        this._basePreparedMetadata = prepared.metadata ? {
            ...prepared.metadata
        } : void 0;
        this._baseRuntimeState = {
            platformId: prepared.platformId,
            title: prepared.title,
            description: prepared.description,
            preview: prepared.preview,
            metadata: this.buildSessionMetadata()
        };
        this._baseExecutionHooks = prepared.executionHooks;
        this._baseSidecars = prepared.sidecars;
        this.resetConnectionToBaseState();
        if (this.sessionManager && !this._activeConnection.agent && !this._activeConnection.session) {
            this.sessionSetupState = this._basePreparedMetadata?.setupState === 'blocked' ? 'blocked' : 'required';
            this.sessionSetupBlockingReason = 'string' == typeof this._basePreparedMetadata?.setupBlockingReason ? this._basePreparedMetadata.setupBlockingReason : void 0;
        }
    }
    setPreviewDescriptor(preview) {
        this.assertNoActiveSessionForBaseStateUpdate("setPreviewDescriptor");
        this._baseRuntimeState = {
            ...this._baseRuntimeState || {},
            preview
        };
        this.resetConnectionToBaseState();
    }
    setRuntimeMetadata(metadata) {
        this.assertNoActiveSessionForBaseStateUpdate('setRuntimeMetadata');
        this._basePreparedMetadata = metadata ? {
            ...metadata
        } : void 0;
        this.syncRuntimeState();
    }
    getRuntimeInfo() {
        return buildRuntimeInfo({
            platformId: this._activeConnection.runtime?.platformId,
            title: this._activeConnection.runtime?.title,
            platformDescription: this._activeConnection.runtime?.description,
            interfaceType: this._activeConnection.agent?.interface?.interfaceType || 'Unknown',
            interfaceDescription: this._activeConnection.agent?.interface?.describe?.() || void 0,
            preview: this._activeConnection.runtime?.preview,
            metadata: this.buildSessionMetadata(),
            supportsScreenshot: 'function' == typeof this._activeConnection.agent?.interface?.screenshotBase64,
            mjpegStreamUrl: this._activeConnection.agent?.interface?.mjpegStreamUrl,
            scrcpyPort: this.scrcpyPort
        });
    }
    getSessionInfo() {
        const connected = this.sessionManager ? Boolean(this._activeConnection.session?.connected && this._activeConnection.agent) : Boolean(this._activeConnection.agent);
        return {
            connected,
            displayName: this._activeConnection.session?.displayName,
            metadata: {
                ...this._activeConnection.session?.metadata || {}
            },
            setupState: this.sessionSetupState,
            setupBlockingReason: this.sessionSetupBlockingReason
        };
    }
    buildSessionMetadata() {
        const sessionConnected = this.sessionManager ? Boolean(this._activeConnection.session?.connected && this._activeConnection.agent) : Boolean(this._activeConnection.agent);
        return {
            ...this._basePreparedMetadata || {},
            ...this._activeConnection.session?.metadata || {},
            sessionConnected,
            sessionDisplayName: this._activeConnection.session?.displayName,
            setupState: this.sessionSetupState,
            ...this.sessionSetupBlockingReason ? {
                setupBlockingReason: this.sessionSetupBlockingReason
            } : {}
        };
    }
    async startSidecars(sidecars) {
        for (const sidecar of sidecars || [])await sidecar.start();
    }
    async stopSidecars(sidecars) {
        for (const sidecar of sidecars || [])await sidecar.stop?.();
    }
    getActiveAgentOrThrow() {
        if (!this._activeConnection.agent) throw new Error('No active session');
        return this._activeConnection.agent;
    }
    async destroyCurrentAgent() {
        if (!this._activeConnection.agent) return;
        try {
            if ('function' == typeof this._activeConnection.agent.destroy) await this._activeConnection.agent.destroy();
        } catch (error) {
            console.warn('Failed to destroy old agent:', error);
        } finally{
            this._activeConnection.agent = null;
            this._configDirty = false;
        }
    }
    async destroyCurrentSession() {
        const previousSession = this._activeConnection.session;
        const previousSidecars = this._activeConnection.sidecars;
        await this.destroyCurrentAgent();
        await this.stopSidecars(previousSidecars);
        if (this.sessionManager?.destroySession) await this.sessionManager.destroySession(previousSession || void 0);
        this.restoreBaseSessionState();
    }
    async applyCreatedSession(session) {
        if (!session.agent && !session.agentFactory) throw new Error('Session creation must provide either an agent or agentFactory');
        const sessionSidecars = session.sidecars || this._baseSidecars;
        await this.startSidecars(sessionSidecars);
        try {
            this._activeConnection = {
                session: {
                    connected: true,
                    displayName: session.displayName,
                    metadata: session.metadata ? {
                        ...session.metadata
                    } : {}
                },
                agent: session.agent || null,
                agentFactory: session.agentFactory || null,
                runtime: {
                    platformId: session.platformId ?? this._baseRuntimeState?.platformId,
                    title: session.title ?? this._baseRuntimeState?.title,
                    description: session.platformDescription ?? this._baseRuntimeState?.description,
                    preview: session.preview ?? this._baseRuntimeState?.preview,
                    metadata: session.metadata ? {
                        ...session.metadata
                    } : {}
                },
                executionHooks: session.executionHooks || this._baseExecutionHooks,
                sidecars: sessionSidecars
            };
            this.sessionSetupState = 'ready';
            this.sessionSetupBlockingReason = void 0;
            this.syncRuntimeState();
        } catch (error) {
            await this.stopSidecars(sessionSidecars).catch(()=>{});
            this.restoreBaseSessionState();
            throw error;
        }
    }
    async getSessionSetupSchema(input) {
        if (!this.sessionManager) return null;
        return this.sessionManager.getSetupSchema ? this.sessionManager.getSetupSchema(input) : null;
    }
    async getSessionTargets() {
        if (!this.sessionManager?.listTargets) return [];
        return this.sessionManager.listTargets();
    }
    get app() {
        return this._app;
    }
    initializeApp() {
        if (this._initialized) return;
        this._app.use(express.json({
            limit: '50mb'
        }));
        this._app.use((req, _res, next)=>{
            const { context } = req.body || {};
            if (this._activeConnection.agent && context && 'updateContext' in this._activeConnection.agent.interface && 'function' == typeof this._activeConnection.agent.interface.updateContext) {
                this._activeConnection.agent.interface.updateContext(context);
                console.log('Context updated by PlaygroundServer middleware');
            }
            next();
        });
        this.setupRoutes();
        this.setupStaticRoutes();
        this._app.use(errorHandler);
        this._initialized = true;
    }
    filePathForUuid(uuid) {
        if (!/^[a-zA-Z0-9-]+$/.test(uuid)) throw new Error('Invalid uuid format');
        const filePath = join(this.tmpDir, `${uuid}.json`);
        const resolvedPath = external_node_path_resolve(filePath);
        const resolvedTmpDir = external_node_path_resolve(this.tmpDir);
        if (!resolvedPath.startsWith(resolvedTmpDir)) throw new Error('Invalid path');
        return filePath;
    }
    saveContextFile(uuid, context) {
        const tmpFile = this.filePathForUuid(uuid);
        console.log(`save context file: ${tmpFile}`);
        writeFileSync(tmpFile, context);
        return tmpFile;
    }
    async recreateAgent() {
        this._agentReady = false;
        console.log('Recreating agent to cancel current task...');
        await this.destroyCurrentAgent();
        if (this._activeConnection.agentFactory) try {
            this._activeConnection.agent = await this._activeConnection.agentFactory();
            this._agentReady = true;
            console.log('Agent recreated successfully');
        } catch (error) {
            this._agentReady = true;
            console.error('Failed to recreate agent:', error);
            throw error;
        }
        else {
            this._agentReady = true;
            console.warn('Agent destroyed but cannot recreate: no factory function provided. Next /execute call will fail.');
        }
    }
    async recoverActiveAgentAfterPreviewError(error, reason) {
        if (!this._activeConnection.agentFactory || !isRecoverablePageSessionError(error)) return null;
        debugMjpeg(`Recovering active agent after ${reason}:`, error);
        try {
            this._interfaceMjpegHub.stopProducer();
            await this.recreateAgent();
            return this._activeConnection.agent;
        } catch (recreateError) {
            debugMjpeg(`Failed to recover active agent after ${reason}:`, recreateError);
            return null;
        }
    }
    findInteractAction(agent, actionType) {
        return agent.interface.actionSpace().find((entry)=>entry.name === actionType);
    }
    canRunBrowserChromeInteractAction(agent, actionType) {
        return 'Stop' === actionType && 'function' == typeof agent.interface.stopLoading;
    }
    async runBrowserChromeInteractAction(agent, actionType) {
        switch(actionType){
            case 'Stop':
                await agent.interface.stopLoading?.();
                return;
        }
    }
    async runInteractAction(agent, actionType, params) {
        if (this.canRunBrowserChromeInteractAction(agent, actionType)) return void await this.runBrowserChromeInteractAction(agent, actionType);
        const action = this.findInteractAction(agent, actionType);
        if (!action || 'function' != typeof action.call) throw new Error(`Action "${actionType}" is not available on the current device`);
        await action.call(params, createManualExecutorContext(actionType, params));
    }
    setupRoutes() {
        this._app.get('/status', async (req, res)=>{
            res.send({
                status: 'ok',
                id: this.id
            });
        });
        this._app.get('/session', async (_req, res)=>{
            res.json(this.getSessionInfo());
        });
        this._app.get('/session/setup', async (req, res)=>{
            try {
                const setup = await this.getSessionSetupSchema(Object.fromEntries(Object.entries(req.query).filter(([, value])=>'string' == typeof value)));
                if (!setup) return res.status(404).json({
                    error: 'Session setup is not available for this playground'
                });
                const targets = await this.getSessionTargets();
                res.json({
                    ...setup,
                    targets: targets.length > 0 ? targets : setup.targets
                });
            } catch (error) {
                res.status(500).json({
                    error: error instanceof Error ? error.message : 'Failed to load session setup'
                });
            }
        });
        this._app.get('/session/targets', async (_req, res)=>{
            try {
                res.json(await this.getSessionTargets());
            } catch (error) {
                res.status(500).json({
                    error: error instanceof Error ? error.message : 'Failed to load session targets'
                });
            }
        });
        this._app.post('/session', async (req, res)=>{
            if (!this.sessionManager) return res.status(404).json({
                error: 'Session creation is not available for this playground'
            });
            if (this.currentTaskId) return res.status(409).json({
                error: 'Cannot replace session while a task is running'
            });
            try {
                await this.destroyCurrentSession();
                const created = await this.sessionManager.createSession(req.body || {});
                await this.applyCreatedSession(created);
                if (!this._activeConnection.agent && this._activeConnection.agentFactory) this._activeConnection.agent = await this._activeConnection.agentFactory();
                if (this._configDirty && this._activeConnection.agentFactory) {
                    this._configDirty = false;
                    await this.recreateAgent();
                }
                res.json({
                    session: this.getSessionInfo(),
                    runtimeInfo: this.getRuntimeInfo()
                });
            } catch (error) {
                const failedSessionSidecars = this._activeConnection.session ? this._activeConnection.sidecars : void 0;
                await this.destroyCurrentAgent();
                await this.stopSidecars(failedSessionSidecars).catch(()=>{});
                this.restoreBaseSessionState();
                res.status(400).json({
                    error: error instanceof Error ? error.message : 'Failed to create session'
                });
            }
        });
        this._app.delete('/session', async (_req, res)=>{
            if (this.currentTaskId) return res.status(409).json({
                error: 'Cannot destroy session while a task is running'
            });
            try {
                await this.destroyCurrentSession();
                res.json({
                    session: this.getSessionInfo(),
                    runtimeInfo: this.getRuntimeInfo()
                });
            } catch (error) {
                res.status(500).json({
                    error: error instanceof Error ? error.message : 'Failed to destroy session'
                });
            }
        });
        this._app.get('/context/:uuid', async (req, res)=>{
            const { uuid } = req.params;
            let contextFile;
            try {
                contextFile = this.filePathForUuid(uuid);
            } catch  {
                return res.status(400).json({
                    error: 'Invalid uuid format'
                });
            }
            if (!existsSync(contextFile)) return res.status(404).json({
                error: 'Context not found'
            });
            const context = readFileSync(contextFile, 'utf8');
            res.json({
                context
            });
        });
        this._app.get('/task-progress/:requestId', async (req, res)=>{
            const { requestId } = req.params;
            const executionDump = this.taskExecutionDumps[requestId] || null;
            res.json({
                executionDump
            });
        });
        this._app.post('/action-space', async (req, res)=>{
            try {
                const agent = this.getActiveAgentOrThrow();
                let actionSpace = [];
                actionSpace = agent.interface.actionSpace();
                const processedActionSpace = actionSpace.map((action)=>{
                    if (action && 'object' == typeof action && 'paramSchema' in action) {
                        const typedAction = action;
                        if (typedAction.paramSchema && 'object' == typeof typedAction.paramSchema) {
                            let processedSchema = null;
                            try {
                                if (typedAction.paramSchema.shape && 'object' == typeof typedAction.paramSchema.shape) {
                                    const rawShape = typedAction.paramSchema.shape;
                                    const serializedShape = {};
                                    for (const [key, field] of Object.entries(rawShape))serializedShape[key] = serializeZodField(field);
                                    processedSchema = {
                                        type: 'ZodObject',
                                        shape: serializedShape
                                    };
                                }
                            } catch (e) {
                                const actionName = 'name' in typedAction && 'string' == typeof typedAction.name ? typedAction.name : 'unknown';
                                console.warn('Failed to process paramSchema for action:', actionName, e);
                            }
                            return {
                                ...typedAction,
                                paramSchema: processedSchema
                            };
                        }
                    }
                    return action;
                });
                res.json(processedActionSpace);
            } catch (error) {
                const errorMessage = error instanceof Error ? error.message : 'Unknown error';
                console.error('Failed to get action space:', error);
                res.status('No active session' === errorMessage ? 409 : 500).json({
                    error: errorMessage
                });
            }
        });
        this._app.post('/playground-with-context', async (req, res)=>{
            const context = req.body.context;
            if (!context) return res.status(400).json({
                error: 'context is required'
            });
            const requestId = utils_uuid();
            this.saveContextFile(requestId, context);
            return res.json({
                location: `/playground/${requestId}`,
                uuid: requestId
            });
        });
        this._app.post('/execute', async (req, res)=>{
            let agent;
            try {
                agent = this.getActiveAgentOrThrow();
            } catch (error) {
                return res.status(409).json({
                    error: error instanceof Error ? error.message : 'No active session'
                });
            }
            const { type, prompt, params, requestId, deepLocate, deepThink, screenshotIncluded, domIncluded, deviceOptions } = req.body;
            if (!type) return res.status(400).json({
                error: 'type is required'
            });
            if (this._activeConnection.agentFactory && this._configDirty) {
                this._configDirty = false;
                this._agentReady = false;
                console.log('AI config changed, recreating agent...');
                try {
                    await this.destroyCurrentAgent();
                    this._activeConnection.agent = await this._activeConnection.agentFactory();
                    agent = this.getActiveAgentOrThrow();
                    this._agentReady = true;
                    console.log('Agent recreated with new config');
                } catch (error) {
                    this._agentReady = true;
                    console.error('Failed to recreate agent:', error);
                    return res.status(500).json({
                        error: `Failed to create agent: ${error instanceof Error ? error.message : 'Unknown error'}`
                    });
                }
            }
            if (deviceOptions) {
                const iface = agent.interface;
                iface.options = {
                    ...iface.options || {},
                    ...deviceOptions
                };
            }
            if (this.currentTaskId) return res.status(409).json({
                error: 'Another task is already running',
                currentTaskId: this.currentTaskId
            });
            if (requestId) {
                this.currentTaskId = requestId;
                this.taskExecutionDumps[requestId] = null;
                agent.onDumpUpdate = (_dump, executionDump)=>{
                    if (executionDump) this.taskExecutionDumps[requestId] = executionDump;
                };
            }
            const response = {
                result: null,
                dump: null,
                error: null,
                reportHTML: null,
                requestId
            };
            const startTime = Date.now();
            try {
                await this._activeConnection.executionHooks?.beforeExecute?.();
                const actionSpace = agent.interface.actionSpace();
                const value = {
                    type,
                    prompt,
                    params
                };
                response.result = await executeAction(agent, type, actionSpace, value, {
                    deepLocate,
                    deepThink,
                    screenshotIncluded,
                    domIncluded,
                    deviceOptions
                });
            } catch (error) {
                response.error = formatErrorMessage(error);
            } finally{
                try {
                    await this._activeConnection.executionHooks?.afterExecute?.();
                } catch (hookError) {
                    console.error('Failed to run execution after hook:', hookError);
                }
            }
            try {
                const dumpString = agent.dumpDataString({
                    inlineScreenshots: true
                });
                if (dumpString) {
                    const groupedDump = ReportActionDump.fromSerializedString(dumpString);
                    response.dump = groupedDump.executions?.[0] || null;
                } else response.dump = null;
                response.reportHTML = agent.reportHTMLString({
                    inlineScreenshots: true
                }) || null;
                agent.writeOutActionDumps();
                agent.resetDump();
            } catch (error) {
                const errorMessage = error instanceof Error ? error.message : 'Unknown error';
                console.error(`write out dump failed: requestId: ${requestId}, ${errorMessage}`);
            } finally{}
            res.send(response);
            const timeCost = Date.now() - startTime;
            if (response.error) console.error(`handle request failed after ${timeCost}ms: requestId: ${requestId}, ${response.error}`);
            else console.log(`handle request done after ${timeCost}ms: requestId: ${requestId}`);
            if (requestId) {
                delete this.taskExecutionDumps[requestId];
                if (this.currentTaskId === requestId) this.currentTaskId = null;
            }
        });
        this._app.post('/cancel/:requestId', async (req, res)=>{
            const { requestId } = req.params;
            if (!requestId) return res.status(400).json({
                error: 'requestId is required'
            });
            try {
                const agent = this.getActiveAgentOrThrow();
                if (this.currentTaskId !== requestId) return res.json({
                    status: 'not_found',
                    message: 'Task not found or already completed'
                });
                console.log(`Cancelling task: ${requestId}`);
                let dump = null;
                let reportHTML = null;
                try {
                    const dumpString = agent.dumpDataString?.({
                        inlineScreenshots: true
                    });
                    if (dumpString) {
                        const groupedDump = ReportActionDump.fromSerializedString(dumpString);
                        dump = groupedDump.executions?.[0] || null;
                    }
                    reportHTML = agent.reportHTMLString?.({
                        inlineScreenshots: true
                    }) || null;
                } catch (error) {
                    console.warn('Failed to get execution data before cancel:', error);
                }
                try {
                    await this.recreateAgent();
                } catch (error) {
                    console.warn('Failed to recreate agent during cancel:', error);
                }
                delete this.taskExecutionDumps[requestId];
                this.currentTaskId = null;
                res.json({
                    status: 'cancelled',
                    message: 'Task cancelled successfully',
                    dump,
                    reportHTML
                });
            } catch (error) {
                const errorMessage = error instanceof Error ? error.message : 'Unknown error';
                console.error(`Failed to cancel: ${errorMessage}`);
                res.status('No active session' === errorMessage ? 409 : 500).json({
                    error: `Failed to cancel: ${errorMessage}`
                });
            }
        });
        this._app.get('/screenshot', async (_req, res)=>{
            try {
                let agent = this.getActiveAgentOrThrow();
                if ('function' != typeof agent.interface.screenshotBase64) return res.status(500).json({
                    error: 'Screenshot method not available on current interface'
                });
                let screenshot;
                try {
                    screenshot = await agent.interface.screenshotBase64();
                } catch (error) {
                    const recoveredAgent = await this.recoverActiveAgentAfterPreviewError(error, 'screenshot capture');
                    if (!recoveredAgent || 'function' != typeof recoveredAgent.interface.screenshotBase64) throw error;
                    agent = recoveredAgent;
                    screenshot = await agent.interface.screenshotBase64();
                }
                res.json({
                    screenshot,
                    timestamp: Date.now()
                });
            } catch (error) {
                const errorMessage = error instanceof Error ? error.message : 'Unknown error';
                const statusCode = 'No active session' === errorMessage ? 409 : 500;
                if (409 !== statusCode) console.error(`Failed to take screenshot: ${errorMessage}`);
                res.status(statusCode).json({
                    error: `Failed to take screenshot: ${errorMessage}`
                });
            }
        });
        this._app.get('/mjpeg', async (req, res)=>{
            const agent = this._activeConnection.agent;
            if (!agent) return res.status(409).json({
                error: 'No active session'
            });
            const nativeUrl = agent.interface?.mjpegStreamUrl;
            const recentlyFailed = false === this._nativeMjpegAvailable && null !== this._nativeMjpegFailedAt && Date.now() - this._nativeMjpegFailedAt < PlaygroundServer.MJPEG_NEGATIVE_CACHE_MS;
            if (nativeUrl && !recentlyFailed) {
                const proxyOk = await this.probeAndProxyNativeMjpeg(nativeUrl, req, res);
                if (proxyOk) return;
            }
            const interfaceStreamStarted = await this._interfaceMjpegHub.streamRequest(req, res, agent.interface, async (startupError)=>(await this.recoverActiveAgentAfterPreviewError(startupError, 'interface MJPEG startup'))?.interface ?? null);
            if (interfaceStreamStarted) return;
            const fallbackAgent = this._activeConnection.agent;
            if ('function' != typeof fallbackAgent?.interface?.screenshotBase64) return res.status(500).json({
                error: 'Screenshot method not available on current interface'
            });
            await this.startPollingMjpegStream(req, res);
        });
        this._app.get('/interface-info', async (_req, res)=>{
            try {
                const runtimeInfo = this.getRuntimeInfo();
                const agent = this._activeConnection.agent;
                let size;
                let navigationState;
                let actionTypes;
                if ('function' == typeof agent?.interface?.size) try {
                    size = await agent.interface.size();
                } catch (error) {
                    debugScreenshot('interface size() failed:', error);
                }
                if ('function' == typeof agent?.interface?.navigationState) try {
                    navigationState = await agent.interface.navigationState();
                } catch (error) {
                    debugScreenshot('interface navigationState() failed:', error);
                }
                if ('function' == typeof agent?.interface?.actionSpace) try {
                    const actions = agent.interface.actionSpace();
                    actionTypes = Array.isArray(actions) ? actions.map((action)=>action?.name).filter((name)=>'string' == typeof name) : void 0;
                } catch (error) {
                    debugScreenshot('interface actionSpace() failed:', error);
                }
                res.json({
                    type: runtimeInfo.interface.type,
                    description: runtimeInfo.interface.description,
                    ...size ? {
                        size
                    } : {},
                    ...navigationState ? {
                        navigationState
                    } : {},
                    ...actionTypes ? {
                        actionTypes
                    } : {}
                });
            } catch (error) {
                const errorMessage = error instanceof Error ? error.message : 'Unknown error';
                console.error(`Failed to get interface info: ${errorMessage}`);
                res.status(500).json({
                    error: `Failed to get interface info: ${errorMessage}`
                });
            }
        });
        this._app.post('/interact', async (req, res)=>{
            let agent;
            try {
                agent = this.getActiveAgentOrThrow();
            } catch (error) {
                return res.status(409).json({
                    error: error instanceof Error ? error.message : 'No active session'
                });
            }
            const { actionType } = req.body ?? {};
            if ('string' != typeof actionType || !actionType) return res.status(400).json({
                error: 'actionType is required'
            });
            if (!this.findInteractAction(agent, actionType) && !this.canRunBrowserChromeInteractAction(agent, actionType)) return res.status(404).json({
                error: `Action "${actionType}" is not available on the current device`
            });
            let params;
            try {
                params = buildInteractParams(actionType, req.body ?? {});
            } catch (error) {
                if (error instanceof InteractParamsValidationError) return res.status(400).json({
                    error: error.message
                });
                const errorMessage = error instanceof Error ? error.message : 'Unknown error';
                console.error(`Failed to build interact params for "${actionType}": ${errorMessage}`);
                return res.status(500).json({
                    error: errorMessage
                });
            }
            try {
                await this.runInteractAction(agent, actionType, params);
                res.json({});
            } catch (error) {
                const recoveredAgent = await this.recoverActiveAgentAfterPreviewError(error, `manual interact action "${actionType}"`);
                if (recoveredAgent) return res.status(409).json({
                    error: 'The page session was closed and has been recreated. Please retry the action.'
                });
                const errorMessage = error instanceof Error ? error.message : 'Unknown error';
                console.error(`Failed to run interact action "${actionType}": ${errorMessage}`);
                res.status(500).json({
                    error: errorMessage
                });
            }
        });
        this._app.get('/runtime-info', async (_req, res)=>{
            try {
                res.json(this.getRuntimeInfo());
            } catch (error) {
                const errorMessage = error instanceof Error ? error.message : 'Unknown error';
                console.error(`Failed to get runtime info: ${errorMessage}`);
                res.status(500).json({
                    error: `Failed to get runtime info: ${errorMessage}`
                });
            }
        });
        this.app.post('/config', async (req, res)=>{
            const { aiConfig } = req.body;
            if (!aiConfig || 'object' != typeof aiConfig) return res.status(400).json({
                error: 'aiConfig is required and must be an object'
            });
            if (0 === Object.keys(aiConfig).length) return res.json({
                status: 'ok',
                message: 'AI config not changed due to empty object'
            });
            const nextConfigSignature = serializeAiConfigSignature(aiConfig);
            const configChanged = nextConfigSignature !== this._lastAiConfigSignature;
            try {
                if (configChanged) {
                    overrideAIConfig(aiConfig);
                    this._lastAiConfigSignature = nextConfigSignature;
                    this._configDirty = Boolean(this._activeConnection.agent);
                }
            } catch (error) {
                const errorMessage = error instanceof Error ? error.message : 'Unknown error';
                console.error(`Failed to update AI config: ${errorMessage}`);
                return res.status(500).json({
                    error: `Failed to update AI config: ${errorMessage}`
                });
            }
            if (!configChanged) return res.json({
                status: 'ok',
                message: 'AI config not changed because it is identical to current'
            });
            try {
                globalModelConfigManager.getModelConfig('default');
            } catch (error) {
                const errorMessage = error instanceof Error ? error.message : 'Unknown error';
                console.error(`AI config validation failed: ${errorMessage}`);
                return res.status(400).json({
                    error: errorMessage
                });
            }
            return res.json({
                status: 'ok',
                message: this._configDirty ? 'AI config updated. Agent will be recreated on next execution.' : 'AI config updated. New sessions will use it immediately.'
            });
        });
        this.app.post('/connectivity-test', async (_req, res)=>{
            try {
                const result = await runConnectivityTest({
                    defaultModelConfig: globalModelConfigManager.getModelConfig('default'),
                    planningModelConfig: globalModelConfigManager.getModelConfig('planning'),
                    insightModelConfig: globalModelConfigManager.getModelConfig('insight')
                });
                return res.json(result);
            } catch (error) {
                const errorMessage = error instanceof Error ? error.message : 'Unknown error';
                console.error(`Connectivity test failed: ${errorMessage}`);
                return res.status(500).json({
                    error: errorMessage
                });
            }
        });
    }
    probeAndProxyNativeMjpeg(nativeUrl, req, res) {
        return new Promise((resolve)=>{
            console.log(`MJPEG: trying native stream from ${nativeUrl}`);
            const proxyReq = node_http.get(nativeUrl, (proxyRes)=>{
                const statusCode = proxyRes.statusCode ?? 0;
                if (statusCode >= 400) {
                    this._nativeMjpegAvailable = false;
                    this._nativeMjpegFailedAt = Date.now();
                    proxyRes.resume();
                    debugMjpeg(`native stream returned HTTP ${statusCode}, using polling mode`);
                    resolve(false);
                    return;
                }
                this._nativeMjpegAvailable = true;
                this._nativeMjpegFailedAt = null;
                console.log('MJPEG: streaming via native WDA MJPEG server');
                const contentType = proxyRes.headers['content-type'];
                if (contentType) res.setHeader('Content-Type', contentType);
                res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
                res.setHeader('Connection', 'keep-alive');
                proxyRes.pipe(res);
                req.on('close', ()=>proxyReq.destroy());
                resolve(true);
            });
            proxyReq.on('error', (err)=>{
                this._nativeMjpegAvailable = false;
                this._nativeMjpegFailedAt = Date.now();
                debugMjpeg(`MJPEG: native stream unavailable (${err.message}), using polling mode`);
                resolve(false);
            });
        });
    }
    probeNativeMjpegLiveness(nativeUrl) {
        return new Promise((resolve)=>{
            const probe = node_http.get(nativeUrl, (probeRes)=>{
                const statusCode = probeRes.statusCode ?? 0;
                const reachable = statusCode >= 200 && statusCode < 400;
                probeRes.destroy();
                resolve(reachable);
            });
            probe.setTimeout(1000, ()=>{
                probe.destroy();
                resolve(false);
            });
            probe.on('error', ()=>resolve(false));
        });
    }
    async startPollingMjpegStream(req, res) {
        const defaultMjpegFps = 10;
        const maxMjpegFps = 30;
        const maxErrorBackoffMs = 3000;
        const errorLogThreshold = 3;
        const nativeProbeIntervalMs = 3000;
        const parsedFps = Number(req.query.fps);
        const fps = Math.min(Math.max(Number.isNaN(parsedFps) ? defaultMjpegFps : parsedFps, 1), maxMjpegFps);
        const interval = Math.round(1000 / fps);
        const boundary = 'mjpeg-boundary';
        console.log(`MJPEG: streaming via polling mode (${fps}fps)`);
        res.setHeader('Content-Type', `multipart/x-mixed-replace; boundary=${boundary}`);
        res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
        res.setHeader('Connection', 'keep-alive');
        let stopped = false;
        let consecutiveErrors = 0;
        const nativeUrl = this._activeConnection.agent?.interface?.mjpegStreamUrl;
        let probeTimer;
        if (nativeUrl) probeTimer = setInterval(async ()=>{
            if (stopped) return;
            const reachable = await this.probeNativeMjpegLiveness(nativeUrl);
            if (reachable && !stopped) {
                console.log('MJPEG: native stream came online, ending polling so client reconnects');
                this._nativeMjpegAvailable = true;
                this._nativeMjpegFailedAt = null;
                stopped = true;
                try {
                    res.destroy();
                } catch  {}
            }
        }, nativeProbeIntervalMs);
        req.on('close', ()=>{
            stopped = true;
            if (probeTimer) clearInterval(probeTimer);
        });
        while(!stopped){
            if (!this._agentReady) {
                await new Promise((r)=>setTimeout(r, 200));
                continue;
            }
            const frameStart = Date.now();
            try {
                const agent = this.getActiveAgentOrThrow();
                const base64 = await agent.interface.screenshotBase64();
                if (stopped) break;
                consecutiveErrors = 0;
                writeMjpegFrame(res, boundary, {
                    data: base64,
                    contentType: 'image/jpeg'
                });
            } catch (err) {
                if (stopped) break;
                const recoveredAgent = await this.recoverActiveAgentAfterPreviewError(err, 'polling MJPEG frame capture');
                if (recoveredAgent) {
                    consecutiveErrors = 0;
                    continue;
                }
                consecutiveErrors++;
                if (consecutiveErrors <= errorLogThreshold) console.error('MJPEG frame error:', err);
                else if (consecutiveErrors === errorLogThreshold + 1) console.error('MJPEG: suppressing further errors, retrying silently...');
                const backoff = Math.min(1000 * consecutiveErrors, maxErrorBackoffMs);
                await new Promise((r)=>setTimeout(r, backoff));
                continue;
            }
            const elapsed = Date.now() - frameStart;
            const remaining = interval - elapsed;
            if (remaining > 0) await new Promise((r)=>setTimeout(r, remaining));
        }
        if (probeTimer) clearInterval(probeTimer);
    }
    setupStaticRoutes() {
        this._app.get('/', (_req, res)=>{
            this.serveHtmlWithPorts(res);
        });
        this._app.get('/index.html', (_req, res)=>{
            this.serveHtmlWithPorts(res);
        });
        this._app.use(express["static"](this.staticPath));
        this._app.get('*', (_req, res)=>{
            this.serveHtmlWithPorts(res);
        });
    }
    serveHtmlWithPorts(res) {
        try {
            const htmlPath = join(this.staticPath, 'index.html');
            let html = readFileSync(htmlPath, 'utf8');
            const scrcpyPort = this.scrcpyPort ?? this.port + 1;
            const configScript = `
        <script>
          window.SCRCPY_PORT = ${scrcpyPort};
        </script>
      `;
            html = html.replace('</head>', `${configScript}</head>`);
            res.setHeader('Content-Type', 'text/html');
            res.send(html);
        } catch (error) {
            console.error('Error serving HTML with ports:', error);
            res.status(500).send('Internal Server Error');
        }
    }
    async launch(port) {
        if (this._activeConnection.agentFactory && !this.sessionManager) {
            console.log('Initializing agent from factory function...');
            this._activeConnection.agent = await this._activeConnection.agentFactory();
            this._activeConnection.session = {
                connected: true,
                metadata: {}
            };
            this.sessionSetupState = 'ready';
            this.syncRuntimeState();
            console.log('Agent initialized successfully');
        }
        this.initializeApp();
        this.port = port || defaultPort;
        return new Promise((resolve)=>{
            const serverPort = this.port ?? defaultPort;
            this.server = this._app.listen(serverPort, '0.0.0.0', ()=>{
                resolve(this);
            });
        });
    }
    async close() {
        await this.destroyCurrentSession().catch((error)=>{
            console.warn('Failed to destroy current session during shutdown:', error);
        });
        return new Promise((resolve, reject)=>{
            if (this.server) {
                this.taskExecutionDumps = {};
                this.server.close((error)=>{
                    if (error) reject(error);
                    else {
                        this.server = void 0;
                        resolve();
                    }
                });
            } else resolve();
        });
    }
    constructor(agent, staticPath = STATIC_PATH, id){
        _define_property(this, "_app", void 0);
        _define_property(this, "tmpDir", void 0);
        _define_property(this, "server", void 0);
        _define_property(this, "port", void 0);
        _define_property(this, "staticPath", void 0);
        _define_property(this, "taskExecutionDumps", void 0);
        _define_property(this, "id", void 0);
        _define_property(this, "scrcpyPort", void 0);
        _define_property(this, "_initialized", false);
        _define_property(this, "_nativeMjpegAvailable", null);
        _define_property(this, "_nativeMjpegFailedAt", null);
        _define_property(this, "_interfaceMjpegHub", createInterfaceMjpegHub({
            initialFrameTimeoutMs: PlaygroundServer.INTERFACE_MJPEG_INITIAL_FRAME_TIMEOUT_MS,
            idleStopMs: PlaygroundServer.INTERFACE_MJPEG_IDLE_STOP_MS,
            debug: debugMjpeg
        }));
        _define_property(this, "sessionManager", void 0);
        _define_property(this, "sessionSetupState", 'ready');
        _define_property(this, "sessionSetupBlockingReason", void 0);
        _define_property(this, "currentTaskId", null);
        _define_property(this, "_agentReady", true);
        _define_property(this, "_configDirty", false);
        _define_property(this, "_lastAiConfigSignature", null);
        _define_property(this, "_baseRuntimeState", void 0);
        _define_property(this, "_basePreparedMetadata", void 0);
        _define_property(this, "_baseExecutionHooks", void 0);
        _define_property(this, "_baseSidecars", void 0);
        _define_property(this, "_activeConnection", {
            session: null,
            agent: null,
            agentFactory: null,
            runtime: void 0,
            executionHooks: void 0,
            sidecars: void 0
        });
        this._app = express();
        this.tmpDir = getTmpDir();
        this.staticPath = staticPath;
        this.taskExecutionDumps = {};
        this.id = id || utils_uuid();
        if ('function' == typeof agent) this._activeConnection.agentFactory = agent;
        else this._activeConnection.agent = agent || null;
    }
}
_define_property(PlaygroundServer, "MJPEG_NEGATIVE_CACHE_MS", 10000);
_define_property(PlaygroundServer, "INTERFACE_MJPEG_INITIAL_FRAME_TIMEOUT_MS", 1500);
_define_property(PlaygroundServer, "INTERFACE_MJPEG_IDLE_STOP_MS", 2000);
const server = PlaygroundServer;
export { InteractParamsValidationError, PlaygroundServer, buildInteractParams, createManualExecutorContext, server as default, serializeZodField };

//# sourceMappingURL=server.mjs.map