'use strict';
const os     = require('os');
const path   = require('path');
const fs     = require('fs');
const crypto = require('crypto');
const ServerApiClient = require('./serverApiClient');
const { TaskType } = require('../contracts/taskTypes');
const { captureAndUpload } = require('./screenshotReporter');
const { SCRIPT_PROTOCOL_VERSION } = require('./scriptExecutor');

// Leer versión del worker desde version.json (empaquetado con el build)
function _readWorkerVersion() {
    try {
        const candidates = [
            path.join(__dirname, '..', 'version.json'),          // dev: raíz del proyecto
            path.join(__dirname, 'version.json'),                 // si está junto al worker/
            path.join(process.resourcesPath || '', 'app', 'version.json'), // Electron packaged
        ];
        for (const p of candidates) {
            if (fs.existsSync(p)) {
                const v = JSON.parse(fs.readFileSync(p, 'utf8'));
                if (v.version) return v.version;
            }
        }
    } catch (_) {}
    return null;
}
const WORKER_LOCAL_VERSION = _readWorkerVersion();

// Pausa después de completar una tarea antes de pedir la siguiente.
const POST_TASK_DELAY_MS    = parseInt(process.env.WORKER_POST_TASK_DELAY_MS || '2000');
const HEARTBEAT_INTERVAL_MS = 20000;
const FETCH_TIMEOUT_MS      = parseInt(process.env.WORKER_FETCH_TIMEOUT_MS || '45000');

/**
 * Calcula el HWID de la máquina local de forma determinista, independientemente
 * de si el proceso se lanzó desde Electron (installer) o desde Node.js (npm run dev).
 * Mismo algoritmo que getWorkerHwid() en main-worker.js.
 *
 * Prioridad:
 *  1. WORKER_HWID en env (set por main-worker.js cuando corre bajo Electron)
 *  2. node-machine-id (cross-platform, usa MachineGuid en Windows)
 *  3. REG QUERY directo (Windows fallback sin depender del paquete)
 *  4. hash(hostname + cpu + ram) — último recurso, puede cambiar si cambia hw
 */
function _computeHwid() {
    if (process.env.WORKER_HWID) return process.env.WORKER_HWID;
    try {
        // node-machine-id puede no estar disponible en todos los entornos
        const { machineIdSync } = require('node-machine-id');
        return machineIdSync();
    } catch (_) {}
    try {
        const { execSync } = require('child_process');
        const out  = execSync(
            'REG QUERY "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography" /v MachineGuid',
            { encoding: 'utf8', timeout: 3000, windowsHide: true }
        );
        const guid = out.split('REG_SZ')[1].replace(/\s+/g, '').toLowerCase();
        return crypto.createHash('sha256').update(guid).digest('hex');
    } catch (_) {}
    return crypto.createHash('sha256')
        .update(os.hostname() + (os.cpus()[0]?.model || '') + String(os.totalmem()))
        .digest('hex');
}

/**
 * Detecta si el proceso corre dentro de una máquina virtual.
 * Usa tres fuentes independientes via PowerShell para reducir falsos positivos:
 *   hw   → Manufacturer/Model del sistema (BIOS/firmware del hypervisor)
 *   proc → Procesos activos de guest tools (vmtoolsd, vboxservice, etc.)
 *   reg  → Claves de registro instaladas por VMware/VirtualBox guest additions
 *
 * Falsos positivos conocidos: equipos con Hyper-V Platform habilitado en BIOS
 * (Windows 11 lo activa para VBS/HVCI) pueden dar hw=true aunque sean PC físicas.
 * Por eso la decisión final (bloquear o no) la toma el servidor con más contexto.
 */
