import { ChildProcess, execFile, spawn } from 'node:child_process';
import { config as appConfig } from '@/config/app';
import { createLogger } from '@/lib/core/logger';
import { ReliableAdb } from '@/lib/android/adb-reliable';
import { resolveAndroidToolPath } from '@/lib/android/sdk';
import type { AndroidDevice, AndroidAgent } from '@/types/android';

const logger = createLogger('emulator-pool');

export type EmulatorState = 'STARTING' | 'BOOTING' | 'IDLE' | 'ACQUIRED' | 'CLEANING' | 'STOPPING' | 'DEAD';
export type EmulatorLaunchMode = 'headless' | 'window';
type MidsceneModelConfig = Record<string, string | number>;

interface BootOptions {
    headless?: boolean;
}

export interface EmulatorHandle {
    id: string;
    currentProjectId?: string;
    avdName: string;
    state: EmulatorState;
    device: AndroidDevice | null;
    agent: AndroidAgent | null;
    acquiredAt: number;
    runId: string;
    packageName?: string;
    clearPackageDataOnRelease?: boolean;
}

export interface EmulatorPoolStatusItem {
    id: string;
    currentProjectId?: string;
    avdName: string;
    state: EmulatorState;
    runId?: string;
    runProjectId?: string;
    runTestCaseId?: string;
    runTestCaseName?: string;
    runTestCaseDisplayId?: string;
    uptimeMs: number;
    memoryUsageMb?: number;
    idleDeadlineAt?: number;
}

export interface EmulatorPoolStatus {
    emulators: EmulatorPoolStatusItem[];
}

interface EmulatorAcquireProbeRequest {
    projectId: string;
    avdName: string;
}

interface EmulatorInstance {
    id: string;
    currentProjectId: string | null;
    avdName: string;
    state: EmulatorState;
    port: number;
    serial: string;
    process: ChildProcess | null;
    adb: ReliableAdb;
    runtimeDevice: MidsceneAndroidDevice | null;
    androidAgentConstructor: MidsceneAndroidAgentConstructor | null;
    device: AndroidDevice | null;
    agent: AndroidAgent | null;
    runId: string | null;
    startedAt: number;
    acquiredAt: number | null;
    idleTimer: NodeJS.Timeout | null;
    idleDeadlineAt: number | null;
    healthCheckTimer: NodeJS.Timeout | null;
    forceReclaimTimer: NodeJS.Timeout | null;
    memoryUsageMb: number | undefined;
    launchMode: EmulatorLaunchMode;
}

interface MidsceneAndroidDevice {
    connect(): Promise<void>;
    screenshotBase64?(): Promise<string>;
}

interface MidsceneAndroidDeviceConstructor {
    new (udid: string): MidsceneAndroidDevice;
}

interface MidsceneAndroidAgentConstructor {
    new (
        device: MidsceneAndroidDevice,
        options?: {
            groupName?: string;
            aiActionContext?: string;
            generateReport?: boolean;
            autoPrintReportMsg?: boolean;
            modelConfig?: MidsceneModelConfig;
        }
    ): AndroidAgent;
}

interface MidsceneAndroidRuntimeModule {
    AndroidDevice: MidsceneAndroidDeviceConstructor;
    AndroidAgent: MidsceneAndroidAgentConstructor;
}

function execFileAsync(file: string, args: string[]): Promise<{ stdout: string; stderr: string }> {
    return new Promise((resolve, reject) => {
        execFile(file, args, { encoding: 'utf8' }, (error, stdout, stderr) => {
            if (error) {
                reject(error);
                return;
            }
            resolve({ stdout: String(stdout), stderr: String(stderr) });
        });
    });
}

export class EmulatorPool {
    private static instance: EmulatorPool;
    private emulators: Map<string, EmulatorInstance> = new Map();
    private usedPorts: Set<number> = new Set();
    private readonly adbPath: string;
    private readonly emulatorPath: string;

    private constructor() {
        this.adbPath = resolveAndroidToolPath('adb');
        this.emulatorPath = resolveAndroidToolPath('emulator');
    }

