#!/usr/bin/env node
'use strict';

const http = require('node:http');
const { URL, URLSearchParams } = require('node:url');

let WebSocketServer;
try {
  ({ WebSocketServer } = require('ws'));
} catch (error) {
  console.error('[wecker-remote] Bitte zuerst "npm install ws" in webso/njs ausfuehren.');
  console.error(error.message);
  process.exit(1);
}

const PORT = Number(process.env.WECKER_WS_PORT || 8080);
const HOST = process.env.WECKER_WS_HOST || '0.0.0.0';
const PUBLIC_HOST = process.env.WECKER_WS_PUBLIC_HOST || 'njs.piclock.cylone-network.com';
const API_URL = process.env.WECKER_API_URL || 'https://piclock.cylone-network.com/api_wecker.php';
const POLL_INTERVAL_MS = Number(process.env.WECKER_POLL_INTERVAL_MS || 10000);

const httpServer = http.createServer((req, res) => {
  if (req.url === '/health') {
    res.writeHead(200, { 'content-type': 'application/json; charset=utf-8' });
    res.end(JSON.stringify({ ok: true, service: 'wecker-remote-ws' }));
    return;
  }

  res.writeHead(404, { 'content-type': 'application/json; charset=utf-8' });
  res.end(JSON.stringify({ ok: false, error: 'Not found' }));
});

const wss = new WebSocketServer({ server: httpServer, path: '/ws' });
const clients = new Set();

function send(client, payload) {
  if (client.readyState !== client.OPEN) {
    return;
  }
  client.send(JSON.stringify(payload));
}

function broadcast(payload) {
  for (const client of clients) {
    send(client, payload);
  }
}

function buildApiUrl(params) {
  const url = new URL(API_URL);
  Object.entries(params).forEach(([key, value]) => {
    url.searchParams.set(key, String(value));
  });
  return url;
}

async function apiGet(action, extra = {}) {
  const url = buildApiUrl({ action, ...extra });
  const response = await fetch(url, {
    method: 'GET',
    headers: {
      Accept: 'application/json',
    },
  });

  if (!response.ok) {
    throw new Error(`API GET ${action} fehlgeschlagen (${response.status})`);
  }
  return response.json();
}

async function apiPost(action, data = {}) {
  const body = new URLSearchParams({ action, ...data });
  const response = await fetch(API_URL, {
    method: 'POST',
    headers: {
      Accept: 'application/json',
      'content-type': 'application/x-www-form-urlencoded; charset=utf-8',
    },
    body,
  });

  if (!response.ok) {
    throw new Error(`API POST ${action} fehlgeschlagen (${response.status})`);
  }
  return response.json();
}

function asInt(value, fallback = 0) {
  const parsed = Number.parseInt(String(value), 10);
  return Number.isFinite(parsed) ? parsed : fallback;
}

async function pushFullState(client) {
  const [list, alarmState] = await Promise.all([
    apiGet('list'),
    apiGet('alarm_state'),
  ]);

  send(client, { type: 'list', payload: list });
  send(client, { type: 'alarm_state', payload: alarmState });
}

async function handleMessage(client, rawMessage) {
  let msg;
  try {
    msg = JSON.parse(rawMessage.toString());
  } catch {
    send(client, { type: 'error', message: 'Ungueltiges JSON' });
    return;
  }

  const type = msg?.type;
  try {
    switch (type) {
      case 'get_state': {
        await pushFullState(client);
        break;
      }

      case 'list': {
        const list = await apiGet('list');
        broadcast({ type: 'list', payload: list });
        break;
      }

      case 'toggle': {
        const id = asInt(msg.id);
        const aktiv = msg.aktiv ? 1 : 0;
        if (id <= 0) {
          throw new Error('toggle erwartet gueltige "id"');
        }
        const result = await apiPost('toggle', { id, aktiv });
        broadcast({ type: 'action_result', action: 'toggle', payload: result });
        await broadcastState();
        break;
      }

      case 'stop_alarm': {
        const result = await apiPost('stop_alarm');
        broadcast({ type: 'action_result', action: 'stop_alarm', payload: result });
        await broadcastState();
        break;
      }

      case 'snooze': {
        const minutes = Math.max(1, Math.min(60, asInt(msg.minutes, 5)));
        const result = await apiPost('snooze', { minutes });
        broadcast({ type: 'action_result', action: 'snooze', payload: result });
        await broadcastState();
        break;
      }

      default:
        send(client, { type: 'error', message: `Unbekannter Typ: ${String(type)}` });
    }
  } catch (error) {
    send(client, {
      type: 'error',
      message: error instanceof Error ? error.message : 'Unbekannter Fehler',
    });
  }
}

async function broadcastState() {
  const [list, alarmState] = await Promise.all([
    apiGet('list'),
    apiGet('alarm_state'),
  ]);
  broadcast({ type: 'list', payload: list });
  broadcast({ type: 'alarm_state', payload: alarmState });
}

wss.on('connection', (socket, req) => {
  clients.add(socket);
  send(socket, {
    type: 'connected',
    payload: {
      message: 'WebSocket verbunden',
      clientCount: clients.size,
      from: req.socket.remoteAddress || 'unknown',
    },
  });

  pushFullState(socket).catch((error) => {
    send(socket, {
      type: 'error',
      message: error instanceof Error ? error.message : 'Initialisierung fehlgeschlagen',
    });
  });

  socket.on('message', (msg) => {
    handleMessage(socket, msg);
  });

  socket.on('close', () => {
    clients.delete(socket);
  });

  socket.on('error', () => {
    clients.delete(socket);
  });
});

httpServer.listen(PORT, HOST, () => {
  console.log(`[wecker-remote] WS intern auf ws://${HOST}:${PORT}/ws`);
  console.log(`[wecker-remote] WS extern auf ws://${PUBLIC_HOST}:${PORT}/ws`);
  console.log(`[wecker-remote] API-Ziel: ${API_URL}`);
});

setInterval(() => {
  if (clients.size === 0) {
    return;
  }
  broadcastState().catch((error) => {
    const message = error instanceof Error ? error.message : 'Polling-Fehler';
    broadcast({ type: 'error', message });
  });
}, POLL_INTERVAL_MS);
