#!/usr/bin/env node
/**
 * OCPP 2.0.1 Charging Station Simulator for Cariflex/CitrineOS
 * Uses raw TCP WebSocket (bypassing ws module subprotocol issues)
 */

const net = require('net');
const crypto = require('crypto');

const CONFIG = {
  host: process.env.OCPP_HOST || 'localhost',
  port: parseInt(process.env.OCPP_PORT || '8082'),
  path: '/1/CP001',
  stationId: 'CP001',
  password: 'DEADBEEFDEADBEEF',
  vendorName: 'Cariflex',
  firmwareVersion: '1.0.0',
};

let msgId = 0;
const pending = new Map();
let socket = null;
let buffer = Buffer.alloc(0);
let wsReady = false;

function encodeFrame(payload) {
  const maskKey = crypto.randomBytes(4);
  const payloadBuf = Buffer.from(payload, 'utf8');
  const len = payloadBuf.length;
  
  let frameLen;
  if (len < 126) {
    frameLen = 6 + len; // 2 header + 4 mask + payload
  } else if (len < 65536) {
    frameLen = 8 + len; // 2 header + 2 ext len + 4 mask + payload
  } else {
    frameLen = 16 + len; // 2 header + 8 ext len + 4 mask + payload
  }
  
  const frame = Buffer.alloc(frameLen);
  frame[0] = 0x81; // FIN + text
  
  if (len < 126) {
    frame[1] = 0x80 | len;
  } else if (len < 65536) {
    frame[1] = 0x80 | 126;
    frame.writeUInt16BE(len, 2);
  } else {
    frame[1] = 0x80 | 127;
    frame.writeBigUInt64BE(BigInt(len), 2);
  }
  
  maskKey.copy(frame, frameLen - len - 4);
  for (let i = 0; i < len; i++) {
    frame[frameLen - len + i] = payloadBuf[i] ^ maskKey[i % 4];
  }
  
  return frame;
}

function parseFrames() {
  const messages = [];
  
  while (buffer.length >= 2) {
    const firstByte = buffer[0];
    const secondByte = buffer[1];
    const opcode = firstByte & 0x0F;
    const masked = (secondByte & 0x80) !== 0;
    let payloadLen = secondByte & 0x7F;
    let headerLen = 2;
    
    if (payloadLen === 126) {
      if (buffer.length < 4) return messages;
      payloadLen = buffer.readUInt16BE(2);
      headerLen = 4;
    } else if (payloadLen === 127) {
      if (buffer.length < 10) return messages;
      payloadLen = Number(buffer.readBigUInt64BE(2));
      headerLen = 10;
    }
    
    if (masked) headerLen += 4;
    
    const totalLen = headerLen + payloadLen;
    if (buffer.length < totalLen) return messages;
    
    let payload = buffer.slice(headerLen, headerLen + payloadLen);
    
    if (masked) {
      const maskKey = buffer.slice(headerLen - 4, headerLen);
      const unmasked = Buffer.alloc(payloadLen);
      for (let i = 0; i < payloadLen; i++) {
        unmasked[i] = payload[i] ^ maskKey[i % 4];
      }
      payload = unmasked;
    }
    
    messages.push({ opcode, payload: payload.toString('utf8') });
    buffer = buffer.slice(totalLen);
  }
  
  return messages;
}

function sendCall(action, payload) {
  return new Promise((resolve, reject) => {
    const id = String(++msgId);
    const msg = JSON.stringify([2, id, action, payload]);
    pending.set(id, { resolve, reject });
    console.log(`[SEND] ${action} (${id})`);
    socket.write(encodeFrame(msg));
    setTimeout(() => {
      if (pending.has(id)) {
        pending.delete(id);
        reject(new Error(`Timeout: ${action}`));
      }
    }, 15000);
  });
}

function handleMessage(msg) {
  try {
    const data = JSON.parse(msg);
    if (data[0] === 3) { // CallResult
      const id = data[1];
      if (pending.has(id)) {
        pending.get(id).resolve(data[2]);
        pending.delete(id);
      }
    } else if (data[0] === 2) { // Call from server
      const id = data[1];
      const action = data[2];
      console.log(`[CALL] ${action} from server`);
      const response = JSON.stringify([3, id, {}]);
      socket.write(encodeFrame(response));
    } else if (data[0] === 4) { // Error
      const id = data[1];
      console.log(`[ERROR] ${data[2]}: ${data[3]}`);
      if (pending.has(id)) {
        pending.get(id).reject(new Error(`${data[2]}: ${data[3]}`));
        pending.delete(id);
      }
    }
  } catch (e) {
    console.error('[PARSE ERROR]', e.message, '- raw:', JSON.stringify(msg.substring(0, 100)));
  }
}