    static getInstance(): EmulatorPool {
        if (!EmulatorPool.instance) {
            EmulatorPool.instance = new EmulatorPool();
        }
        return EmulatorPool.instance;
    }

    async initialize(): Promise<void> {
        await this.ensureAdbServer();
    }

    async boot(projectId: string | null, avdName: string, options?: BootOptions): Promise<EmulatorHandle> {
        const launchMode: EmulatorLaunchMode = options?.headless === false ? 'window' : 'headless';
        const port = await this.allocatePort();
        if (port === null) {
            throw new Error('No available ports for emulator');
        }

        const serial = `emulator-${port}`;
        const id = serial;
        const adb = new ReliableAdb(serial, this.adbPath);

        const instance: EmulatorInstance = {
            id,
            currentProjectId: projectId,
            avdName,
            state: 'STARTING',
            port,
            serial,
            process: null,
            adb,
            runtimeDevice: null,
            androidAgentConstructor: null,
            device: null,
            agent: null,
            runId: null,
            startedAt: Date.now(),
            acquiredAt: null,
            idleTimer: null,
            idleDeadlineAt: null,
            healthCheckTimer: null,
            forceReclaimTimer: null,
            memoryUsageMb: undefined,
            launchMode,
        };

        this.emulators.set(id, instance);
        this.usedPorts.add(port);

        try {
            await this.launchEmulatorProcess(instance);
            await Promise.race([
                this.waitForBoot(instance),
                new Promise<never>((_, reject) =>
                    setTimeout(
                        () => reject(new Error(`Emulator ${id} boot timed out (${appConfig.emulator.bootTimeoutMs}ms)`)),
                        appConfig.emulator.bootTimeoutMs
                    )
                ),
            ]);
            await this.attachAndroidRuntime(instance);
        } catch (error) {
            await this.stopInstance(instance);
            throw error;
        }

        instance.state = 'IDLE';
        this.scheduleIdleTimeout(instance);
        this.scheduleHealthCheck(instance);
        logger.info(`Emulator ${id} is IDLE`);

        return this.makeHandle(instance);
    }

    async acquire(
        projectId: string,
        avdName: string,
        runId: string,
        signal?: AbortSignal,
        modelConfig?: MidsceneModelConfig
    ): Promise<EmulatorHandle> {
        if (signal?.aborted) throw new Error('Acquisition cancelled');

        await this.reclaimStaleBootingInstances();
        await this.stopOrphanedAdbEmulators();

        const reusableIdleEmulator = this.findIdleEmulator(avdName, 'headless')
            ?? this.findIdleEmulator(avdName, 'window');
        if (reusableIdleEmulator) {
            logger.info(`Attempting to reuse idle emulator ${reusableIdleEmulator.id} for AVD ${avdName}`);
            const health = await reusableIdleEmulator.adb.healthCheck().catch(() => null);
            if (health?.healthy) {
                return this.lockEmulator(reusableIdleEmulator, runId, projectId, modelConfig);
            }
            logger.warn(`Idle emulator ${reusableIdleEmulator.id} is unhealthy, stopping before replacement`, health);
            await this.stopInstance(reusableIdleEmulator);
        }

        const handle = await this.bootWithRetries(projectId, avdName, signal, { headless: true });
        const instance = this.emulators.get(handle.id);
        if (!instance) {
            throw new Error(`Emulator ${handle.id} disappeared after boot`);
        }
        return this.lockEmulator(instance, runId, projectId, modelConfig);
    }

