'use strict';

const { app, BrowserWindow, ipcMain, Menu, Tray, nativeImage, shell } = require('electron');
const path   = require('path');
const fs     = require('fs');
const os     = require('os');
const crypto = require('crypto');
const { spawn, execSync } = require('child_process');

// ── AppUserModelId (Windows taskbar) ─────────────────────────────────────────
// DEBE llamarse antes de app.whenReady() y antes de que se cree cualquier ventana.
// Sin esto, Windows agrupa el proceso bajo "electron.exe" y usa el ícono de Electron
// en la barra de tareas en vez del ícono de la app.
if (process.platform === 'win32') {
    app.setAppUserModelId('com.xponencia.worker');
}

// ── Single instance ───────────────────────────────────────────────────────────
const gotLock = app.requestSingleInstanceLock();
if (!gotLock) { app.quit(); }

let isQuitting    = false;
let mainWindow    = null;
let tray          = null;
let workerProcess = null;

// ── Licencias ─────────────────────────────────────────────────────────────────
let activationWindow = null;
let licensePoller    = null;
let _cachedHwid      = null;

function getWorkerHwid() {
    if (_cachedHwid) return _cachedHwid;
    try {
        const { machineIdSync } = require('node-machine-id');
        _cachedHwid = machineIdSync();
        return _cachedHwid;
    } catch (_) {}
    try {
        const out = execSync(
            'REG QUERY "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography" /v MachineGuid',
            { encoding: 'utf8', timeout: 5000, windowsHide: true }
        );
        const guid = out.split('REG_SZ')[1].replace(/\r+|\n+|\s+/gi, '').toLowerCase();
        _cachedHwid = crypto.createHash('sha256').update(guid).digest('hex');
        return _cachedHwid;
    } catch (_) {}
    _cachedHwid = crypto.createHash('sha256')
        .update(os.hostname() + (os.cpus()[0]?.model || '') + String(os.totalmem()))
        .digest('hex');
    return _cachedHwid;
}

async function checkLicenseWithServer(hwid, serverUrl) {
    try {
        const controller = new AbortController();
        const tid = setTimeout(() => controller.abort(), 8000);
        const res = await fetch(`${serverUrl}/api/license/status?hwid=${encodeURIComponent(hwid)}`, {
            signal: controller.signal,
        });
        clearTimeout(tid);
        if (!res.ok) return null;
        return await res.json();
    } catch (_) {
        return null;
    }
}

function transitionToMain() {
    if (licensePoller) { clearInterval(licensePoller); licensePoller = null; }
    if (activationWindow && !activationWindow.isDestroyed()) {
        activationWindow.close();
    }
    if (!mainWindow) createWindow();
}

function showActivationWindow(hwid) {
    activationWindow = new BrowserWindow({
        width: 580,
        height: 640,
        resizable: false,
        backgroundColor: '#0a0a0f',
        title: 'xponencIA — Activación',
        webPreferences: {
            preload: path.join(__dirname, 'preload-worker.js'),
            contextIsolation: true,
            nodeIntegration: false,
        },
        show: false,
    });

    activationWindow.loadFile(path.join(__dirname, 'activate-worker.html'));
    activationWindow.once('ready-to-show', () => activationWindow.show());
    activationWindow.on('closed', () => {
        activationWindow = null;
        // Si no hay ventana principal y no estamos en transición, salir
        if (!mainWindow && !isQuitting) {
            isQuitting = true;
            app.quit();
        }
    });
    Menu.setApplicationMenu(null);

    // Polling de aprobación cada 30 segundos
    licensePoller = setInterval(async () => {
        const env = readEnv();
        const serverUrl = env.TASK_SERVER_URL;
        if (!serverUrl) return;
        const status = await checkLicenseWithServer(hwid, serverUrl);
        if (status?.estado === 'aprobado') {
            transitionToMain();
        } else if (activationWindow && !activationWindow.isDestroyed()) {
            activationWindow.webContents.send('license-update', {
                estado: status?.estado || 'no_registrado',
            });
        }
    }, 30000);
}

// APP_DIR: directorio del código del app (resources/app/ en producción, raíz del proyecto en dev).
// Contiene archivos del bundle que NO son datos del usuario: version.json, .env.example, etc.
// Nunca se usa para guardar datos del usuario — solo para leer assets del bundle.
const APP_DIR = path.join(__dirname, '..', '..');

// SCRIPT_DIR: directorio de datos del usuario (sesiones, .env, schedule.json).
// Se elige según el modo de ejecución para separar código de datos:
//
//   ZIP portable  → carpeta del .exe  (PORTABLE_EXECUTABLE_DIR, junto a sessions/ y .env)
//   NSIS instalado → %AppData%\xponencIA Worker  (app.getPath('userData'))
//                    Separado de resources/app/ → las reinstalaciones no tocan los datos.
//   Dev (sin empaquetar) → raíz del proyecto (mismo comportamiento anterior)
const SCRIPT_DIR = process.env.PORTABLE_EXECUTABLE_DIR
    || (app.isPackaged ? app.getPath('userData') : APP_DIR);

// Asegurar que el directorio de datos existe (especialmente la primera vez en NSIS/portable).
fs.mkdirSync(SCRIPT_DIR, { recursive: true });

// Si no existe .env en SCRIPT_DIR, inicializarlo desde .env.example del bundle.
// Esto garantiza que el worker tenga TASK_SERVER_URL, BROWSER_ENGINE, etc. desde el
// primer arranque, sin que el usuario tenga que abrir Configuración → Guardar.
{
    const targetEnv = path.join(SCRIPT_DIR, '.env');
    if (!fs.existsSync(targetEnv)) {
        const candidates = [
            path.join(APP_DIR, '.env.example'),
            path.join(APP_DIR, '.env'),
        ];
        for (const src of candidates) {
            if (fs.existsSync(src)) { try { fs.copyFileSync(src, targetEnv); } catch (_) {} break; }
        }
    }
}

// Soporte para dev: WORKER_ENV_FILE puede ser path absoluto o relativo a SCRIPT_DIR
const ENV_PATH = (() => {
    const wef = process.env.WORKER_ENV_FILE;
    if (!wef) return path.join(SCRIPT_DIR, '.env');
    if (path.isAbsolute(wef)) return wef;           // ruta absoluta (dev-worker.js)
    return path.resolve(SCRIPT_DIR, wef);           // relativa al directorio de datos
})();
const SCHEDULE_PATH    = path.join(SCRIPT_DIR, 'schedule.json');
// Marker que persiste entre reinicios: al arrancar lo detectamos y mostramos "✓ Actualizado a vX.X"
const JUST_UPDATED_PATH = path.join(SCRIPT_DIR, '.just-updated');

// URL del servidor por defecto — igual al placeholder del input de configuración.
// Se escribe en .env al primer arranque si no está definida.
const DEFAULT_SERVER_URL = 'http://72.61.130.23:4010';

// ── Helpers ───────────────────────────────────────────────────────────────────

