#!/usr/bin/env node
/**
 * OpenHamClock WSJT-X Relay Agent
 * 
 * Captures WSJT-X UDP datagrams on your local machine and relays
 * decoded messages to a remote OpenHamClock instance (e.g. openhamclock.com).
 * 
 * WSJT-X sends UDP only on the local network — this bridge lets your
 * cloud-hosted dashboard see your decodes in real time.
 * 
 * Zero dependencies — uses only Node.js built-in modules.
 * 
 * Usage:
 *   node relay.js --url https://openhamclock.com --key YOUR_RELAY_KEY
 *   
 *   Or with environment variables:
 *   OPENHAMCLOCK_URL=https://openhamclock.com RELAY_KEY=abc123 node relay.js
 * 
 * In WSJT-X: Settings → Reporting → UDP Server
 *   Address: 127.0.0.1   Port: 2237
 */

const dgram = require('dgram');
const http = require('http');
const https = require('https');
const { URL } = require('url');

// ============================================
// CONFIGURATION
// ============================================

function parseArgs() {
  const args = process.argv.slice(2);
  const config = {
    url: process.env.OPENHAMCLOCK_URL || '',
    key: process.env.RELAY_KEY || process.env.OPENHAMCLOCK_RELAY_KEY || '',
    session: process.env.RELAY_SESSION || '',
    port: parseInt(process.env.WSJTX_UDP_PORT || '2237'),
    batchInterval: parseInt(process.env.BATCH_INTERVAL || '2000'),
    verbose: process.env.VERBOSE === 'true',
  };
  
  for (let i = 0; i < args.length; i++) {
    switch (args[i]) {
      case '--url': case '-u': config.url = args[++i]; break;
      case '--key': case '-k': config.key = args[++i]; break;
      case '--session': case '-s': config.session = args[++i]; break;
      case '--port': case '-p': config.port = parseInt(args[++i]); break;
      case '--interval': case '-i': config.batchInterval = parseInt(args[++i]); break;
      case '--verbose': case '-v': config.verbose = true; break;
      case '--help': case '-h':
        console.log(`
OpenHamClock WSJT-X Relay Agent

Captures WSJT-X UDP on your local machine and forwards decodes to
a remote OpenHamClock server.

Options:
  --url, -u <url>       OpenHamClock server URL (required)
  --key, -k <key>       Relay authentication key (required)
  --session, -s <id>    Browser session ID (required for per-user isolation)
  --port, -p <port>     Local UDP port to listen on (default: 2237)
  --interval, -i <ms>   Batch send interval in ms (default: 2000)
  --verbose, -v         Show all decoded messages
  --help, -h            Show this help

Environment variables:
  OPENHAMCLOCK_URL      Same as --url
  RELAY_KEY             Same as --key
  RELAY_SESSION         Same as --session
  WSJTX_UDP_PORT        Same as --port
  BATCH_INTERVAL        Same as --interval
  VERBOSE               Set to 'true' for verbose output

Example:
  node relay.js --url https://openhamclock.com --key mySecretKey123
`);
        process.exit(0);
    }
  }
  
  return config;
}

const config = parseArgs();

// Validate
if (!config.url) {
  console.error('❌ Error: --url is required (e.g. --url https://openhamclock.com)');
  console.error('   Run with --help for usage info');
  process.exit(1);
}
if (!config.key) {
  console.error('❌ Error: --key is required (set WSJTX_RELAY_KEY in your server .env)');
  console.error('   Run with --help for usage info');
  process.exit(1);
}
if (!config.session) {
  console.error('❌ Error: --session is required (auto-generated by the download script)');
  console.error('   Re-download the relay from your OpenHamClock dashboard');
  process.exit(1);
}

// Normalize URL
const serverUrl = config.url.replace(/\/$/, '');
const relayEndpoint = `${serverUrl}/api/wsjtx/relay`;

// ============================================
// WSJT-X BINARY PROTOCOL PARSER
// ============================================

const WSJTX_MAGIC = 0xADBCCBDA;

const WSJTX_MSG = {
  HEARTBEAT: 0, STATUS: 1, DECODE: 2, CLEAR: 3,
  REPLY: 4, QSO_LOGGED: 5, CLOSE: 6, REPLAY: 7,
  HALT_TX: 8, FREE_TEXT: 9, WSPR_DECODE: 10,
  LOCATION: 11, LOGGED_ADIF: 12,
};

class WSJTXReader {
  constructor(buffer) { this.buf = buffer; this.offset = 0; }
  remaining() { return this.buf.length - this.offset; }
  
