/**
 * bridge.js  â€” multi-device orchestrator
 *
 * Endpoints:
 *   GET  /health                â†’ { status, devices_connected, running_jobs }
 *   GET  /adb/devices           â†’ list of connected ADB devices
 *   POST /adb/pair              â†’ { ip, port, code }
 *   POST /adb/connect           â†’ { ip, port }
 *   POST /adb/disconnect        â†’ { id? }
 *   POST /run                   â†’ { phones: [], token: “jwt” }
 *                                 Splits phones across all connected devices,
 *                                 spawns one batch.js per device in parallel.
 *   POST /run-queue             â†’ { phones: [], token: “jwt”, batch_id?, chunk_size? }
 *                                 Splits phones into sequential chunks and runs
 *                                 them one after another automatically.
 *   POST /stop                  â†’ kills all running batch processes (also clears queue)
 *   GET  /status                â†’ { running, jobs: [{device_id, done, total, ...}], queue? }
 *   GET  /logs?device=<id>      â†’ SSE stream of live log lines for that device
 *   GET  /logs/all              â†’ SSE stream of ALL devices merged
 */

import { exec, execSync, spawn, execFile } from 'child_process';
import { promisify } from 'util';
const execFileAsync = promisify(execFile);
import http from 'http';
import path from 'path';
import fs from 'fs';
import os from 'os';
import { fileURLToPath } from 'url';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const PORT    = parseInt(process.env.PORT || '3001', 10);
const BACKEND = (process.env.BACKEND_URL || 'https://thibitisha.xyz/api').trim();

// If adb.exe (and cloudflared.exe) are bundled alongside this script, prefer
// them over anything on the system PATH — lets the bridge run with zero
// separate installs on a fresh Windows PC.
process.env.PATH = `${__dirname}${path.delimiter}${process.env.PATH || ''}`;

// â”€â”€ Job state â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
// Map<deviceId, { process, logs: string[], done, total, matched, mismatch, unknown, failed, started_at }>
const jobs = new Map();

// SSE clients: Map<deviceId|'all', Set<res>>
const sseClients = new Map();

// â”€â”€ Queue state â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
const queue = {
    chunks:      [],   // remaining phone chunks
    token:       null,
    batchId:     null,
    total:       0,    // total phones across all chunks
    processed:   0,    // phones completed so far
    chunkIndex:  0,    // chunk currently running (1-based)
    totalChunks: 0,
    active:      false,
};

function addLog(deviceId, line) {
    if (!jobs.has(deviceId)) return;
    jobs.get(deviceId).logs.push(line);

    // Parse progress from log lines
    const job = jobs.get(deviceId);
    const progressMatch = line.match(/\[(\d+)\/(\d+)\]/);
    if (progressMatch) {
        job.current = parseInt(progressMatch[1]);
        job.total   = parseInt(progressMatch[2]);
    }
    if (line.includes('MATCHED'))   job.matched++;
    if (line.includes('MISMATCH'))  job.mismatch++;
    if (line.includes('UNKNOWN'))   job.unknown++;
    if (line.includes('SCRAPE FAILED') || line.includes('NAME NOT FOUND') || line.includes('BACKEND FAILED')) job.failed++;

    // Broadcast to SSE listeners
    const event = `data: ${JSON.stringify({ device_id: deviceId, line, ts: Date.now() })}\n\n`;

    // Per-device subscribers
    const devClients = sseClients.get(deviceId);
    if (devClients) devClients.forEach(r => { try { r.write(event); } catch {} });

    // All-devices subscribers
    const allClients = sseClients.get('all');
    if (allClients) allClients.forEach(r => { try { r.write(event); } catch {} });
}

function registerSSE(key, res) {
    if (!sseClients.has(key)) sseClients.set(key, new Set());
    sseClients.get(key).add(res);
    res.on('close', () => sseClients.get(key)?.delete(res));
}

