'use strict';
/**
 * chromeImporter.js — Extrae cookies de instagram.com del perfil de Chrome real
 * y las devuelve en formato Playwright para inyectarlas en CloakBrowser.
 *
 * Esto hace que Instagram vea el mismo ig_did que ya conoce del navegador del
 * afiliado → no trata la sesión como "dispositivo nuevo" → menos desafíos.
 *
 * Flujo:
 *  1. Lee %LOCALAPPDATA%\Google\Chrome\User Data\Local State
 *     → extrae la master key encriptada con DPAPI
 *  2. PowerShell → CryptUnprotectData → master key AES-256 en claro
 *  3. Parsea Chrome\Default\Network\Cookies (SQLite3, reader mínimo sin deps)
 *  4. Desencripta valores con AES-256-GCM
 *  5. Devuelve array de cookies para context.addCookies()
 *
 * Requiere: Chrome cerrado (o al menos que el perfil Default no esté bloqueado).
 * Sin dependencias extra — usa solo Node built-ins + PowerShell (Windows).
 */

const fs      = require('fs');
const path    = require('path');
const crypto  = require('crypto');
const os      = require('os');
const { spawnSync } = require('child_process');

// ─────────────────────────────────────────────────────────────────────────────
// DPAPI — desencripta con PowerShell 5.1 (siempre disponible en Win10/11)
// ─────────────────────────────────────────────────────────────────────────────
function dpApiDecrypt(encryptedBuf) {
    // Convertir a array de bytes en PowerShell 5.1 (sin FromHexString)
    const bytes = Array.from(encryptedBuf).map(b => b.toString(10)).join(',');
    const psScript = [
        `$b = [byte[]](${bytes})`,
        'Add-Type -AssemblyName System.Security',
        '$d = [System.Security.Cryptography.ProtectedData]::Unprotect($b, $null, [System.Security.Cryptography.DataProtectionScope]::CurrentUser)',
        '[Convert]::ToBase64String($d)',
    ].join('; ');

    const r = spawnSync('powershell', [
        '-NoProfile', '-NonInteractive', '-WindowStyle', 'Hidden',
        '-Command', psScript,
    ], { encoding: 'utf8', timeout: 15000, windowsHide: true });

    if (r.status !== 0 || !r.stdout.trim()) {
        throw new Error('DPAPI decrypt falló: ' + (r.stderr?.trim() || r.error?.message || 'sin salida'));
    }
    return Buffer.from(r.stdout.trim(), 'base64');
}

// ─────────────────────────────────────────────────────────────────────────────
// AES-256-GCM — desencripta valor de cookie Chrome v10/v20
// ─────────────────────────────────────────────────────────────────────────────
function decryptChromeValue(encBlob, masterKey) {
    if (!Buffer.isBuffer(encBlob) || encBlob.length < 3) return '';
    const prefix = encBlob.slice(0, 3).toString('ascii');
    if (prefix !== 'v10' && prefix !== 'v20') return '';
    try {
        // format: [3 prefix][12 iv][ciphertext...][16 auth-tag]
        const iv       = encBlob.slice(3, 15);
        const authTag  = encBlob.slice(encBlob.length - 16);
        const ctext    = encBlob.slice(15, encBlob.length - 16);
        const d = crypto.createDecipheriv('aes-256-gcm', masterKey, iv);
        d.setAuthTag(authTag);
        return Buffer.concat([d.update(ctext), d.final()]).toString('utf8');
    } catch (_) { return ''; }
}

// ─────────────────────────────────────────────────────────────────────────────
// SQLite3 mínimo — solo lo necesario para Chrome's cookies file
// ─────────────────────────────────────────────────────────────────────────────

/** Lee un varint SQLite. Retorna { value, bytes }. Valores > 2^53 truncados. */
function readVi(buf, off) {
    let v = 0, i = 0;
    for (; i < 9; i++) {
        const b = buf[off + i];
        if (i === 8) { v = v * 256 + b; i++; break; }
        v = v * 128 + (b & 0x7F);
        if (!(b & 0x80)) { i++; break; }
    }
    return { value: v, bytes: i };
}

/** Tamaño en bytes de un serial type SQLite. */
function stSize(t) {
    if (t === 0) return 0;
    if (t <= 4) return t;
    if (t === 5) return 6;
    if (t === 6 || t === 7) return 8;
    if (t === 8 || t === 9) return 0;
    if (t >= 12 && t % 2 === 0) return (t - 12) >> 1;
    if (t >= 13 && t % 2 === 1) return (t - 13) >> 1;
    return 0;
}