  readUInt8() {
    if (this.remaining() < 1) return null;
    return this.buf.readUInt8(this.offset++);
  }
  readInt32() {
    if (this.remaining() < 4) return null;
    const v = this.buf.readInt32BE(this.offset); this.offset += 4; return v;
  }
  readUInt32() {
    if (this.remaining() < 4) return null;
    const v = this.buf.readUInt32BE(this.offset); this.offset += 4; return v;
  }
  readUInt64() {
    if (this.remaining() < 8) return null;
    const hi = this.buf.readUInt32BE(this.offset);
    const lo = this.buf.readUInt32BE(this.offset + 4);
    this.offset += 8;
    return hi * 0x100000000 + lo;
  }
  readBool() { const v = this.readUInt8(); return v === null ? null : v !== 0; }
  readDouble() {
    if (this.remaining() < 8) return null;
    const v = this.buf.readDoubleBE(this.offset); this.offset += 8; return v;
  }
  readUtf8() {
    const len = this.readUInt32();
    if (len === null || len === 0xFFFFFFFF) return null;
    if (len === 0) return '';
    if (this.remaining() < len) return null;
    const str = this.buf.toString('utf8', this.offset, this.offset + len);
    this.offset += len; return str;
  }
  readQTime() {
    const ms = this.readUInt32();
    if (ms === null) return null;
    const h = Math.floor(ms / 3600000);
    const m = Math.floor((ms % 3600000) / 60000);
    const s = Math.floor((ms % 60000) / 1000);
    return { ms, hours: h, minutes: m, seconds: s,
      formatted: `${String(h).padStart(2,'0')}:${String(m).padStart(2,'0')}:${String(s).padStart(2,'0')}` };
  }
  readQDateTime() {
    const julianDay = this.readUInt64();
    const time = this.readQTime();
    const timeSpec = this.readUInt8();
    if (timeSpec === 2) this.readInt32();
    return { julianDay, time, timeSpec };
  }
}

function parseWSJTXMessage(buffer) {
  const reader = new WSJTXReader(buffer);
  const magic = reader.readUInt32();
  if (magic !== WSJTX_MAGIC) return null;
  
  const schema = reader.readUInt32();
  const type = reader.readUInt32();
  const id = reader.readUtf8();
  if (type === null || id === null) return null;
  
  const msg = { type, id, schema, timestamp: Date.now() };
  
  try {
    switch (type) {
      case WSJTX_MSG.HEARTBEAT:
        msg.maxSchema = reader.readUInt32();
        msg.version = reader.readUtf8();
        msg.revision = reader.readUtf8();
        break;
      case WSJTX_MSG.STATUS:
        msg.dialFrequency = reader.readUInt64();
        msg.mode = reader.readUtf8();
        msg.dxCall = reader.readUtf8();
        msg.report = reader.readUtf8();
        msg.txMode = reader.readUtf8();
        msg.txEnabled = reader.readBool();
        msg.transmitting = reader.readBool();
        msg.decoding = reader.readBool();
        msg.rxDF = reader.readUInt32();
        msg.txDF = reader.readUInt32();
        msg.deCall = reader.readUtf8();
        msg.deGrid = reader.readUtf8();
        msg.dxGrid = reader.readUtf8();
        msg.txWatchdog = reader.readBool();
        msg.subMode = reader.readUtf8();
        msg.fastMode = reader.readBool();
        msg.specialOp = reader.readUInt8();
        msg.freqTolerance = reader.readUInt32();
        msg.trPeriod = reader.readUInt32();
        msg.configName = reader.readUtf8();
        msg.txMessage = reader.readUtf8();
        break;
      case WSJTX_MSG.DECODE:
        msg.isNew = reader.readBool();
        msg.time = reader.readQTime();
        msg.snr = reader.readInt32();
        msg.deltaTime = reader.readDouble();
        msg.deltaFreq = reader.readUInt32();
        msg.mode = reader.readUtf8();
        msg.message = reader.readUtf8();
        msg.lowConfidence = reader.readBool();
        msg.offAir = reader.readBool();
        break;
      case WSJTX_MSG.CLEAR:
        msg.window = reader.readUInt8();
        break;
      case WSJTX_MSG.QSO_LOGGED:
        msg.dateTimeOff = reader.readQDateTime();
        msg.dxCall = reader.readUtf8();
        msg.dxGrid = reader.readUtf8();
        msg.txFrequency = reader.readUInt64();
        msg.mode = reader.readUtf8();
        msg.reportSent = reader.readUtf8();
        msg.reportRecv = reader.readUtf8();
        msg.txPower = reader.readUtf8();
        msg.comments = reader.readUtf8();
        msg.name = reader.readUtf8();
        msg.dateTimeOn = reader.readQDateTime();
        msg.operatorCall = reader.readUtf8();
        msg.myCall = reader.readUtf8();
        msg.myGrid = reader.readUtf8();
        msg.exchangeSent = reader.readUtf8();
        msg.exchangeRecv = reader.readUtf8();
        msg.adifPropMode = reader.readUtf8();
        break;
      case WSJTX_MSG.WSPR_DECODE:
        msg.isNew = reader.readBool();
        msg.time = reader.readQTime();
        msg.snr = reader.readInt32();
        msg.deltaTime = reader.readDouble();
        msg.frequency = reader.readUInt64();
        msg.drift = reader.readInt32();
        msg.callsign = reader.readUtf8();
        msg.grid = reader.readUtf8();
        msg.power = reader.readInt32();
        msg.offAir = reader.readBool();
        break;
      case WSJTX_MSG.LOGGED_ADIF:
        msg.adif = reader.readUtf8();
        break;
      case WSJTX_MSG.CLOSE:
        break;
      default:
        return null;
    }
  } catch (e) { return null; }
  
  return msg;
}