    async canAcquireBatchImmediately(requests: ReadonlyArray<EmulatorAcquireProbeRequest>): Promise<boolean> {
        if (requests.length === 0) {
            return true;
        }

        await this.reclaimStaleBootingInstances();

        const activeInstances = Array.from(this.emulators.values()).filter((instance) => instance.state !== 'DEAD');
        const reservedInstanceIds = new Set<string>();

        const findMatchingIdle = (request: EmulatorAcquireProbeRequest, launchMode: EmulatorLaunchMode): EmulatorInstance | null => {
            for (const instance of activeInstances) {
                if (reservedInstanceIds.has(instance.id)) {
                    continue;
                }
                if (instance.state !== 'IDLE') {
                    continue;
                }
                if (instance.launchMode !== launchMode) {
                    continue;
                }
                if (instance.avdName !== request.avdName) {
                    continue;
                }
                return instance;
            }
            return null;
        };

        for (const request of requests) {
            const idleHeadless = findMatchingIdle(request, 'headless');
            if (idleHeadless) {
                reservedInstanceIds.add(idleHeadless.id);
                continue;
            }

            const idleWindow = findMatchingIdle(request, 'window');
            if (idleWindow) {
                reservedInstanceIds.add(idleWindow.id);
                continue;
            }

            // No fixed instance cap; capacity is now bounded by runtime resources/ports only.
        }

        return true;
    }

    async release(handle: EmulatorHandle): Promise<void> {
        const instance = this.emulators.get(handle.id);
        if (!instance) {
            logger.warn(`Tried to release unknown emulator ${handle.id}`);
            return;
        }

        if (instance.state !== 'ACQUIRED') {
            logger.warn(`Tried to release emulator ${handle.id} in state ${instance.state}`);
            return;
        }

        if (instance.forceReclaimTimer) {
            clearTimeout(instance.forceReclaimTimer);
            instance.forceReclaimTimer = null;
        }

        instance.state = 'CLEANING';
        instance.currentProjectId = null;
        instance.runId = null;
        instance.acquiredAt = null;
        logger.info(`Releasing emulator ${instance.id}`);

        const packageToClear = handle.clearPackageDataOnRelease === false ? undefined : handle.packageName;
        const cleanSuccess = await this.cleanEmulator(instance, packageToClear);
        if (!cleanSuccess) {
            await this.stopInstance(instance);
            return;
        }

        instance.state = 'IDLE';
        this.scheduleIdleTimeout(instance);
        this.scheduleHealthCheck(instance);
        logger.info(`Emulator ${instance.id} returned to IDLE`);
    }

    async stop(emulatorId: string): Promise<void> {
        const instance = this.emulators.get(emulatorId);
        if (!instance) {
            return;
        }
        await this.stopInstance(instance);
    }

    getStatus(projectIds?: ReadonlySet<string>): EmulatorPoolStatus {
        void projectIds;
        const now = Date.now();
        const emulators = Array.from(this.emulators.values())
            .filter((instance) => instance.state !== 'DEAD');

        return {
            emulators: emulators.map((instance) => ({
                id: instance.id,
                currentProjectId: instance.currentProjectId ?? undefined,
                avdName: instance.avdName,
                state: instance.state,
                runId: instance.runId ?? undefined,
                uptimeMs: now - instance.startedAt,
                memoryUsageMb: instance.memoryUsageMb,
                idleDeadlineAt: instance.state === 'IDLE' ? (instance.idleDeadlineAt ?? undefined) : undefined,
            })),
        };
    }

    async listInstalledPackages(emulatorId: string): Promise<string[]> {
        const instance = this.emulators.get(emulatorId);
        if (!instance || instance.state === 'DEAD') {
            throw new Error(`Emulator "${emulatorId}" is not available`);
        }

        const output = await instance.adb.shell('pm list packages', { timeoutMs: 30_000, retries: 1 });
        const packages = output
            .split('\n')
            .map((line) => line.trim())
            .filter((line) => line.startsWith('package:'))
            .map((line) => line.slice('package:'.length).trim())
            .filter((line) => line.length > 0);

        return Array.from(new Set(packages)).sort((a, b) => a.localeCompare(b));
    }

