import mqtt from 'mqtt';
import * as utils from '../../core/utils.js';
import { createStoreServer } from './store-server.js';
import { StoreClient } from './store-client.js';


export default class LoadService {
    constructor(config) {
        this.config = config;
        this.testdata = config.loadTestData;
        if (config.resetSession) {
            this.testdata.sessionConfig.resetSession = true;
        }
        this.instances = config.instances;
        this.uuid = utils.getUUID();
        this.testdata.sessionConfig.client = { "uuid": this.uuid, "workerCount": this.instances, "state": 0, "workers": [] };
        this.mqtt = mqtt;
        this.utils = utils;
        this.client = null;
        this.workers = [];
        this.state = 0; // 0: preparing, 1: initialized, 2: starting, 3: holding, 4: running, 5: error, 6: done
        this.sessionState = 0;
        this.workerIndex = -1;
        this.syncpoint = false;
        this.storePort = config.storePort || 3333;
        this.storeClient = new StoreClient(this.storePort);
        this.storeServer = null;
        // Timing control is now handled per worker in beforeSession
        
        this.setupGlobalErrorHandlers();
    }
    
    setupGlobalErrorHandlers() {
        // Handle uncaught exceptions 
        process.on('uncaughtException', (error) => {
            console.error(`[PROCESS ERROR] Uncaught exception:`, error);
            this.handleProcessError(error);
        });
        
        // Handle unhandled promise rejections
        process.on('unhandledRejection', (reason, promise) => {
            console.error(`[PROCESS ERROR] Unhandled rejection at:`, promise, 'reason:', reason);
            this.handleProcessError(reason);
        });
        
        // Handle SIGTERM and SIGINT gracefully
        process.on('SIGTERM', () => {
            console.log('[PROCESS] Received SIGTERM, shutting down gracefully');
            this.shutdown();
        });
        
        process.on('SIGINT', () => {
            console.log('[PROCESS] Received SIGINT, shutting down gracefully');
            this.shutdown();
        });
    }
    
    handleProcessError(error) {
        console.error(`[PROCESS ERROR] Fatal error occurred:`, error.message || error.toString());
        
        // Exit immediately - orchestrator will detect via exit code
        process.exit(1);
    }
    
    shutdown() {
        if (this.client) {
            this.client.end();
        }
        if (this.storeServer) {
            this.storeServer.close();
        }
        process.exit(0);
    }



    async onPrepare(config, capabilities) {
        console.log('executing onPrepare() ')
        let loadService = this;

        // Start store server
        console.log(`Starting store server on port ${loadService.storePort}`);
        loadService.storeServer = createStoreServer(loadService.storePort);
        await loadService.storeClient.clear();

        console.log("Connecting to mqtt server");
        loadService.client = loadService.mqtt.connect(loadService.config.loadOrchestrator);
        loadService.client.on('connect', function () {
            loadService.client.subscribe(loadService.testdata.sessionConfig.sessionUuid + "/" + loadService.uuid);
            loadService.client.subscribe(loadService.testdata.sessionConfig.sessionUuid + "/all");
            loadService.client.publish('clientRegistrationQ', JSON.stringify(loadService.testdata.sessionConfig));
            console.log("Connected");
        });
        loadService.client.on('message', function (topic, message) {
            let messageStr = message.toString();
            console.log('Received on ', topic, ": ", messageStr);
            if (topic == loadService.testdata.sessionConfig.sessionUuid + "/" + loadService.uuid) {
                let mess = JSON.parse(message);
                if (mess.workerStart && mess.workerEnd) {
                    let count = mess.workerStart;
                    while (count <= mess.workerEnd) {
                        loadService.workers.push({ "testIndex": count, "syncpoint": null, "state": 0 });
                        count++;
                    }
                    console.log('Initialized ', loadService.workers.length, " workers");
                } else if (mess.workerStart !== undefined && mess.rampupDelay) {
                    console.log(`[onPrepare] Unexpected worker start signal received - orchestrator should no longer send these`);
                }
            } else if (topic == loadService.testdata.sessionConfig.sessionUuid + "/all") {
                if (message == "start") {
                    // 0: preparing, 1: initialized, 2: starting, 3: holding, 4: running, 5: error, 6: done
                    loadService.state = 1;
                } else if (message == "error") {
                    throw new Error("LOAD test is in ERROR state, stopping test ASAP");
                }
            }
        });
        // Check if worker rampup is configured
        const hasWorkerRampupDelay = loadService.testdata.sessionConfig.workerRampupDelay && loadService.testdata.sessionConfig.workerRampupDelay > 0;
        
        if (hasWorkerRampupDelay) {
            // With worker rampup, don't wait for global start signal - individual workers will poll for permission
            console.log(`[onPrepare] Worker rampup detected - skipping global start signal wait`);
            while (loadService.workers.length < loadService.instances) {
                console.log(`[onPrepare] Waiting for workers to be initialized... (${loadService.workers.length}/${loadService.instances})`);
                await loadService.utils.delay(50);
            }
        } else {
            // Without worker rampup, wait for both workers and global start signal
            while (loadService.state < 1 || loadService.workers.length < loadService.instances) {
                await loadService.utils.delay(50);
            }
        }
        
        let workerConfig = {
            "sessionUuid": loadService.testdata.sessionConfig.sessionUuid,
            "clientUuid": loadService.uuid,
            "workers": loadService.workers,
            "syncpoint": false
        }
        console.log("Registering workers");
        loadService.client.publish('workerRegistrationQ', JSON.stringify(workerConfig));

        // Store worker config
        await loadService.storeClient.setValue('load-workerConfig', workerConfig);

        loadService.client.end(true);
    }