// ============================================
// MESSAGE QUEUE & RELAY
// ============================================

let messageQueue = [];
let sendInFlight = false;
let consecutiveErrors = 0;
let totalSent = 0;
let totalDecodes = 0;

const MSG_TYPE_NAMES = {
  0: 'Heartbeat', 1: 'Status', 2: 'Decode', 3: 'Clear',
  5: 'QSO Logged', 6: 'Close', 10: 'WSPR',
};

function queueMessage(msg) {
  messageQueue.push(msg);
  
  if (config.verbose && msg.type === WSJTX_MSG.DECODE) {
    const snr = msg.snr != null ? (msg.snr >= 0 ? `+${msg.snr}` : msg.snr) : '?';
    console.log(`  📻 ${msg.time?.formatted || '??'} ${snr}dB ${msg.deltaFreq}Hz ${msg.message}`);
  }
}

function sendBatch() {
  if (sendInFlight || messageQueue.length === 0) return;
  
  const batch = messageQueue.splice(0, messageQueue.length);
  sendInFlight = true;
  
  const body = JSON.stringify({ messages: batch, session: config.session });
  const parsed = new URL(relayEndpoint);
  const transport = parsed.protocol === 'https:' ? https : http;
  
  const reqOpts = {
    hostname: parsed.hostname,
    port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
    path: parsed.pathname,
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Content-Length': Buffer.byteLength(body),
      'Authorization': `Bearer ${config.key}`,
      'X-Relay-Version': '1.0.0',
    },
    timeout: 10000,
  };
  
  const req = transport.request(reqOpts, (res) => {
    let data = '';
    res.on('data', (chunk) => data += chunk);
    res.on('end', () => {
      sendInFlight = false;
      
      if (res.statusCode === 200) {
        consecutiveErrors = 0;
        totalSent += batch.length;
        const decodes = batch.filter(m => m.type === WSJTX_MSG.DECODE).length;
        if (decodes > 0 || config.verbose) {
          process.stdout.write(`  ✅ Relayed ${batch.length} msg${batch.length > 1 ? 's' : ''} (${decodes} decode${decodes !== 1 ? 's' : ''}) — total: ${totalSent}\r`);
        }
      } else if (res.statusCode === 401 || res.statusCode === 403) {
        console.error(`\n  ❌ Authentication failed (${res.statusCode}) — check your relay key`);
        console.error(`     Server: ${serverUrl}`);
        consecutiveErrors++;
      } else {
        console.error(`\n  ⚠️  Server returned ${res.statusCode}: ${data.substring(0, 100)}`);
        consecutiveErrors++;
        // Re-queue on server error
        if (res.statusCode >= 500) messageQueue.unshift(...batch);
      }
    });
  });
  
  req.on('error', (err) => {
    sendInFlight = false;
    consecutiveErrors++;
    // Re-queue on network error
    messageQueue.unshift(...batch);
    
    if (consecutiveErrors <= 3 || consecutiveErrors % 10 === 0) {
      console.error(`\n  ⚠️  Connection error (attempt ${consecutiveErrors}): ${err.message}`);
    }
  });
  
  req.on('timeout', () => {
    req.destroy();
    sendInFlight = false;
    consecutiveErrors++;
    messageQueue.unshift(...batch);
  });
  
  req.write(body);
  req.end();
}

// Adaptive batch interval — back off on errors
function getInterval() {
  if (consecutiveErrors === 0) return config.batchInterval;
  if (consecutiveErrors < 5) return config.batchInterval * 2;
  if (consecutiveErrors < 20) return 10000; // 10s
  return 30000; // 30s max backoff
}

let batchTimer = null;
function scheduleBatch() {
  if (batchTimer) clearTimeout(batchTimer);
  batchTimer = setTimeout(() => {
    sendBatch();
    scheduleBatch();
  }, getInterval());
}

