const { WebSocketServer, WebSocket } = require('ws');
const http = require('http');
const fs = require('fs');
const path = require('path');

const PORT = parseInt(process.env.PORT || '5000', 10);
const MAX_SUBS = 10;
const THROTTLE_MS = 50;
const MAX_PER_WINDOW = 5;

let pub = null;
const subs = [];
const cache = new Map();

function broadcastSubCount() {
  const msg = `{"type":"subCount","data":${subs.length}}`;
  for (const s of subs) {
    if (s.readyState === WebSocket.OPEN) s.send(msg);
  }
}

function throttledForward(data, isBinary) {
  if (subs.length === 0) return;
  const now = Date.now();
  let deadCount = 0;
  for (let i = subs.length - 1; i >= 0; i--) {
    const sub = subs[i];
    if (sub.readyState !== WebSocket.OPEN) {
      subs[i] = null;
      deadCount++;
      continue;
    }
    if (!sub._windowStart || (now - sub._windowStart) >= THROTTLE_MS) {
      sub._windowStart = now;
      sub._windowCount = 0;
    }
    if (sub._windowCount >= MAX_PER_WINDOW) continue;
    if (sub.bufferedAmount > 131072) continue;
    sub._windowCount++;
    sub.send(data, { binary: isBinary });
  }
  if (deadCount > 0) {
    const before = subs.length;
    for (let i = subs.length - 1; i >= 0; i--) {
      if (subs[i] === null) subs.splice(i, 1);
    }
    if (subs.length !== before) broadcastSubCount();
  }
}

function extractType(str) {
  const i = str.indexOf('"type":"');
  if (i === -1) return null;
  const start = i + 8;
  const end = str.indexOf('"', start);
  return end > start ? str.substring(start, end) : null;
}

const MIME = {
  '.html': 'text/html; charset=utf-8',
  '.css':  'text/css; charset=utf-8',
  '.js':   'application/javascript; charset=utf-8',
  '.json': 'application/json',
  '.png':  'image/png',
  '.jpg':  'image/jpeg',
  '.ico':  'image/x-icon',
  '.svg':  'image/svg+xml',
};

const PUBLIC = path.join(__dirname, 'public');

const server = http.createServer((req, res) => {
  let url = decodeURIComponent(req.url.split('?')[0]);
  if (url === '/') url = '/index.html';

  const filePath = path.join(PUBLIC, url);
  if (!filePath.startsWith(PUBLIC)) {
    res.writeHead(403); res.end('Forbidden'); return;
  }

  const ext = path.extname(filePath).toLowerCase();
  const mime = MIME[ext] || 'application/octet-stream';

  fs.readFile(filePath, (err, data) => {
    if (err) {
      if (url !== '/index.html') {
        res.writeHead(404); res.end('Not found'); return;
      }
      res.writeHead(500); res.end('index.html not found'); return;
    }
    res.writeHead(200, { 'Content-Type': mime, 'Cache-Control': 'no-cache' });
    res.end(data);
  });
});

const wss = new WebSocketServer({
  server,
  perMessageDeflate: false,
  skipUTF8Validation: true,
  maxPayload: 512 * 1024,
  clientTracking: false,
});

wss.on('connection', (ws, req) => {
  let authed = false, role = null;

  const sock = ws._socket;
  if (sock) { sock.setNoDelay(true); sock.setKeepAlive(true, 30000); }

  const authTimer = setTimeout(() => { if (!authed) ws.close(4001, 'Auth timeout'); }, 5000);

  ws.on('message', (data, isBinary) => {
    if (!authed) {
      clearTimeout(authTimer);
      try {
        const msg = JSON.parse(data.toString());
        if (msg.action !== 'auth' || !['pub','sub'].includes(msg.role)) {
          ws.send('{"ok":false,"error":"Invalid auth"}');
          ws.close(4002); return;
        }
        role = msg.role;

        if (role === 'pub') {
          if (pub && pub.readyState === WebSocket.OPEN) {
            pub.close(4003, 'Replaced');
          }
          pub = ws;
          cache.clear();
        } else {
          if (subs.length >= MAX_SUBS) {
            ws.send(`{"ok":false,"error":"已达上限(${MAX_SUBS}人)"}`);
            ws.close(4004); return;
          }
          subs.push(ws);
          ws._windowStart = 0; ws._windowCount = 0;
          for (const [, cached] of cache) {
            ws.send(cached, { binary: false });
          }
          broadcastSubCount();
        }
        authed = true;
        const hasPub = !!(pub && pub.readyState === WebSocket.OPEN);
        ws.send(`{"ok":true,"role":"${role}","subs":${subs.length},"pub":${hasPub}}`);
        console.log(`[+] ${role} subs=${subs.length}`);
      } catch (e) {
        ws.send('{"ok":false,"error":"Parse error"}');
        ws.close(4002);
      }
      return;
    }

    if (role !== 'pub') return;

    if (!isBinary) {
      const str = data.toString();
      const type = extractType(str);
      if (type === 'mapName') {
        cache.set('mapName', str);
        for (const s of subs) {
          if (s.readyState === WebSocket.OPEN) s.send(str);
        }
      } else if (type) {
        cache.set(type, str);
        throttledForward(str, false);
      }
    } else {
      throttledForward(data, true);
    }
  });

  ws.on('close', () => {
    clearTimeout(authTimer);
    if (role === 'pub' && pub === ws) {
      pub = null;
      const msg = '{"type":"status","data":{"connected":false}}';
      for (const s of subs) {
        if (s.readyState === WebSocket.OPEN) s.send(msg);
      }
      console.log(`[-] pub disconnected`);
    } else if (role === 'sub') {
      const idx = subs.indexOf(ws);
      if (idx !== -1) subs.splice(idx, 1);
      broadcastSubCount();
      console.log(`[-] sub disconnected, subs=${subs.length}`);
    }
  });

  ws.on('error', (err) => {
    if (authed) console.log(`[WS] error ${role}: ${err.message}`);
  });
});

setInterval(() => {
  if (pub || subs.length > 0) {
    console.log(`[Status] pub=${pub ? 'yes' : 'no'} subs=${subs.length}`);
  }
}, 30000);

server.listen(PORT, '0.0.0.0', () => {
  console.log(`[Relay] ws+http on http://0.0.0.0:${PORT} (pid ${process.pid})`);
});