    async beforeSession(config, capabilities, specs, cid) {
        let loadService = this;
        loadService.workerIndex = Number((cid.split('-')).pop());
        console.log(`execute beforeSession ${loadService.workerIndex}`);

        // Get worker config with retries
        let attempts = 0;
        const maxAttempts = 30;
        let workerConfig = null;

        while (!workerConfig && attempts < maxAttempts) {
            try {
                workerConfig = await loadService.storeClient.getValue('load-workerConfig');
                if (!workerConfig) {
                    await new Promise(resolve => setTimeout(resolve, 1000));
                    attempts++;
                    console.log(`Attempt ${attempts} to get worker config`);
                }
            } catch (error) {
                await new Promise(resolve => setTimeout(resolve, 1000));
                attempts++;
                console.log(`Attempt ${attempts} to get worker config failed: ${error.message}`);
            }
        }

        if (!workerConfig) {
            throw new Error('Failed to get worker configuration after 30 attempts');
        }

        workerConfig.workerIndex = loadService.workerIndex;
        workerConfig.testIndex = workerConfig.workers[loadService.workerIndex].testIndex;
        workerConfig.dataIndex = workerConfig.workers[loadService.workerIndex].testIndex - 1;
        global.workerConfig = workerConfig;

        loadService.uuid = workerConfig.clientUuid;
        loadService.client = loadService.mqtt.connect(loadService.config.loadOrchestrator);
        loadService.client.on('connect', function () {
            loadService.client.subscribe(loadService.testdata.sessionConfig.sessionUuid + "/" + loadService.uuid);
            loadService.client.subscribe(loadService.testdata.sessionConfig.sessionUuid + "/all");
            console.log("Connected");
        });
        loadService.client.on('message', function (topic, message) {
            if (topic == loadService.testdata.sessionConfig.sessionUuid + "/all") {
                if (message == "sync") {
                    loadService.syncpoint = false;
                } else if (message == "error") {
                    loadService.state = 5;
                }
            } else if (topic == loadService.testdata.sessionConfig.sessionUuid + "/" + loadService.uuid) {
                let mess = JSON.parse(message);
                if (mess.state) {
                    loadService.state = mess.state;
                } else if (mess.workerStart !== undefined && mess.rampupDelay) {
                    console.log(`[beforeSession] Unexpected worker start signal - these should no longer be sent`);
                }
            }
        });
        await loadService.utils.delay(1000);
        
        // Check if we should wait for individual worker start signal (worker rampup mode)
        const hasWorkerRampupDelay = loadService.testdata.sessionConfig.workerRampupDelay && loadService.testdata.sessionConfig.workerRampupDelay > 0;
        
        if (hasWorkerRampupDelay) {
            console.log(`[beforeSession][Worker ${loadService.workerIndex}] WorkerRampupDelay configured - staggering was handled in onWorkerStart, proceeding with session setup`);
        }
        
        let stateConfig = {
            "sessionUuid": loadService.testdata.sessionConfig.sessionUuid,
            "clientUuid": loadService.uuid,
            "worker": loadService.workerIndex,
            "state": 3,
            "syncpoint": false
        };

        if (loadService.state == 5) {
            console.error("Throwing error in beforeSession")
            throw new Error("LOAD test is in ERROR state, stopping test ASAP");
        }
        loadService.client.publish('updateState', JSON.stringify(stateConfig));
        while (loadService.state < 3) {
            await loadService.utils.delay(50);
        }
    }