function readEnv() {
    const vars = {};
    if (!fs.existsSync(ENV_PATH)) return vars;
    fs.readFileSync(ENV_PATH, 'utf8').split('\n').forEach(line => {
        const m = line.match(/^\s*([^#\s][^=]*?)\s*=\s*(.*?)\s*$/);
        if (m) vars[m[1]] = m[2];
    });
    return vars;
}

function writeEnv(updates) {
    let content = fs.existsSync(ENV_PATH) ? fs.readFileSync(ENV_PATH, 'utf8') : '';
    for (const [k, v] of Object.entries(updates)) {
        const re = new RegExp(`^${k}=.*$`, 'm');
        if (re.test(content)) content = content.replace(re, `${k}=${v}`);
        else content += `\n${k}=${v}`;
    }
    fs.writeFileSync(ENV_PATH, content.trim() + '\n', 'utf8');
}

function getVersionJson() {
    // version.json es parte del bundle del app, no datos del usuario → APP_DIR, no SCRIPT_DIR.
    // Si estuviéramos en SCRIPT_DIR (AppData), quedaría desactualizado tras cada update.
    const p = path.join(APP_DIR, 'version.json');
    if (!fs.existsSync(p)) return { version: 'desconocida' };
    try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch (_) { return { version: 'error' }; }
}

function resolveNodeBin() {
    const candidates = [
        'C:\\Program Files\\nodejs\\node.exe',
        'C:\\Program Files (x86)\\nodejs\\node.exe',
        path.join(SCRIPT_DIR, 'node.exe'),
    ];
    for (const p of candidates) if (fs.existsSync(p)) return p;
    try {
        const { execSync } = require('child_process');
        const found = execSync('where node', { encoding: 'utf8', stdio: ['ignore','pipe','ignore'] })
            .trim().split('\n')[0].trim();
        if (found && fs.existsSync(found)) return found;
    } catch (_) {}
    return process.execPath;
}

const NODE_BIN = resolveNodeBin();

// ── Worker process management ─────────────────────────────────────────────────

function sendToRenderer(channel, data) {
    if (mainWindow && !mainWindow.isDestroyed()) {
        mainWindow.webContents.send(channel, data);
    }
}

function startWorker() {
    if (workerProcess) return { ok: false, error: 'El worker ya está corriendo' };
    // startWorker.js es CÓDIGO del bundle → APP_DIR, no SCRIPT_DIR (que es el directorio de datos).
    // En ZIP portable: APP_DIR=resources/app/ mientras SCRIPT_DIR=directorio del exe.
    // En dev: ambos son ROOT, pero la distinción importa en producción.
    const scriptPath = path.join(APP_DIR, 'worker', 'startWorker.js');
    if (!fs.existsSync(scriptPath)) return { ok: false, error: `startWorker.js no encontrado en ${scriptPath}` };

    const env = { ...process.env, ...readEnv() };
    if (NODE_BIN === process.execPath) env.ELECTRON_RUN_AS_NODE = '1';
    env.WORKER_HWID = getWorkerHwid();
    // Asegurar que el worker use el mismo directorio de datos que la GUI (donde se guardan las sesiones).
    // Sin esto, workerConfig.js resuelve AppData (ruta de producción) en vez de SCRIPT_DIR,
    // y el worker no encuentra las sesiones guardadas por link-account.
    env.XPONENCIA_DATA_DIR = env.XPONENCIA_DATA_DIR || SCRIPT_DIR;

    workerProcess = spawn(NODE_BIN, [scriptPath], {
        cwd: SCRIPT_DIR,
        env,
        stdio: ['ignore', 'pipe', 'pipe'],
    });

    workerProcess.stdout.on('data', d => {
        for (const line of d.toString().split('\n').filter(l => l.trim()))
            sendToRenderer('worker-log', { level: 'info', msg: line });
    });
    workerProcess.stderr.on('data', d => {
        for (const line of d.toString().split('\n').filter(l => l.trim()))
            sendToRenderer('worker-log', { level: 'error', msg: line });
    });
    workerProcess.on('close', code => {
        workerProcess = null;
        sendToRenderer('worker-log', { level: 'system', msg: `Worker detenido (código: ${code ?? 'N/A'})` });
        if (!isQuitting) {
            sendToRenderer('worker-status', { running: false, reconnecting: true });
            sendToRenderer('worker-log', { level: 'system', msg: 'Reiniciando worker en 5 segundos...' });
            setTimeout(() => {
                if (!isQuitting && !workerProcess) {
                    sendToRenderer('worker-log', { level: 'system', msg: 'Reiniciando...' });
                    startWorker();
                }
            }, 5000);
        } else {
            sendToRenderer('worker-status', { running: false });
        }
    });
    workerProcess.on('error', err => {
        workerProcess = null;
        sendToRenderer('worker-status', { running: false });
        sendToRenderer('worker-log', { level: 'error', msg: `Error del worker: ${err.message}` });
    });

    sendToRenderer('worker-status', { running: true, pid: workerProcess.pid });
    return { ok: true, pid: workerProcess.pid };
}

function stopWorker() {
    if (!workerProcess) return { ok: false, error: 'No hay worker corriendo' };
    workerProcess.kill('SIGTERM');
    return { ok: true };
}

// ── Link account (Playwright login manual) ─────────────────────────────────────

let linkBrowser = null;   // referencia para cancelación

ipcMain.handle('link-account', async (_, { account_type = 'prospectora', expected_username = null } = {}) => {
    const linkEnv            = readEnv();
    const usePlainPlaywright = (linkEnv.BROWSER_ENGINE || process.env.BROWSER_ENGINE) === 'playwright';
    // INJECT_CHROME_COOKIES: por defecto true en Playwright (fingerprint coincide),
    // false en CloakBrowser (ig_did vinculado al Chrome real genera mismatch con el seed).
    const injectCookiesDefault = usePlainPlaywright ? 'true' : 'false';
    const injectCookies = (linkEnv.INJECT_CHROME_COOKIES ?? injectCookiesDefault) !== 'false';

    const sessionsDirName = account_type === 'seguidora' ? 'sessions_seguidoras' : 'sessions';
    const sessionsDir = path.join(SCRIPT_DIR, sessionsDirName);
    fs.mkdirSync(sessionsDir, { recursive: true });

    const tmpName = `.tmp_login_${Date.now()}`;
    const tmpDir  = path.join(sessionsDir, tmpName);
    fs.mkdirSync(tmpDir, { recursive: true });

    console.log(`\n[LINK] ── Vinculando cuenta (tipo: ${account_type}) ──────────────`);
    console.log(`[LINK] Motor: ${usePlainPlaywright ? 'Playwright estándar' : 'CloakBrowser (Real PC Mode)'}`);
    console.log(`[LINK] Cookies Chrome: ${injectCookies ? 'sí (INJECT_CHROME_COOKIES=true)' : 'no (INJECT_CHROME_COOKIES=false)'}`);
    sendToRenderer('link-account-progress', { status: 'opening', msg: 'Abriendo navegador...' });

    // ── a. Lanzar navegador ───────────────────────────────────────────────────
    let context;
    if (usePlainPlaywright) {
        // Patchright (→ fallback playwright) + stealth patch (BROWSER_ENGINE=playwright en .env)
        const { getChromium, applyPlaywrightStealth } = require('../realPcContext');
        const chromium = getChromium();
        const channel  = (linkEnv.BROWSER_CHANNEL || process.env.BROWSER_CHANNEL) || undefined;
        console.log(`[LINK] Lanzando Patchright + canal=${channel || 'chromium'} + stealth...`);
        context = await chromium.launchPersistentContext(tmpDir, {
            headless: false,
            channel,
            args: [
                '--no-sandbox',
                '--disable-blink-features=AutomationControlled',
            ],
            viewport: null,   // null = Chrome decide el viewport según la ventana real
            locale: 'es-AR',
            timezoneId: 'America/Argentina/Buenos_Aires',
        });
        await applyPlaywrightStealth(context);
    } else {
        // CloakBrowser — Real PC Mode (modo por defecto)
        let launchPersistentContext;
        try {
            const cb = await import('cloakbrowser');
            launchPersistentContext = cb.launchPersistentContext;
        } catch (_) {
            return { ok: false, error: 'CloakBrowser no disponible. Reinstalá la aplicación.' };
        }

        const { REAL_PC_LAUNCH_OPTIONS, getLaunchArgs, applyRealPcPatches } = require('../realPcContext');
        // Pantalla en CSS pixels (Electron siempre devuelve valores lógicos, dpi-agnostic)
        const { screen: electronScreen } = require('electron');
        const primaryDisplay = electronScreen.getPrimaryDisplay();
        const cssScreen = { width: primaryDisplay.size.width, height: primaryDisplay.size.height };
        console.log(`[LINK] Pantalla detectada: ${cssScreen.width}x${cssScreen.height} CSS pixels`);
        // GPU: generada por el seed para mantener consistencia con canvas/audio.

        context = await launchPersistentContext({
            userDataDir: tmpDir,
            headless: false,
            ...REAL_PC_LAUNCH_OPTIONS,
            args: getLaunchArgs(tmpDir, cssScreen),
        });
        await applyRealPcPatches(context);
    }

    try {
        linkBrowser = context;

        // ── b. Inyectar cookies de dispositivo de Chrome ─────────────────────
        // En modo Playwright: el fingerprint coincide con Chrome real → seguro inyectar.
        // En modo CloakBrowser: el ig_did está vinculado al fingerprint real → mismatch
        //   con el seed → puede causar captcha. Controlado por INJECT_CHROME_COOKIES en .env.
        sendToRenderer('link-account-progress', { status: 'opening', msg: 'Preparando navegador...' });
        if (injectCookies) {
            try {
                const { extractChromeDeviceCookies } = require('../chromeImporter');
                console.log('[LINK] Leyendo cookies de dispositivo desde Chrome...');
                const { device } = await extractChromeDeviceCookies();
                if (device.length > 0) {
                    await context.addCookies(device);
                    const names = device.map(c => c.name).join(', ');
                    console.log(`[LINK] ✓ Cookies de Chrome inyectadas: ${names}`);
                    sendToRenderer('link-account-progress', {
                        status: 'opening',
                        msg: `✓ Identidad de Chrome importada: ${names}`,
                    });
                } else {
                    console.log('[LINK] ⚠ Chrome no tiene cookies de Instagram — identidad nueva');
                    sendToRenderer('link-account-progress', {
                        status: 'opening',
                        msg: 'Chrome no tiene sesión previa de Instagram — se creará una identidad nueva',
                    });
                }
            } catch (importErr) {
                console.log(`[LINK] ⚠ Chrome no disponible: ${importErr.message}`);
                sendToRenderer('link-account-progress', {
                    status: 'opening',
                    msg: `Navegador listo (Chrome no disponible: ${importErr.message})`,
                });
            }
        } else {
            console.log('[LINK] Cookies de Chrome omitidas (INJECT_CHROME_COOKIES=false)');
            sendToRenderer('link-account-progress', { status: 'opening', msg: 'Navegador listo (identidad limpia)' });
        }

        const page = context.pages()[0] || await context.newPage();

        // ── c. Navegar a Instagram ────────────────────────────────────────────
        // El ig_did ya está inyectado → Instagram ve "dispositivo conocido".
        // Ir directo a instagram.com es más limpio que el rodeo por Google
        // (que causaba redirects google.com → google.com.ar → instagram.com).
        console.log('[LINK] Navegando a instagram.com...');
        await page.goto('https://www.instagram.com/', { waitUntil: 'domcontentloaded' });
        await page.waitForTimeout(800 + Math.floor(Math.random() * 600));

        sendToRenderer('link-account-progress', {
            status: 'waiting',
            msg: expected_username
                ? `Iniciá sesión con @${expected_username} en Instagram.\nSe verificará que el usuario coincida — no cierres esta ventana.`
                : 'Iniciá sesión en Instagram.\nEl usuario se detectará automáticamente — no cierres esta ventana.',
        });

        let detected = null;
        const TIMEOUT = 6 * 60 * 1000;
        const start   = Date.now();

        while (!detected && !page.isClosed() && Date.now() - start < TIMEOUT) {
            await new Promise(r => setTimeout(r, 2000));
            if (page.isClosed()) break;

            const hasHome = await page.$([
                'svg[aria-label*="Inicio"]',
                'svg[aria-label*="Home"]',
                'a[href*="/direct/inbox/"]',
            ].join(',')).catch(() => null);
            if (!hasHome) continue;

            // Método 1: link de perfil con avatar en el nav
            detected = await page.evaluate(() => {
                const links = Array.from(document.querySelectorAll('a[href]'));
                const SKIP = new Set(['/', '/reels/', '/explore/', '/direct/', '/direct/inbox/',
                                      '/stories/', '/tv/', '/accounts/', '#']);
                for (const a of links) {
                    const href = a.getAttribute('href');
                    if (!href || SKIP.has(href) || href.startsWith('/accounts/')) continue;
                    const m = href.match(/^\/([a-z0-9._]+)\/?$/i);
                    if (m && a.querySelector('img')) return m[1];
                }
                return null;
            }).catch(() => null);

            // Método 2: API interna de Instagram
            if (!detected) {
                detected = await page.evaluate(async () => {
                    try {
                        const r = await fetch('/api/v1/accounts/current_user/?edit=true', {
                            credentials: 'include',
                            headers: { 'x-ig-app-id': '936619743392459' },
                        });
                        if (!r.ok) return null;
                        const d = await r.json();
                        return d?.user?.username || null;
                    } catch { return null; }
                }).catch(() => null);
            }
        }

        await context.close().catch(() => {});
        linkBrowser = null;

        if (!detected) {
            fs.rmSync(tmpDir, { recursive: true, force: true });
            return { ok: false, error: 'No se detectó la sesión. Intentá de nuevo.' };
        }

        // Si se esperaba un username específico, verificar que coincida
        if (expected_username && detected.toLowerCase() !== expected_username.toLowerCase()) {
            fs.rmSync(tmpDir, { recursive: true, force: true });
            return { ok: false, error: `Cuenta incorrecta: iniciaste sesión con @${detected} pero se esperaba @${expected_username}.` };
        }

        // Mover sesión al directorio definitivo.
        // Si ya existe una sesión previa (posiblemente contaminada de un intento
        // anterior), la REEMPLAZAMOS con la nueva sesión limpia.
        // El usuario completó el login manualmente → quiere esta sesión, no la vieja.
        const finalDir = path.join(sessionsDir, detected);
        const hadOldSession = fs.existsSync(finalDir);
        if (hadOldSession) {
            fs.rmSync(finalDir, { recursive: true, force: true });
        }
        try {
            fs.renameSync(tmpDir, finalDir);
        } catch (_) {
            // renameSync puede fallar cross-device — fallback a copy+delete
            fs.mkdirSync(finalDir, { recursive: true });
            const cpAll = (src, dst) => {
                for (const e of fs.readdirSync(src, { withFileTypes: true })) {
                    const s = path.join(src, e.name), d = path.join(dst, e.name);
                    e.isDirectory() ? (fs.mkdirSync(d, { recursive: true }), cpAll(s, d)) : fs.copyFileSync(s, d);
                }
            };
            cpAll(tmpDir, finalDir);
            fs.rmSync(tmpDir, { recursive: true, force: true });
        }

        console.log(`[LINK] ✓ Sesión de @${detected} guardada en: ${finalDir}`);
        if (hadOldSession) console.log(`[LINK]   (sesión anterior eliminada y reemplazada)`);
        sendToRenderer('link-account-progress', {
            status: 'saving',
            msg: hadOldSession
                ? `Sesión anterior de @${detected} reemplazada por sesión limpia ✓`
                : `Guardando sesión de @${detected}...`,
        });

        if (expected_username) {
            // Cuenta ya aprobada en el servidor — solo actualizar worker_id sin pasar por el flujo de aprobación
            const env = readEnv();
            const serverUrl = env.TASK_SERVER_URL || 'http://72.61.130.23:4010';
            const workerId  = env.WORKER_ID || require('os').hostname();
            await fetch(`${serverUrl}/api/accounts`, {
                method:  'POST',
                headers: { 'Content-Type': 'application/json' },
                body:    JSON.stringify({ username: detected, worker_id: workerId, hwid: getWorkerHwid(), tipo_cuenta: account_type }),
            }).catch(() => {});  // ignorar errores de red, la sesión ya fue guardada
        } else {
            // Cuenta nueva — registrar y esperar aprobación del admin
            const env = readEnv();
            const serverUrl = env.TASK_SERVER_URL || 'http://72.61.130.23:4010';
            const workerId  = env.WORKER_ID || require('os').hostname();
            const res = await fetch(`${serverUrl}/api/accounts`, {
                method:  'POST',
                headers: { 'Content-Type': 'application/json' },
                body:    JSON.stringify({ username: detected, enabled: true, worker_id: workerId, hwid: getWorkerHwid(), tipo_cuenta: account_type }),
            });
            const data = await res.json().catch(() => ({}));
            if (!res.ok) {
                // El servidor rechazó el registro (ej. 409 = cuenta ya vinculada a otro worker,
                // 403 = límite de cuentas alcanzado). Limpiar la sesión guardada localmente
                // para no dejar datos inconsistentes ni permitir que el worker opere
                // con una cuenta que el servidor no autorizó.
                try { fs.rmSync(finalDir, { recursive: true, force: true }); } catch (_) {}
                console.log(`[LINK] ✗ Servidor rechazó @${detected}: HTTP ${res.status} — ${data.error || 'sin detalle'}`);
                return { ok: false, error: data.error || `Error ${res.status} al registrar la cuenta en el servidor` };
            }
        }

        return { ok: true, username: detected };

    } catch (err) {
        if (context) await context.close().catch(() => {});
        linkBrowser = null;
        try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_) {}
        return { ok: false, error: err.message };
    }
});

// ── IPC handlers ──────────────────────────────────────────────────────────────

ipcMain.handle('get-hwid', () => ({ hwid: getWorkerHwid() }));

ipcMain.handle('check-license-status', async () => {
    const env = readEnv();
    const hwid = getWorkerHwid();
    const serverUrl = env.TASK_SERVER_URL;
    if (!serverUrl) return { ok: false, error: 'Sin URL de servidor configurada' };
    const status = await checkLicenseWithServer(hwid, serverUrl);
    if (!status) return { ok: false, error: 'No se pudo conectar al servidor' };
    // Si ya fue aprobado, transicionar de inmediato (sin esperar al poller)
    if (status.estado === 'aprobado') {
        setImmediate(() => transitionToMain());
    }
    return { ok: true, ...status };
});

ipcMain.handle('submit-license-request', async (_, { nombre, email, info_adicional } = {}) => {
    const env = readEnv();
    const hwid = getWorkerHwid();
    const serverUrl = env.TASK_SERVER_URL;
    if (!serverUrl) return { ok: false, error: 'Sin URL de servidor configurada' };
    try {
        const res = await fetch(`${serverUrl}/api/license/request`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ hwid, nombre, email, info_adicional }),
        });
        const data = await res.json();
        return { ok: res.ok, ...data };
    } catch (e) {
        return { ok: false, error: e.message };
    }
});

