const express = require('express');
const endpoints = require('./endpoints');
const session = require('express-session');
const bodyParser = require('body-parser');
const http = require('http');
const readline = require('readline');
const app = express();
const httpServer = http.createServer(app);
const WebSocket = require('ws');
const path = require('path');
const net = require('net');
const { SerialPort } = require('serialport');
const tunnel = require('./tunnel');
const { createChatServer } = require('./chat');
const figlet = require('figlet');

const helpers = require('./helpers');
const { findServerFiles, startPluginsWithDelay } = helpers;

const dataHandler = require('./datahandler');
const fmdxList = require('./fmdx_list');
const { logError, logInfo, logWarn } = require('./console');
const storage = require('./storage');
const { serverConfig, configExists } = require('./server_config');
const pluginsApi = require('./plugins_api');
const pjson = require('../package.json');

const client = new net.Socket();
const wss = new WebSocket.Server({ noServer: true });
const rdsWss = new WebSocket.Server({ noServer: true });
const pluginsWss = new WebSocket.Server({ noServer: true, perMessageDeflate: true });

storage.websocket_delegation.set("/text", wss);
storage.websocket_delegation.set("/rds", rdsWss);
storage.websocket_delegation.set("/rdsspy", rdsWss);
storage.websocket_delegation.set("/data_plugins", pluginsWss);

// Get all plugins from config and find corresponding server files
const plugins = findServerFiles(serverConfig.plugins);

// Start the first plugin after 3 seconds, then the rest with 3 seconds delay
if (plugins.length > 0) {
  setTimeout(() => {
    startPluginsWithDelay(plugins, 3000); // Start plugins with 3 seconds interval
  }, 4000); // Initial delay of 4 seconds for the first plugin
}

const terminalWidth = readline.createInterface({input: process.stdin, output: process.stdout}).output.columns;

console.log('\x1b[32m' + figlet.textSync("FM-DX Webserver"));
console.log('\x1b[32m\x1b[2mby Noobish @ \x1b[4mFMDX.org + KubaPro010\x1b[0m');
console.log("v" + pjson.version)
console.log('\x1b[90m' + '─'.repeat(terminalWidth - 1) + '\x1b[0m');

require('./stream/ws.js');
require('./stream/index');
require('./plugins');

let currentUsers = 0;
let serialport;
let timeoutAntenna;

app.use(bodyParser.urlencoded({ extended: true }));
const sessionMiddleware = session({
  secret: 'GTce3tN6U8odMwoI', // Cool
  resave: false,
  saveUninitialized: true,
});
app.use(sessionMiddleware);
app.use(bodyParser.json());
createChatServer(storage);

tunnel.download();
connectToXdrd();
connectToSerial();

// Serialport retry code when port is open but communication is lost (additional code in datahandler.js)
dataHandler.state.isSerialportRetrying = false;

setInterval(() => {
  if (!dataHandler.state.isSerialportAlive && serverConfig.xdrd.wirelessConnection === false) {
    dataHandler.state.isSerialportAlive = true;
    dataHandler.state.isSerialportRetrying = true;
    if (serialport && serialport.isOpen) {
      logWarn('Communication lost from ' + serverConfig.xdrd.comPort + ', force closing serialport.');
      setTimeout(() => {
        serialport.close((err) => {
          if (err) logError('Error closing serialport: ', err.message);
        });
      }, 1000);
    } else logWarn('Communication lost from ' + serverConfig.xdrd.comPort + '.');
  }
}, 2000);