/** Lee el valor de un campo dada su serial type. */
function stRead(buf, off, t) {
    if (t === 0)  return null;
    if (t === 1)  return buf.readInt8(off);
    if (t === 2)  return buf.readInt16BE(off);
    if (t === 3)  return buf.readIntBE(off, 3);
    if (t === 4)  return buf.readInt32BE(off);
    if (t === 5)  return buf.readIntBE(off, 6);
    // readIntBE en Node.js solo admite byteLength 1-6 — implementar 8-byte manualmente
    if (t === 6)  return buf.readUInt32BE(off) * 0x100000000 + buf.readUInt32BE(off + 4);
    if (t === 7)  return buf.readDoubleBE(off);
    if (t === 8)  return 0;
    if (t === 9)  return 1;
    if (t >= 12 && t % 2 === 0) { const n = (t-12)>>1; return buf.slice(off, off+n); }
    if (t >= 13 && t % 2 === 1) { const n = (t-13)>>1; return buf.slice(off, off+n).toString('utf8'); }
    return null;
}

/**
 * Lee todas las celdas de una leaf page (tipo 0x0D).
 * @param {Buffer} buf    — buffer completo del archivo SQLite
 * @param {number} pgOff  — offset en bytes del inicio de la página
 * @param {boolean} isPg1 — true si es la primera página (tiene 100-byte file header)
 */
function readLeafPage(buf, pgOff, isPg1) {
    const hdrOff = isPg1 ? pgOff + 100 : pgOff;
    if (buf[hdrOff] !== 0x0D) return []; // not a table leaf page
    const nCells = buf.readUInt16BE(hdrOff + 3);
    const rows = [];
    for (let i = 0; i < nCells; i++) {
        const cellPtr = buf.readUInt16BE(hdrOff + 8 + i * 2);
        const cellOff = pgOff + cellPtr;
        try {
            let pos = cellOff;
            const pl = readVi(buf, pos); pos += pl.bytes; // payload length
            const ri = readVi(buf, pos); pos += ri.bytes; // rowid (unused)
            void ri;

            const recStart = pos;
            const hl = readVi(buf, pos); pos += hl.bytes;
            const hdrEnd = recStart + hl.value;

            const types = [];
            while (pos < hdrEnd) {
                const t = readVi(buf, pos); pos += t.bytes;
                types.push(t.value);
            }
            pos = hdrEnd;
            const vals = [];
            for (const t of types) {
                vals.push(stRead(buf, pos, t));
                pos += stSize(t);
            }
            rows.push(vals);
        } catch (_) { /* celda malformada — saltar */ }
    }
    return rows;
}

/**
 * Extrae los nombres de columna del CREATE TABLE del schema de cookies.
 * Soporta cualquier versión de Chrome (el schema cambió varias veces).
 */
function parseCookiesColumns(sql) {
    if (!sql) return null;
    const m = sql.match(/\((.+)\)/s);
    if (!m) return null;
    return m[1]
        .split(',')
        .map(c => c.trim().match(/^(\w+)/)?.[1]?.toLowerCase())
        .filter(Boolean);
}

/**
 * Lee el archivo SQLite de Chrome y retorna las cookies del dominio indicado.
 * @param {string} sqlitePath — ruta al archivo Cookies de Chrome
 * @param {string} domain     — ej. '.instagram.com' o 'www.instagram.com'
 * @returns {{ rows: Array[], cols: string[] }}
 */
function parseChromeDb(sqlitePath, domain) {
    const buf = fs.readFileSync(sqlitePath);
    if (buf.slice(0, 6).toString('ascii') !== 'SQLite') throw new Error('No es un archivo SQLite válido');

    let pgSize = buf.readUInt16BE(16);
    if (pgSize === 1) pgSize = 65536; // special case
    const pgCount = buf.readUInt32BE(28);

    // ── Leer sqlite_master (siempre en página 1) para obtener schema ──────────
    let cookiesCols = null;
    const masterRows = readLeafPage(buf, 0, true);
    for (const r of masterRows) {
        // sqlite_master: type, name, tbl_name, rootpage, sql
        if (typeof r[0] === 'string' && r[0] === 'table' && r[1] === 'cookies') {
            cookiesCols = parseCookiesColumns(typeof r[4] === 'string' ? r[4] : '');
            break;
        }
    }

    // ── Escanear todas las páginas leaf para encontrar filas de cookies ────────
    const allRows = [];
    for (let p = 1; p <= Math.min(pgCount, 1000); p++) {
        const pgOff  = (p - 1) * pgSize;
        if (pgOff + pgSize > buf.length) break;
        const rows = readLeafPage(buf, pgOff, p === 1);
        for (const row of rows) {
            // Filtrar: filas de cookies tienen un INTEGER grande como primera columna
            // (creation_utc, un timestamp de microsegundos ~1.7×10^16)
            // Las filas de sqlite_master tienen string 'table'/'index' como primera columna
            if (typeof row[0] !== 'number' || row[0] < 1e12) continue;
            // Verificar que host_key contenga el dominio buscado
            const hasHost = row.some(v => typeof v === 'string' && v.includes('instagram.com'));
            if (hasHost) allRows.push(row);
        }
    }

    return { rows: allRows, cols: cookiesCols };
}

