'use strict';
const path = require('path');
const fs = require('fs');
const { execSync } = require('child_process');
const { ScriptExecutor } = require('../scriptExecutor');
const { captureAndUpload } = require('../screenshotReporter');

/**
 * Mata cualquier proceso chrome.exe que tenga TOMADO este userDataDir.
 *
 * Causa raíz del error `browserType.launchPersistentContext: Target page, context
 * or browser has been closed`: Chrome es single-instance por perfil. Si un task
 * anterior dejó un chrome.exe vivo sobre `sessions/<usuario>` (típico cuando la PC
 * se suspende a mitad de una sesión larga de inbox y el browser queda zombie), al
 * lanzar el task siguiente Chrome detecta el perfil tomado, le pasa el control a la
 * instancia existente y el proceso nuevo se cierra al instante → Playwright ve que
 * el proceso murió → ese error.
 *
 * Borrar SingletonLock/Socket/Cookie NO alcanza: esos archivos los re-crea el
 * proceso vivo. Hay que matar el proceso dueño del perfil ANTES de lanzar.
 *
 * Best-effort, solo Windows. Devuelve la cantidad de procesos eliminados.
 */
function killChromeHoldingProfile(userDataDir, log) {
    if (process.platform !== 'win32') return 0; // matcheo por command-line: solo Windows por ahora
    try {
        // Normalizar el perfil para comparar contra la command-line de cada chrome.exe:
        // backslashes + minúsculas. La command-line trae `--user-data-dir="C:\...\sessions\usuario"`.
        const norm = String(userDataDir).replace(/\//g, '\\').toLowerCase();
        // PowerShell vía -EncodedCommand (base64 UTF-16LE): evita todo el infierno de
        // escapar comillas/backslashes al pasar el script por la línea de comandos.
        const ps = [
            "$ErrorActionPreference='SilentlyContinue';",
            `$dir=${JSON.stringify(norm)};`,
            "Get-CimInstance Win32_Process -Filter \"Name='chrome.exe'\" |",
            "Where-Object { $_.CommandLine -and (($_.CommandLine.ToLower() -replace '/','\\').Contains('--user-data-dir')) -and (($_.CommandLine.ToLower() -replace '/','\\').Contains($dir)) } |",
            "ForEach-Object { Stop-Process -Id $_.ProcessId -Force; $_.ProcessId }",
        ].join(' ');
        const encoded = Buffer.from(ps, 'utf16le').toString('base64');
        const out = execSync(
            `powershell -NoProfile -NonInteractive -EncodedCommand ${encoded}`,
            { encoding: 'utf8', timeout: 15000, windowsHide: true }
        );
        const killed = out.split(/\r?\n/).map(s => s.trim()).filter(Boolean);
        if (killed.length) {
            log(`Chrome zombie sobre el perfil: ${killed.length} proceso(s) eliminado(s) antes de lanzar (PIDs: ${killed.join(', ')})`);
        }
        return killed.length;
    } catch (e) {
        log(`No se pudo verificar/matar Chrome zombie sobre el perfil: ${e.message}`);
        return 0;
    }
}

// Clasifica el motivo de captura a partir del mensaje/URL del error.
// /accounts/suspended|disabled/ → baneo; /challenge|checkpoint/ → challenge; resto → failure.
function classifyShotReason(errMsg = '') {
    const m = String(errMsg).toLowerCase();
    if (/suspended|disabled/.test(m)) return 'ban';
    if (/challenge|checkpoint|challenge_detected|two_factor/.test(m)) return 'challenge';
    return 'failure';
}

/**
 * Handler para tareas de tipo EXECUTE_SCRIPT.
 *
 * Recibe un array de steps en task.payload.steps y los ejecuta con ScriptExecutor.
 * Usa CloakBrowser con contexto persistente (sesión guardada en disco) para que
 * el login de Instagram se mantenga entre tareas.
 *
 * La lógica de negocio (selectores, flujos, Gemini) vive exclusivamente en el
 * servidor, que genera el array de steps. Este handler no contiene ninguna
 * referencia a Instagram.
 */
class ScriptTaskHandler {
    log(msg) {
        console.log(`[SCRIPT_HANDLER] ${new Date().toISOString()} ${msg}`);
    }

    async execute(task, hooks = {}) {
        const { username } = task.account || {};
        const { steps = [], sessionDir, contextOptions = {} } = task.payload || {};

        if (!steps.length) throw new Error('El payload no contiene pasos (steps)');

        this.log(`Ejecutando script de ${steps.length} pasos para @${username}`);

        // ── Directorio de sesión persistente ─────────────────────────────────
        // El servidor indica qué directorio usar (ej. "sessions/usuario").
        // Si no se especifica, se usa "sessions/<username>" por defecto.
        const dataDir = process.env.XPONENCIA_DATA_DIR || require('../workerConfig').dataDir || process.cwd();
        const relDir = sessionDir || `sessions/${username || 'default'}`;
        const userDataDir = path.isAbsolute(relDir) ? relDir : path.join(dataDir, relDir);

        // ── Guardia de sesión local ───────────────────────────────────────────
        // Si la sesión no existe en ESTE worker, intentar restaurarla desde las
        // cookies guardadas en el servidor antes de rechazar la tarea.
        const sessionHasContent = fs.existsSync(userDataDir) &&
            fs.readdirSync(userDataDir).some(f => !f.startsWith('.')); // al menos un archivo no-oculto
        let cookiesToRestore = null;
        if (!sessionHasContent) {
            // Intentar recuperar cookies desde el servidor
            const serverUrl = (process.env.TASK_SERVER_URL || '').replace(/\/$/, '');
            if (serverUrl && username) {
                try {
                    const r = await fetch(
                        `${serverUrl}/api/accounts/${encodeURIComponent(username)}/cookies`,
                        { signal: AbortSignal.timeout(8000) }
                    );
                    if (r.ok) {
                        const data = await r.json();
                        if (Array.isArray(data.cookies) && data.cookies.length > 0) {
                            cookiesToRestore = data.cookies;
                            this.log(`@${username}: sesión no encontrada localmente — restaurando ${cookiesToRestore.length} cookies desde servidor (guardadas: ${data.updated_at || '?'})`);
                        }
                    }
                } catch (fetchErr) {
                    this.log(`@${username}: no se pudo consultar cookies del servidor: ${fetchErr.message}`);
                }
            }
            if (!cookiesToRestore) {
                throw new Error(
                    `Sin sesión local para @${username} en este worker (buscada en: ${relDir}). ` +
                    `Esta tarea pertenece a otro dispositivo — vinculá la cuenta en el worker correcto.`
                );
            }
            // Crear el directorio vacío para que launchPersistentContext arranque limpio
            fs.mkdirSync(userDataDir, { recursive: true });
        }

        // Si llegamos aquí, la sesión existe: asegurar que el directorio esté accesible.
        fs.mkdirSync(userDataDir, { recursive: true });

        // ── Matar Chrome zombie que tenga tomado este perfil ──────────────────
        // Antes de tocar los lock files: si quedó un chrome.exe vivo de un task
        // anterior (PC suspendida a mitad de sesión, etc.), hay que matarlo o el
        // launch fallará con "Target page, context or browser has been closed".
        const killed = killChromeHoldingProfile(userDataDir, m => this.log(m));
        if (killed > 0) {
            // Dar tiempo al SO a liberar los handles del perfil antes de relanzar.
            await new Promise(r => setTimeout(r, 800));
        }

        // ── Limpiar archivos de bloqueo de Chromium ───────────────────────────
        for (const lockFile of ['SingletonLock', 'SingletonSocket', 'SingletonCookie']) {
            const lp = path.join(userDataDir, lockFile);
            if (fs.existsSync(lp)) {
                try { fs.unlinkSync(lp); } catch (_) { /* ignorar */ }
            }
        }

        const headless     = process.env.BROWSER_HEADLESS === 'true';
        // Default: playwright (patchright + stealth + channel chrome).
        // CloakBrowser solo si BROWSER_ENGINE=cloakbrowser explícitamente.
        const useCloakBrowser = process.env.BROWSER_ENGINE === 'cloakbrowser';

        // ── Abrir navegador con contexto persistente ──────────────────────────
        let browser;

        if (useCloakBrowser) {
            // CloakBrowser — Real PC Mode (solo si BROWSER_ENGINE=cloakbrowser)
            const { REAL_PC_LAUNCH_OPTIONS, getLaunchArgs, applyRealPcPatches } = require('../realPcContext');
            try {
                const { launchPersistentContext } = await require('../../src/cloakbrowserLoader').getCloakBrowser();
                browser = await launchPersistentContext({
                    userDataDir,
                    headless,
                    ...REAL_PC_LAUNCH_OPTIONS,
                    args: getLaunchArgs(userDataDir),
                    ...contextOptions,
                });
                await applyRealPcPatches(browser);
                this.log(`CloakBrowser lanzado con sesión en: ${userDataDir}`);
            } catch (cloakErr) {
                // Fallback a Patchright/Playwright + stealth si CloakBrowser no está disponible
                this.log(`CloakBrowser no disponible (${cloakErr.message}), fallback a Patchright+stealth`);
                const { getChromium, applyPlaywrightStealth } = require('../realPcContext');
                const chromium = getChromium();
                const channel  = process.env.BROWSER_CHANNEL || undefined;
                browser = await chromium.launchPersistentContext(userDataDir, {
                    headless,
                    channel,
                    args: ['--disable-blink-features=AutomationControlled'],
                    viewport: null,
                    locale: 'es-AR',
                    timezoneId: 'America/Argentina/Buenos_Aires',
                    ...contextOptions,
                });
                await applyPlaywrightStealth(browser, headless);
            }
        } else {
            // Patchright (→ fallback playwright-core) + stealth + channel chrome (modo por defecto)
            const { getChromium, applyPlaywrightStealth } = require('../realPcContext');
            const chromium = getChromium();
            const channel  = process.env.BROWSER_CHANNEL || undefined; // 'chrome' → Google Chrome del sistema
            this.log(`Browser: patchright + channel=${channel || 'chromium'} + stealth`);
            browser = await chromium.launchPersistentContext(userDataDir, {
                headless,
                channel,
                args: [
                    '--disable-blink-features=AutomationControlled',
                ],
                viewport: null,   // null = Chrome decide el viewport según la ventana real
                locale: 'es-AR',
                timezoneId: 'America/Argentina/Buenos_Aires',
                ...contextOptions,
            });
            await applyPlaywrightStealth(browser, headless);
        }

        // Usar la primera página existente o abrir una nueva
        const pages = browser.pages ? browser.pages() : [];
        const page = pages.length ? pages[0] : await browser.newPage();

        // ── Restaurar cookies desde servidor (sesión recuperada de DB) ────────
        if (cookiesToRestore) {
            try {
                await browser.addCookies(cookiesToRestore);
                this.log(`@${username}: ${cookiesToRestore.length} cookies inyectadas en el contexto`);
            } catch (injErr) {
                this.log(`@${username}: error inyectando cookies restauradas: ${injErr.message}`);
            }
        }

        // ── Pre-check: detectar challenge/ban ANTES de ejecutar pasos ────────
        // Si la sesión ya está en una URL de challenge o suspensión (ej. la cuenta
        // fue baneada entre tareas y el usuario no lo vio), lanzar ChallengeError
        // inmediatamente sin ejecutar ningún paso.
        // Nota: hay que navegar primero a instagram.com para refrescar la URL de la
        // sesión, ya que el contexto persistente puede tener una blank page cargada.
        {
            const { detect: detectChallengeOnLoad } = require('../challengeDetector');
            let urlOnLoad = page.url ? page.url() : '';

            // Si la página está en blanco (about:blank) o no es instagram, navegar
            // al home para forzar la redirección de la sesión y detectar el estado real.
            // Se usa 'networkidle' para esperar a que posibles redirecciones JS terminen
            // (Instagram a veces redirige a /accounts/disabled/ via JS después del DOMContentLoaded).
            if (!urlOnLoad || urlOnLoad === 'about:blank' || !urlOnLoad.includes('instagram.com')) {
                try {
                    await page.goto('https://www.instagram.com/', {
                        waitUntil: 'domcontentloaded',
                        timeout: 20000,
                    });
                    // Esperar hasta 3s para que se resuelvan redirecciones JS post-carga
                    // (ban/suspend redirect suele ocurrir 500-1500ms después del DOMContentLoaded)
                    await page.waitForTimeout(3000);
                    urlOnLoad = page.url ? page.url() : '';
                } catch (navErr) {
                    this.log(`Pre-check: error navegando a IG: ${navErr.message}`);
                }
            } else {
                // Si ya hay una URL de instagram cargada, dar un momento para que se
                // resuelva cualquier redirect pendiente (ej. cuenta baneada entre tareas)
                await page.waitForTimeout(800);
                urlOnLoad = page.url ? page.url() : '';
            }

            const preCheck = detectChallengeOnLoad(urlOnLoad);
            if (preCheck.detected) {
                // Telemetría: capturar la pantalla del challenge/baneo ANTES de cerrar el browser.
                await captureAndUpload(page, {
                    reason:   classifyShotReason(`${preCheck.pattern} ${preCheck.url}`),
                    workerId: process.env.WORKER_ID,
                    taskId:   task.id,
                    note:     `Detectado al abrir sesión: ${preCheck.pattern}`,
                }).catch(() => {});
                await browser.close().catch(() => {});
                throw new Error(
                    `CHALLENGE_DETECTED: ${preCheck.pattern} — ${preCheck.url} ` +
                    `(detectado al abrir sesión, antes de ejecutar pasos)`
                );
            }
        }

        // ── Detector de cierre inesperado del navegador ───────────────────────
        // Si el usuario cierra la ventana o el proceso del browser muere,
        // rechazamos inmediatamente con un error para que workerRuntime
        // llame a reportFailure y la tarea no quede colgada en RUNNING.
        let _browserClosedReject = null;
        const browserClosedPromise = new Promise((_, reject) => {
            _browserClosedReject = reject;
        });
        const onBrowserDisconnect = () => {
            if (_browserClosedReject) {
                // Distinguir: si el cancelToken ya estaba activo = cierre programático (stale/cancelación)
                //             si no = el usuario cerró Chrome manualmente
                const isCancelled = cancelToken?.cancelled;
                const reason = isCancelled
                    ? 'TASK_CANCELLED: tarea cancelada por el servidor (stale o cancelación manual)'
                    : 'BROWSER_CLOSED_BY_USER: el usuario cerró la ventana de Chrome manualmente';
                _browserClosedReject(new Error(reason));
            }
        };
        browser.on('disconnected', onBrowserDisconnect);

        // Watcher: si el cancelToken se activa mientras el executor está colgado en
        // una operación de Playwright (click con timeout 30s, navigate, etc.), cerrar
        // el browser sin esperar — esto dispara 'disconnected', que ya rechaza
        // browserClosedPromise y corta el Promise.race inmediatamente.
        const cancelToken = task._cancelToken || null;
        const cancelWatcher = cancelToken
            ? setInterval(() => {
                if (cancelToken.cancelled) {
                    clearInterval(cancelWatcher);
                    browser.close().catch(() => {});
                }
            }, 300)
            : null;

        // Telemetría on-demand: exponer el page activo al workerRuntime para que el
        // heartbeat pueda capturarlo cuando el admin lo pida. Se limpia en el finally.
        if (typeof hooks.onPageReady === 'function') hooks.onPageReady(page);

        try {
            const executor = new ScriptExecutor(page, {
                serverUrl:   process.env.TASK_SERVER_URL,
                workerId:    process.env.WORKER_ID,
                taskId:      task.id,
                cancelToken,
            });
            // Correr la ejecución en carrera con el detector de cierre:
            // si el browser se cierra (o el cancelWatcher lo cierra), browserClosedPromise
            // rechaza y corta la ejecución de inmediato sin esperar el paso actual.
            const result = await Promise.race([
                executor.execute(steps),
                browserClosedPromise,
            ]);
            this.log(`@${username} script completado (${result.stepsCompleted}/${result.totalSteps} pasos)`);
            return { processedType: 'EXECUTE_SCRIPT', account: username, ...result };
        } catch (err) {
            // Telemetría: capturar la pantalla en el momento del fallo, ANTES de cerrar
            // el browser. Saltear cancelaciones y cierres manuales (no aportan diagnóstico
            // y el page suele estar ya muerto).
            const msg = err?.message || String(err);
            if (!/TASK_CANCELLED|BROWSER_CLOSED_BY_USER/.test(msg)) {
                await captureAndUpload(page, {
                    reason:   classifyShotReason(msg),
                    workerId: process.env.WORKER_ID,
                    taskId:   task.id,
                    note:     msg.slice(0, 200),
                }).catch(() => {});
            }
            throw err;
        } finally {
            if (typeof hooks.onPageReady === 'function') hooks.onPageReady(null);
            if (cancelWatcher) clearInterval(cancelWatcher);
            browser.off('disconnected', onBrowserDisconnect);
            _browserClosedReject = null;

            // ── Guardar cookies en el servidor antes de cerrar el browser ────
            if (username) {
                const serverUrl = (process.env.TASK_SERVER_URL || '').replace(/\/$/, '');
                if (serverUrl) {
                    try {
                        const cookies = await browser.cookies();
                        if (cookies.length > 0) {
                            await fetch(
                                `${serverUrl}/api/accounts/${encodeURIComponent(username)}/cookies`,
                                {
                                    method: 'PUT',
                                    headers: { 'Content-Type': 'application/json' },
                                    body: JSON.stringify({ cookies, worker_id: process.env.WORKER_ID }),
                                    signal: AbortSignal.timeout(8000),
                                }
                            );
                            this.log(`@${username}: ${cookies.length} cookies guardadas en servidor`);
                        }
                    } catch (saveErr) {
                        this.log(`@${username}: no se pudieron guardar cookies en servidor: ${saveErr.message}`);
                    }
                }
            }

            await browser.close().catch(() => {});
        }
    }
}

module.exports = ScriptTaskHandler;