// ============================================
// UDP LISTENER
// ============================================

const socket = dgram.createSocket('udp4');

socket.on('message', (buf, rinfo) => {
  const msg = parseWSJTXMessage(buf);
  if (!msg) return;
  
  // Track decodes for local stats
  if (msg.type === WSJTX_MSG.DECODE && msg.isNew) totalDecodes++;
  
  // Queue for relay — skip REPLAY type (bulk replay request)
  if (msg.type !== 7) { // REPLAY
    queueMessage(msg);
  }
});

socket.on('error', (err) => {
  if (err.code === 'EADDRINUSE') {
    console.error(`\n❌ Port ${config.port} is already in use.`);
    console.error('   Is another WSJT-X listener running? (e.g. local OpenHamClock, JTAlert)');
    console.error('   Try a different port: node relay.js --port 2238');
    console.error('   Then update WSJT-X to match.');
  } else {
    console.error(`\n❌ UDP error: ${err.message}`);
  }
  process.exit(1);
});

socket.on('listening', () => {
  const addr = socket.address();
  
  console.log('');
  console.log('╔══════════════════════════════════════════════╗');
  console.log('║  OpenHamClock WSJT-X Relay Agent v1.0.0     ║');
  console.log('╚══════════════════════════════════════════════╝');
  console.log('');
  console.log(`  🎧 Listening for WSJT-X on UDP ${addr.address}:${addr.port}`);
  console.log(`  🌐 Relaying to ${serverUrl}`);
  console.log(`  ⏱️  Batch interval: ${config.batchInterval}ms`);
  console.log('');
  console.log('  Configure WSJT-X:');
  console.log(`    Settings → Reporting → UDP Server`);
  console.log(`    Address: 127.0.0.1   Port: ${config.port}`);
  console.log('    ☑ Accept UDP requests (check this box)');
  console.log('');
  console.log('  Waiting for WSJT-X packets...');
  console.log('');
  
  // Start batch relay loop
  scheduleBatch();
  
  // Send relay heartbeat immediately, then every 30s
  // This tells the server the relay is alive even before WSJT-X sends any packets
  function sendHeartbeat() {
    const body = JSON.stringify({ relay: true, version: '1.0.0', port: config.port, session: config.session });
    const parsed = new URL(relayEndpoint);
    const transport = parsed.protocol === 'https:' ? https : http;
    
    const reqOpts = {
      hostname: parsed.hostname,
      port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
      path: parsed.pathname,
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Content-Length': Buffer.byteLength(body),
        'Authorization': `Bearer ${config.key}`,
        'X-Relay-Heartbeat': 'true',
      },
      timeout: 10000,
    };
    
    const req = transport.request(reqOpts, (res) => {
      res.resume();
      if (res.statusCode === 200 && consecutiveErrors > 0) {
        console.log('\n  ✅ Server connection restored');
        consecutiveErrors = 0;
      }
    });
    req.on('error', () => {});
    req.on('timeout', () => req.destroy());
    req.write(body);
    req.end();
  }
  
  sendHeartbeat();
  setInterval(sendHeartbeat, 30000);
  
  // Periodic health check — verify server is reachable
  setInterval(() => {
    const parsed = new URL(`${serverUrl}/api/wsjtx`);
    const transport = parsed.protocol === 'https:' ? https : http;
    const req = transport.get(parsed.href, { timeout: 5000 }, (res) => {
      if (res.statusCode === 200 && consecutiveErrors > 0) {
        console.log('\n  ✅ Server connection restored');
        consecutiveErrors = 0;
      }
      res.resume(); // consume response
    });
    req.on('error', () => {}); // silent
    req.on('timeout', () => req.destroy());
  }, 60000); // every minute
});

// Bind to all interfaces so WSJT-X can reach it from any address
socket.bind(config.port, '0.0.0.0');

// ============================================
// GRACEFUL SHUTDOWN
// ============================================

function shutdown(sig) {
  console.log(`\n\n  ${sig} received — sending final batch...`);
  if (messageQueue.length > 0) {
    sendBatch();
    // Give it a moment to flush
    setTimeout(() => {
      console.log(`  📊 Session stats: ${totalDecodes} decodes, ${totalSent} messages relayed`);
      console.log('  73!');
      process.exit(0);
    }, 2000);
  } else {
    console.log(`  📊 Session stats: ${totalDecodes} decodes, ${totalSent} messages relayed`);
    console.log('  73!');
    process.exit(0);
  }
}

process.on('SIGINT', () => shutdown('SIGINT'));
process.on('SIGTERM', () => shutdown('SIGTERM'));