// â”€â”€ Input validators â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
function validateIP(ip) {
    if (typeof ip !== 'string' || !/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(ip))
        throw new Error('Invalid IP address');
    return ip;
}
function validatePort(port) {
    const p = parseInt(port, 10);
    if (isNaN(p) || p < 1 || p > 65535) throw new Error('Invalid port');
    return String(p);
}
function validateCode(code) {
    if (!/^\d{4,8}$/.test(String(code || ''))) throw new Error('Invalid pairing code â€” must be 4-8 digits');
    return String(code);
}
function validateSerial(id) {
    if (typeof id !== 'string' || !/^[a-zA-Z0-9.\-_:]+$/.test(id) || id.length > 64)
        throw new Error('Invalid device serial');
    return id;
}

// â”€â”€ ADB helpers â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
function adbExec(cmd) {
    return new Promise(resolve => {
        exec(cmd, { timeout: 15000 }, (err, stdout, stderr) => {
            resolve({ ok: !err, stdout: (stdout || '').trim(), stderr: (stderr || '').trim() });
        });
    });
}

// Safe versions â€” no shell, validated args only
async function adbConnect(ip, port) {
    try {
        const { stdout, stderr } = await execFileAsync('adb', ['connect', `${ip}:${port}`], { timeout: 15000 });
        const out = (stdout || '').trim();
        return { ok: out.includes('connected') && !out.includes('failed'), stdout: out, stderr: (stderr || '').trim() };
    } catch (e) { return { ok: false, stdout: '', stderr: e.message }; }
}

async function adbDisconnectSafe(serial) {
    try {
        const args = serial ? ['disconnect', serial] : ['disconnect'];
        const { stdout } = await execFileAsync('adb', args, { timeout: 15000 });
        return { ok: true, stdout: (stdout || '').trim() };
    } catch (e) { return { ok: false, stdout: e.message }; }
}

async function adbPair(ip, port, code) {
    return new Promise(resolve => {
        // Use stdin to supply pairing code â€” avoids any shell injection
        const child = spawn('adb', ['pair', `${ip}:${port}`], { stdio: ['pipe', 'pipe', 'pipe'] });
        let stdout = '', stderr = '';
        child.stdin.write(code + '\n');
        child.stdin.end();
        child.stdout.on('data', d => stdout += d);
        child.stderr.on('data', d => stderr += d);
        child.on('close', () => resolve({
            ok: stdout.includes('paired') || stdout.includes('success'),
            stdout: stdout.trim(), stderr: stderr.trim()
        }));
        setTimeout(() => { try { child.kill(); } catch {} resolve({ ok: false, stdout: '', stderr: 'timeout' }); }, 15000);
    });
}

async function getConnectedDevices() {
    const r = await adbExec('adb devices -l');
    const all = r.stdout
        .split('\n')
        .slice(1)
        .filter(l => l.trim() && !l.includes('* daemon') && l.includes('device'))
        .map(l => {
            const parts = l.trim().split(/\s+/);
            return {
                id:     parts[0],
                status: parts[1],
                model:  (l.match(/model:(\S+)/) || [])[1] || 'unknown',
            };
        })
        .filter(d => d.id && d.status === 'device');

    // Deduplicate: Android wireless debugging lists the same phone twice â€”
    // once as IP:port and once as an mDNS name (adb-XXXX._adb-tls-connect._tcp).
    // Keep the IP:port entry; drop the mDNS duplicate for the same model.
    return all.filter(d => {
        if (/^\d/.test(d.id)) return true;
        return !all.some(o => /^\d/.test(o.id) && o.model === d.model);
    });
}

// â”€â”€ Split array into N roughly equal chunks â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
function splitPhones(phones, n) {
    const chunks = Array.from({ length: n }, () => []);
    phones.forEach((p, i) => chunks[i % n].push(p));
    return chunks;
}

// â”€â”€ Queue: advance to the next chunk â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
async function startNextQueueChunk() {
    if (!queue.active || !queue.chunks.length) {
        queue.active = false;
        console.log('[bridge] Queue complete — all chunks finished');
        return;
    }

    const devices = await getConnectedDevices();
    if (!devices.length) {
        queue.active = false;
        console.log('[bridge] Queue paused — no ADB devices connected');
        return;
    }

    const chunk = queue.chunks.shift();
    queue.chunkIndex++;
    console.log(`[bridge] Queue chunk ${queue.chunkIndex}/${queue.totalChunks} — ${chunk.length} phones`);

    // Kill any leftover jobs before starting next chunk
    jobs.forEach(job => {
        if (job.process) { try { job.process.kill('SIGTERM'); } catch {} }
    });
    jobs.clear();

    const subChunks = splitPhones(chunk, devices.length);
    const activeDevices = devices.filter((_, i) => subChunks[i]?.length);
    let doneCount = 0;

    activeDevices.forEach((dev, idx) => {
        const sub = subChunks[idx];
        if (!sub?.length) return;
        const child = spawnBatch(dev.id, sub, queue.token, queue.batchId);
        child.on('close', () => {
            queue.processed += sub.length;
            doneCount++;
            if (doneCount >= activeDevices.length) {
                // Small gap so logs flush and ADB recovers before next chunk
                setTimeout(startNextQueueChunk, 2000);
            }
        });
    });
}