    private async launchEmulatorProcess(instance: EmulatorInstance): Promise<void> {
        const modeArgs = instance.launchMode === 'headless' ? appConfig.emulator.launchArgs.headless : [];
        const args = [
            '-avd', instance.avdName,
            '-port', String(instance.port),
            ...modeArgs,
            ...appConfig.emulator.launchArgs.shared,
        ];

        logger.info(`Launching emulator process for ${instance.avdName} on ${instance.serial}`);

        const processHandle = spawn(this.emulatorPath, args, {
            detached: true,
            stdio: 'ignore',
        });

        const spawnReady = new Promise<void>((resolve, reject) => {
            let settled = false;
            const done = (fn: () => void) => {
                if (settled) {
                    return;
                }
                settled = true;
                fn();
            };

            processHandle.once('error', (error) => {
                done(() => reject(error));
            });

            setTimeout(() => {
                done(() => resolve());
            }, 200);
        });

        processHandle.unref();
        instance.process = processHandle;
        this.bindProcessLifecycle(instance);

        await spawnReady;
        instance.state = 'BOOTING';
    }

    private bindProcessLifecycle(instance: EmulatorInstance): void {
        const processHandle = instance.process;
        if (!processHandle) {
            return;
        }

        processHandle.once('exit', (code, signal) => {
            if (instance.state === 'DEAD' || instance.state === 'STOPPING') {
                return;
            }

            logger.warn(`Emulator process exited unexpectedly for ${instance.id} (code=${String(code)}, signal=${String(signal)})`);
            this.transitionToDead(instance);
        });
    }

    private async attachAndroidRuntime(instance: EmulatorInstance): Promise<void> {
        try {
            const runtimeModule = await import('@midscene/android') as unknown as MidsceneAndroidRuntimeModule;
            const runtimeDevice = new runtimeModule.AndroidDevice(instance.serial);
            await runtimeDevice.connect();

            instance.device = {
                deviceId: instance.serial,
                shell: async (command: string) => instance.adb.shell(command),
                screenshotBase64: typeof runtimeDevice.screenshotBase64 === 'function'
                    ? async () => runtimeDevice.screenshotBase64!()
                    : undefined,
            };
            instance.runtimeDevice = runtimeDevice;
            instance.androidAgentConstructor = runtimeModule.AndroidAgent;
            instance.agent = this.createAndroidAgentForInstance(instance, instance.currentProjectId ?? 'global');
        } catch (error) {
            const message = error instanceof Error ? error.message : String(error);
            throw new Error(`Failed to attach Android runtime for ${instance.id}: ${message}`);
        }
    }

    private findIdleEmulator(avdName: string, launchMode?: EmulatorLaunchMode): EmulatorInstance | null {
        for (const instance of this.emulators.values()) {
            const modeMatches = !launchMode || instance.launchMode === launchMode;
            if (instance.state === 'IDLE' && instance.avdName === avdName && modeMatches) {
                return instance;
            }
        }
        return null;
    }

    private lockEmulator(
        instance: EmulatorInstance,
        runId: string,
        projectId: string,
        modelConfig?: MidsceneModelConfig
    ): EmulatorHandle {
        if (instance.idleTimer) {
            clearTimeout(instance.idleTimer);
            instance.idleTimer = null;
        }
        instance.idleDeadlineAt = null;
        if (instance.healthCheckTimer) {
            clearTimeout(instance.healthCheckTimer);
            instance.healthCheckTimer = null;
        }

        instance.agent = this.createAndroidAgentForInstance(instance, projectId, modelConfig);
        instance.state = 'ACQUIRED';
        instance.currentProjectId = projectId;
        instance.runId = runId;
        instance.acquiredAt = Date.now();

        const forceReclaimMs = appConfig.test.maxDuration * 1000 + 60_000;
        instance.forceReclaimTimer = setTimeout(async () => {
            logger.warn(`Force-reclaiming stuck emulator ${instance.id} from run ${instance.runId}`);
            await this.stopInstance(instance);
        }, forceReclaimMs);

        return this.makeHandle(instance);
    }

    private createAndroidAgentForInstance(
        instance: EmulatorInstance,
        projectId: string,
        modelConfig?: MidsceneModelConfig
    ): AndroidAgent {
        if (!instance.runtimeDevice || !instance.androidAgentConstructor) {
            throw new Error(`Android runtime not attached for emulator ${instance.id}`);
        }

        return new instance.androidAgentConstructor(instance.runtimeDevice, {
            groupName: `${projectId}-${instance.avdName}-${instance.port}`,
            generateReport: appConfig.test.midscene.generateReport,
            autoPrintReportMsg: appConfig.test.midscene.autoPrintReportMsg,
            modelConfig,
        });
    }