// ─────────────────────────────────────────────────────────────────────────────
// Copia de archivo con soporte para archivos bloqueados (Chrome abierto)
// ─────────────────────────────────────────────────────────────────────────────

/**
 * Copia src → dst tolerando que src esté abierto por Chrome (SQLite WAL lock).
 * Intenta primero fs.copyFileSync (rápido), si falla usa PowerShell con
 * FileShare.ReadWrite que puede leer archivos abiertos por otros procesos.
 * @returns {boolean} true si la copia fue exitosa
 */
function tryCopyWithSharing(src, dst) {
    // Intento 1: copia normal (funciona si Chrome está cerrado)
    try {
        fs.copyFileSync(src, dst);
        return true;
    } catch (_) { /* intentar con PowerShell */ }

    // Intento 2: PowerShell con FileShare.ReadWrite
    // Chrome abre el archivo SQLite con FILE_SHARE_READ|WRITE — podemos leerlo
    // si abrimos con el mismo nivel de compartición.
    const s = src.replace(/'/g, "''");
    const d = dst.replace(/'/g, "''");
    const psScript =
        `try {` +
        `  $fs=[System.IO.File]::Open('${s}',[System.IO.FileMode]::Open,[System.IO.FileAccess]::Read,[System.IO.FileShare]::ReadWrite);` +
        `  $fw=[System.IO.File]::Create('${d}');` +
        `  $fs.CopyTo($fw); $fw.Flush(); $fw.Close(); $fs.Close();` +
        `  exit 0` +
        `} catch { Write-Error $_.Exception.Message; exit 1 }`;

    const r = spawnSync('powershell', [
        '-NoProfile', '-NonInteractive', '-WindowStyle', 'Hidden',
        '-Command', psScript,
    ], { timeout: 12000, windowsHide: true });

    return r.status === 0 && fs.existsSync(dst);
}

// ─────────────────────────────────────────────────────────────────────────────
// API pública
// ─────────────────────────────────────────────────────────────────────────────

/**
 * Retorna la primera User Data dir de Chromium encontrada en la máquina.
 * Busca Chrome, Edge, Brave y Chrome Beta/Canary en orden de prioridad.
 */
function findChromiumBase() {
    const local  = process.env.LOCALAPPDATA  || path.join(os.homedir(), 'AppData', 'Local');
    const roaming = process.env.APPDATA      || path.join(os.homedir(), 'AppData', 'Roaming');
    const candidates = [
        // Google Chrome (stable)
        path.join(local, 'Google', 'Chrome', 'User Data'),
        // Microsoft Edge
        path.join(local, 'Microsoft', 'Edge', 'User Data'),
        // Brave
        path.join(local, 'BraveSoftware', 'Brave-Browser', 'User Data'),
        // Chrome Beta
        path.join(local, 'Google', 'Chrome Beta', 'User Data'),
        // Chrome Canary
        path.join(local, 'Google', 'Chrome SxS', 'User Data'),
        // Chromium
        path.join(local, 'Chromium', 'User Data'),
    ];
    const found = candidates.find(p => fs.existsSync(p));
    if (!found) throw new Error(
        'No se encontró ningún navegador Chromium instalado.\n' +
        'Buscado en: Chrome, Edge, Brave, Chrome Beta, Chrome Canary.'
    );
    return found;
}

/**
 * Convierte filas SQLite + master key → array de cookies en formato Playwright.
 * @internal
 */
function rowsToCookies(rows, cols, masterKey) {
    const COL_DEFAULTS = {
        host_key: 1, name: 2, value: 3, encrypted_value: 4,
        path: 5, expires_utc: 6, is_secure: 7, is_httponly: 8, samesite: 13,
    };
    const colIdx = { ...COL_DEFAULTS };
    if (cols) {
        for (const k of Object.keys(COL_DEFAULTS)) {
            const i = cols.indexOf(k);
            if (i !== -1) colIdx[k] = i;
        }
    }

    const result = [];
    for (const row of rows) {
        const hostKey     = row[colIdx.host_key]        ?? '';
        const name        = row[colIdx.name]            ?? '';
        const rawValue    = row[colIdx.value]           ?? '';
        const encValue    = row[colIdx.encrypted_value] ?? Buffer.alloc(0);
        const cookiePath  = row[colIdx.path]            ?? '/';
        const expiresUtc  = row[colIdx.expires_utc]     ?? 0;
        const isSecure    = !!row[colIdx.is_secure];
        const isHttpOnly  = !!row[colIdx.is_httponly];
        const sameSiteRaw = row[colIdx.samesite]        ?? -1;

        if (!hostKey || !name) continue;

        const value = Buffer.isBuffer(encValue) && encValue.length > 0
            ? decryptChromeValue(encValue, masterKey)
            : (typeof rawValue === 'string' ? rawValue : '');

        // Chrome: microsegundos desde 1601-01-01 → Playwright: segundos desde Unix epoch
        const expiresUnix = expiresUtc > 0
            ? Math.floor((expiresUtc - 11644473600000000) / 1000000)
            : -1;

        const sameSiteMap = { '-1': 'None', 0: 'None', 1: 'Lax', 2: 'Strict' };
        const sameSite = sameSiteMap[sameSiteRaw] ?? 'None';

        const finalDomain = hostKey.startsWith('.') ? hostKey : `.${hostKey.replace(/^www\./, '')}`;

        result.push({
            name,
            value,
            domain:   finalDomain,
            path:     typeof cookiePath === 'string' ? cookiePath : '/',
            expires:  expiresUnix,
            httpOnly: isHttpOnly,
            secure:   isSecure,
            sameSite,
        });
    }
    return result;
}

/**
 * Extrae las cookies de instagram.com del Chrome instalado en la máquina.
 * Escanea TODOS los perfiles (Default, Profile 1, Profile 2…) — no solo Default.
 * Retorna un array en formato Playwright listo para context.addCookies().
 *
 * @param {object} opts
 * @param {string} [opts.profileDir]  — forzar un perfil específico (ruta absoluta)
 * @param {string} [opts.domain]      — dominio a filtrar (default: 'instagram.com')
 * @returns {Promise<Array>}           — cookies en formato Playwright
 */
async function extractChromeIgCookies({ profileDir, domain = 'instagram.com' } = {}) {
    const chromeBase = findChromiumBase();

    // ── 1. Master key via DPAPI (una sola por instalación de Chrome) ──────────
    const localState = JSON.parse(fs.readFileSync(path.join(chromeBase, 'Local State'), 'utf8'));
    const encKeyB64  = localState?.os_crypt?.encrypted_key;
    if (!encKeyB64) throw new Error('No se encontró encrypted_key en Chrome Local State');
    const masterKey = dpApiDecrypt(Buffer.from(encKeyB64, 'base64').slice(5));
    if (masterKey.length !== 32) throw new Error(`Master key inválida: ${masterKey.length} bytes`);

    // ── 2. Enumerar perfiles a escanear ───────────────────────────────────────
    let profilesToScan;
    if (profileDir) {
        profilesToScan = [profileDir];
    } else {
        // Escanear todos los perfiles: Default + Profile N + cualquier carpeta con Preferences
        profilesToScan = [];
        try {
            for (const entry of fs.readdirSync(chromeBase, { withFileTypes: true })) {
                if (!entry.isDirectory()) continue;
                const d = entry.name;
                // Perfiles conocidos: Default, Profile 1, Profile 2, Person 1, etc.
                if (d === 'Default' || /^Profile \d+$/.test(d) || /^Person \d+$/.test(d)) {
                    const p = path.join(chromeBase, d);
                    // Verificar que sea un perfil real (tiene Preferences)
                    if (fs.existsSync(path.join(p, 'Preferences'))) profilesToScan.push(p);
                }
            }
        } catch (_) {}
        // Siempre incluir Default aunque no tenga Preferences
        const def = path.join(chromeBase, 'Default');
        if (!profilesToScan.includes(def)) profilesToScan.unshift(def);
    }

    // ── 3. Leer cookies de cada perfil, tomar el primero con datos de Instagram ─
    for (const profile of profilesToScan) {
        const cookiesFile = [
            path.join(profile, 'Network', 'Cookies'),  // Chrome 96+
            path.join(profile, 'Cookies'),
        ].find(p => fs.existsSync(p));
        if (!cookiesFile) continue;

        const tmp = path.join(os.tmpdir(), `xponencia_cookies_${Date.now()}.sqlite`);
        const copied = tryCopyWithSharing(cookiesFile, tmp);
        if (!copied) continue; // archivo inaccesible → probar el siguiente perfil

        let rows, cols;
        try {
            ({ rows, cols } = parseChromeDb(tmp, domain));
        } catch (_) {
            continue;
        } finally {
            try { fs.unlinkSync(tmp); } catch (_) {}
        }

        if (!rows.length) continue; // este perfil no tiene cookies de instagram → siguiente

        const cookies = rowsToCookies(rows, cols, masterKey);
        if (cookies.length > 0) return cookies; // ← primer perfil con cookies de Instagram
    }

    return []; // ningún perfil tiene cookies de Instagram
}

/**
 * Lista los perfiles de Chrome disponibles en la máquina.
 * @returns {Array<{name, displayName, path}>}
 */
function listChromeProfiles() {
    let chromeBase;
    try { chromeBase = findChromiumBase(); } catch (_) { return []; }

    const profiles = [];
    let localState = {};
    try { localState = JSON.parse(fs.readFileSync(path.join(chromeBase, 'Local State'), 'utf8')); } catch (_) {}
    const profileInfo = localState?.profile?.info_cache ?? {};

    for (const dir of ['Default', ...fs.readdirSync(chromeBase)]) {
        if (dir !== 'Default' && !dir.match(/^Profile \d+$/)) continue;
        const profilePath = path.join(chromeBase, dir);
        if (!fs.existsSync(path.join(profilePath, 'Preferences'))) continue;

        const info = profileInfo[dir] || {};
        profiles.push({
            id:          dir,
            displayName: info.name || info.gaia_name || dir,
            email:       info.gaia_email || '',
            path:        profilePath,
        });
    }
    return profiles;
}

// ─────────────────────────────────────────────────────────────────────────────
// Cookies de DISPOSITIVO — independientes de la cuenta de Instagram
// ─────────────────────────────────────────────────────────────────────────────
// Estas cookies identifican el DISPOSITIVO/NAVEGADOR, no la cuenta.
// Son las que queremos copiar a TODAS las cuentas por igual.
// Las cookies de SESIÓN (sessionid, ds_user_id, rur) son por-cuenta y no se copian.
const DEVICE_COOKIE_NAMES = new Set([
    'ig_did',       // Device ID permanente — el más crítico
    'mid',          // Machine ID del navegador
    'datr',         // Meta device authentication
    'sb',           // Security browser cookie
    'ig_nrcb',      // No-restriction cookie browser
    'dpr',          // Device pixel ratio
    'wd',           // Window dimensions
]);

const SESSION_COOKIE_NAMES = new Set([
    'sessionid',    // Token de sesión de la cuenta — NO copiar
    'ds_user_id',   // ID de usuario de la cuenta — NO copiar
    'rur',          // Routing token por sesión — NO copiar
    'csrftoken',    // CSRF (se renueva en login) — NO copiar
]);

/**
 * Extrae SOLO las cookies de dispositivo de Chrome (ig_did, mid, datr...).
 * No incluye cookies de sesión (sessionid, ds_user_id, etc.).
 * Estas se pueden inyectar en CUALQUIER cuenta — identifican el dispositivo,
 * no la persona logueada.
 */
async function extractChromeDeviceCookies() {
    const all = await extractChromeIgCookies();
    const device = all.filter(c => DEVICE_COOKIE_NAMES.has(c.name));
    const session = all.filter(c => SESSION_COOKIE_NAMES.has(c.name));
    return {
        device,   // inyectar en todas las cuentas
        session,  // solo si querés clonar la sesión activa de Chrome
        all,
    };
}

module.exports = {
    extractChromeIgCookies,
    extractChromeDeviceCookies,
    listChromeProfiles,
    decryptChromeValue,
    dpApiDecrypt,       // exportado para sessionDiag.js
    parseChromeDb,      // exportado para sessionDiag.js
    DEVICE_COOKIE_NAMES,
    SESSION_COOKIE_NAMES,
};