// Serial Connection
function connectToSerial() {
  if (serverConfig.xdrd.wirelessConnection === true) return;

  serialport = new SerialPort({
    path: serverConfig.xdrd.comPort,
    baudRate: 115200,
    autoOpen: false,
    dtr: false, rts: false
  });

  serialport.open((err) => {
    if (err) {
      logError('Error opening port: ' + err.message);
      setTimeout(() => {
          connectToSerial();
      }, 5000);
      return;
    }

    logInfo('Using serial port: ' + serverConfig.xdrd.comPort);
    dataHandler.state.isSerialportAlive = true;
    pluginsApi.setOutput(serialport);
    setTimeout(() => {
        serialport.write('x\n');
    }, 2500);

    setTimeout(() => {
      serialport.write('Q0\n');
      serialport.write('M0\n');
      serialport.write(`Z${serverConfig.antennaStartup}\n`); // Antenna on startup

      if (serverConfig.defaultFreq && serverConfig.enableDefaultFreq === true) {
        serialport.write('T' + Math.round(serverConfig.defaultFreq * 1000) + '\n');
        dataHandler.initialData.freq = Number(serverConfig.defaultFreq).toFixed(3);
        dataHandler.dataToSend.freq = Number(serverConfig.defaultFreq).toFixed(3);
      } else if (dataHandler.state.lastFrequencyAlive && dataHandler.state.isSerialportRetrying) { // Serialport retry code when port is open but communication is lost
        serialport.write('T' + (dataHandler.state.lastFrequencyAlive * 1000) + '\n');
      } else serialport.write('T87500\n');
      dataHandler.state.isSerialportRetrying = false;

      if (serverConfig.device === 'si47xx') serialport.write('A0\n');
      serialport.write('F-1\n');
      serialport.write('W0\n');
      serverConfig.webserver.rdsMode ? serialport.write('D1\n') : serialport.write('D0\n');
      serialport.write(`G${serverConfig.ceqStartup}${serverConfig.imsStartup}\n`);
      serialport.write(`B${serverConfig.stereoStartup}\n`); // Mono
      serverConfig.audio.startupVolume
        ? serialport.write('Y' + (serverConfig.audio.startupVolume * 100).toFixed(0) + '\n')
        : serialport.write('Y100\n');
    }, 6000);

    serialport.on('data', (data) => {
      helpers.resolveDataBuffer(data, wss, rdsWss);
    });

    serialport.on('error', (error) => {
      logError(error.message);
    });
  });

  // Handle port closure
  serialport.on('close', () => {
    pluginsApi.setOutput(null);
    logWarn('Disconnected from ' + serverConfig.xdrd.comPort + '. Attempting to reconnect.');
    setTimeout(() => {
        dataHandler.state.isSerialportRetrying = true;
        connectToSerial();
    }, 5000);
  });
  return serialport;
}

// xdrd connection
let authFlags = {};

function connectToXdrd() {
  const { xdrd } = serverConfig;

  if (xdrd.wirelessConnection && configExists()) {
    client.connect(xdrd.xdrdPort, xdrd.xdrdIp, () => {
      logInfo('Connection to xdrd established successfully.');
      pluginsApi.setOutput(client);

      authFlags = {
        authMsg: false,
        firstClient: false,
        receivedSalt: '',
        receivedPassword: false,
        messageCount: 0,
      };
    });
  }
}

client.on('data', (data) => {
  const { xdrd } = serverConfig;

  helpers.resolveDataBuffer(data, wss, rdsWss);
  if (authFlags.authMsg == true && authFlags.messageCount > 1) return;

  authFlags.messageCount++;
  const receivedData = data.toString();
  const lines = receivedData.split('\n');

  for (const line of lines) {
    if (authFlags.receivedPassword === false) {
      authFlags.receivedSalt = line.trim();
      authFlags.receivedPassword = true;
      helpers.authenticateWithXdrd(client, authFlags.receivedSalt, xdrd.xdrdPassword);
    } else {
      if (line.startsWith('a')) {
        authFlags.authMsg = true;
        logWarn('Authentication with xdrd failed. Is your password set correctly?');
      } else if (line.startsWith('o1,')) authFlags.firstClient = true;
      else if (line.startsWith('T') && line.length <= 7) {
        const freq = line.slice(1) / 1000;
        dataHandler.dataToSend.freq = freq.toFixed(3);
      } else if (line.startsWith('OK')) {
        authFlags.authMsg = true;
        logInfo('Authentication with xdrd successful.');
      } else if (line.startsWith('G')) {
        dataHandler.initialData.eq = line.charAt(1);
        dataHandler.dataToSend.eq = line.charAt(1);
        dataHandler.initialData.ims = line.charAt(2);
        dataHandler.dataToSend.ims = line.charAt(2);
      } else if (line.startsWith('Z')) {
        let modifiedLine = line.slice(1);
        dataHandler.initialData.ant = modifiedLine;
        dataHandler.dataToSend.ant = modifiedLine;
      }

      if (authFlags.authMsg === true && authFlags.firstClient === true) {
        client.write('x\n');
        client.write(serverConfig.defaultFreq && serverConfig.enableDefaultFreq === true ? 'T' + Math.round(serverConfig.defaultFreq * 1000) + '\n' : 'T87500\n');
        dataHandler.initialData.freq = serverConfig.defaultFreq && serverConfig.enableDefaultFreq === true ? Number(serverConfig.defaultFreq).toFixed(3) : (87.5).toFixed(3);
        dataHandler.dataToSend.freq = serverConfig.defaultFreq && serverConfig.enableDefaultFreq === true ? Number(serverConfig.defaultFreq).toFixed(3) : (87.5).toFixed(3);
        if (serverConfig.device === 'si47xx') serialport.write('A0\n');
        client.write(serverConfig.audio.startupVolume ? 'Y' + (serverConfig.audio.startupVolume * 100).toFixed(0) + '\n' : 'Y100\n');
        serverConfig.webserver.rdsMode ? client.write('D1\n') : client.write('D0\n');
        return;
      }
    }
  }
});

