// NOTA: copia di sync-server/broker.js (zero-dep, stabile) — tenuta in desktop/ per electron-builder.
'use strict';
/**
 * The sync broker as a reusable, zero-dependency module: a tiny WebSocket server (RFC 6455 framing by hand)
 * that aligns SMIL playback across devices. Used by the standalone CLI (server.js) AND embedded in the
 * Electron desktop shell (desktop/syncCoordinator.js) so any device elected master can host it.
 *
 * createBroker({port, host, barrierMs}) → { start(), stop(), isRunning(), health() }
 *
 * Barrier: per group, the first waiter opens a barrier; releases when every currently-joined member has
 * called wait, or after barrierMs. Released value = max numeric sync index. Value bus relays broadcastValue.
 * Protocol (JSON/WS): c→s join|leave|wait|bcast|ping ; s→c release|value|pong|joined.
 */
const http = require('http');
const crypto = require('crypto');

const WS_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';

function acceptKey(key) {
	return crypto.createHash('sha1').update(key + WS_GUID).digest('base64');
}

function parseFrames(buf) {
	const messages = [];
	let close = false;
	let off = 0;
	while (off + 2 <= buf.length) {
		const b1 = buf[off + 1];
		const opcode = buf[off] & 0x0f;
		const masked = (b1 & 0x80) !== 0;
		let len = b1 & 0x7f;
		let p = off + 2;
		if (len === 126) {
			if (p + 2 > buf.length) break;
			len = buf.readUInt16BE(p);
			p += 2;
		} else if (len === 127) {
			if (p + 8 > buf.length) break;
			len = buf.readUInt32BE(p) * 0x100000000 + buf.readUInt32BE(p + 4);
			p += 8;
		}
		let mask = null;
		if (masked) {
			if (p + 4 > buf.length) break;
			mask = buf.slice(p, p + 4);
			p += 4;
		}
		if (p + len > buf.length) break;
		let payload = buf.slice(p, p + len);
		if (masked) {
			const out = Buffer.allocUnsafe(len);
			for (let i = 0; i < len; i++) out[i] = payload[i] ^ mask[i & 3];
			payload = out;
		}
		off = p + len;
		if (opcode === 0x8) { close = true; break; }
		if (opcode === 0x9 || opcode === 0xa) continue;
		if (opcode === 0x1) messages.push(payload.toString('utf8'));
	}
	return { messages, rest: buf.slice(off), close };
}

function encodeText(str) {
	const payload = Buffer.from(str, 'utf8');
	const len = payload.length;
	let header;
	if (len < 126) {
		header = Buffer.from([0x81, len]);
	} else if (len < 65536) {
		header = Buffer.alloc(4);
		header[0] = 0x81; header[1] = 126; header.writeUInt16BE(len, 2);
	} else {
		header = Buffer.alloc(10);
		header[0] = 0x81; header[1] = 127; header.writeUInt32BE(0, 2); header.writeUInt32BE(len, 6);
	}
	return Buffer.concat([header, payload]);
}