function _detectVm() {
    if (process.platform !== 'win32') return { isVm: false, indicators: [] };
    try {
        const { spawnSync } = require('child_process');
        const ps = [
            `$ErrorActionPreference='SilentlyContinue'`,
            `$s = Get-WmiObject Win32_ComputerSystem`,
            `$b = Get-WmiObject Win32_BaseBoard`,
            `$v = @('VMware','VirtualBox','VBOX','innotek','Xen','KVM','QEMU','Parallels','bhyve')`,
            `$hw = ($v | Where-Object { $s.Manufacturer -like "*$_*" -or $s.Model -like "*$_*" -or $b.Manufacturer -like "*$_*" }).Count -gt 0`,
            `if ($s.Model -eq 'Virtual Machine') { $hw = $true }`,
            `$vp = @('vmtoolsd','vboxservice','vboxtray','vmwaretray','vmwareuser','prl_tools','prl_cc')`,
            `$proc = ($vp | Where-Object { Get-Process $_ -ErrorAction SilentlyContinue }) -ne $null`,
            `$reg = (Test-Path 'HKLM:\\SOFTWARE\\VMware, Inc.\\VMware Tools') -or (Test-Path 'HKLM:\\SOFTWARE\\Oracle\\VirtualBox Guest Additions')`,
            `Write-Output "$hw|$proc|$reg"`,
        ].join('; ');
        const r = spawnSync('powershell',
            ['-NoProfile', '-NonInteractive', '-WindowStyle', 'Hidden', '-Command', ps],
            { encoding: 'utf8', timeout: 7000, windowsHide: true }
        );
        if (r.status !== 0 || !r.stdout?.trim()) return { isVm: false, indicators: [] };
        const [hw, proc, reg] = r.stdout.trim().split('|').map(v => v.trim().toLowerCase() === 'true');
        const indicators = [...(hw ? ['hw'] : []), ...(proc ? ['proc'] : []), ...(reg ? ['reg'] : [])];
        return { isVm: hw || proc || reg, indicators };
    } catch (_) {
        return { isVm: false, indicators: [] };
    }
}

class WorkerRuntime {
    constructor() {
        // ID estable por HWID: garantiza que npm run dev y el installer generan el mismo ID
        // para la misma máquina física, sin importar cómo se lanzó el proceso.
        // Prioridad: WORKER_ID explícito en .env → hostname-hwid[:8]
        const _hwid = _computeHwid();
        this.workerId = process.env.WORKER_ID || `${os.hostname()}-${_hwid.slice(0, 8)}`;
        // Asegurar que WORKER_HWID y WORKER_ID están disponibles para submódulos
        if (!process.env.WORKER_HWID)  process.env.WORKER_HWID  = _hwid;
        if (!process.env.WORKER_ID)    process.env.WORKER_ID    = this.workerId;
        this.api = new ServerApiClient(process.env.TASK_SERVER_URL);
        this.running = false;
        this.currentTaskId = null;
        this.heartbeatTimer = null;
        this._taskCancelledByServer = false;
        this._activeCancelToken = null;
        // Página Playwright de la tarea en curso (la setea el handler vía hook onPageReady).
        // Permite que el heartbeat capture un screenshot on-demand del browser abierto.
        this._currentPage = null;

        // Detección de VM: se ejecuta una vez al arrancar y se envía en el registro.
        // Timeout generoso (7s) porque WMI puede tardar en sistemas lentos.
        this._vmInfo = _detectVm();
        if (this._vmInfo.isVm) {
            console.log(`[WORKER] VM detectada (indicadores: ${this._vmInfo.indicators.join(', ')})`);
        }

        // Registro lazy de handlers
        this._handlers = null;
    }

    get handlers() {
        if (!this._handlers) {
            // Worker es ejecutor puro: solo sabe ejecutar scripts de pasos.
            // Toda la lógica de Instagram vive en el servidor (step generators).
            this._handlers = {
                [TaskType.EXECUTE_SCRIPT]: new (require('./handlers/scriptTaskHandler'))(),
            };
        }
        return this._handlers;
    }

    log(msg) {
        console.log(`[WORKER ${this.workerId}] ${new Date().toISOString()} ${msg}`);
    }

