/**
 * Follows one service live over the websocket and prints a summary line once per second.
 *
 *   npm install ws
 *   WILDIX_WEBSOCKET_URL=<websocket url> WILDIX_TOKEN=<access token> SERVICE_ID=<full service id> npx tsx liveStats.ts
 *
 * SERVICE_ID is the full id from ListServices, for example `<company>_<pbx>_<number>`. Optional environment:
 * PERIOD_MINUTES (15, 30, 45 or 60; default 15) or PERIOD_START=today (the day so far), SERVICE_LEVEL_SECONDS (5, 10, 15, 20, 25 or 30;
 * default 20).
 *
 * What the example shows:
 *  - authorize, then subscribe: the answer to the subscribe is a snapshot with the stats, the agents and the calls;
 *  - later stats arrive as `service_stats_update` events (at most every 5 seconds per service, when something
 *    changed, and when a 15-minute bucket rolls over), call and agent events carry none;
 *  - a stale or repeated stats update is ignored by its version;
 *  - on a reconnect, and once an hour, the client subscribes again, which also refreshes the snapshot.
 */
import WebSocket from 'ws';

import {
  abandonRate,
  agentStateCounts,
  averageTalkMs,
  averageWaitMs,
  formatDuration,
  formatPercent,
  longestWaitMs,
  maxWaitIncludingLiveMs,
  serviceLevel,
} from './deriveMetrics';
import {ServiceStatsTracker} from './liveTracker';

const url = process.env.WILDIX_WEBSOCKET_URL!;
const token = process.env.WILDIX_TOKEN!;
const serviceId = process.env.SERVICE_ID!;
const periodMinutes = Number(process.env.PERIOD_MINUTES ?? 15);
// PERIOD_START=today asks for the stats since midnight of this machine's time zone instead of a rolling lookback.
// The day start must be on a UTC hour, so a time zone with a half-hour offset falls back to UTC.
const localOffsetMinutes = -new Date().getTimezoneOffset();
const periodWindow =
  process.env.PERIOD_START === 'today'
    ? {periodStart: 'today' as const, utcOffsetMinutes: localOffsetMinutes % 60 === 0 ? localOffsetMinutes : 0}
    : {periodMinutes};
const serviceLevelSeconds = Number(process.env.SERVICE_LEVEL_SECONDS ?? 20);
const RESUBSCRIBE_MS = 60 * 60 * 1000;

const tracker = new ServiceStatsTracker(serviceId);
let socket: WebSocket | undefined;
let reconnectDelayMs = 1000;
let resubscribeTimer: NodeJS.Timeout | undefined;

function subscribe(): void {
  // Subscribing again with the same filter replaces the earlier settings and answers with a fresh snapshot.
  tracker.resetForSubscribe();
  socket?.send(
    JSON.stringify({action: 'subscribe', topic: 'services', filter: [[serviceId]], ...periodWindow, serviceLevelSeconds}),
  );
}

function connect(): void {
  socket = new WebSocket(url);

  socket.on('open', () => {
    socket?.send(JSON.stringify({action: 'authorize', token}));
  });

  socket.on('message', (raw) => {
    const message = JSON.parse(raw.toString()) as {event?: string; message?: string};

    switch (message.event) {
      case 'authorized':
        reconnectDelayMs = 1000;
        subscribe();
        clearInterval(resubscribeTimer);
        resubscribeTimer = setInterval(subscribe, RESUBSCRIBE_MS);
        break;
      case 'subscribed':
        console.log(`subscribed to ${serviceId}`);
        break;
      case 'error':
        console.error('websocket error:', message.message ?? JSON.stringify(message));
        break;
      default:
        tracker.apply(message);
    }
  });

  socket.on('close', () => {
    clearInterval(resubscribeTimer);
    console.warn(`connection closed, reconnecting in ${reconnectDelayMs / 1000}s`);
    setTimeout(connect, reconnectDelayMs);
    reconnectDelayMs = Math.min(reconnectDelayMs * 2, 30_000);
  });

  socket.on('error', (error) => console.error('socket error:', error.message));
}

function printSummary(): void {
  const {stats} = tracker;

  if (stats == null) {
    return;
  }

  const now = Date.now();
  const agents = agentStateCounts(stats);
  const staleSeconds = Math.round((now - tracker.statsReceivedAt) / 1000);

  console.log(
    [
      `SL ${formatPercent(serviceLevel(stats))}`,
      `abandon ${formatPercent(abandonRate(stats))}`,
      `avg wait ${formatDuration(averageWaitMs(stats))}`,
      `avg talk ${formatDuration(averageTalkMs(stats))}`,
      `max wait ${formatDuration(maxWaitIncludingLiveMs(stats, now))}`,
      `| waiting ${tracker.waitingCalls().length} (longest ${formatDuration(longestWaitMs(stats, now))})`,
      `talking ${tracker.talkingCalls().length}`,
      `| agents ${agents.total}: ${agents.available} free, ${agents.talking} talking, ${agents.paused} paused, ` +
        `${agents.wrapUp} wrap-up, ${agents.loggedOff} logged off`,
      `| stats ${staleSeconds}s old (v${tracker.statsVersion})`,
    ].join(' '),
  );
}

connect();
setInterval(printSummary, 1000);