    private makeHandle(instance: EmulatorInstance): EmulatorHandle {
        return {
            id: instance.id,
            currentProjectId: instance.currentProjectId ?? undefined,
            avdName: instance.avdName,
            state: instance.state,
            device: instance.device,
            agent: instance.agent,
            acquiredAt: instance.acquiredAt ?? Date.now(),
            runId: instance.runId ?? '',
        };
    }

    private async waitForBoot(instance: EmulatorInstance): Promise<void> {
        const pollMs = 3000;
        while (true) {
            await this.sleep(pollMs);
            if (instance.state === 'DEAD') {
                throw new Error(`Emulator ${instance.id} died during boot`);
            }

            try {
                const bootCompleted = await instance.adb.shell('getprop sys.boot_completed', {
                    timeoutMs: 5000,
                    retries: 1,
                });

                if (bootCompleted.trim() === '1') {
                    const health = await instance.adb.healthCheck();
                    if (health.healthy) {
                        return;
                    }
                }
            } catch {
                // Keep waiting until boot completes.
            }
        }
    }

    private async cleanEmulator(instance: EmulatorInstance, packageName?: string): Promise<boolean> {
        try {
            let cleanupFailed = false;

            if (packageName) {
                try {
                    await instance.adb.shell(`am force-stop ${packageName}`, { timeoutMs: 15_000, retries: 1 });
                } catch (error) {
                    cleanupFailed = true;
                    logger.warn(`Failed to force-stop app on emulator ${instance.id} during cleanup`, error);
                }
            }
            try {
                await instance.adb.shell('input keyevent KEYCODE_HOME', { timeoutMs: 15_000, retries: 1 });
            } catch (error) {
                cleanupFailed = true;
                logger.warn(`Failed to send HOME keyevent on emulator ${instance.id} during cleanup`, error);
            }
            try {
                await instance.adb.shell('am kill-all', { timeoutMs: 15_000, retries: 1 });
            } catch (error) {
                cleanupFailed = true;
                logger.warn(`Failed to kill background apps on emulator ${instance.id} during cleanup`, error);
            }

            const health = await instance.adb.healthCheck();
            if (!health.healthy) {
                logger.warn(`Emulator ${instance.id} failed health check during cleanup`, health);
            }
            return !cleanupFailed && health.healthy;
        } catch (error) {
            logger.warn(`Failed to clean emulator ${instance.id}`, error);
            return false;
        }
    }

    private transitionToDead(instance: EmulatorInstance): void {
        this.clearInstanceTimers(instance);
        instance.state = 'DEAD';
        instance.process = null;
        instance.runtimeDevice = null;
        instance.androidAgentConstructor = null;
        instance.device = null;
        instance.agent = null;
        this.usedPorts.delete(instance.port);
        this.emulators.delete(instance.id);
        logger.info(`Emulator ${instance.id} is DEAD`);
    }

    private async stopInstance(instance: EmulatorInstance): Promise<void> {
        if (instance.state === 'DEAD' || instance.state === 'STOPPING') {
            return;
        }

        this.clearInstanceTimers(instance);
        instance.state = 'STOPPING';
        const processHandle = instance.process;
        const killTarget = this.getProcessKillTarget(processHandle);

        try {
            await this.sendAdbEmulatorKill(instance);
            this.tryKillProcess(killTarget, 'SIGTERM');
        } catch (error) {
            logger.warn(`Error stopping emulator ${instance.id}`, error);
        }

        await this.ensureProcessStopped(processHandle, killTarget);

        this.transitionToDead(instance);
    }

    private clearInstanceTimers(instance: EmulatorInstance): void {
        if (instance.idleTimer) {
            clearTimeout(instance.idleTimer);
            instance.idleTimer = null;
        }
        instance.idleDeadlineAt = null;
        if (instance.healthCheckTimer) {
            clearTimeout(instance.healthCheckTimer);
            instance.healthCheckTimer = null;
        }
        if (instance.forceReclaimTimer) {
            clearTimeout(instance.forceReclaimTimer);
            instance.forceReclaimTimer = null;
        }
    }

