import { NodeSSH } from "node-ssh";
import { readFileSync } from "fs";
import { dirname, join } from "path";
import { fileURLToPath } from "url";

const __dirname = dirname(fileURLToPath(import.meta.url));
const sshConnections = new Map();
const failedHosts = new Map(); // Cache failed connections
const FAILED_HOST_TTL = 60000; // 60 seconds before retry
const SSH_TIMEOUT = 5000;

function loadConfig() {
  return JSON.parse(readFileSync(join(__dirname, "..", "config.json"), "utf-8"));
}

// Check if host is marked as failed (non-blocking)
export function isHostFailed(host, username = "root") {
  const key = username + "@" + host;
  const failedAt = failedHosts.get(key);
  return failedAt && Date.now() - failedAt < FAILED_HOST_TTL;
}

// Mark host as failed
export function markHostFailed(host, username = "root") {
  const key = username + "@" + host;
  failedHosts.set(key, Date.now());
}

// Clear failed status
export function clearHostFailed(host, username = "root") {
  const key = username + "@" + host;
  failedHosts.delete(key);
}

async function getConnection(host, username = "root") {
  const key = username + "@" + host;
  
  // Check if host recently failed - throw immediately
  if (isHostFailed(host, username)) {
    throw new Error("Host recently unreachable");
  }
  
  if (sshConnections.has(key)) {
    const conn = sshConnections.get(key);
    if (conn.isConnected()) return conn;
    sshConnections.delete(key);
  }

  const ssh = new NodeSSH();
  try {
    await ssh.connect({
      host,
      username,
      privateKeyPath: "/root/.ssh/id_ed25519",
      readyTimeout: SSH_TIMEOUT
    });
    clearHostFailed(host, username);
    sshConnections.set(key, ssh);
    return ssh;
  } catch (err) {
    markHostFailed(host, username);
    throw err;
  }
}

// Returns: "online", "starting", "stopping", "offline"
export async function getServerStatus(server) {
  try {
    const ssh = await getConnection(server.host, server.sshUser);

    if (server.runtime === 'docker') {
      const result = await ssh.execCommand(`docker inspect --format='{{.State.Status}}' ${server.containerName} 2>/dev/null`);
      const status = result.stdout.trim();
      if (status === 'running') return 'online';
      if (status === 'restarting' || status === 'created') return 'starting';
      if (status === 'removing' || status === 'paused') return 'stopping';
      return 'offline';
    } else if (server.runtime === 'systemd') {
      const result = await ssh.execCommand(`systemctl is-active ${server.serviceName}`);
      const status = result.stdout.trim();
      if (status === 'active') return 'online';
      if (status === 'activating' || status === 'reloading') return 'starting';
      if (status === 'deactivating') return 'stopping';
      return 'offline';
    } else if (server.runtime === 'pm2') {
      const nvmPrefix = "source ~/.nvm/nvm.sh && ";
      const result = await ssh.execCommand(nvmPrefix + "pm2 jlist");
      try {
        const processes = JSON.parse(result.stdout);
        const proc = processes.find(p => p.name === server.serviceName);
        if (!proc) return "offline";
        if (proc.pm2_env.status === "online") return "online";
        if (proc.pm2_env.status === "launching") return "starting";
        if (proc.pm2_env.status === "stopping") return "stopping";
        return "offline";
      } catch { return "offline"; }
    } else {
      const result = await ssh.execCommand(`screen -ls | grep -E "\\.${server.screenName}[[:space:]]"`);
      if (result.code === 0) {
        const uptimeResult = await ssh.execCommand(`ps -o etimes= -p $(screen -ls | grep "\\.${server.screenName}" | awk '{print $1}' | cut -d. -f1) 2>/dev/null | head -1`);
        const uptime = parseInt(uptimeResult.stdout.trim()) || 999;
        if (uptime < 60) return 'starting';
        return 'online';
      }
      return 'offline';
    }
  } catch (err) {
    console.error(`Failed to get status for ${server.id}:`, err.message);
    return 'offline';
  }
}