// â”€â”€ Spawn batch.js for one device â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
function spawnBatch(deviceId, phoneChunk, token, batchId) {
    // Init job state
    jobs.set(deviceId, {
        process:  null,
        logs:     [],
        total:    phoneChunk.length,
        current:  0,
        matched:  0,
        mismatch: 0,
        unknown:  0,
        failed:   0,
        started_at: Date.now(),
        done:     false,
    });

    // Write phones to a temp file to avoid Windows command-line length limits (ENAMETOOLONG)
    const tmpFile = path.join(os.tmpdir(), `thibiti_phones_${deviceId.replace(/[^a-z0-9]/gi, '_')}_${Date.now()}.txt`);
    fs.writeFileSync(tmpFile, phoneChunk.join('\n'), 'utf8');

    const args = [
        path.join(__dirname, 'batch.js'),
        '--device', deviceId,
        '--token',  token,
        ...(batchId ? ['--batch-id', String(batchId)] : []),
        '--file', tmpFile,
    ];

    console.log(`[bridge] Spawning batch for device ${deviceId} with ${phoneChunk.length} numbers`);

    // process.execPath is whichever node binary is currently running this
    // process — works for a system Node install (dev) and for a bundled
    // portable node.exe (Windows distributable) without any extra config.
    const child = spawn(process.execPath, args, {
        cwd: __dirname,
        env: { ...process.env, BACKEND_URL: BACKEND },
    });
    jobs.get(deviceId).process = child;

    child.stdout.on('data', data => {
        data.toString().split('\n').filter(Boolean).forEach(line => addLog(deviceId, line));
    });

    child.stderr.on('data', data => {
        data.toString().split('\n').filter(Boolean).forEach(line => addLog(deviceId, `[ERR] ${line}`));
    });

    child.on('close', code => {
        const job = jobs.get(deviceId);
        if (job) {
            job.done    = true;
            job.process = null;
        }
        addLog(deviceId, `[bridge] Process exited (code ${code})`);
        console.log(`[bridge] Device ${deviceId} finished (code ${code})`);
        try { fs.unlinkSync(tmpFile); } catch {}
    });

    return child;
}