    private scheduleIdleTimeout(instance: EmulatorInstance): void {
        if (instance.idleTimer) {
            clearTimeout(instance.idleTimer);
        }

        const idleTimeoutMs = this.getIdleTimeoutMs(instance.launchMode);
        instance.idleDeadlineAt = Date.now() + idleTimeoutMs;
        instance.idleTimer = setTimeout(async () => {
            if (instance.state === 'IDLE') {
                logger.info(`Emulator ${instance.id} stopped after ${idleTimeoutMs}ms idle`);
                await this.stopInstance(instance);
            }
        }, idleTimeoutMs);
    }

    private getIdleTimeoutMs(launchMode: EmulatorLaunchMode): number {
        return launchMode === 'window'
            ? appConfig.emulator.windowIdleTimeoutMs
            : appConfig.emulator.idleTimeoutMs;
    }

    private scheduleHealthCheck(instance: EmulatorInstance): void {
        if (instance.healthCheckTimer) {
            clearTimeout(instance.healthCheckTimer);
        }

        instance.healthCheckTimer = setTimeout(async () => {
            if (instance.state !== 'IDLE') {
                return;
            }

            const result = await instance.adb.healthCheck();
            if (!result.healthy) {
                logger.warn(`Emulator ${instance.id} failed health check, attempting reconnect`);
                const reconnected = await instance.adb.reconnect();
                if (!reconnected) {
                    logger.error(`Emulator ${instance.id} unrecoverable, stopping`);
                    await this.stopInstance(instance);
                    return;
                }
            }

            this.scheduleHealthCheck(instance);
        }, appConfig.emulator.healthCheckIntervalMs);
    }

    private async bootWithRetries(
        projectId: string,
        avdName: string,
        signal?: AbortSignal,
        options?: BootOptions
    ): Promise<EmulatorHandle> {
        const maxAttempts = Math.max(1, appConfig.emulator.bootMaxAttempts);
        let lastError: unknown = null;

        for (let attempt = 1; attempt <= maxAttempts; attempt++) {
            if (signal?.aborted) {
                throw new Error('Acquisition cancelled');
            }

            try {
                return await this.boot(projectId, avdName, options);
            } catch (error) {
                lastError = error;
                logger.warn(`Emulator boot attempt ${attempt}/${maxAttempts} failed for AVD ${avdName}`, error);

                if (attempt < maxAttempts) {
                    await this.sleep(appConfig.emulator.bootRetryDelayMs);
                }
            }
        }

        throw new Error(
            `Failed to boot emulator for AVD "${avdName}" after ${maxAttempts} attempt(s): ` +
            `${lastError instanceof Error ? lastError.message : String(lastError)}`
        );
    }

    private async allocatePort(): Promise<number | null> {
        const adbUsedPorts = await this.getAdbEmulatorPorts();

        for (let port = appConfig.emulator.basePort; port <= 65534; port += 2) {
            if (this.usedPorts.has(port)) {
                continue;
            }
            if (adbUsedPorts.has(port)) {
                continue;
            }
            return port;
        }

        return null;
    }

    private async getAdbEmulatorPorts(): Promise<Set<number>> {
        const ports = new Set<number>();
        try {
            const { stdout } = await execFileAsync(this.adbPath, ['devices']);
            const lines = stdout.split('\n').map((line) => line.trim()).filter((line) => line.length > 0);
            for (const line of lines) {
                const match = line.match(/^emulator-(\d+)\s+/);
                if (!match) {
                    continue;
                }
                const port = Number.parseInt(match[1], 10);
                if (Number.isFinite(port)) {
                    ports.add(port);
                }
            }
        } catch (error) {
            logger.warn('Failed to inspect ADB emulator ports', error);
        }

        return ports;
    }