export async function startServer(server, options = {}) {
  const ssh = await getConnection(server.host, server.sshUser);

  if (server.runtime === 'docker') {
    // Factorio with specific save
    if (server.type === 'factorio' && options.save) {
      const saveName = options.save.endsWith('.zip') ? options.save.replace('.zip', '') : options.save;
      // Stop and remove existing container
      await ssh.execCommand(`docker stop ${server.containerName} 2>/dev/null || true`);
      await ssh.execCommand(`docker rm ${server.containerName} 2>/dev/null || true`);
      // Start with specific save
      await ssh.execCommand(`
        docker run -d \
          --name ${server.containerName} \
          -p 34197:34197/udp \
          -p 27015:27015/tcp \
          -v /srv/docker/factorio/data:/factorio \
          -e SAVE_NAME=${saveName} -e LOAD_LATEST_SAVE=false \
          -e TZ=Europe/Berlin \
          -e PUID=845 \
          -e PGID=845 \
          --restart=unless-stopped \
          factoriotools/factorio
      `);
    } else {
      await ssh.execCommand(`docker start ${server.containerName}`);
    }
  } else if (server.runtime === 'systemd') {
    const sudoCmd = server.external ? "sudo " : ""; await ssh.execCommand(`${sudoCmd}systemctl start ${server.serviceName}`);
  } else if (server.runtime === 'pm2') {
    const nvmPrefix = "source ~/.nvm/nvm.sh && ";
    await ssh.execCommand(nvmPrefix + "pm2 start " + server.serviceName);
  } else {
    await ssh.execCommand(`screen -S ${server.screenName} -X quit 2>/dev/null`);
    await new Promise(resolve => setTimeout(resolve, 1000));
    await ssh.execCommand(`cd ${server.workDir} && screen -dmS ${server.screenName} ${server.startCmd}`);
  }
}

export async function stopServer(server) {
  const ssh = await getConnection(server.host, server.sshUser);

  if (server.runtime === 'docker') {
    await ssh.execCommand(`docker stop ${server.containerName}`);
  } else if (server.runtime === 'systemd') {
    const sudoCmd2 = server.external ? "sudo " : ""; await ssh.execCommand(`${sudoCmd2}systemctl stop ${server.serviceName}`);
  } else if (server.runtime === 'pm2') {
    const nvmPrefix = "source ~/.nvm/nvm.sh && ";
    await ssh.execCommand(nvmPrefix + "pm2 stop " + server.serviceName);
  } else {
    // Different stop commands per server type
    const stopCmd = server.type === 'zomboid' ? 'quit' : 'stop';
    await ssh.execCommand(`screen -S ${server.screenName} -p 0 -X stuff '${stopCmd}\n'`);

    for (let i = 0; i < 30; i++) {
      await new Promise(resolve => setTimeout(resolve, 2000));
      const check = await ssh.execCommand(`screen -ls | grep -E "\\.${server.screenName}[[:space:]]"`);
      if (check.code !== 0) {
        console.log(`Server ${server.id} stopped after ${(i + 1) * 2} seconds`);
        return;
      }
    }

    console.log(`Force killing ${server.id} after timeout`);
    await ssh.execCommand(`screen -S ${server.screenName} -X quit`);
    await new Promise(resolve => setTimeout(resolve, 2000));
  }
}

export async function restartServer(server) {
  await stopServer(server);
  await new Promise(resolve => setTimeout(resolve, 2000));
  await startServer(server);
}