ipcMain.handle('get-env', () => readEnv());
ipcMain.handle('save-env', (_, updates) => { writeEnv(updates); return { ok: true }; });
ipcMain.handle('get-version', () => getVersionJson());
ipcMain.handle('get-is-dev', () => ({
    dev: !!(process.env.WORKER_ENV_FILE || process.env.NODE_ENV === 'development'),
}));

ipcMain.handle('get-schedule', () => {
    if (!fs.existsSync(SCHEDULE_PATH)) return null;
    try { return JSON.parse(fs.readFileSync(SCHEDULE_PATH, 'utf8')); } catch (_) { return null; }
});
ipcMain.handle('save-schedule', (_, data) => {
    fs.writeFileSync(SCHEDULE_PATH, JSON.stringify(data, null, 2), 'utf8');
    return { ok: true };
});
ipcMain.handle('worker-start', () => startWorker());
ipcMain.handle('worker-stop', () => stopWorker());
ipcMain.handle('worker-is-running', () => ({ running: !!workerProcess }));

// Lista los perfiles de Chrome instalados en la máquina (para importar sesión)
ipcMain.handle('list-chrome-profiles', () => {
    try {
        const { listChromeProfiles } = require('../chromeImporter');
        return { ok: true, profiles: listChromeProfiles() };
    } catch (e) {
        return { ok: false, error: e.message, profiles: [] };
    }
});