// â”€â”€ HTTP server â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
const server = http.createServer(async (req, res) => {
    res.setHeader('Access-Control-Allow-Origin', '*');
    res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type');

    if (req.method === 'OPTIONS') { res.writeHead(200); res.end(); return; }

    const url = req.url.split('?')[0];
    const qs  = Object.fromEntries(new URLSearchParams(req.url.includes('?') ? req.url.split('?')[1] : ''));

    // â”€â”€ GET /health â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
    if (req.method === 'GET' && url === '/health') {
        const devices = await getConnectedDevices();
        const running = [...jobs.values()].filter(j => !j.done).length;
        res.setHeader('Content-Type', 'application/json');
        res.writeHead(200);
        res.end(JSON.stringify({ status: 'ok', devices_connected: devices.length, running_jobs: running }));
        return;
    }

    // â”€â”€ GET /adb/devices â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
    if (req.method === 'GET' && url === '/adb/devices') {
        const devices = await getConnectedDevices();
        res.setHeader('Content-Type', 'application/json');
        res.writeHead(200);
        res.end(JSON.stringify({ devices }));
        return;
    }

    // â”€â”€ POST /adb/pair â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
    if (req.method === 'POST' && url === '/adb/pair') {
        let body = ''; req.on('data', d => body += d);
        req.on('end', async () => {
            try {
                const { ip, port, code } = JSON.parse(body);
                if (!ip || !port || !code) { res.writeHead(400); res.end(JSON.stringify({ error: 'ip, port, code required' })); return; }
                const r = await adbPair(validateIP(ip), validatePort(port), validateCode(code));
                res.setHeader('Content-Type', 'application/json'); res.writeHead(200);
                res.end(JSON.stringify({ ok: r.ok, message: r.stdout || r.stderr }));
            } catch (e) { res.writeHead(400); res.end(JSON.stringify({ error: e.message })); }
        });
        return;
    }

    // â”€â”€ POST /adb/connect â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
    if (req.method === 'POST' && url === '/adb/connect') {
        let body = ''; req.on('data', d => body += d);
        req.on('end', async () => {
            try {
                const { ip, port } = JSON.parse(body);
                if (!ip || !port) { res.writeHead(400); res.end(JSON.stringify({ error: 'ip and port required' })); return; }
                const r = await adbConnect(validateIP(ip), validatePort(port));
                res.setHeader('Content-Type', 'application/json'); res.writeHead(200);
                res.end(JSON.stringify({ ok: r.ok, message: r.stdout || r.stderr }));
            } catch (e) { res.writeHead(400); res.end(JSON.stringify({ error: e.message })); }
        });
        return;
    }

    // â”€â”€ POST /adb/disconnect â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
    if (req.method === 'POST' && url === '/adb/disconnect') {
        let body = ''; req.on('data', d => body += d);
        req.on('end', async () => {
            try {
                const parsed = JSON.parse(body || '{}');
                const serial = parsed.id ? validateSerial(parsed.id) : null;
                const r = await adbDisconnectSafe(serial);
                res.setHeader('Content-Type', 'application/json'); res.writeHead(200);
                res.end(JSON.stringify({ ok: true, message: r.stdout }));
            } catch (e) { res.writeHead(400); res.end(JSON.stringify({ error: e.message })); }
        });
        return;
    }

    // â”€â”€ POST /run â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
    if (req.method === 'POST' && url === '/run') {
        let body = ''; req.on('data', d => body += d);
        req.on('end', async () => {
            let parsed;
            try { parsed = JSON.parse(body); } catch {
                res.writeHead(400); res.end(JSON.stringify({ error: 'Invalid JSON' })); return;
            }

            const { phones, token, batch_id } = parsed;
            if (!phones?.length) { res.writeHead(400); res.end(JSON.stringify({ error: 'No phones provided' })); return; }
            if (!token) { res.writeHead(400); res.end(JSON.stringify({ error: 'token required' })); return; }

            // Kill any existing jobs
            jobs.forEach((job, deviceId) => {
                if (job.process) {
                    try { job.process.kill('SIGTERM'); } catch {}
                }
            });
            jobs.clear();

            // Detect connected devices
            const devices = await getConnectedDevices();
            if (!devices.length) {
                res.writeHead(400); res.end(JSON.stringify({ error: 'No ADB devices connected' })); return;
            }

            console.log(`[bridge] Found ${devices.length} device(s): ${devices.map(d => d.id).join(', ')}`);
            console.log(`[bridge] Splitting ${phones.length} phones across ${devices.length} device(s)`);

            // Split phones evenly
            const chunks = splitPhones(phones, devices.length);

            // Spawn one batch per device
            const jobInfo = [];
            devices.forEach((dev, idx) => {
                const chunk = chunks[idx];
                if (!chunk.length) return;
                spawnBatch(dev.id, chunk, token, batch_id);
                jobInfo.push({ device_id: dev.id, model: dev.model, count: chunk.length });
            });

            res.setHeader('Content-Type', 'application/json');
            res.writeHead(200);
            res.end(JSON.stringify({ status: 'started', total: phones.length, jobs: jobInfo }));
        });
        return;
    }

    // â”€â”€ POST /run-queue â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
    if (req.method === 'POST' && url === '/run-queue') {
        let body = ''; req.on('data', d => body += d);
        req.on('end', async () => {
            let parsed;
            try { parsed = JSON.parse(body); } catch {
                res.writeHead(400); res.end(JSON.stringify({ error: 'Invalid JSON' })); return;
            }

            const { phones, token, batch_id, chunk_size } = parsed;
            if (!phones?.length) { res.writeHead(400); res.end(JSON.stringify({ error: 'No phones provided' })); return; }
            if (!token) { res.writeHead(400); res.end(JSON.stringify({ error: 'token required' })); return; }

            const size = Math.max(1, parseInt(chunk_size || 500, 10));

            // Kill any running jobs and clear previous queue
            jobs.forEach(job => { if (job.process) { try { job.process.kill('SIGTERM'); } catch {} } });
            jobs.clear();

            // Build chunks
            const allChunks = [];
            for (let i = 0; i < phones.length; i += size) {
                allChunks.push(phones.slice(i, i + size));
            }

            queue.chunks      = allChunks.slice(1); // remaining after first
            queue.token       = token;
            queue.batchId     = batch_id || null;
            queue.total       = phones.length;
            queue.processed   = 0;
            queue.chunkIndex  = 0;
            queue.totalChunks = allChunks.length;
            queue.active      = true;

            const devices = await getConnectedDevices();
            if (!devices.length) {
                res.writeHead(400); res.end(JSON.stringify({ error: 'No ADB devices connected' })); return;
            }

            // Start first chunk immediately
            const firstChunk = allChunks[0];
            queue.chunkIndex  = 1;
            console.log(`[bridge] Queue started: ${phones.length} phones in ${allChunks.length} chunks of ~${size}`);

            const subChunks = splitPhones(firstChunk, devices.length);
            const activeDevices = devices.filter((_, i) => subChunks[i]?.length);
            let doneCount = 0;

            activeDevices.forEach((dev, idx) => {
                const sub = subChunks[idx];
                if (!sub?.length) return;
                const child = spawnBatch(dev.id, sub, token, batch_id);
                child.on('close', () => {
                    queue.processed += sub.length;
                    doneCount++;
                    if (doneCount >= activeDevices.length) {
                        setTimeout(startNextQueueChunk, 2000);
                    }
                });
            });

            res.setHeader('Content-Type', 'application/json');
            res.writeHead(200);
            res.end(JSON.stringify({
                status: 'queued',
                total: phones.length,
                chunks: allChunks.length,
                chunk_size: size,
                first_chunk: firstChunk.length,
            }));
        });
        return;
    }

    // â”€â”€ POST /stop â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
    if (req.method === 'POST' && url === '/stop') {
        // Clear any active queue so it doesn't auto-advance after stop
        queue.active = false;
        queue.chunks = [];

        let killed = 0;
        jobs.forEach((job, deviceId) => {
            if (job.process) {
                try { job.process.kill('SIGTERM'); killed++; } catch {}
                job.done = true;
                job.process = null;
            }
        });
        if (process.platform === 'win32') {
            exec('taskkill /F /FI “WINDOWTITLE eq batch.js” >nul 2>&1');
        } else {
            exec('pkill -f “batch.js” 2>/dev/null; pkill -f “adb shell” 2>/dev/null');
        }
        res.setHeader('Content-Type', 'application/json');
        res.writeHead(200);
        res.end(JSON.stringify({ status: 'stopped', killed }));
        return;
    }

    // â”€â”€ POST /adb/kill â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
    if (req.method === 'POST' && url === '/adb/kill') {
        exec('adb kill-server', (err, stdout, stderr) => {
            res.setHeader('Content-Type', 'application/json');
            res.writeHead(200);
            res.end(JSON.stringify({ ok: !err, message: err ? (stderr || err.message) : 'ADB server killed' }));
        });
        return;
    }

    // â”€â”€ POST /adb/restart â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
    if (req.method === 'POST' && url === '/adb/restart') {
        exec('adb kill-server && adb start-server', (err, stdout, stderr) => {
            res.setHeader('Content-Type', 'application/json');
            res.writeHead(200);
            res.end(JSON.stringify({ ok: !err, message: err ? (stderr || err.message) : 'ADB server restarted' }));
        });
        return;
    }

    // â”€â”€ GET /status â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
    if (req.method === 'GET' && url === '/status') {
        const jobList = [...jobs.entries()].map(([device_id, j]) => ({
            device_id,
            done:     j.done,
            total:    j.total,
            current:  j.current,
            matched:  j.matched,
            mismatch: j.mismatch,
            unknown:  j.unknown,
            failed:   j.failed,
            started_at: j.started_at,
            pct: j.total ? Math.round((j.current / j.total) * 100) : 0,
        }));
        const anyRunning = jobList.some(j => !j.done);
        const response = { running: anyRunning, jobs: jobList };
        if (queue.active || queue.totalChunks > 0) {
            response.queue = {
                active:        queue.active,
                chunk:         queue.chunkIndex,
                total_chunks:  queue.totalChunks,
                phones_total:  queue.total,
                phones_done:   queue.processed,
                pct:           queue.total ? Math.round((queue.processed / queue.total) * 100) : 0,
            };
        }
        res.setHeader('Content-Type', 'application/json');
        res.writeHead(200);
        res.end(JSON.stringify(response));
        return;
    }

    // â”€â”€ GET /logs?device=<id> â€” SSE per device â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
    if (req.method === 'GET' && url === '/logs') {
        const deviceFilter = qs.device || 'all';
        res.setHeader('Content-Type', 'text/event-stream');
        res.setHeader('Cache-Control', 'no-cache');
        res.setHeader('Connection', 'keep-alive');
        res.writeHead(200);

        // Replay existing logs
        if (deviceFilter === 'all') {
            jobs.forEach((job, did) => {
                job.logs.forEach(line => {
                    res.write(`data: ${JSON.stringify({ device_id: did, line, ts: Date.now() })}\n\n`);
                });
            });
            registerSSE('all', res);
        } else {
            const job = jobs.get(deviceFilter);
            if (job) {
                job.logs.forEach(line => {
                    res.write(`data: ${JSON.stringify({ device_id: deviceFilter, line, ts: Date.now() })}\n\n`);
                });
            }
            registerSSE(deviceFilter, res);
        }
        return;
    }

    res.setHeader('Content-Type', 'application/json');
    res.writeHead(404);
    res.end(JSON.stringify({ error: 'Not found' }));
});