client.on('close', () => {
  pluginsApi.setOutput(null);
  if(serverConfig.autoShutdown === false) {
    logWarn('Disconnected from xdrd. Attempting to reconnect.');
    setTimeout(function () {
      connectToXdrd();
    }, 2000)
  } else logWarn('Disconnected from xdrd.');
});

client.on('error', (err) => {
  switch (true) {
    case err.message.includes("ECONNRESET"):
      logError("Connection to xdrd lost. Reconnecting...");
      break;
    case err.message.includes("ETIMEDOUT"):
      logError("Connection to xdrd @ " + serverConfig.xdrd.xdrdIp + ":" + serverConfig.xdrd.xdrdPort + " timed out.");
      break;
    case err.message.includes("ECONNREFUSED"):
      logError("Connection to xdrd @ " + serverConfig.xdrd.xdrdIp + ":" + serverConfig.xdrd.xdrdPort + " failed. Is xdrd running?");
      break;
    case err.message.includes("EINVAL"):
      logError("Attempts to reconnect are failing repeatedly. Consider checking your settings or restarting xdrd.");
      break;
    default:
      logError("Unhandled error: ", err.message);
      break;
  }
});

app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, '../views'));
app.use('/', endpoints);

const tunerLockTracker = new WeakMap();
const ipConnectionCounts = new Map(); // Per-IP limit variables
const ipLogTimestamps = new Map();
const MAX_CONNECTIONS_PER_IP = 4;
const IP_LOG_INTERVAL_MS = 60000;
// Remove old per-IP limit addresses
setInterval(() => {
  const now = Date.now();

  for (const [ip, count] of ipConnectionCounts.entries()) {
    const lastSeen = ipLogTimestamps.get(ip) || 0;
    const inactive = now - lastSeen > 60 * 60 * 1000;

    if (count === 0 && inactive) {
      ipConnectionCounts.delete(ip);
      ipLogTimestamps.delete(ip);
    }
  }
}, 60 * 60 * 1000); // Run every hour