/**
 * Diagnostica la sesión de una cuenta: lee las cookies del perfil de CloakBrowser
 * guardado en sessions/<username>/ y muestra qué hay (ig_did, sessionid, etc.).
 * Usa el mismo lector SQLite del chromeImporter pero con la clave del perfil local.
 */
ipcMain.handle('diagnose-session', async (_, { username, account_type }) => {
    const dirName = account_type === 'seguidora' ? 'sessions_seguidoras' : 'sessions';
    const sessionDir = path.join(SCRIPT_DIR, dirName, username);

    if (!fs.existsSync(sessionDir)) {
        return { ok: false, error: 'No existe sesión local para esta cuenta' };
    }

    try {
        // CloakBrowser guarda las cookies en el mismo formato que Chrome:
        // Local State → clave AES encriptada con DPAPI del usuario Windows
        // Network/Cookies (o Cookies) → SQLite con cookies encriptadas
        const { dpApiDecryptFromLocalState, parseSQLiteCookiesWithKey } = require('../sessionDiag');
        const result = await dpApiDecryptFromLocalState(sessionDir);
        return { ok: true, ...result };
    } catch (e) {
        return { ok: false, error: e.message };
    }
});

// Proxy de llamadas a la API del servidor
ipcMain.handle('server-api', async (_, { method, path: apiPath, body }) => {
    const env = readEnv();
    const serverUrl = env.TASK_SERVER_URL || 'http://72.61.130.23:4010';
    try {
        const opts = {
            method: method || 'GET',
            headers: { 'Content-Type': 'application/json' },
        };
        if (body) opts.body = JSON.stringify(body);
        const res  = await fetch(`${serverUrl}${apiPath}`, opts);
        const text = await res.text();
        let data;
        try { data = JSON.parse(text); } catch (_) { data = { _raw: text.slice(0, 200) }; }
        return { ok: res.ok, status: res.status, data };
    } catch (err) {
        return { ok: false, error: err.message };
    }
});