export async function getConsoleLog(server, lines = 50) {
  const ssh = await getConnection(server.host, server.sshUser);

  if (server.runtime === 'docker') {
    const result = await ssh.execCommand(`/usr/local/bin/docker-logs-tz ${server.containerName} ${lines}`);
    return result.stdout || result.stderr;
  } else if (server.runtime === 'systemd') {
    let cmd = `tail -n ${lines} ${server.workDir}/logs/VRisingServer.log 2>/dev/null || journalctl -u ${server.serviceName} -n ${lines} --no-pager`;
    // Filter out REST API spam for Palworld
    if (server.type === 'palworld') {
      cmd = `journalctl -u ${server.serviceName} -n ${lines * 2} --no-pager | grep -v "REST accessed endpoint" | tail -n ${lines}`;
    }
    const result = await ssh.execCommand(cmd);
    return result.stdout || result.stderr;
  } else if (server.runtime === 'pm2') {
    const nvmPrefix = "source ~/.nvm/nvm.sh && ";
    const result = await ssh.execCommand(nvmPrefix + "pm2 logs " + server.serviceName + " --lines " + lines + " --nostream");
    return result.stdout || result.stderr;
  } else if (server.logFile) {
    const result = await ssh.execCommand(`tail -n ${lines} ${server.logFile} 2>/dev/null || echo No log file found`);
    return result.stdout;
  } else {
    const result = await ssh.execCommand(`tail -n ${lines} ${server.workDir}/logs/latest.log 2>/dev/null || echo No log file found`);
    return result.stdout;
  }
}

export async function getProcessUptime(server) {
  try {
    const ssh = await getConnection(server.host, server.sshUser);

    if (server.runtime === "docker") {
      const result = await ssh.execCommand(`docker inspect --format="{{.State.StartedAt}}" ${server.containerName} 2>/dev/null`);
      if (result.stdout.trim()) {
        const startTime = new Date(result.stdout.trim());
        if (!isNaN(startTime.getTime())) {
          return Math.floor((Date.now() - startTime.getTime()) / 1000);
        }
      }
    } else if (server.runtime === "systemd") {
      const result = await ssh.execCommand(`systemctl show ${server.serviceName} --property=ActiveEnterTimestamp | cut -d= -f2 | xargs -I{} date -d "{}" +%s 2>/dev/null`);
      const startEpoch = parseInt(result.stdout.trim());
      if (!isNaN(startEpoch)) {
        return Math.floor(Date.now() / 1000) - startEpoch;
      }
    } else if (server.runtime === "pm2") {
      const nvmPrefix = "source ~/.nvm/nvm.sh && ";
      const result = await ssh.execCommand(nvmPrefix + "pm2 jlist");
      try {
        const processes = JSON.parse(result.stdout);
        const proc = processes.find(p => p.name === server.serviceName);
        if (proc && proc.pm2_env.pm_uptime) {
          return Math.floor((Date.now() - proc.pm2_env.pm_uptime) / 1000);
        }
      } catch {}
      return 0;
    } else {
      const result = await ssh.execCommand(`ps -o etimes= -p $(pgrep -f "SCREEN.*${server.screenName}") 2>/dev/null | head -1`);
      const uptime = parseInt(result.stdout.trim());
      if (!isNaN(uptime)) return uptime;
    }
    return 0;
  } catch (err) {
    console.error(`Failed to get uptime for ${server.id}:`, err.message);
    return 0;
  }
}

// ============ FACTORIO-SPECIFIC FUNCTIONS ============

export async function listFactorioSaves(server) {
  const ssh = await getConnection(server.host, server.sshUser);
  const result = await ssh.execCommand('ls -la /srv/docker/factorio/data/saves/*.zip 2>/dev/null');

  if (result.code !== 0 || !result.stdout.trim()) {
    return [];
  }

  const saves = [];
  const lines = result.stdout.trim().split('\n');

  for (const line of lines) {
    const match = line.match(/(\S+)\s+(\d+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+\s+\d+\s+[\d:]+)\s+(.+)$/);
    if (match) {
      const filename = match[7].split('/').pop();
      // Skip autosaves
      if (filename.startsWith('_autosave')) continue;

      const size = match[5];
      const modified = match[6];

      saves.push({
        name: filename.replace('.zip', ''),
        filename,
        size,
        modified
      });
    }
  }

  return saves;
}