// -- Auto-start a Cloudflare Tunnel if cloudflared is bundled next to this script --
// Lets a packaged Windows distributable expose the bridge with zero manual
// steps. If cloudflared isn't found (e.g. plain dev checkout), this is a
// no-op and the existing "run npx cloudflared manually" flow still works.
function startTunnel() {
    const bin = process.platform === 'win32' ? 'cloudflared.exe' : 'cloudflared';
    const cloudflaredPath = path.join(__dirname, bin);
    if (!fs.existsSync(cloudflaredPath)) return;

    console.log('  Starting Cloudflare Tunnel...\n');
    const tunnel = spawn(cloudflaredPath, ['tunnel', '--url', `http://localhost:${PORT}`]);
    let printed = false;

    const watchForUrl = data => {
        if (printed) return;
        const match = data.toString().match(/https:\/\/[a-zA-Z0-9-]+\.trycloudflare\.com/);
        if (!match) return;
        printed = true;
        console.log(`\n  ====================================================`);
        console.log(`    TUNNEL URL: ${match[0]}`);
        console.log(`    Paste this into the dashboard's Bridge URL field.`);
        console.log(`  ====================================================\n`);
    };
    tunnel.stdout.on('data', watchForUrl);
    tunnel.stderr.on('data', watchForUrl);
    tunnel.on('close', code => console.log(`[bridge] Tunnel process exited (code ${code})`));
    process.on('exit', () => { try { tunnel.kill(); } catch {} });
}

// Listen on all interfaces so Cloudflare Tunnel and LAN can reach the bridge.
// The bridge is protected by JWT â€” the token is required for /run.
server.listen(PORT, '0.0.0.0', () => {
    console.log(`\nâ•”â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•—`);
    console.log(`â•‘              THIBITISHA BRIDGE  v2                       â•‘`);
    console.log(`â•šâ•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•`);
    console.log(`  Local  : http://127.0.0.1:${PORT}`);
    console.log(`  Backend: ${BACKEND}`);
    console.log(``);
    console.log(`  To share this bridge remotely, run in another terminal:`);
    console.log(`    npx cloudflared tunnel --url http://localhost:${PORT}`);
    console.log(`  Then paste the https://xxx.trycloudflare.com URL into`);
    console.log(`  the dashboard Bridge URL field.\n`);
    startTunnel();
});