ipcMain.handle('open-env-file', () => {
    shell.openPath(ENV_PATH);
});

ipcMain.handle('open-external', (_, url) => {
    shell.openExternal(url);
});

// Verifica si existe sesión local para un conjunto de usernames
ipcMain.handle('check-local-sessions', (_, usernames) => {
    const result = {};
    for (const username of usernames) {
        const dirs = [
            path.join(SCRIPT_DIR, 'sessions', username),
            path.join(SCRIPT_DIR, 'sessions_seguidoras', username),
        ];
        const found = dirs.find(d => {
            try { return fs.existsSync(d) && fs.readdirSync(d).length > 0; } catch (_) { return false; }
        });
        result[username] = found ? 'ok' : 'missing';
    }
    return result;
});

// Resetea la sesión local de un username (borra carpeta) — útil para re-login
ipcMain.handle('reset-local-session', (_, { username, account_type }) => {
    const dirName = account_type === 'seguidora' ? 'sessions_seguidoras' : 'sessions';
    const sessionDir = path.join(SCRIPT_DIR, dirName, username);
    try {
        if (fs.existsSync(sessionDir)) {
            fs.rmSync(sessionDir, { recursive: true, force: true });
        }
        return { ok: true };
    } catch (err) {
        return { ok: false, error: err.message };
    }
});

// ── Auto-updater ──────────────────────────────────────────────────────────────
// Descarga la actualización via Node.js (no Chrome) → sin bloqueos de AV por download de browser.
// Extrae con tar.exe (nativo en Windows 10+). Aplica al reiniciar.
// Solo funciona para WORKER_VARIANT=zip (portable ZIP).

const https   = require('https');
const http    = require('http');
const zlib    = require('zlib');

let _pendingUpdatePath    = null;   // ruta del ZIP descargado listo para aplicar
let _pendingUpdateIsPatch = false;  // true = patch ZIP (sin node_modules), false = ZIP completo

ipcMain.handle('auto-update-check', async () => {
    try {
        const env = readEnv();
        const serverUrl = (env.TASK_SERVER_URL || '').replace(/\/$/, '');
        if (!serverUrl) return { available: false, error: 'No hay servidor configurado' };

        const localVer = (() => {
            try { return JSON.parse(fs.readFileSync(path.join(APP_DIR, 'version.json'), 'utf8')).version || '0.0.0'; }
            catch (_) { return '0.0.0'; }
        })();

        const remote = await new Promise((resolve, reject) => {
            const url = serverUrl + '/api/version';
            const mod = url.startsWith('https') ? https : http;
            const req = mod.get(url, { timeout: 8000 }, (res) => {
                let data = '';
                res.on('data', d => data += d);
                res.on('end', () => {
                    try { resolve(JSON.parse(data)); } catch (_) { resolve({}); }
                });
            });
            req.on('error', reject);
            req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
        });

        const remoteVer = remote.version || '0.0.0';
        // Comparar versiones: solo actualizar si la remota es MAYOR que la local.
        // Formato: "YYYY.M.DD-HHMM" — parseamos a timestamp para comparar correctamente.
        // (string comparison falla cuando cambia el mes: "2026.10.01" < "2026.5.01" por comparación de chars)
        const parseVer = (v) => {
            const m = v.match(/^(\d{4})\.(\d{1,2})\.(\d{2})-(\d{2})(\d{2})$/);
            if (!m) return 0;
            return new Date(
                parseInt(m[1]), parseInt(m[2]) - 1, parseInt(m[3]),
                parseInt(m[4]), parseInt(m[5])
            ).getTime();
        };
        const available = remoteVer !== 'unknown' && parseVer(remoteVer) > parseVer(localVer);
        return { available, localVer, remoteVer, serverUrl };
    } catch (e) {
        return { available: false, error: e.message };
    }
});

ipcMain.handle('auto-update-download', async (event, { serverUrl, usePatch = false, fullZipFile = null }) => {
    const env = readEnv();
    const variant = (env.WORKER_VARIANT || 'zip').trim();

    // Elegir qué descargar:
    //   - patch ZIP (solo código .js, sin node_modules) cuando usePatch=true y variante zip
    //   - ZIP completo: usa fullZipFile del /api/version (ej. "xponencia-latest-win.zip"),
    //     con fallback a "latest-win.zip" para compatibilidad con servidores viejos
    let releaseFile;
    if (variant !== 'zip') {
        releaseFile = 'latest-setup.exe';
    } else if (usePatch) {
        releaseFile = 'latest-win-patch.zip';
    } else {
        releaseFile = fullZipFile || 'xponencia-latest-win.zip';
    }
    const downloadUrl = serverUrl + '/releases/' + releaseFile;

    const tmpDir = path.join(os.tmpdir(), 'xponencia-update');
    fs.mkdirSync(tmpDir, { recursive: true });
    const tmpFile = path.join(tmpDir, releaseFile);

    return new Promise((resolve) => {
        const mod = downloadUrl.startsWith('https') ? https : http;
        const req = mod.get(downloadUrl, { timeout: 120000 }, (res) => {
            if (res.statusCode !== 200) {
                return resolve({ ok: false, error: `HTTP ${res.statusCode}`, releaseFile });
            }
            const total = parseInt(res.headers['content-length'] || '0', 10);
            let received = 0;
            const out = fs.createWriteStream(tmpFile);
            res.on('data', chunk => {
                received += chunk.length;
                if (total > 0) {
                    const pct = Math.round(received / total * 100);
                    if (mainWindow) mainWindow.webContents.send('update-progress', { pct, received, total });
                }
            });
            res.pipe(out);
            out.on('finish', () => {
                _pendingUpdatePath = tmpFile;
                _pendingUpdateIsPatch = usePatch && variant === 'zip';
                resolve({ ok: true, path: tmpFile, variant, isPatch: _pendingUpdateIsPatch, releaseFile });
            });
            out.on('error', (e) => resolve({ ok: false, error: e.message }));
        });
        req.on('error', (e) => resolve({ ok: false, error: e.message }));
        req.on('timeout', () => { req.destroy(); resolve({ ok: false, error: 'timeout' }); });
    });
});