export async function deleteFactorioSave(server, saveName) {
  const ssh = await getConnection(server.host, server.sshUser);
  const filename = saveName.endsWith('.zip') ? saveName : `${saveName}.zip`;

  // Security check - prevent path traversal
  if (filename.includes('/') || filename.includes('..')) {
    throw new Error('Invalid save name');
  }

  const result = await ssh.execCommand(`rm /srv/docker/factorio/data/saves/${filename}`);
  if (result.code !== 0) {
    throw new Error(result.stderr || 'Failed to delete save');
  }
}

export async function createFactorioWorld(server, saveName, settings) {
  const ssh = await getConnection(server.host, server.sshUser);

  // Security check
  if (saveName.includes("/") || saveName.includes("..") || saveName.includes(" ")) {
    throw new Error("Invalid save name - no spaces or special characters allowed");
  }

  // Write map-gen-settings.json
  const settingsJson = JSON.stringify(settings, null, 2);
  await ssh.execCommand(`cat > /srv/docker/factorio/data/config/map-gen-settings.json << 'SETTINGSEOF'
${settingsJson}
SETTINGSEOF`);

  // Create new world using --create flag (correct method)
  console.log(`Creating new Factorio world: ${saveName}`);
  const createResult = await ssh.execCommand(`
    docker run --rm \
      -v /srv/docker/factorio/data:/factorio \
      factoriotools/factorio \
      /opt/factorio/bin/x64/factorio \
      --create /factorio/saves/${saveName}.zip \
      --map-gen-settings /factorio/config/map-gen-settings.json
  `, { execOptions: { timeout: 120000 } });

  console.log("World creation output:", createResult.stdout, createResult.stderr);

  // Verify save was created
  const checkResult = await ssh.execCommand(`ls /srv/docker/factorio/data/saves/${saveName}.zip`);
  if (checkResult.code !== 0) {
    throw new Error("World creation failed - save file not found. Output: " + createResult.stderr);
  }

  return true;
}

export async function getFactorioCurrentSave(server) {
  const ssh = await getConnection(server.host, server.sshUser);

  // Check if container has SAVE_NAME env set
  const envResult = await ssh.execCommand(`docker inspect --format="{{range .Config.Env}}{{println .}}{{end}}" ${server.containerName} 2>/dev/null | grep "^SAVE_NAME="`);
  if (envResult.stdout.trim()) {
    const saveName = envResult.stdout.trim().replace("SAVE_NAME=", "");
    return { save: saveName, source: "configured" };
  }

  // Otherwise find newest save file
  const result = await ssh.execCommand("ls -t /srv/docker/factorio/data/saves/*.zip 2>/dev/null | head -1");
  if (result.stdout.trim()) {
    const filename = result.stdout.trim().split("/").pop().replace(".zip", "");
    return { save: filename, source: "newest" };
  }

  return { save: null, source: null };
}



// ============ ZOMBOID CONFIG FUNCTIONS ============

const ZOMBOID_CONFIG_PATH = "/home/pzuser/Zomboid/Server";
const ALLOWED_CONFIG_FILES = ["Project.ini", "Project_SandboxVars.lua", "Project_spawnpoints.lua", "Project_spawnregions.lua"];

export async function listZomboidConfigs(server) {
  const ssh = await getConnection(server.host, server.sshUser);
  const cmd = `ls -la ${ZOMBOID_CONFIG_PATH}/*.ini ${ZOMBOID_CONFIG_PATH}/*.lua 2>/dev/null`;
  const result = await ssh.execCommand(cmd);

  if (result.code !== 0 || !result.stdout.trim()) {
    return [];
  }

  const files = [];
  const lines = result.stdout.trim().split("\n");

  for (const line of lines) {
    const match = line.match(/(\S+)\s+(\d+)\s+(\S+)\s+(\S+)\s+(\d+)\s+(\S+\s+\d+\s+[\d:]+)\s+(.+)$/);
    if (match) {
      const fullPath = match[7];
      const filename = fullPath.split("/").pop();

      if (!ALLOWED_CONFIG_FILES.includes(filename)) continue;

      files.push({
        filename,
        size: parseInt(match[5]),
        modified: match[6]
      });
    }
  }

  return files;
}

