'use strict';
// Minimale WebSocket-server (RFC 6455) zonder externe dependencies.
// Genoeg voor kleine JSON-berichten: tekstframes, ping/pong, close, client-masking.
const crypto = require('crypto');
const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';

class WSConn {
  constructor(socket) {
    this.socket = socket;
    this.alive = true;
    this._buf = Buffer.alloc(0);
    this.onmessage = null;
    this.onclose = null;
  }
  _frame(data, opcode = 0x1) {
    const payload = Buffer.from(data);
    const len = payload.length;
    let header;
    if (len < 126) {
      header = Buffer.alloc(2);
      header[1] = len;
    } else if (len < 65536) {
      header = Buffer.alloc(4);
      header[1] = 126;
      header.writeUInt16BE(len, 2);
    } else {
      header = Buffer.alloc(10);
      header[1] = 127;
      header.writeBigUInt64BE(BigInt(len), 2);
    }
    header[0] = 0x80 | opcode; // FIN + opcode
    try { this.socket.write(Buffer.concat([header, payload])); } catch (_) {}
  }
  send(str) { this._frame(str, 0x1); }
  ping() { this._frame('', 0x9); }
  close() { try { this._frame('', 0x8); this.socket.end(); } catch (_) {} }

  _ingest(chunk) {
    this._buf = Buffer.concat([this._buf, chunk]);
    while (this._buf.length >= 2) {
      const b0 = this._buf[0], b1 = this._buf[1];
      const opcode = b0 & 0x0f;
      const masked = (b1 & 0x80) !== 0;
      let len = b1 & 0x7f;
      let offset = 2;
      if (len === 126) {
        if (this._buf.length < offset + 2) return;
        len = this._buf.readUInt16BE(offset); offset += 2;
      } else if (len === 127) {
        if (this._buf.length < offset + 8) return;
        len = Number(this._buf.readBigUInt64BE(offset)); offset += 8;
      }
      const maskLen = masked ? 4 : 0;
      if (this._buf.length < offset + maskLen + len) return; // wacht op meer
      let payload = this._buf.slice(offset + maskLen, offset + maskLen + len);
      if (masked) {
        const mask = this._buf.slice(offset, offset + 4);
        const out = Buffer.alloc(len);
        for (let i = 0; i < len; i++) out[i] = payload[i] ^ mask[i & 3];
        payload = out;
      }
      this._buf = this._buf.slice(offset + maskLen + len);

      if (opcode === 0x8) { this._dead(); return; }              // close
      else if (opcode === 0x9) { this._frame(payload, 0xA); }     // ping -> pong
      else if (opcode === 0xA) { this.alive = true; }             // pong
      else if (opcode === 0x1 || opcode === 0x0) {                // text
        if (this.onmessage) { try { this.onmessage(payload.toString('utf8')); } catch (_) {} }
      }
    }
  }
  _dead() {
    if (!this.alive && this._closed) return;
    this._closed = true;
    if (this.onclose) this.onclose();
    try { this.socket.destroy(); } catch (_) {}
  }
}

// Hang een WS-server aan een bestaande http.Server. Roept onConnection(conn) aan.
function attach(server, onConnection) {
  const conns = new Set();
  server.on('upgrade', (req, socket) => {
    const key = req.headers['sec-websocket-key'];
    if (!key) { socket.destroy(); return; }
    const accept = crypto.createHash('sha1').update(key + GUID).digest('base64');
    socket.write(
      'HTTP/1.1 101 Switching Protocols\r\n' +
      'Upgrade: websocket\r\n' +
      'Connection: Upgrade\r\n' +
      'Sec-WebSocket-Accept: ' + accept + '\r\n\r\n'
    );
    const conn = new WSConn(socket);
    conns.add(conn);
    conn.onclose = () => conns.delete(conn);
    socket.on('data', (c) => conn._ingest(c));
    socket.on('error', () => conn._dead());
    socket.on('close', () => conn._dead());
    onConnection(conn);
  });

  // Heartbeat: dode clients opruimen
  setInterval(() => {
    for (const c of conns) {
      if (!c.alive) { c._dead(); continue; }
      c.alive = false; c.ping();
    }
  }, 15000).unref();

  return {
    broadcast(str) { for (const c of conns) c.send(str); },
    get size() { return conns.size; },
  };
}

module.exports = { attach };