wss.on('connection', (ws, request) => {
    const output = serverConfig.xdrd.wirelessConnection ? client : serialport;
    let clientIp = helpers.getIpAddress(request);
    const userCommandHistory = {};

    // Per-IP limit connection open
    if (clientIp) {
        const isLocalIp = (
            clientIp === '127.0.0.1' || clientIp === '::1' || clientIp === '::ffff:127.0.0.1' ||
            clientIp.startsWith('192.168.') || clientIp.startsWith('10.') || clientIp.startsWith('172.16.'));
        if (!isLocalIp) {
            if (!ipConnectionCounts.has(clientIp)) ipConnectionCounts.set(clientIp, 0);
            const currentCount = ipConnectionCounts.get(clientIp);
            if (currentCount >= MAX_CONNECTIONS_PER_IP) {
                ws.close(1008, 'Too many open connections from this IP');
                const lastLogTime = ipLogTimestamps.get(clientIp) || 0;
                const now = Date.now();
                if (now - lastLogTime > IP_LOG_INTERVAL_MS) {
                    logWarn(`Web client \x1b[31mclosed: limit exceeded\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]`);
                    ipLogTimestamps.set(clientIp, now);
                }
                return;
            }
            ipConnectionCounts.set(clientIp, currentCount + 1);
        }
    }

    if (clientIp !== '127.0.0.1' ||
      (request.socket && request.socket.remoteAddress && request.socket.remoteAddress !== '::ffff:127.0.0.1') ||
      (request.headers && request.headers['origin'] && request.headers['origin'].trim() !== '')) {
      currentUsers++;
    }

    if (timeoutAntenna) clearTimeout(timeoutAntenna);

    helpers.handleConnect(clientIp, currentUsers, ws, (result) => {
      if (result === "User banned") {
          ws.close(1008, 'Banned IP');
          return;
      }
      dataHandler.showOnlineUsers(currentUsers);

      if (currentUsers === 1 && serverConfig.autoShutdown === true && serverConfig.xdrd.wirelessConnection) serverConfig.xdrd.wirelessConnection ? connectToXdrd() : serialport.write('x\n');
    });

    const userCommands = {};
    let lastWarn = { time: 0 };

    ws.on('message', (message) => {
        const command = helpers.antispamProtection(message, clientIp, ws, userCommands, lastWarn, userCommandHistory, '18', 'text', 16 * 1024);

        if (!clientIp.includes("127.0.0.1")) {
            if (((command.startsWith('X') || command.startsWith('Y')) && !request.session.isAdminAuthenticated) ||
               ((command.startsWith('F') || command.startsWith('W')) && serverConfig.bwSwitch === false)) {
                logWarn(`User \x1b[90m${clientIp}\x1b[0m attempted to send a potentially dangerous command: ${command.slice(0, 64)}.`);
                return;
            }
        }

        if (command.includes("\'")) return;

        const { isAdminAuthenticated, isTuneAuthenticated } = request.session || {};

        if (command.startsWith('w') && (isAdminAuthenticated || isTuneAuthenticated)) {
            switch (command) {
                case 'wL1':
                    if (isAdminAuthenticated) serverConfig.lockToAdmin = true;
                    break;
                case 'wL0':
                    if (isAdminAuthenticated) serverConfig.lockToAdmin = false;
                    break;
                case 'wT0':
                    serverConfig.publicTuner = true;
                    if(!isAdminAuthenticated) tunerLockTracker.delete(ws);
                    break;
                case 'wT1':
                    serverConfig.publicTuner = false;
                    if(!isAdminAuthenticated) tunerLockTracker.set(ws, true);
                    break;
                default:
                    break;
            }
        }

        if (command.startsWith('T')) {
            const tuneFreq = Number(command.slice(1)) / 1000;
            const { tuningLimit, tuningLowerLimit, tuningUpperLimit } = serverConfig.webserver;

            if (tuningLimit && (tuneFreq < tuningLowerLimit || tuneFreq > tuningUpperLimit) || isNaN(tuneFreq)) return;
        }

        if ((serverConfig.publicTuner && !serverConfig.lockToAdmin) || isAdminAuthenticated || (!serverConfig.publicTuner && !serverConfig.lockToAdmin && isTuneAuthenticated)) output.write(`${command}\n`);
    });

    ws.on('close', (code, reason) => {
      // Per-IP limit connection closed
      if (clientIp) {
        const isLocalIp = (
          clientIp === '127.0.0.1' ||
          clientIp === '::1' ||
          clientIp.startsWith('192.168.') ||
          clientIp.startsWith('10.') ||
          clientIp.startsWith('172.16.')
        );
        if (!isLocalIp) {
          const current = ipConnectionCounts.get(clientIp) || 1;
          ipConnectionCounts.set(clientIp, Math.max(0, current - 1));
        }
      }

      if (clientIp !== '::ffff:127.0.0.1' ||
        (request.socket && request.socket.remoteAddress && request.socket.remoteAddress !== '::ffff:127.0.0.1') ||
        (request.headers && request.headers['origin'] && request.headers['origin'].trim() !== '')) {
        currentUsers--;
      }
      dataHandler.showOnlineUsers(currentUsers);

      const index = storage.connectedUsers.findIndex(user => user.ip === clientIp);
      if (index !== -1) storage.connectedUsers.splice(index, 1);

      if (currentUsers === 0) {
          storage.connectedUsers = [];

          if (serverConfig.bwAutoNoUsers === "1") output.write("W0\n"); // Auto BW 'Enabled'

          // cEQ and iMS combinations
          if (serverConfig.ceqNoUsers === "1" && serverConfig.imsNoUsers === "1") output.write("G00\n"); // Both Disabled
          else if (serverConfig.ceqNoUsers === "1" && serverConfig.imsNoUsers === "0") output.write(`G0${dataHandler.dataToSend.ims}\n`);
          else if (serverConfig.ceqNoUsers === "0" && serverConfig.imsNoUsers === "1") output.write(`G${dataHandler.dataToSend.eq}0\n`);
          else if (serverConfig.ceqNoUsers === "2" && serverConfig.imsNoUsers === "0") output.write(`G1${dataHandler.dataToSend.ims}\n`);
          else if (serverConfig.ceqNoUsers === "0" && serverConfig.imsNoUsers === "2") output.write(`G${dataHandler.dataToSend.eq}1\n`);
          else if (serverConfig.ceqNoUsers === "2" && serverConfig.imsNoUsers === "1") output.write("G10\n"); // Only cEQ enabled
          else if (serverConfig.ceqNoUsers === "1" && serverConfig.imsNoUsers === "2") output.write("G01\n"); // Only iMS enabled
          else if (serverConfig.ceqNoUsers === "2" && serverConfig.imsNoUsers === "2") output.write("G11\n"); // Both Enabled

          // Handle stereo mode
          if (serverConfig.stereoNoUsers === "1") output.write("B0\n");
          else if (serverConfig.stereoNoUsers === "2") output.write("B1\n");

          // Handle Antenna selection
          if (timeoutAntenna) clearTimeout(timeoutAntenna);
          timeoutAntenna = setTimeout(() => {
              if (serverConfig.antennaNoUsers === "1") output.write("Z0\n");
              else if (serverConfig.antennaNoUsers === "2") output.write("Z1\n");
              else if (serverConfig.antennaNoUsers === "3") output.write("Z2\n");
              else if (serverConfig.antennaNoUsers === "4") output.write("Z3\n");
          }, serverConfig.antennaNoUsersDelay ? 15000 : 0);
      }

      if (tunerLockTracker.has(ws)) {
        logInfo(`User who locked the tuner left. Unlocking the tuner.`);
        output.write('wT0\n')
        tunerLockTracker.delete(ws);
        serverConfig.publicTuner = true;
      }

      if (currentUsers === 0 && serverConfig.enableDefaultFreq === true &&
          serverConfig.autoShutdown !== true && serverConfig.xdrd.wirelessConnection === true) {
          setTimeout(function() {
              if (currentUsers === 0) {
                  output.write('T' + Math.round(serverConfig.defaultFreq * 1000) + '\n');
                  dataHandler.resetToDefault(dataHandler.dataToSend);
                  dataHandler.dataToSend.freq = Number(serverConfig.defaultFreq).toFixed(3);
                  dataHandler.initialData.freq = Number(serverConfig.defaultFreq).toFixed(3);
              }
          }, 10000);
      }

      if (currentUsers === 0 && serverConfig.autoShutdown === true && serverConfig.xdrd.wirelessConnection === true) client.write('X\n');

      if (code !== 1008) logInfo(`Web client \x1b[31mdisconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]`);
    });

    ws.on('error', console.error);
});