export async function readZomboidConfig(server, filename) {
  if (!ALLOWED_CONFIG_FILES.includes(filename)) {
    throw new Error("File not allowed");
  }
  if (filename.includes("/") || filename.includes("..")) {
    throw new Error("Invalid filename");
  }

  const ssh = await getConnection(server.host, server.sshUser);
  const result = await ssh.execCommand(`cat ${ZOMBOID_CONFIG_PATH}/${filename}`);

  if (result.code !== 0) {
    throw new Error(result.stderr || "Failed to read config file");
  }

  return result.stdout;
}

export async function writeZomboidConfig(server, filename, content) {
  if (!ALLOWED_CONFIG_FILES.includes(filename)) {
    throw new Error("File not allowed");
  }
  if (filename.includes("/") || filename.includes("..")) {
    throw new Error("Invalid filename");
  }

  const ssh = await getConnection(server.host, server.sshUser);

  // Create backup
  const backupName = `${filename}.backup.${Date.now()}`;
  await ssh.execCommand(`cp ${ZOMBOID_CONFIG_PATH}/${filename} ${ZOMBOID_CONFIG_PATH}/${backupName} 2>/dev/null || true`);

  // Write file using sftp
  const sftp = await ssh.requestSFTP();
  const filePath = `${ZOMBOID_CONFIG_PATH}/${filename}`;

  await new Promise((resolve, reject) => {
    sftp.writeFile(filePath, content, (err) => {
      if (err) reject(err);
      else resolve();
    });
  });

  // Clean up old backups (keep last 5)
  await ssh.execCommand(`ls -t ${ZOMBOID_CONFIG_PATH}/${filename}.backup.* 2>/dev/null | tail -n +6 | xargs rm -f 2>/dev/null || true`);

  return true;
}

// ============ PALWORLD CONFIG ============
const PALWORLD_CONFIG_PATH = "/opt/palworld/Pal/Saved/Config/LinuxServer";
const PALWORLD_ALLOWED_FILES = ["PalWorldSettings.ini", "Engine.ini", "GameUserSettings.ini"];

export async function listPalworldConfigs(server) {
  const ssh = await getConnection(server.host);
  const cmd = `ls -la ${PALWORLD_CONFIG_PATH}/*.ini 2>/dev/null`;
  const result = await ssh.execCommand(cmd);

  if (result.code !== 0 || !result.stdout.trim()) {
    return [];
  }

  const files = [];
  const lines = result.stdout.trim().split("\n");

  for (const line of lines) {
    const match = line.match(/(\S+)\s+(\d+)\s+(\S+)\s+(\S+)\s+(\d+)\s+(\S+\s+\d+\s+[\d:]+)\s+(.+)$/);
    if (match) {
      const fullPath = match[7];
      const filename = fullPath.split("/").pop();

      if (!PALWORLD_ALLOWED_FILES.includes(filename)) continue;

      files.push({
        filename,
        size: parseInt(match[5]),
        modified: match[6]
      });
    }
  }

  return files;
}

export async function readPalworldConfig(server, filename) {
  if (!PALWORLD_ALLOWED_FILES.includes(filename)) {
    throw new Error("File not allowed");
  }
  if (filename.includes("/") || filename.includes("..")) {
    throw new Error("Invalid filename");
  }

  const ssh = await getConnection(server.host);
  const result = await ssh.execCommand(`cat ${PALWORLD_CONFIG_PATH}/${filename}`);

  if (result.code !== 0) {
    throw new Error(result.stderr || "Failed to read config file");
  }

  return result.stdout;
}