    // Empuja el error real de una tarea fallida al feed de logs del servidor
    // (worker_logs), no solo al campo `error` de ig_tasks. Sin esto, el crash
    // solo queda visible en el detalle de la tarea — invisible en la consola
    // del panel, que es donde se suele mirar primero.
    _reportTaskError(taskId, err) {
        const stack = (err?.stack || '').split('\n').slice(0, 4).join(' | ');
        const message = `💥 Tarea ${taskId} crasheó: ${err?.message || String(err)}${stack ? ` — ${stack}` : ''}`;
        if (!process.env.TASK_SERVER_URL) return;
        fetch(`${process.env.TASK_SERVER_URL}/api/worker/log`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ workerId: this.workerId, taskId, level: 'ERROR', message }),
        }).catch(() => {});
    }

    startHeartbeat(taskId) {
        this._taskCancelledByServer = false;
        this.heartbeatTimer = setInterval(async () => {
            try {
                const r = await this.api.heartbeat(taskId, this.workerId);
                if (r?.cancelled && !this._taskCancelledByServer) {
                    this.log(`Tarea ${taskId} cancelada por el servidor — se abortará al completar la operación actual`);
                    this._taskCancelledByServer = true;
                    if (this._activeCancelToken) this._activeCancelToken.cancelled = true;
                }
                // Propagar alertas del servidor (challenges, baneos) al proceso padre (Electron)
                // vía stdout usando el protocolo __WORKER_STATUS__:challenge_detected:<json>
                if (r?.alerts?.length) {
                    for (const alert of r.alerts) {
                        // Señal estructurada que main-worker.js parsea y convierte en notificación
                        process.stdout.write(
                            `__WORKER_STATUS__:challenge_detected:${JSON.stringify(alert)}\n`
                        );
                    }
                }
                // NOTA: la señal force_update ya NO se entrega por heartbeat. El heartbeat solo
                // corre durante una tarea activa, y reiniciar ahí mataba la tarea a la mitad.
                // El servidor la entrega vía /tasks/pull cuando el worker está ocioso (ver runOnce).

                // Captura on-demand: el admin pidió un screenshot. El heartbeat es el canal
                // correcto porque corre durante la tarea → el browser está abierto.
                if (r?.capture_screenshot && this._currentPage) {
                    this.log('[SHOT] Captura on-demand solicitada por el servidor');
                    captureAndUpload(this._currentPage, {
                        reason:   'on_demand',
                        workerId: this.workerId,
                        taskId,
                    }).then(res => {
                        if (!res.ok) this.log(`[SHOT] Captura on-demand falló: ${res.error}`);
                    }).catch(() => {});
                }
            } catch (_) {}
        }, HEARTBEAT_INTERVAL_MS);
    }

    stopHeartbeat() {
        if (this.heartbeatTimer) {
            clearInterval(this.heartbeatTimer);
            this.heartbeatTimer = null;
        }
    }

    async runOnce() {
        let res;
        try {
            res = await this.api.pullTask(this.workerId, FETCH_TIMEOUT_MS);
        } catch (err) {
            if (err.code === 'SESSION_KEY_MISMATCH') {
                // decryptPayload ya invalidó this.api._sessionKey — re-registrar ahora
                // mismo para obtener una clave fresca en sincronía con la del servidor,
                // en vez de repetir el mismo fallo de descifrado en cada pull.
                this.log(`${err.message} — re-registrando para refrescar la clave de sesión`);
                await this.api.registerWorker(this.workerId, Object.values(TaskType), process.env.WORKER_HWID || null, this._vmInfo, WORKER_LOCAL_VERSION, SCRIPT_PROTOCOL_VERSION)
                    .catch(e => this.log(`Re-registro tras SESSION_KEY_MISMATCH falló: ${e.message}`));
                return false;
            }
            this.log(`Error contactando servidor: ${err.message}`);
            // Espera breve antes de reintentar si el servidor no está disponible
            await new Promise(r => setTimeout(r, 5000));
            return false;
        }

        const task = res?.task;
        if (!task) {
            // El servidor solicitó actualización forzada y no hay tarea corriendo →
            // ventana segura para reiniciar. Pedimos al proceso padre que ejecute el ciclo.
            if (res?.force_update) {
                this.log('[UPDATE] Servidor solicitó actualización (worker ocioso) — disparando ciclo');
                process.stdout.write('__WORKER_STATUS__:force_update\n');
            }
            return false; // Long-poll expiró sin tarea
        }

        this.currentTaskId = task.id;
        this._activeCancelToken = { cancelled: false };
        task._cancelToken = this._activeCancelToken;
        this.log(`Tarea recibida: ${task.id} (${task.type}) para @${task.account?.username}`);
        this.startHeartbeat(task.id);

        try {
            const handler = this.handlers[task.type];
            if (!handler) throw new Error(`Sin handler para tipo: ${task.type}`);

            const result = await handler.execute(task, {
                onPageReady: (page) => { this._currentPage = page; },
            });

            if (this._taskCancelledByServer) {
                this.log(`Tarea ${task.id} completó ejecución pero fue cancelada — descartando resultado`);
            } else {
                await this.api.reportResult(task.id, this.workerId, {
                    status: 'DONE',
                    metrics: result || {},
                    finishedAt: new Date().toISOString(),
                });
                this.log(`Tarea completada: ${task.id}`);
            }
        } catch (err) {
            if (this._taskCancelledByServer) {
                this.log(`Tarea ${task.id} abortada por cancelación del servidor`);
            } else {
                this.log(`Tarea fallida: ${task.id} — ${err.message}`);
                this._reportTaskError(task.id, err);
                await this.api.reportFailure(task.id, this.workerId, err).catch(() => {});
            }
        } finally {
            this.stopHeartbeat();
            this.currentTaskId = null;
            this._taskCancelledByServer = false;
            this._activeCancelToken = null;
            this._currentPage = null; // el browser se cerró en el handler — soltar la referencia
        }
        return true; // indica que se ejecutó una tarea
    }

    async runLoop() {
        this.running = true;

        while (this.running) {
            try {
                const regResult = await this.api.registerWorker(this.workerId, Object.values(TaskType), process.env.WORKER_HWID || null, this._vmInfo, WORKER_LOCAL_VERSION, SCRIPT_PROTOCOL_VERSION);

                // ── Aplicar configuración enviada por el servidor ───────────────────
                // El admin controla estos valores desde el panel — el worker obedece.
                // 'headless': si el servidor lo indica, sobrescribir el .env local.
                // Esto garantiza que BROWSER_HEADLESS refleje la voluntad del servidor,
                // no lo que el afiliado tenga en su .env.
                if (regResult?.workerConfig) {
                    const wc = regResult.workerConfig;
                    if (typeof wc.headless === 'boolean') {
                        process.env.BROWSER_HEADLESS = wc.headless ? 'true' : 'false';
                        this.log(`Config servidor → BROWSER_HEADLESS=${process.env.BROWSER_HEADLESS}`);
                    }
                }

                // Limpiar flag de bloqueo si ahora pudo registrarse
                console.log('__WORKER_STATUS__:registered');
                break;
            } catch (err) {
                // Detectar conflicto de IP (409) — error específico, no reintenta ciegamente
                const isIpConflict = err.message.includes('409') || err.message.includes('IP_CONFLICT');
                if (isIpConflict) {
                    // Extraer el mensaje de error del body JSON si está disponible
                    const bodyMatch = err.message.match(/\{.*\}/s);
                    let detail = 'IP ya en uso por otro worker';
                    let conflictWorkerAlias = '';
                    if (bodyMatch) {
                        try {
                            const body = JSON.parse(bodyMatch[0]);
                            detail = body.error || detail;
                            conflictWorkerAlias = body.conflict_worker_alias || '';
                        } catch (_) {}
                    }
                    this.log(`BLOQUEADO POR CONFLICTO DE IP: ${detail}`);
                    // Señal especial para que main-worker.js actualice la UI
                    console.log(`__WORKER_STATUS__:ip_conflict:${conflictWorkerAlias}`);
                    // Reintentar menos agresivamente (cada 60s) — la IP libera cuando el otro worker se desconecta
                    await new Promise(r => setTimeout(r, 60000));
                } else {
                    this.log(`Servidor no disponible: ${err.message}. Reintentando en 5s...`);
                    await new Promise(r => setTimeout(r, 5000));
                }
            }
        }
        if (!this.running) return;

        this.log('Worker registrado, iniciando ciclo de polling (long-poll)');

        while (this.running) {
            const executedTask = await this.runOnce();
            // Si se ejecutó una tarea, esperar antes de pedir la siguiente
            // (el servidor ya gestiona el tiempo de espera cuando no hay tareas)
            if (executedTask) {
                await new Promise(resolve => setTimeout(resolve, POST_TASK_DELAY_MS));
            }
        }
    }

    stop() {
        this.running = false;
        this.stopHeartbeat();
        this.log('Worker detenido');
    }
}

module.exports = WorkerRuntime;
