#!/usr/bin/env node
const net = require('net');
const crypto = require('crypto');

const PASSWORD='DEADBEEFDEADBEEF';
const STATIONS = Array.from({length: 15}, (_, i) => ({
  id: `CP${String(i+1).padStart(3, '0')}`,
  path: `/1/CP${String(i+1).padStart(3, '0')}`,
}));

const WS_HOST = process.env.OCPP_HOST || 'cariflex-citrineos-server';
const WS_PORT = parseInt(process.env.OCPP_PORT || '8082');
let msgId = 0;

function encodeFrame(payload) {
  const maskKey = crypto.randomBytes(4);
  const pb = Buffer.from(payload, 'utf8');
  const len = pb.length;
  const fl = len < 126 ? 6 + len : 8 + len;
  const f = Buffer.alloc(fl);
  f[0] = 0x81;
  if (len < 126) { f[1] = 0x80 | len; }
  else { f[1] = 0x80 | 126; f.writeUInt16BE(len, 2); }
  maskKey.copy(f, fl - len - 4);
  for (let i = 0; i < len; i++) f[fl - len + i] = pb[i] ^ maskKey[i % 4];
  return f;
}

function parseFrames(buf) {
  const msgs = [];
  while (buf.length >= 2) {
    const op = buf[0] & 0x0F;
    const masked = (buf[1] & 0x80) !== 0;
    let pl = buf[1] & 0x7F, hl = 2;
    if (pl === 126) { if (buf.length < 4) return msgs; pl = buf.readUInt16BE(2); hl = 4; }
    else if (pl === 127) { if (buf.length < 10) return msgs; pl = Number(buf.readBigUInt64BE(2)); hl = 10; }
    if (masked) hl += 4;
    const tl = hl + pl;
    if (buf.length < tl) return msgs;
    let payload = buf.slice(hl, hl + pl);
    if (masked) {
      const mk = buf.slice(hl - 4, hl);
      const u = Buffer.alloc(pl);
      for (let i = 0; i < pl; i++) u[i] = payload[i] ^ mk[i % 4];
      payload = u;
    }
    msgs.push({ op, payload: payload.toString('utf8') });
    buf = buf.slice(tl);
  }
  return msgs;
}

function connect(station) {
  return new Promise((resolve, reject) => {
    const sock = net.createConnection(WS_PORT, WS_HOST, () => {
      const key = crypto.randomBytes(16).toString('base64');
      const enc = Buffer.from(station.id + ':' + PASSWORD).toString('base64');
      sock.write('GET ' + station.path + ' HTTP/1.1\r\nHost: ' + WS_HOST + ':' + WS_PORT + '\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: ' + key + '\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Protocol: ocpp2.0.1\r\nAuthorization: Basic ' + enc + '\r\n\r\n');
    });
    let buf = Buffer.alloc(0), ready = false;
    const sp = new Map();
    sock.on('data', (data) => {
      buf = Buffer.concat([buf, data]);
      if (!ready) {
        const s = buf.toString('utf8');
        const he = s.indexOf('\r\n\r\n');
        if (he === -1) return;
        const sl = s.split('\r\n')[0];
        if (sl.includes('101')) { ready = true; buf = buf.slice(he + 4); resolve({ sock, station, sendCall: mkSend(sp, sock), sp }); }
        else { console.log('[' + station.id + '] Handshake: ' + sl); sock.end(); reject(new Error('fail')); }
        return;
      }
      const msgs = parseFrames(buf);
      for (const m of msgs) { if (m.op === 0x1) handleMsg(m.payload, sp, sock); else if (m.op === 0x8) sock.end(); }
    });
    sock.on('error', (e) => { console.error('[' + station.id + '] ' + e.message); reject(e); });
    sock.on('close', () => console.log('[' + station.id + '] DC'));
    setTimeout(() => reject(new Error('timeout')), 10000);
  });
}

function mkSend(sp, sock) {
  return function(action, payload) {
    return new Promise((res, rej) => {
      const id = String(++msgId);
      sp.set(id, { res, rej });
      sock.write(encodeFrame(JSON.stringify([2, id, action, payload])));
      setTimeout(() => { if (sp.has(id)) { sp.delete(id); rej(new Error('timeout:' + action)); } }, 15000);
    });
  };
}

function handleMsg(msg, sp, sock) {
  try {
    const d = JSON.parse(msg);
    if (d[0] === 3 && sp.has(d[1])) { sp.get(d[1]).res(d[2]); sp.delete(d[1]); }
    else if (d[0] === 2) { sock.write(encodeFrame(JSON.stringify([3, d[1], {}]))); }
  } catch(e) {}
}

async function bootStation(station) {
  // Retry connection up to 10 times with 3s delay
  for (let attempt = 1; attempt <= 10; attempt++) {
    try {
      const { sock, sendCall } = await connect(station);
      console.log('[' + station.id + '] Connected (attempt ' + attempt + ')');
      const boot = await sendCall('BootNotification', { chargingStation: { model: station.id, vendorName: 'Cariflex', firmwareVersion: '1.0.0', serialNumber: station.id }, reason: 'PowerUp' });
      console.log('[' + station.id + '] Boot: ' + boot.status);
      await sendCall('StatusNotification', { evseId: 1, connectorId: 1, connectorStatus: 'Available', timestamp: new Date().toISOString() });
      console.log('[' + station.id + '] Available');
      return { sock, sendCall };
    } catch(e) {
      console.log('[' + station.id + '] Attempt ' + attempt + ' failed: ' + e.message);
      if (attempt < 10) await new Promise(r => setTimeout(r, 3000));
    }
  }
  throw new Error('Failed after 10 attempts');
}

async function main() {
  const tenantId = '1';
  console.log('Simulating ' + STATIONS.length + ' stations...');
  const online = [];
  for (const st of STATIONS) {
    try { const s = await bootStation(st); online.push(Object.assign({}, st, s)); await new Promise(r => setTimeout(r, 300)); }
    catch(e) { console.error('[' + st.id + '] ' + e.message); }
  }
  console.log('\n=== ' + online.length + '/' + STATIONS.length + ' ONLINE ===');
  setInterval(async () => { for (const s of online) { try { await s.sendCall('Heartbeat', {}); } catch(e) {} } console.log('[HB] ' + online.length); }, 60000);
}

process.on('SIGINT', () => { console.log('exit'); process.exit(0); });
main().catch(e => { console.error(e); process.exit(1); });