ipcMain.handle('auto-update-apply', async (event, { remoteVer = null } = {}) => {
    if (!_pendingUpdatePath || !fs.existsSync(_pendingUpdatePath)) {
        return { ok: false, error: 'No hay actualización descargada' };
    }

    const zipPath = _pendingUpdatePath;
    const env = readEnv();
    const variant = (env.WORKER_VARIANT || 'zip').trim();

    if (variant === 'zip') {
        // Para ZIP portable: extraer con PowerShell y reiniciar.
        //
        // ESTRUCTURA DE CADA ZIP:
        //   Patch (latest-win-patch.zip): app/ → app/worker/, app/src/, app/version.json, …
        //     Destino correcto: APP_DIR  (resources/app/)
        //     Después de strip: worker/, src/, version.json, … en APP_DIR  ✓
        //
        //   Full  (latest-win.zip):  xponencIA Worker/ → xponencIA Worker.exe, resources/, locales/, …
        //     Destino correcto: directorio del .exe  (un nivel arriba de resources/)
        //     Después de strip: xponencIA Worker.exe, resources/, locales/ en exe_dir  ✓
        //
        // TÉCNICA: no usamos tar (falla con ZIPs en Windows).
        // Usamos PowerShell con Expand-Archive → temp dir → Copy-Item del directorio interno.
        // Esto equivale a --strip-components=1 sin depender de tar.
        const zipPathW  = zipPath.replace(/\//g, '\\');
        const exePathW  = app.getPath('exe').replace(/\//g, '\\');
        const isPatch   = _pendingUpdateIsPatch;
        const appDirW   = APP_DIR.replace(/\//g, '\\');
        const exeDirW   = path.dirname(app.getPath('exe')).replace(/\//g, '\\');

        // Destino de extracción según tipo de ZIP
        const extractDest = isPatch ? appDirW : exeDirW;

        // ── ANTI-LOOP: pre-escribir version.json en APP_DIR antes de salir ──────
        // Garantiza que incluso si el script PowerShell falla al extraer el ZIP
        // (estructura inesperada, permisos, etc.), la nueva versión quede registrada
        // y el worker NO entre en un bucle descargando la misma actualización.
        // El PS1 sobreescribe este archivo con el contenido completo del ZIP,
        // que en condiciones normales contiene los mismos campos.
        let actualRemoteVer = remoteVer;
        if (!actualRemoteVer) {
            // Fallback: intentar leer version.json del ZIP antes de extraerlo.
            // Usamos PowerShell ZipFile para no necesitar dependencias extra.
            try {
                const psReadVer = [
                    `Add-Type -Assembly 'System.IO.Compression.FileSystem'`,
                    `$z = [System.IO.Compression.ZipFile]::OpenRead('${zipPathW.replace(/'/g, "''")}')`,
                    `$e = $z.Entries | Where-Object { $_.FullName -match 'version\\.json$' -and $_.FullName -notmatch 'node_modules' } | Select-Object -First 1`,
                    `if ($e) { $r = New-Object System.IO.StreamReader($e.Open()); Write-Output $r.ReadToEnd(); $r.Close() }`,
                    `$z.Dispose()`,
                ].join('; ');
                const { spawnSync } = require('child_process');
                const res = spawnSync('powershell', ['-NoProfile', '-NonInteractive', '-Command', psReadVer], { encoding: 'utf8', timeout: 5000 });
                const parsed = JSON.parse(res.stdout || '{}');
                if (parsed.version) actualRemoteVer = parsed.version;
            } catch (_) {}
        }
        if (actualRemoteVer) {
            try {
                const existing = (() => { try { return JSON.parse(fs.readFileSync(path.join(APP_DIR, 'version.json'), 'utf8')); } catch (_) { return {}; } })();
                existing.version = actualRemoteVer;
                fs.writeFileSync(path.join(APP_DIR, 'version.json'), JSON.stringify(existing, null, 2) + '\n', 'utf8');
            } catch (_) {}
        }

        // ── Script PowerShell (archivo .ps1 separado para evitar infierno de escapado en .bat)
        // Algoritmo:
        //   1. Expand-Archive al temp dir
        //   2. Obtener el único subdirectorio raíz (el nivel que se "stripea")
        //   3. Copy-Item del contenido del subdirectorio al destino real
        //   4. Limpiar temp
        const ps1Path = path.join(os.tmpdir(), 'xponencia-apply-update.ps1');
        // Escapar comillas simples en las rutas (por si acaso hay apostrofes en el path)
        const zipSafe     = zipPathW.replace(/'/g, "''");
        const destSafe    = extractDest.replace(/'/g, "''");
        const appDirSafe  = appDirW.replace(/'/g, "''");
        const remoteVerSafe = (actualRemoteVer || '').replace(/'/g, "''");
        const ps1Lines = [
            `$zipPath     = '${zipSafe}'`,
            `$extractDest = '${destSafe}'`,
            `$appDir      = '${appDirSafe}'`,
            `$remoteVer   = '${remoteVerSafe}'`,
            `$tmp = Join-Path $env:TEMP 'xponencia-extract-tmp'`,
            `if (Test-Path $tmp) { Remove-Item $tmp -Recurse -Force }`,
            `New-Item -ItemType Directory -Force $tmp | Out-Null`,
            `Expand-Archive -LiteralPath $zipPath -DestinationPath $tmp -Force`,
            // Soportar dos estructuras:
            // 1. ZIP con carpeta raíz (normal): "xponencIA Worker/" o "app/"
            // 2. ZIP plano (repack falló): archivos directamente en el root
            `$rootDirs = Get-ChildItem $tmp -Directory`,
            `$rootFiles = Get-ChildItem $tmp -File`,
            `if ($rootDirs.Count -eq 1 -and $rootFiles.Count -eq 0) {`,
            `    $inner = $rootDirs[0].FullName`,
            `    Get-ChildItem -Path $inner | Copy-Item -Destination $extractDest -Recurse -Force`,
            `} elseif ($rootDirs.Count -gt 0 -or $rootFiles.Count -gt 0) {`,
            `    Get-ChildItem -Path $tmp | Copy-Item -Destination $extractDest -Recurse -Force`,
            `} else {`,
            `    Write-Error 'ZIP vacio o estructura no reconocida'; exit 1`,
            `}`,
            `Remove-Item $tmp -Recurse -Force`,
            // ── ANTI-LOOP: forzar version.json con remoteVer DESPUÉS de la extracción ──
            // El ZIP puede tener una version.json desactualizada (ej. full ZIP nunca rebuildeado).
            // Este paso garantiza que APP_DIR/version.json tenga la versión que el servidor
            // reportó, sin importar qué venía en el ZIP. Sin esto, el worker entra en bucle.
            // Nota: usamos [IO.File]::WriteAllText en vez de Set-Content para evitar BOM en
            // PowerShell 5.1 (-Encoding utf8 agrega BOM, rompiendo JSON.parse en Node.js).
            `if ($remoteVer -ne '') {`,
            `    $vPath = Join-Path $appDir 'version.json'`,
            `    if (Test-Path $vPath) {`,
            `        try {`,
            `            $vObj = Get-Content -LiteralPath $vPath -Raw | ConvertFrom-Json`,
            `            $vObj | Add-Member -MemberType NoteProperty -Name 'version' -Value $remoteVer -Force`,
            `            $json = $vObj | ConvertTo-Json -Depth 10 -Compress`,
            `            [IO.File]::WriteAllText($vPath, $json, [Text.Encoding]::UTF8)`,
            `        } catch {}`,
            `    }`,
            `}`,
            `exit 0`,
        ];
        fs.writeFileSync(ps1Path, ps1Lines.join('\r\n'), 'utf8');

        // ── Bat: espera → PowerShell → reinicia
        const batPath = path.join(os.tmpdir(), 'xponencia-apply-update.bat');
        const batLines = [
            '@echo off',
            'timeout /t 3 /nobreak > nul',
            `echo Aplicando actualizacion xponencIA (${isPatch ? 'patch' : 'completa'})...`,
            `powershell -NoProfile -ExecutionPolicy Bypass -File "${ps1Path}"`,
            'if errorlevel 1 (',
            '    echo ERROR: La actualizacion no se pudo aplicar.',
            '    pause',
            '    goto :eof',
            ')',
            `del "${zipPathW}" 2>nul`,
            'echo Actualizacion aplicada. Reiniciando...',
            `start "" "${exePathW}"`,
            `del "${ps1Path}" 2>nul`,
            'del "%~f0"',
        ];
        fs.writeFileSync(batPath, batLines.join('\r\n'), 'utf8');

        // Marker para "✓ Actualizado a vX.X" en el siguiente arranque.
        // Guardamos la versión real para que el toast la muestre correctamente.
        try { fs.writeFileSync(JUST_UPDATED_PATH, actualRemoteVer || '1', 'utf8'); } catch (_) {}
        app.relaunch({ execPath: 'cmd.exe', args: ['/c', batPath] });
        app.exit(0);
        return { ok: true, isPatch };
    } else {
        // Para instalador .exe: abrir el instalador y salir
        shell.openPath(zipPath);
        setTimeout(() => app.quit(), 1500);
        return { ok: true };
    }
});

// Devuelve { updated: true, version } si acabamos de aplicar un update, y borra el marker.
// Devuelve { updated: false } si es un arranque normal.
ipcMain.handle('check-just-updated', () => {
    if (!fs.existsSync(JUST_UPDATED_PATH)) return { updated: false };
    let markerContent = '1';
    try { markerContent = fs.readFileSync(JUST_UPDATED_PATH, 'utf8').trim(); } catch (_) {}
    try { fs.unlinkSync(JUST_UPDATED_PATH); } catch (_) {}
    // El marker puede contener la versión real (ej. "2026.5.30-2044") o sólo "1" (legacy).
    const version = (markerContent && markerContent !== '1') ? markerContent : getVersionJson().version;
    return { updated: true, version };
});

// ── Remote uninstall / deactivation ───────────────────────────────────────────
// Cuando el servidor revoca la licencia de este worker (estado='revocado'),
// el cliente se "desinstala" limpiamente: saca el inicio automático, borra el
// acceso directo del Menú Inicio, para el worker y sale.

async function doUninstall(reason = 'Acceso revocado por el servidor') {
    console.log(`[WORKER-GUI] Desinstalando: ${reason}`);

    // 1. Sacar del inicio automático de Windows
    try { app.setLoginItemSettings({ openAtLogin: false, name: 'xponencIA Worker' }); } catch (_) {}

    // 2. Borrar acceso directo del Menú Inicio
    try {
        const shortcutPath = path.join(
            app.getPath('appData'),
            'Microsoft', 'Windows', 'Start Menu', 'Programs',
            'xponencIA Worker.lnk'
        );
        if (fs.existsSync(shortcutPath)) fs.unlinkSync(shortcutPath);
    } catch (_) {}

    // 3. Notificar al renderer (muestra pantalla de revocación)
    sendToRenderer('worker-revoked', { reason });

    // 4. Parar el worker process
    stopWorker();

    // 5. Cerrar la app tras un breve delay para que el usuario vea el mensaje
    setTimeout(() => { isQuitting = true; app.quit(); }, 4000);
}

// Polling de licencia mientras la app está corriendo.
// Si el admin revoca el acceso → desinstalar.
let _licenseWatchTimer = null;
function startLicenseWatch() {
    if (_licenseWatchTimer) return;
    _licenseWatchTimer = setInterval(async () => {
        const env = readEnv();
        const serverUrl = env.TASK_SERVER_URL;
        if (!serverUrl) return;
        const status = await checkLicenseWithServer(getWorkerHwid(), serverUrl).catch(() => null);
        if (status?.estado === 'revocado') {
            clearInterval(_licenseWatchTimer);
            _licenseWatchTimer = null;
            doUninstall('Acceso revocado por el administrador');
        }
    }, 5 * 60 * 1000); // cada 5 minutos
}

// ── Window ────────────────────────────────────────────────────────────────────

// ── Iconos ─────────────────────────────────────────────────────────────────────
// APP_ICON_PATH: versión circular (948×948, alpha) — para BrowserWindow, tray,
//   acceso directo del Menú Inicio y exe (taskbar de Windows).
//   Al ser cuadrada y con fondo transparente queda bien en todos los contextos
//   del sistema operativo (taskbar dark/light, bandeja del sistema, alt-tab).
const APP_ICON_PATH = (() => {
    const candidates = [
        path.join(__dirname, 'icon.png'),                           // worker/gui/icon.png (circular, copiado en build)
        path.join(APP_DIR, 'media', 'xponencia_logo_1 _circular.png'),
        path.join(APP_DIR, 'worker', 'gui', 'icon.png'),
    ];
    return candidates.find(p => fs.existsSync(p)) || null;
})();

function createWindow(hidden = false) {
    mainWindow = new BrowserWindow({
        width: 960,
        height: 720,
        minWidth: 800,
        minHeight: 560,
        backgroundColor: '#0a0a0f',
        title: 'xponencIA Worker',
        icon: APP_ICON_PATH || undefined,
        webPreferences: {
            preload: path.join(__dirname, 'preload-worker.js'),
            contextIsolation: true,
            nodeIntegration: false,
        },
        show: false,
    });

    mainWindow.loadFile(path.join(__dirname, 'index.html'));
    mainWindow.once('ready-to-show', () => {
        // Establecer el ícono explícitamente DESPUÉS de ready-to-show.
        // El parámetro `icon` del constructor setea el título, pero en Windows
        // el ícono de la barra de tareas requiere setIcon() explícito después
        // de que la ventana está lista para mostrarse.
        if (APP_ICON_PATH) {
            try {
                mainWindow.setIcon(nativeImage.createFromPath(APP_ICON_PATH));
            } catch (_) {}
        }

        // Si arrancó con Windows (--hidden), quedarse en la bandeja y no abrir ventana.
        // El usuario puede abrirla haciendo doble-click en el ícono de la bandeja.
        if (!hidden) mainWindow.show();
        // Auto-iniciar el worker al abrir la GUI
        startWorker();
    });

    mainWindow.on('close', (e) => {
        if (!isQuitting) {
            e.preventDefault();
            mainWindow.hide();
        }
    });
    mainWindow.on('closed', () => { mainWindow = null; });

    Menu.setApplicationMenu(null);
}

function createTray() {
    const icon = APP_ICON_PATH
        ? nativeImage.createFromPath(APP_ICON_PATH).resize({ width: 16, height: 16 })
        : nativeImage.createEmpty();

    tray = new Tray(icon);
    tray.setToolTip('xponencIA Worker');
    tray.setContextMenu(Menu.buildFromTemplate([
        { label: 'Abrir panel', click: () => { if (mainWindow) mainWindow.show(); else createWindow(); } },
        { type: 'separator' },
        { label: 'Salir', click: () => { isQuitting = true; app.quit(); } },
    ]));
    tray.on('double-click', () => { if (mainWindow) mainWindow.show(); else createWindow(); });
    tray.setToolTip('xponencIA Worker — corriendo en segundo plano');
}

// ── App lifecycle ─────────────────────────────────────────────────────────────

app.whenReady().then(async () => {
    // ── Garantizar valores clave en .env (por si .env.example no los tenía todos) ──
    // El bloque de SCRIPT_DIR (más arriba) ya copió .env.example si no existía .env.
    // Aquí solo completamos claves que el .env.example podría no tener.
    {
        const existing = readEnv();
        const defaults  = {};
        if (!existing.TASK_SERVER_URL) defaults.TASK_SERVER_URL = DEFAULT_SERVER_URL;
        if (!existing.BROWSER_ENGINE)  defaults.BROWSER_ENGINE  = 'playwright';
        if (!existing.BROWSER_CHANNEL) defaults.BROWSER_CHANNEL = 'chrome';
        if (Object.keys(defaults).length) {
            writeEnv(defaults);
            console.log('[WORKER-GUI] .env completado con defaults:', Object.keys(defaults).join(', '));
        }
    }

    // ── Verificar estado de Chrome al inicio ──────────────────────────────────
    try {
        const { extractChromeDeviceCookies } = require('../chromeImporter');
        const { device } = await extractChromeDeviceCookies();
        if (device.length > 0) {
            const names = device.map(c => c.name).join(', ');
            console.log(`[CHROME] ✓ Cookies de dispositivo disponibles: ${names}`);
            const igDid = device.find(c => c.name === 'ig_did');
            if (igDid) console.log(`[CHROME]   ig_did: ${igDid.value.slice(0, 12)}…`);
        } else {
            console.log('[CHROME] ⚠ Chrome no tiene cookies de Instagram. Abrí instagram.com en Chrome para generar el ig_did.');
        }
    } catch (e) {
        console.log(`[CHROME] ⚠ No se pudo leer Chrome: ${e.message}`);
    }

    const env = readEnv();

    // ── Inicio automático con Windows ─────────────────────────────────────────
    // Siempre activo — el afiliado no puede desactivarlo.
    // Se pasa --hidden para que arranque en la bandeja sin abrir la ventana.
    if (!process.env.ELECTRON_DEV) {
        app.setLoginItemSettings({
            openAtLogin: true,
            name:        'xponencIA Worker',
            args:        ['--hidden'],   // arranca silencioso en la bandeja del sistema
        });
    }

    // ── Acceso directo en el Menú Inicio (tecla Windows) ──────────────────────
    // Crea/actualiza el .lnk en AppData/Roaming/Microsoft/Windows/Start Menu/Programs/.
    // Se actualiza siempre para reflejar la ruta del exe en caso de que el afiliado
    // haya movido la carpeta del ZIP (portable).
    if (!process.env.ELECTRON_DEV && app.isPackaged) {
        try {
            const startMenuDir = path.join(
                app.getPath('appData'),
                'Microsoft', 'Windows', 'Start Menu', 'Programs'
            );
            fs.mkdirSync(startMenuDir, { recursive: true });
            const shortcutPath = path.join(startMenuDir, 'xponencIA Worker.lnk');
            const exePath = app.getPath('exe');
            const op = fs.existsSync(shortcutPath) ? 'update' : 'create';
            shell.writeShortcutLink(shortcutPath, op, {
                target:      exePath,
                description: 'xponencIA Worker — Automatización Instagram',
                icon:        APP_ICON_PATH || exePath,
                iconIndex:   0,
            });
        } catch (_) { /* no crítico */ }
    }

    // ── ¿Arrancó desde Windows (doble sesión de usuario) o fue el usuario? ────
    // Electron pasa --hidden cuando Windows lo lanza al inicio de sesión
    // (porque así lo registramos en setLoginItemSettings).
    const startHidden = process.argv.includes('--hidden');

    createTray();

    // En modo DEBUG se omite la validación de licencia
    if (env.DEBUG_MODE === 'true') {
        createWindow(startHidden);
        return;
    }

    const hwid = getWorkerHwid();
    const serverUrl = env.TASK_SERVER_URL;
    let approved = false;

    if (serverUrl) {
        const status = await checkLicenseWithServer(hwid, serverUrl);
        approved = (status?.estado === 'aprobado');
    }

    if (approved) {
        createWindow(startHidden);
        // Vigilar la licencia cada 5 min — si el admin la revoca, desinstalar
        startLicenseWatch();
    } else {
        // Si arrancó con Windows y ya no está aprobado (raro), mostrar activación igual.
        showActivationWindow(hwid);
    }
});

app.on('second-instance', () => {
    if (mainWindow) { if (mainWindow.isMinimized()) mainWindow.restore(); mainWindow.show(); mainWindow.focus(); }
});

app.on('window-all-closed', () => { /* vive en la bandeja */ });

app.on('before-quit', () => {
    isQuitting = true;
    if (licensePoller) { clearInterval(licensePoller); licensePoller = null; }
    if (linkBrowser) { try { linkBrowser.close(); } catch (_) {} }
    if (workerProcess) { try { workerProcess.kill('SIGKILL'); } catch (_) {} }
});