function connect() {
  return new Promise((resolve, reject) => {
    console.log(`Connecting to ${CONFIG.host}:${CONFIG.port}${CONFIG.path}...`);
    
    socket = net.createConnection(CONFIG.port, CONFIG.host, () => {
      const key = crypto.randomBytes(16).toString('base64');
      const encoded = Buffer.from(CONFIG.stationId + ':' + CONFIG.password).toString('base64');
      
      const handshake = `GET ${CONFIG.path} HTTP/1.1\r\n` +
        `Host: ${CONFIG.host}:${CONFIG.port}\r\n` +
        `Upgrade: websocket\r\n` +
        `Connection: Upgrade\r\n` +
        `Sec-WebSocket-Key: ${key}\r\n` +
        `Sec-WebSocket-Version: 13\r\n` +
        `Sec-WebSocket-Protocol: ocpp2.0.1\r\n` +
        `Authorization: Basic ${encoded}\r\n` +
        `\r\n`;
      
      socket.write(handshake);
    });
    
    socket.on('data', (data) => {
      buffer = Buffer.concat([buffer, data]);
      
      if (!wsReady) {
        const str = buffer.toString('utf8');
        const headerEnd = str.indexOf('\r\n\r\n');
        if (headerEnd === -1) return; // Wait for full HTTP response
        
        const statusLine = str.split('\r\n')[0];
        console.log('Handshake:', statusLine);
        
        if (statusLine.includes('101')) {
          wsReady = true;
          buffer = buffer.slice(headerEnd + 4); // Skip past HTTP headers
          console.log('✅ WebSocket connected!');
          resolve();
        } else {
          console.log('❌ Handshake failed');
          console.log(str.substring(0, 500));
          socket.end();
          reject(new Error('Handshake failed'));
        }
        return;
      }
      
      // Parse WebSocket frames
      const messages = parseFrames();
      for (const msg of messages) {
        if (msg.opcode === 0x1) { // Text frame
          handleMessage(msg.payload);
        } else if (msg.opcode === 0x8) { // Close frame
          console.log('[CLOSE] Server sent close frame');
          socket.end();
        } else if (msg.opcode === 0x9) { // Ping
          // Send pong
          socket.write(encodeFrame(''));
        }
      }
    });
    
    socket.on('error', (e) => {
      console.error('[SOCKET ERROR]', e.message);
      reject(e);
    });
    
    socket.on('close', () => {
      console.log('[DISCONNECTED]');
    });
    
    setTimeout(() => reject(new Error('Connection timeout')), 10000);
  });
}

async function run() {
  try {
    await connect();
    
    // BootNotification
    console.log('Sending BootNotification...');
    const bootResult = await sendCall('BootNotification', {
      chargingStation: {
        model: CONFIG.stationId,
        vendorName: CONFIG.vendorName,
        firmwareVersion: CONFIG.firmwareVersion,
        serialNumber: CONFIG.stationId,
      },
      reason: 'PowerUp',
    });
    console.log('✅ BootNotification result:', bootResult);
    
    // StatusNotification - Available
    console.log('Sending StatusNotification (Available)...');
    await sendCall('StatusNotification', {
      evseId: 1,
      connectorId: 1,
      connectorStatus: 'Available',
      timestamp: new Date().toISOString(),
    });
    console.log('✅ Connector 1 is Available');
    
    // Heartbeat
    console.log('Sending Heartbeat...');
    const hbResult = await sendCall('Heartbeat', {});
    console.log('✅ Heartbeat result:', hbResult);
    
    console.log('');
    console.log('=== ✅ Station CP001 is ONLINE and READY ===');
    console.log('=== Press Ctrl+C to stop ===');
    
    // Keep alive with heartbeats every 60s
    setInterval(async () => {
      try {
        await sendCall('Heartbeat', {});
        console.log('[HEARTBEAT] ✅');
      } catch (e) {
        console.error('[HEARTBEAT] ❌', e.message);
      }
    }, 60000);
    
  } catch (e) {
    console.error('❌ Failed:', e.message);
    process.exit(1);
  }
}

process.on('SIGINT', () => {
  console.log('\nShutting down...');
  if (socket) socket.end();
  process.exit(0);
});

run();