function createBroker(opts) {
	opts = opts || {};
	const PORT = parseInt(opts.port, 10) || 4800;
	const HOST = opts.host || '0.0.0.0';
	const BARRIER_MS = parseInt(opts.barrierMs, 10) || 5000;
	const DEBUG = !!opts.debug;

	let nextClientId = 1;
	const clients = new Map();
	const groups = new Map();
	let server = null;
	let running = false;

	function group(name) {
		let g = groups.get(name);
		if (!g) { g = { members: new Set(), barrier: null }; groups.set(name, g); }
		return g;
	}
	function send(client, obj) {
		try { client.socket.write(encodeText(JSON.stringify(obj))); } catch (e) { /* gone */ }
	}
	function joinGroup(client, name) {
		const g = group(name);
		g.members.add(client.id);
		client.groups.add(name);
		send(client, { t: 'joined', group: name, size: g.members.size });
	}
	function leaveGroup(client, name) {
		const g = groups.get(name);
		if (!g) return;
		g.members.delete(client.id);
		client.groups.delete(name);
		if (g.barrier) { g.barrier.arrived.delete(client.id); maybeRelease(name); }
	}
	function agreedValue(values) {
		let max = null, sawNumber = false;
		for (const v of values) {
			const n = Number(v);
			if (Number.isFinite(n) && v !== '' && v !== null && v !== undefined) {
				sawNumber = true;
				if (max === null || n > max) max = n;
			}
		}
		if (sawNumber) return max;
		for (const v of values) if (v !== undefined) return v;
		return '';
	}
	function maybeRelease(name) {
		const g = groups.get(name);
		if (!g || !g.barrier) return;
		if (g.barrier.arrived.size >= g.members.size && g.members.size > 0) release(name, 'all-arrived');
	}
	function release(name, reason) {
		const g = groups.get(name);
		if (!g || !g.barrier) return;
		if (g.barrier.timer) clearTimeout(g.barrier.timer);
		const value = agreedValue(Array.from(g.barrier.arrived.values()));
		const waiters = Array.from(g.barrier.arrived.keys());
		g.barrier = null;
		for (const cid of waiters) {
			const c = clients.get(cid);
			if (c) send(c, { t: 'release', group: name, value });
		}
		if (DEBUG) console.log(`[sync] release ${name} (${reason}) → ${JSON.stringify(value)} to ${waiters.length}`);
	}
	function onWait(client, name, value, timeout) {
		const g = group(name);
		g.members.add(client.id);
		client.groups.add(name);
		if (!g.barrier) g.barrier = { arrived: new Map(), timer: null };
		g.barrier.arrived.set(client.id, value);
		if (!g.barrier.timer) {
			const ms = Math.max(250, Math.min(60000, parseInt(timeout, 10) || BARRIER_MS));
			g.barrier.timer = setTimeout(() => release(name, 'timeout'), ms);
		}
		maybeRelease(name);
	}
	function handleMessage(client, msg) {
		let m;
		try { m = JSON.parse(msg); } catch (e) { return; }
		switch (m.t) {
			case 'hello': if (m.device) client.deviceId = String(m.device).slice(0, 128); break;
			case 'join':
				if (m.device) client.deviceId = String(m.device).slice(0, 128);
				if (m.group) joinGroup(client, String(m.group).slice(0, 256));
				break;
			case 'leave': if (m.group) leaveGroup(client, String(m.group).slice(0, 256)); break;
			case 'wait': if (m.group) onWait(client, String(m.group).slice(0, 256), m.value, m.timeout); break;
			case 'bcast':
				if (m.group) {
					const g = group(String(m.group).slice(0, 256));
					for (const cid of g.members) { const c = clients.get(cid); if (c) send(c, { t: 'value', group: m.group, key: m.key, value: m.value }); }
				}
				break;
			case 'ping': send(client, { t: 'pong', ts: m.ts, serverTs: Date.now() }); break;
			default: break;
		}
	}

	function health() {
		return {
			ok: true, service: 'videomood-sync', running, clients: clients.size,
			groups: Array.from(groups.entries()).map(([n, g]) => ({ name: n, members: g.members.size, waiting: g.barrier ? g.barrier.arrived.size : 0 })),
		};
	}

	function start() {
		if (running) return;
		server = http.createServer((req, res) => {
			if (req.url === '/' || req.url === '/health') {
				res.setHeader('Content-Type', 'application/json');
				res.end(JSON.stringify(health()));
				return;
			}
			res.statusCode = 426;
			res.end('Upgrade Required');
		});
		server.on('upgrade', (req, socket) => {
			const key = req.headers['sec-websocket-key'];
			if (!key) { socket.destroy(); return; }
			socket.write(
				'HTTP/1.1 101 Switching Protocols\r\n' +
				'Upgrade: websocket\r\n' +
				'Connection: Upgrade\r\n' +
				'Sec-WebSocket-Accept: ' + acceptKey(key) + '\r\n\r\n');
			const client = { id: nextClientId++, socket, deviceId: null, groups: new Set() };
			clients.set(client.id, client);
			let buf = Buffer.alloc(0);
			socket.on('data', (chunk) => {
				buf = Buffer.concat([buf, chunk]);
				if (buf.length > 2 * 1024 * 1024) { socket.destroy(); return; }
				const { messages, rest, close } = parseFrames(buf);
				buf = rest;
				for (const msg of messages) handleMessage(client, msg);
				if (close) socket.end();
			});
			const cleanup = () => {
				for (const name of Array.from(client.groups)) leaveGroup(client, name);
				clients.delete(client.id);
			};
			socket.on('close', cleanup);
			socket.on('error', cleanup);
		});
		server.on('error', (e) => { if (DEBUG) console.error('[sync] server error', e.message); running = false; });
		server.listen(PORT, HOST, () => { running = true; if (DEBUG) console.log(`[sync] broker on ws://${HOST}:${PORT}`); });
	}

	function stop() {
		running = false;
		for (const c of clients.values()) { try { c.socket.destroy(); } catch (e) { /* */ } }
		clients.clear();
		groups.clear();
		if (server) { try { server.close(); } catch (e) { /* */ } server = null; }
	}

	return { start, stop, isRunning: () => running, health, port: PORT };
}

module.exports = { createBroker };
