'use strict';

const WebSocket = require('ws');

/**
 * Bot-side relay client — TECH_SPEC_V5 §5, "remote access via relay".
 *
 * OUTBOUND-ONLY and OPT-IN. Off unless RELAY_URL, RELAY_TOKEN and
 * RELAY_SESSION are all set. The bot dials out to the operator's relay; nothing
 * dials in to the bot. This is the whole point — the local dashboard stays on
 * 127.0.0.1, and remote viewing never opens a listening port on the customer's
 * machine.
 *
 * Forwards status/log out, accepts pause/resume in, and nothing else. The
 * inbound side is narrowed to the exact two control actions before it can
 * touch the bot — a compromised or malicious relay must not be able to do more
 * to the bot than a local user could, and a local user can only pause/resume.
 */

const STATUS_INTERVAL_MS = 2000;
const ALLOWED_ACTIONS = new Set(['pause', 'resume']);

function createRelayClient({ state, controls, env = process.env, logger = console }) {
  const url = env.RELAY_URL ? String(env.RELAY_URL).trim().replace(/\/+$/, '') : null;
  const token = env.RELAY_TOKEN ? String(env.RELAY_TOKEN).trim() : null;
  const session = env.RELAY_SESSION ? String(env.RELAY_SESSION).trim() : null;

  const enabled = Boolean(url && token && session);

  let ws = null;
  let statusTimer = null;
  let reconnectDelay = 1000;
  let stopped = false;
  let lastLogIndex = 0;

  function connect() {
    if (stopped) return;
    // Token goes in the query of an outbound dial to the operator's own relay.
    // It never appears in a log line here.
    const target =
      url + '/bot?token=' + encodeURIComponent(token) + '&session=' + encodeURIComponent(session);

    try {
      ws = new WebSocket(target);
    } catch (err) {
      scheduleReconnect();
      return;
    }

    ws.on('open', () => {
      reconnectDelay = 1000;
      logger.info && logger.info('Relay connected (outbound) — remote viewing is active.');
      pushStatus();
      statusTimer = setInterval(pushStatus, STATUS_INTERVAL_MS);
      if (statusTimer.unref) statusTimer.unref();
    });

    ws.on('message', (data) => {
      let msg;
      try { msg = JSON.parse(data.toString()); } catch { return; }
      if (!msg || msg.type !== 'control') return;

      // Narrowed to exactly pause/resume before it can reach the bot. Anything
      // else a relay sends is ignored — the relay is not a trusted controller.
      if (!ALLOWED_ACTIONS.has(msg.action)) return;
      if (msg.action === 'pause') controls.pause();
      else if (msg.action === 'resume') controls.resume();
    });

    ws.on('close', () => {
      clearStatusTimer();
      if (!stopped) scheduleReconnect();
    });

    ws.on('error', () => { /* close handler drives reconnection */ });
  }

  function pushStatus() {
    if (!ws || ws.readyState !== WebSocket.OPEN) return;
    const snap = state.snapshot();
    // Forward the same shape the local dashboard renders. It contains the
    // public address only — never key material (guaranteed by state.js).
    ws.send(JSON.stringify({ type: 'status', payload: snap }));

    // Ship only new log lines, so a long session does not resend its history
    // every tick.
    const log = snap.log || [];
    for (let i = lastLogIndex; i < log.length; i++) {
      ws.send(JSON.stringify({ type: 'log', payload: log[i] }));
    }
    lastLogIndex = log.length;
  }

  function scheduleReconnect() {
    if (stopped) return;
    setTimeout(connect, reconnectDelay);
    reconnectDelay = Math.min(reconnectDelay * 2, 15000);
  }

  function clearStatusTimer() {
    if (statusTimer) { clearInterval(statusTimer); statusTimer = null; }
  }

  return {
    enabled,
    start() {
      if (!enabled) return false;
      connect();
      return true;
    },
    stop() {
      stopped = true;
      clearStatusTimer();
      if (ws) { try { ws.close(); } catch {} }
    },
  };
}

module.exports = { createRelayClient };