export async function writePalworldConfig(server, filename, content) {
  if (!PALWORLD_ALLOWED_FILES.includes(filename)) {
    throw new Error("File not allowed");
  }
  if (filename.includes("/") || filename.includes("..")) {
    throw new Error("Invalid filename");
  }

  const ssh = await getConnection(server.host);

  // Create backup
  const backupName = `${filename}.backup.${Date.now()}`;
  await ssh.execCommand(`cp ${PALWORLD_CONFIG_PATH}/${filename} ${PALWORLD_CONFIG_PATH}/${backupName} 2>/dev/null || true`);

  // Write file using sftp
  const sftp = await ssh.requestSFTP();
  const filePath = `${PALWORLD_CONFIG_PATH}/${filename}`;

  await new Promise((resolve, reject) => {
    sftp.writeFile(filePath, content, (err) => {
      if (err) reject(err);
      else resolve();
    });
  });

  // Clean up old backups (keep last 5)
  await ssh.execCommand(`ls -t ${PALWORLD_CONFIG_PATH}/${filename}.backup.* 2>/dev/null | tail -n +6 | xargs rm -f 2>/dev/null || true`);

  return true;
}

// ============ TERRARIA CONFIG ============
const TERRARIA_CONFIG_PATH = "/home/terraria/serverconfig.txt";

export async function readTerrariaConfig(server) {
  const ssh = await getConnection(server.host, server.sshUser);
  const result = await ssh.execCommand(`cat ${TERRARIA_CONFIG_PATH}`);

  if (result.code !== 0) {
    throw new Error(result.stderr || "Failed to read config file");
  }

  return result.stdout;
}

export async function writeTerrariaConfig(server, content) {
  const ssh = await getConnection(server.host, server.sshUser);

  // Create backup
  const backupName = `serverconfig.txt.backup.${Date.now()}`;
  await ssh.execCommand(`cp ${TERRARIA_CONFIG_PATH} /home/terraria/${backupName} 2>/dev/null || true`);

  // Write file using sftp
  const sftp = await ssh.requestSFTP();

  await new Promise((resolve, reject) => {
    sftp.writeFile(TERRARIA_CONFIG_PATH, content, (err) => {
      if (err) reject(err);
      else resolve();
    });
  });

  // Clean up old backups (keep last 5)
  await ssh.execCommand(`ls -t /home/terraria/serverconfig.txt.backup.* 2>/dev/null | tail -n +6 | xargs rm -f 2>/dev/null || true`);

  return true;
}

// OpenTTD Config
const OPENTTD_CONFIG_PATH = "/opt/openttd/.openttd/openttd.cfg";

export async function readOpenTTDConfig(server) {
  const ssh = await getConnection(server.host, server.sshUser);
  const result = await ssh.execCommand(`cat ${OPENTTD_CONFIG_PATH}`);
  if (result.code !== 0) {
    throw new Error(result.stderr || "Failed to read config file");
  }
  return result.stdout;
}

export async function writeOpenTTDConfig(server, content) {
  const ssh = await getConnection(server.host, server.sshUser);
  const backupName = `openttd.cfg.backup.${Date.now()}`;
  await ssh.execCommand(`cp ${OPENTTD_CONFIG_PATH} /opt/openttd/.openttd/${backupName} 2>/dev/null || true`);
  const sftp = await ssh.requestSFTP();
  await new Promise((resolve, reject) => {
    sftp.writeFile(OPENTTD_CONFIG_PATH, content, (err) => {
      if (err) reject(err);
      else resolve();
    });
  });
  await ssh.execCommand(`ls -t /opt/openttd/.openttd/openttd.cfg.backup.* 2>/dev/null | tail -n +6 | xargs rm -f 2>/dev/null || true`);
  return true;
}
