import { exec } from "node:child_process";
import node_path from "node:path";
import { promisify } from "node:util";
import { select as prompts_select } from "@inquirer/prompts";
import { AndroidAgent, AndroidDevice, getConnectedDevices } from "@midscene/android";
import { playgroundForAgentFactory } from "@midscene/playground";
import { PLAYGROUND_SERVER_PORT, SCRCPY_SERVER_PORT } from "@midscene/shared/constants";
import { findAvailablePort } from "@midscene/shared/node";
import { createReadStream } from "node:fs";
import { createServer } from "node:http";
import { fileURLToPath } from "node:url";
import { getDebug } from "@midscene/shared/logger";
import cors from "cors";
import express from "express";
import { Server } from "socket.io";
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 debugPage = getDebug('android:playground');
const promiseExec = promisify(exec);
const LOOPBACK_HOSTS = new Set([
    'localhost',
    '127.0.0.1',
    '::1',
    '[::1]'
]);
function isLoopbackOrigin(origin) {
    if (!origin) return true;
    try {
        const url = new URL(origin);
        return LOOPBACK_HOSTS.has(url.hostname);
    } catch  {
        return false;
    }
}
class ScrcpyServer {
    setupApiRoutes() {
        this.app.get('/api/devices', async (req, res)=>{
            try {
                const devices = await this.getDevicesList();
                res.json({
                    devices,
                    currentDeviceId: this.currentDeviceId
                });
            } catch (error) {
                res.status(500).json({
                    error: error.message || 'Failed to get devices list'
                });
            }
        });
    }
    async getDevicesList() {
        try {
            debugPage('start to get devices list');
            const client = await this.getAdbClient();
            if (!client) {
                console.warn('failed to get adb client');
                return [];
            }
            debugPage('success to get adb client, start to request devices list');
            let devices;
            try {
                devices = await client.getDevices();
                debugPage('original devices list:', devices);
            } catch (error) {
                console.error('failed to get devices list:', error);
                return [];
            }
            if (!devices || 0 === devices.length) return [];
            const formattedDevices = devices.map((device)=>{
                const result = {
                    id: device.serial,
                    name: device.product || device.model || device.serial,
                    status: device.state || 'device'
                };
                return result;
            });
            return formattedDevices;
        } catch (error) {
            console.error('failed to get devices list:', error);
            return [];
        }
    }
    async getAdbClient() {
        const { AdbServerClient } = await import("@yume-chan/adb");
        const { AdbServerNodeTcpConnector } = await import("@yume-chan/adb-server-node-tcp");
        try {
            if (this.adbClient) debugPage('use existing adb client');
            else {
                await promiseExec('adb start-server');
                debugPage('adb server started');
                debugPage('initialize adb client');
                this.adbClient = new AdbServerClient(new AdbServerNodeTcpConnector({
                    host: '127.0.0.1',
                    port: 5037
                }));
                await debugPage('success to initialize adb client');
            }
            return this.adbClient;
        } catch (error) {
            console.error('failed to get adb client:', error);
            return null;
        }
    }
    async getAdb(deviceId) {
        const { Adb } = await import("@yume-chan/adb");
        try {
            const client = await this.getAdbClient();
            if (!client) return null;
            const targetDeviceId = deviceId || this.currentDeviceId;
            if (targetDeviceId) {
                this.currentDeviceId = targetDeviceId;
                return new Adb(await client.createTransport({
                    serial: targetDeviceId
                }));
            }
            const devices = await client.getDevices();
            if (0 === devices.length) return null;
            this.currentDeviceId = devices[0].serial;
            return new Adb(await client.createTransport(devices[0]));
        } catch (error) {
            console.error('failed to get adb client:', error);
            return null;
        }
    }
    async startScrcpy(adb, options = {}) {
        const { AdbScrcpyClient, AdbScrcpyOptions3_3_3 } = await import("@yume-chan/adb-scrcpy");
        const { ReadableStream } = await import("@yume-chan/stream-extra");
        const { DefaultServerPath } = await import("@yume-chan/scrcpy");
        const currentDir = 'undefined' != typeof __dirname ? __dirname : node_path.dirname(fileURLToPath(import.meta.url));
        const serverBinPath = node_path.resolve(currentDir, '../../bin/scrcpy-server');
        try {
            await AdbScrcpyClient.pushServer(adb, ReadableStream.from(createReadStream(serverBinPath)));
            const scrcpyOptions = new AdbScrcpyOptions3_3_3({
                audio: false,
                control: true,
                maxSize: 1024,
                videoBitRate: 2000000,
                ...options
            });
            return await AdbScrcpyClient.start(adb, DefaultServerPath, scrcpyOptions);
        } catch (error) {
            console.error('failed to start scrcpy:', error);
            throw error;
        }
    }
    setupSocketHandlers() {
        this.io.on('connection', async (socket)=>{
            debugPage('client connected, id: %s, client address: %s', socket.id, socket.handshake.address);
            let scrcpyClient = null;
            let adb = null;
            const sendDevicesList = async ()=>{
                try {
                    debugPage('Socket request to get devices list');
                    const devices = await this.getDevicesList();
                    debugPage('send devices list to client:', devices);
                    socket.emit('devices-list', {
                        devices,
                        currentDeviceId: this.currentDeviceId
                    });
                } catch (error) {
                    console.error('failed to send devices list:', error);
                    socket.emit('error', {
                        message: 'failed to get devices list'
                    });
                }
            };
            await sendDevicesList();
            socket.on('get-devices', async ()=>{
                debugPage('received client request to get devices list');
                await sendDevicesList();
            });
            socket.on('switch-device', async (deviceId)=>{
                debugPage('received client request to switch device:', deviceId);
                try {
                    if (scrcpyClient) {
                        await scrcpyClient.close();
                        scrcpyClient = null;
                    }
                    this.currentDeviceId = deviceId;
                    debugPage('device switched to:', deviceId);
                    socket.emit('device-switched', {
                        deviceId
                    });
                    this.io.emit('global-device-switched', {
                        deviceId,
                        timestamp: Date.now()
                    });
                } catch (error) {
                    console.error('failed to switch device:', error);
                    socket.emit('error', {
                        message: `Failed to switch device: ${error?.message || 'Unknown error'}`
                    });
                }
            });
            socket.on('connect-device', async (options)=>{
                const { ScrcpyVideoCodecId } = await import("@yume-chan/scrcpy");
                try {
                    debugPage('received device connection request, options: %s, client id: %s', options, socket.id);
                    adb = await this.getAdb(this.currentDeviceId || void 0);
                    if (!adb) {
                        console.error('no available device found');
                        socket.emit('error', {
                            message: 'No device found'
                        });
                        return;
                    }
                    debugPage('starting scrcpy service, device id: %s', this.currentDeviceId);
                    scrcpyClient = await this.startScrcpy(adb, options);
                    debugPage('scrcpy service started successfully');
                    debugPage('check scrcpyClient object structure: %s', Object.getOwnPropertyNames(scrcpyClient).map((name)=>{
                        const type = typeof scrcpyClient[name];
                        const isPromise = 'object' === type && scrcpyClient[name] && 'function' == typeof scrcpyClient[name].then;
                        return `${name}: ${type}${isPromise ? ' (Promise)' : ''}`;
                    }));
                    try {
                        if (scrcpyClient.videoStream) {
                            debugPage('videoStream exists, type: %s', typeof scrcpyClient.videoStream);
                            let videoStream;
                            if ('object' == typeof scrcpyClient.videoStream && 'function' == typeof scrcpyClient.videoStream.then) {
                                debugPage('videoStream is a Promise, waiting for resolution...');
                                videoStream = await scrcpyClient.videoStream;
                            } else {
                                debugPage('videoStream is not a Promise, directly use');
                                videoStream = scrcpyClient.videoStream;
                            }
                            debugPage('video stream fetched successfully, metadata: %s', videoStream.metadata);
                            const metadata = videoStream.metadata || {};
                            debugPage('original metadata: %s', metadata);
                            if (!metadata.codec) {
                                debugPage('metadata does not have codec field, use H264 by default');
                                metadata.codec = ScrcpyVideoCodecId.H264;
                            }
                            if (!metadata.width || !metadata.height) {
                                debugPage('metadata does not have width or height field, use default values');
                                metadata.width = metadata.width || 1080;
                                metadata.height = metadata.height || 1920;
                            }
                            debugPage('prepare to send video-metadata event to client, data: %s', JSON.stringify(metadata));
                            socket.emit('video-metadata', metadata);
                            debugPage('video-metadata event sent to client, id: %s', socket.id);
                            const { stream } = videoStream;
                            const reader = stream.getReader();
                            const processStream = async ()=>{
                                try {
                                    while(true){
                                        const { done, value } = await reader.read();
                                        if (done) break;
                                        const frameType = value.type || 'data';
                                        socket.emit('video-data', {
                                            data: Array.from(value.data),
                                            type: frameType,
                                            timestamp: Date.now(),
                                            keyFrame: value.keyFrame
                                        });
                                    }
                                } catch (error) {
                                    console.error('error processing video stream:', error);
                                    socket.emit('error', {
                                        message: 'video stream processing error'
                                    });
                                }
                            };
                            processStream();
                        } else {
                            console.error('scrcpyClient object does not have videoStream property');
                            socket.emit('error', {
                                message: 'Video stream not available in scrcpy client'
                            });
                        }
                    } catch (error) {
                        console.error('error processing video stream:', error);
                        socket.emit('error', {
                            message: `Video stream processing error: ${error.message}`
                        });
                    }
                    if (scrcpyClient?.controller) socket.emit('control-ready');
                } catch (error) {
                    console.error('failed to connect device:', error);
                    socket.emit('error', {
                        message: `Failed to connect device: ${error?.message || 'Unknown error'}`
                    });
                }
            });
            socket.on('disconnect', async (reason)=>{
                debugPage('client disconnected, id: %s, reason: %s', socket.id, reason);
                if (scrcpyClient) {
                    try {
                        debugPage('closing scrcpy client');
                        await scrcpyClient.close();
                    } catch (error) {
                        console.error('failed to close scrcpy client:', error);
                    }
                    scrcpyClient = null;
                }
            });
        });
    }
    async launch(port) {
        this.port = port || this.defaultPort;
        return new Promise((resolve)=>{
            this.httpServer.listen(this.port, ()=>{
                console.log(`Scrcpy server running at: http://localhost:${this.port}`);
                this.startDeviceMonitoring();
                resolve(this);
            });
        });
    }
    startDeviceMonitoring() {
        this.devicePollInterval = setInterval(async ()=>{
            try {
                const devices = await this.getDevicesList();
                const currentDevicesJson = JSON.stringify(devices);
                if (this.lastDeviceList !== currentDevicesJson) {
                    debugPage('devices list changed, push to all connected clients');
                    this.lastDeviceList = currentDevicesJson;
                    if (!this.currentDeviceId && devices.length > 0) {
                        const onlineDevices = devices.filter((device)=>'device' === device.status.toLowerCase());
                        if (onlineDevices.length > 0) {
                            this.currentDeviceId = onlineDevices[0].id;
                            debugPage('auto select the first online device:', this.currentDeviceId);
                        }
                    }
                    this.io.emit('devices-list', {
                        devices,
                        currentDeviceId: this.currentDeviceId
                    });
                }
            } catch (error) {
                console.error('device monitoring error:', error);
            }
        }, 3000);
    }
    close() {
        if (this.devicePollInterval) {
            clearInterval(this.devicePollInterval);
            this.devicePollInterval = null;
        }
        if (this.httpServer) return this.httpServer.close();
    }
    constructor(){
        _define_property(this, "app", void 0);
        _define_property(this, "httpServer", void 0);
        _define_property(this, "io", void 0);
        _define_property(this, "port", void 0);
        _define_property(this, "defaultPort", SCRCPY_SERVER_PORT);
        _define_property(this, "adbClient", null);
        _define_property(this, "currentDeviceId", null);
        _define_property(this, "devicePollInterval", null);
        _define_property(this, "lastDeviceList", '');
        this.app = express();
        this.httpServer = createServer(this.app);
        this.io = new Server(this.httpServer, {
            cors: {
                origin (origin, callback) {
                    callback(null, isLoopbackOrigin(origin));
                },
                methods: [
                    'GET',
                    'POST'
                ],
                credentials: true
            }
        });
        this.app.use(cors({
            origin (origin, callback) {
                callback(null, isLoopbackOrigin(origin));
            },
            credentials: true
        }));
        this.setupSocketHandlers();
        this.setupApiRoutes();
    }
}
promisify(exec);
async function getAdbDevices() {
    try {
        const devices = await getConnectedDevices();
        return devices.filter((device)=>'device' === device.state).map((device)=>({
                id: device.udid,
                status: device.state,
                name: device.udid
            }));
    } catch (error) {
        console.error('Error getting ADB devices:', error);
        return [];
    }
}
async function selectDevice() {
    console.log('🔍 Scanning for Android devices...');
    const devices = await getAdbDevices();
    if (0 === devices.length) {
        console.error('❌ No Android devices found!');
        console.log('📱 Please ensure:');
        console.log('  • Your device is connected via USB');
        console.log('  • USB debugging is enabled');
        console.log('  • Device is authorized for debugging');
        process.exit(1);
    }
    if (1 === devices.length) {
        console.log(`📱 Found device: ${devices[0].name} (${devices[0].id})`);
        return devices[0].id;
    }
    const choices = devices.map((device)=>({
            name: `${device.name} (${device.id})`,
            value: device.id
        }));
    const selectedDevice = await prompts_select({
        message: '📱 Multiple devices found. Please select one:',
        choices
    });
    return selectedDevice;
}
const staticDir = node_path.join(__dirname, '../../static');
const main = async ()=>{
    const { default: open } = await import("open");
    try {
        const selectedDeviceId = await selectDevice();
        console.log(`✅ Selected device: ${selectedDeviceId}`);
        const scrcpyServer = new ScrcpyServer();
        scrcpyServer.currentDeviceId = selectedDeviceId;
        console.log('🚀 Starting servers...');
        const availablePlaygroundPort = await findAvailablePort(PLAYGROUND_SERVER_PORT);
        const availableScrcpyPort = await findAvailablePort(SCRCPY_SERVER_PORT);
        if (availablePlaygroundPort !== PLAYGROUND_SERVER_PORT) console.log(`⚠️  Port ${PLAYGROUND_SERVER_PORT} is busy, using port ${availablePlaygroundPort} instead`);
        if (availableScrcpyPort !== SCRCPY_SERVER_PORT) console.log(`⚠️  Port ${SCRCPY_SERVER_PORT} is busy, using port ${availableScrcpyPort} instead`);
        const [playgroundResult] = await Promise.all([
            playgroundForAgentFactory(async ()=>{
                const device = new AndroidDevice(selectedDeviceId);
                await device.connect();
                return new AndroidAgent(device);
            }).launch({
                port: availablePlaygroundPort,
                openBrowser: false,
                verbose: false,
                staticPath: staticDir,
                configureServer (server) {
                    server.scrcpyPort = availableScrcpyPort;
                }
            }),
            scrcpyServer.launch(availableScrcpyPort)
        ]);
        const playgroundServer = playgroundResult.server;
        console.log('');
        console.log('✨ Midscene Android Playground is ready!');
        console.log(`🎮 Playground: http://localhost:${playgroundServer.port}`);
        console.log(`📱 Device: ${selectedDeviceId}`);
        console.log(`🔑 Generated Server ID: ${playgroundServer.id}`);
        console.log('');
        open(`http://localhost:${playgroundServer.port}`);
    } catch (error) {
        console.error('Failed to start servers:', error);
        process.exit(1);
    }
};
main();