// Additional web socket for using plugins
pluginsWss.on('connection', (ws, request) => {
    const clientIp = helpers.getIpAddress(request);
    const userCommandHistory = {};
    // Anti-spam tracking for each client
    const userCommands = {};
    let lastWarn = { time: 0 };

    ws.on('message', message => {
        // Anti-spam
        helpers.antispamProtection(message, clientIp, ws, userCommands, lastWarn, userCommandHistory, '10', 'data_plugins');

        try {
            let messageData = JSON.parse(message); // Attempt to parse the JSON

            if (messageData.type === "GPS" && messageData.value) {
                const gpsData = messageData.value;
                const { status, time, lat, lon, alt, mode } = gpsData;

                if (status === "active") {
                    Latitude = parseFloat(lat);
                    Longitude = parseFloat(lon);
                }
            }
        } catch (error) {}

        // Broadcast the message to all other clients
        pluginsWss.clients.forEach(client => {
            if (client.readyState === WebSocket.OPEN) client.send(message); // Send the message to all clients
        });
    });

    ws.on('error', error => {
        logError('WebSocket Extra error: ' + error); // Use custom logError function
    });
});

httpServer.on('upgrade', (request, socket, head) => {
  if (serverConfig.webserver.banlist?.includes(helpers.getIpAddress(request))) {
    socket.destroy();
    return;
  }

  const upgradeWss = storage.websocket_delegation.get(request.url);
  if(upgradeWss) {
    sessionMiddleware(request, {}, () => {
      upgradeWss.handleUpgrade(request, socket, head, (ws) => {
        upgradeWss.emit('connection', ws, request);
      });
    });
  } else socket.destroy();
});

app.use(express.static(path.join(__dirname, '../web'))); // Serve the entire web folder to the user
pluginsApi.registerServerContext({ wss, pluginsWss, httpServer, serverConfig });

const ipv4Address = serverConfig.webserver.webserverIp === '0.0.0.0' ? 'localhost' : serverConfig.webserver.webserverIp;
const port = serverConfig.webserver.webserverPort;

const logServerStart = (address) => {
  logInfo(`Web server has started on address \x1b[34mhttp://${address}:${port}\x1b[0m.`);
};

const startServer = (address) => {
  httpServer.listen(port, address, () => {
    if (!configExists()) logInfo(`Open your browser and proceed to \x1b[34mhttp://${address}:${port}\x1b[0m to continue with setup.`);
    else logServerStart(address);
  });
};


startServer(ipv4Address);
tunnel.connect();
fmdxList.update();