    private async listConnectedAdbEmulatorSerials(): Promise<string[]> {
        try {
            const { stdout } = await execFileAsync(this.adbPath, ['devices']);
            const lines = stdout.split('\n').map((line) => line.trim()).filter((line) => line.length > 0);
            return lines
                .filter((line) => !line.startsWith('List of devices attached'))
                .map((line) => line.split(/\s+/)[0])
                .filter((serial) => serial.startsWith('emulator-'));
        } catch (error) {
            logger.warn('Failed to list ADB emulator serials', error);
            return [];
        }
    }

    private async stopOrphanedAdbEmulators(): Promise<void> {
        const managedSerials = new Set(Array.from(this.emulators.values()).map((instance) => instance.serial));
        const connectedSerials = await this.listConnectedAdbEmulatorSerials();

        for (const serial of connectedSerials) {
            if (managedSerials.has(serial)) {
                continue;
            }

            logger.warn(`Stopping orphaned ADB emulator ${serial} before acquisition`);
            await execFileAsync(this.adbPath, ['-s', serial, 'emu', 'kill']).catch((error) => {
                logger.warn(`Failed to stop orphaned ADB emulator ${serial}`, error);
            });
        }
    }

    private async ensureAdbServer(): Promise<void> {
        try {
            await execFileAsync(this.adbPath, ['kill-server']).catch(() => {});
            await execFileAsync(this.adbPath, ['start-server']);
            logger.info('ADB server started');
        } catch (error) {
            logger.warn('Failed to restart ADB server', error);
        }
    }

    private async reclaimStaleBootingInstances(): Promise<void> {
        const maxBootAgeMs = appConfig.emulator.bootTimeoutMs + appConfig.emulator.bootRetryDelayMs;
        const now = Date.now();
        const staleInstances = Array.from(this.emulators.values()).filter((instance) =>
            (instance.state === 'STARTING' || instance.state === 'BOOTING') &&
            now - instance.startedAt > maxBootAgeMs
        );

        for (const instance of staleInstances) {
            logger.warn(`Reclaiming stale emulator ${instance.id} in state ${instance.state}`);
            await this.stopInstance(instance);
        }
    }

    private sleep(ms: number): Promise<void> {
        return new Promise((resolve) => setTimeout(resolve, ms));
    }

    private getProcessKillTarget(processHandle: ChildProcess | null): number | null {
        if (!processHandle?.pid) {
            return null;
        }
        return process.platform === 'win32' ? processHandle.pid : -processHandle.pid;
    }

    private async sendAdbEmulatorKill(instance: EmulatorInstance): Promise<void> {
        await execFileAsync(this.adbPath, ['-s', instance.serial, 'emu', 'kill']).catch(() => {});
    }

    private tryKillProcess(killTarget: number | null, signal: NodeJS.Signals): void {
        if (killTarget === null) {
            return;
        }
        try {
            process.kill(killTarget, signal);
        } catch {
            // Process may have already exited.
        }
    }

    private async ensureProcessStopped(processHandle: ChildProcess | null, killTarget: number | null): Promise<void> {
        if (!processHandle) {
            return;
        }

        const exitedAfterTerm = await this.waitForProcessExit(processHandle, 5000);
        if (exitedAfterTerm || killTarget === null) {
            return;
        }

        this.tryKillProcess(killTarget, 'SIGKILL');
        await this.waitForProcessExit(processHandle, 3000);
    }

    private waitForProcessExit(processHandle: ChildProcess, timeoutMs: number): Promise<boolean> {
        if (processHandle.exitCode !== null || processHandle.signalCode !== null) {
            return Promise.resolve(true);
        }

        return new Promise((resolve) => {
            let settled = false;
            const done = (result: boolean) => {
                if (settled) {
                    return;
                }
                settled = true;
                clearTimeout(timeoutId);
                processHandle.off('exit', onExit);
                resolve(result);
            };
            const onExit = () => done(true);
            const timeoutId = setTimeout(() => done(false), timeoutMs);
            processHandle.once('exit', onExit);
        });
    }
}

export const emulatorPool = EmulatorPool.getInstance();