    async afterSession() {
        let loadService = this;
        if (loadService.client) {
            loadService.client.end();
        }
    }

    async onWorkerStart(cid, caps, specs, args, execArgv) {

        let workerIndex = Number((cid.split('-')).pop());
        console.log(`[onWorkerStart][Worker ${workerIndex}] Starting worker initialization`);
        
        try {
            // Check if sessionConfig has workerRampupDelay configured
            const hasWorkerRampupDelay = this.testdata.sessionConfig.workerRampupDelay && this.testdata.sessionConfig.workerRampupDelay > 0;
            
            if (hasWorkerRampupDelay) {
                console.log(`[onWorkerStart][Worker ${workerIndex}] WorkerRampupDelay detected (${this.testdata.sessionConfig.workerRampupDelay}ms) - implementing staggered browser startup HERE`);
                
                // Calculate delay based on worker index - this MUST happen BEFORE browser process creation
                const delay = workerIndex * this.testdata.sessionConfig.workerRampupDelay;
                
                if (delay > 0) {
                    console.log(`[onWorkerStart][Worker ${workerIndex}] DELAYING worker process by ${delay}ms to stagger browser startup`);
                    await this.utils.delay(delay);
                    console.log(`[onWorkerStart][Worker ${workerIndex}] Delay complete - browser process will now start`);
                } else {
                    console.log(`[onWorkerStart][Worker ${workerIndex}] No delay needed - starting browser process immediately`);
                }
            } else {
                // No workerRampupDelay - wait for global start signal
                console.log(`[onWorkerStart][Worker ${workerIndex}] No workerRampupDelay - waiting for global start signal...`);
                let globalWaitAttempts = 0;
                const maxGlobalWaitAttempts = 6000; // 300 seconds max wait (6000 * 50ms)
                
                while (this.state < 1 && globalWaitAttempts < maxGlobalWaitAttempts) {
                    if (globalWaitAttempts % 20 === 0) { // Log every second
                        console.log(`[onWorkerStart][Worker ${workerIndex}] Waiting for global start signal... (attempt ${globalWaitAttempts})`);
                    }
                    await this.utils.delay(50);
                    globalWaitAttempts++;
                }
                
                if (this.state < 1) {
                    console.log(`[onWorkerStart][Worker ${workerIndex}] [COMPATIBILITY] No global start signal received within 300 seconds - proceeding with immediate start`);
                } else {
                    console.log(`[onWorkerStart][Worker ${workerIndex}] Global start signal received!`);
                }
            }
            
            // Log memory usage before starting worker
            const used = process.memoryUsage();
            console.log(`[onWorkerStart][Worker ${workerIndex}] Memory before start: RSS=${Math.round(used.rss / 1024 / 1024)}MB, Heap=${Math.round(used.heapUsed / 1024 / 1024)}MB`);
            
            // Force garbage collection if available to reduce memory pressure
            if (global.gc) {
                console.log(`[onWorkerStart][Worker ${workerIndex}] Running garbage collection`);
                global.gc();
            }
            
            // Basic memory monitoring and V8 optimization
            try {
                const memUsage = process.memoryUsage();
                const heapUsedMB = Math.round(memUsage.heapUsed / 1024 / 1024);
                const heapTotalMB = Math.round(memUsage.heapTotal / 1024 / 1024);
                console.log(`[onWorkerStart][Worker ${workerIndex}] Memory usage: RSS=${Math.round(memUsage.rss / 1024 / 1024)}MB, Heap=${heapUsedMB}MB/${heapTotalMB}MB`);
                
                // Light GC if available to start with clean slate
                if (global.gc) {
                    global.gc();
                    console.log(`[onWorkerStart][Worker ${workerIndex}] Performed garbage collection`);
                }
                
            } catch (memError) {
                console.warn(`[onWorkerStart][Worker ${workerIndex}] Memory check failed:`, memError);
            }
            
            // Set worker state to 2 - worker is initialized but not yet ready to run
            console.log(`[onWorkerStart][Worker ${workerIndex}] Setting worker state to 2 (initialization complete)`);
            this.workers[workerIndex].state = 2;
            this.state = 2;
            
            // Store whether this worker should use rampup delays for later use
            this.hasWorkerRampupDelay = hasWorkerRampupDelay;
            
            // Log final memory usage
            const finalUsed = process.memoryUsage();
            console.log(`[onWorkerStart][Worker ${workerIndex}] Memory after initialization: RSS=${Math.round(finalUsed.rss / 1024 / 1024)}MB, Heap=${Math.round(finalUsed.heapUsed / 1024 / 1024)}MB`);
            console.log(`[onWorkerStart][Worker ${workerIndex}] Worker initialization completed successfully`);
            
        } catch (error) {
            console.error(`[onWorkerStart][Worker ${workerIndex}] Error during worker initialization:`, error);
            
            // Set error state and re-throw - let the process crash naturally
            // Exit code will be detected by onWorkerEnd for restart handling
            this.state = 5;
            throw error;
        }
    }
    onWorkerEnd(cid, exitCode, specs, retries) {
        let workerIndex = Number((cid.split('-')).pop());
        console.log(`[onWorkerEnd][Worker ${workerIndex}] Worker ended with exit code: ${exitCode}`);
        
        // Handle null/undefined exit codes as crashes (WebDriverIO returns null for process crashes)
        // Normal exit should be 0, any non-zero or null/undefined indicates a crash
        const isCrash = exitCode !== 0 && exitCode !== undefined;
        const isNullCrash = exitCode === null || exitCode === undefined;
        
        if (isCrash || isNullCrash) {
            if (isNullCrash) {
                console.error(`[onWorkerEnd][Worker ${workerIndex}] Worker crashed with null/undefined exit code - likely fatal V8 error or abrupt process termination`);
            } else {
                console.error(`[onWorkerEnd][Worker ${workerIndex}] Worker crashed with exit code: ${exitCode}`);
            }
            
            // Determine likely cause based on exit code
            let errorType = 'unknown_crash';
            let error = `Worker crashed with exit code ${exitCode}`;
            let effectiveExitCode = exitCode;
            
                    if (isNullCrash) {
            errorType = 'v8_fatal_error';
            error = `Worker crashed with null exit code - V8 fatal error (e.g., unreachable code, deserialization failure)`;
            effectiveExitCode = 1; // Treat null as exit code 1 for restart logic
            console.error(`[onWorkerEnd][Worker ${workerIndex}] Null exit code confirms V8 fatal error - process terminated before exit code could be set`);
        }
        // Exit code 1 indicates other types of errors (not V8 fatal errors)
        else if (exitCode === 1) {
            errorType = 'general_error';
            error = `Worker crashed with exit code 1 - general error or uncaught exception`;
            console.error(`[onWorkerEnd][Worker ${workerIndex}] Exit code 1 indicates general error, not V8 fatal error`);
        }
            // Exit code 134 (SIGABRT) often indicates assertion failures or abort() calls
            else if (exitCode === 134) {
                errorType = 'process_abort';
                error = `Worker crashed with SIGABRT (exit code 134) - process was aborted`;
            }
            // Exit code 139 (SIGSEGV) indicates segmentation fault
            else if (exitCode === 139) {
                errorType = 'segmentation_fault';
                error = `Worker crashed with SIGSEGV (exit code 139) - segmentation fault`;
            }
            
            // Notify orchestrator about worker crash for monitoring and reporting
            if (this.client) {
                const crashMessage = {
                    sessionUuid: this.testdata.sessionConfig.sessionUuid,
                    clientUuid: this.uuid,
                    workerIndex: workerIndex,
                    exitCode: effectiveExitCode,
                    error: error,
                    errorType: errorType,
                    specs: specs,
                    timestamp: new Date().toISOString(),
                    message: `Worker ${workerIndex} crashed with ${errorType}: ${error}`
                };
                
                // Log detailed crash information
                if (isNullCrash) {
                    console.log('\x1b[31m%s\x1b[0m', `[V8 FATAL ERROR] Worker ${workerIndex} crashed with null exit code - V8 fatal error (unreachable code, etc.)`);
                } else if (exitCode === 1) {
                    console.log('\x1b[33m%s\x1b[0m', `[PROCESS CRASH] Worker ${workerIndex} crashed with exit code 1 - general error or uncaught exception`);
                } else {
                    console.log('\x1b[33m%s\x1b[0m', `[PROCESS CRASH] Worker ${workerIndex} crashed with exit code ${exitCode}`);
                }
                
                console.log('\x1b[33m%s\x1b[0m', `[WORKER CRASH] Worker ${workerIndex} crash will be handled by WebDriverIO's specFileRetries mechanism if configured`);
                
                this.client.publish('workerCrash', JSON.stringify(crashMessage));
            }
        } else {
            console.log(`[onWorkerEnd][Worker ${workerIndex}] Worker ended normally`);
        }
    }
    async before() {
        let loadService = this;
        if (loadService.state == 5) {
            console.error("Throwing error in before")
            throw new Error("LOAD test is in ERROR state, stopping test ASAP");
        }
        
        // Staggering is now handled in beforeSession, so proceed normally here
        let stateConfig = {
            "sessionUuid": loadService.testdata.sessionConfig.sessionUuid,
            "clientUuid": loadService.uuid,
            "worker": loadService.workerIndex,
            "state": 4,
            "syncpoint": false
        };
        loadService.client.publish('updateState', JSON.stringify(stateConfig));
        
        if (loadService.hasWorkerRampupDelay) {
            // With worker rampup delay, we can proceed immediately since timing was controlled in beforeSession
            console.log(`[before][Worker ${loadService.workerIndex}] Proceeding immediately (timing controlled in beforeSession)`);
            loadService.state = 4;
        } else {
            // Without worker rampup delay, use normal orchestrator synchronization
            console.log(`[before][Worker ${loadService.workerIndex}] Waiting for orchestrator state synchronization...`);
            while (loadService.state < 4) {
                await loadService.utils.delay(50);
            }
        }
    }
    beforeSuite() {
        if (this.state == 5) {
            console.error("Throwing error in beforeSuite")
            throw new Error("LOAD test is in ERROR state, stopping test ASAP");
        }
    }
    beforeHook() {
    }
    afterHook() {
        // console.log('execute afterHook(test, context, { error, result, duration, passed, retries })')
    }
    async beforeTest() {
        if (this.state == 5) {
            console.error("Throwing error in beforeTest")
            throw new Error("LOAD test is in ERROR state, stopping test ASAP");
        }
        // console.log('execute beforeTest(test, context) worker: ' + this.workerIndex);
        // console.log('execute beforeCommand(commandName, args) worker: ' + this.workerIndex);
        while (this.state < 4 || this.syncpoint) {
            await this.utils.delay(100);
        }
    }
    async beforeCommand() {
        // if (this.state == 5) {
        //     console.error("Throwing error in beforeCommand")
        //     throw new Error("LOAD test is in ERROR state, stopping test ASAP");
        // }
        // console.log('execute beforeCommand(commandName, args) worker: ' + this.workerIndex);
    }
    afterCommand() {
        // console.log('execute afterCommand(commandName, args, result, error)')
    }
    async afterTest() {
        let loadService = this;
        if (loadService.state == 5) {
            console.error("Throwing error in afterTest")
            throw new Error("LOAD test is in ERROR state, stopping test ASAP");
        }
        if (global.workerConfig.syncpoint) {
            loadService.syncpoint = global.workerConfig.syncpoint;
            global.workerConfig.syncpoint = false;
            let stateConfig = {
                "sessionUuid": loadService.testdata.sessionConfig.sessionUuid,
                "clientUuid": loadService.uuid,
                "worker": loadService.workerIndex,
                "state": loadService.state,
                "syncpoint": loadService.syncpoint
            };
            // console.log(stateConfig);
            loadService.client.publish('updateState', JSON.stringify(stateConfig));
        }
    }
    afterSuite() {
        // console.log('execute afterSuite(suite)')
    }
    after() {
        // console.log('execute after(result, capabilities, specs)')
    }
    async onComplete(exitCode, config, capabilities, results) {
        console.log('execute onComplete(exitCode, config, capabilities, results)');
        if (this.storeServer) {
            this.storeServer.close(() => {
                console.log('Store server closed');
            });
        }
    }
    onReload() {
        // console.log('execute onReloadedSessionId, newSessionId)')
    }
    beforeFeature() {
        // console.log('execute beforeFeature(uri, feature, scenarios)')
    }
    beforeScenario() {
        // console.log('execute beforeScenario(uri, feature, scenario, sourceLocation, context)')
    }
    beforeStep() {
        let loadService = this;
        if (loadService.state == 5) {
            console.error("Throwing error in beforeStep")
            throw new Error("LOAD test is in ERROR state, stopping test ASAP");
        }
    }
    afterStep() {
        // console.log('execute afterStep({ uri, feature, step }, context, { error, result, duration, passed, retries })')
    }
    afterScenario() {
        // console.log('execute afterScenario(uri, feature, scenario, result, sourceLocation, context)')
    }
    afterFeature() {
        // console.log('execute afterFeature(uri, feature, scenarios)')
    }
}