const {
  app,
  BrowserWindow,
  shell,
  ipcMain,
  dialog,
  Menu,
  session,
  safeStorage,
  Tray,
  clipboard,
  nativeImage,
} = require("electron");
const path = require("path");
const { getUnpackedAppRoot } = require("./backend-paths.cjs");
const fs = require("fs");
const os = require("os");
const https = require("https");
const http = require("http");
const net = require("net");
const tls = require("tls");
const zlib = require("zlib");
const crypto = require("crypto");
const { URL } = require("url");
const { fork, spawn } = require("child_process");
const pty = require("node-pty");
const WebSocket = require("ws");
const remoteSync = require("./remote-sync.cjs");
const { launchNativeRdp } = require("./native-rdp.cjs");
const { isCloseActiveTabInput } = require("./keyboard-shortcuts.cjs");
const { quitApp } = require("./app-quit.cjs");
const { selectLinuxPasswordStore } = require("./linux-password-store.cjs");
const { resolveLocalShell } = require("./local-shell.cjs");

const localTerminalSessions = new Map();

function ownedLocalTerminal(event, sessionId) {
  if (typeof sessionId !== "string" || !/^[a-f0-9-]{36}$/.test(sessionId)) {
    return null;
  }
  const session = localTerminalSessions.get(sessionId);
  return session?.ownerId === event.sender.id ? session : null;
}

function closeLocalTerminalsFor(ownerId) {
  for (const [sessionId, session] of localTerminalSessions) {
    if (session.ownerId !== ownerId) continue;
    session.process.kill();
    localTerminalSessions.delete(sessionId);
  }
}

// The main process's Node.js networking (the `https`/`http` modules used by
// httpFetch below, and the global `fetch` used by remote-sync.cjs) only
// trusts Node's bundled Mozilla CA list by default, not the OS/system trust
// store. Chromium (the renderer, i.e. the web app and the login iframe) uses
// the OS trust store instead, so a certificate that's valid in-browser --
// e.g. one issued by a reverse proxy's internal/corporate CA, or a system
// CA installed via Keychain/certmgr -- can still fail main-process requests
// with UNABLE_TO_VERIFY_LEAF_SIGNATURE. Merge the system store in so remote
// sync and the connection health check see the same trust as the browser.
try {
  if (typeof tls.setDefaultCACertificates === "function") {
    tls.setDefaultCACertificates([
      ...tls.getCACertificates("default"),
      ...tls.getCACertificates("system"),
    ]);
  }
} catch (error) {
  console.error("Failed to merge system CA certificates:", error);
}

// Portable mode: if a `.portable` marker exists next to the executable,
// store all data in a `data` folder beside the exe instead of %APPDATA%.
(function setupPortableDataDir() {
  const exeDir = path.dirname(process.execPath);
  const markerPath = path.join(exeDir, ".portable");
  const envOverride = process.env.TERMIX_DATA_DIR;

  let portableDataDir = null;
  if (envOverride) {
    portableDataDir = envOverride;
  } else if (app.isPackaged && fs.existsSync(markerPath)) {
    portableDataDir = path.join(exeDir, "data");
  }

  if (portableDataDir) {
    try {
      if (!fs.existsSync(portableDataDir)) {
        fs.mkdirSync(portableDataDir, { recursive: true });
      }
      app.setPath("userData", portableDataDir);
    } catch {
      // Fall back to default userData if we can't create the directory.
    }
  }
})();

const logFile = path.join(app.getPath("userData"), "termix-main.log");
const electronAuthCookiesPath = path.join(
  app.getPath("userData"),
  "electron-auth-cookies.json",
);
const electronAuthCookies = new Map();

function logToFile(...args) {
  const timestamp = new Date().toISOString();
  const msg = args
    .map((a) => (typeof a === "object" ? JSON.stringify(a) : String(a)))
    .join(" ");
  const line = `[${timestamp}] ${msg}\n`;
  try {
    fs.appendFileSync(logFile, line);
  } catch {
    // ignore
  }
  console.log(...args);
}

function getCookieOrigin(url) {
  try {
    const parsedUrl = new URL(url);
    const protocol =
      parsedUrl.protocol === "ws:"
        ? "http:"
        : parsedUrl.protocol === "wss:"
          ? "https:"
          : parsedUrl.protocol;
    return `${protocol}//${parsedUrl.host}`;
  } catch {
    return null;
  }
}

function parseCookieTarget(url) {
  try {
    const parsedUrl = new URL(url);
    if (parsedUrl.protocol === "ws:") {
      parsedUrl.protocol = "http:";
    } else if (parsedUrl.protocol === "wss:") {
      parsedUrl.protocol = "https:";
    }
    return parsedUrl;
  } catch {
    return null;
  }
}

function getElectronAuthCookieKey(name, origin) {
  return origin ? `${origin}|${name}` : null;
}

function getSafeStorageAvailable() {
  try {
    return safeStorage.isEncryptionAvailable();
  } catch {
    return false;
  }
}

function encodeElectronAuthCookieValue(value) {
  return {
    encrypted: true,
    value: safeStorage.encryptString(value).toString("base64"),
  };
}

function decodeElectronAuthCookieValue(record) {
  if (!record.encrypted || !getSafeStorageAvailable()) {
    return null;
  }

  try {
    return safeStorage.decryptString(Buffer.from(record.value, "base64"));
  } catch (error) {
    logToFile(
      "Failed to decrypt persisted Electron auth cookie:",
      error.message,
    );
    return null;
  }
}

function isElectronAuthCookieExpired(cookie) {
  return Number.isFinite(cookie.expiresAt) && cookie.expiresAt <= Date.now();
}

function saveElectronAuthCookiesToDisk() {
  try {
    if (!getSafeStorageAvailable()) {
      if (fs.existsSync(electronAuthCookiesPath)) {
        fs.rmSync(electronAuthCookiesPath, { force: true });
      }
      return;
    }

    const records = [];

    for (const [key, cookie] of electronAuthCookies.entries()) {
      if (isElectronAuthCookieExpired(cookie)) {
        electronAuthCookies.delete(key);
        continue;
      }

      records.push({
        key,
        name: cookie.name,
        origin: cookie.origin,
        path: cookie.path,
        expiresAt: cookie.expiresAt,
        ...encodeElectronAuthCookieValue(cookie.value),
      });
    }

    fs.writeFileSync(
      electronAuthCookiesPath,
      JSON.stringify({ version: 1, records }, null, 2),
    );
  } catch (error) {
    logToFile("Failed to persist Electron auth cookies:", error.message);
  }
}

function loadElectronAuthCookiesFromDisk() {
  electronAuthCookies.clear();

  try {
    if (!getSafeStorageAvailable()) {
      if (fs.existsSync(electronAuthCookiesPath)) {
        fs.rmSync(electronAuthCookiesPath, { force: true });
      }
      return;
    }

    if (!fs.existsSync(electronAuthCookiesPath)) {
      return;
    }

    const data = JSON.parse(fs.readFileSync(electronAuthCookiesPath, "utf8"));
    const records = Array.isArray(data.records) ? data.records : [];

    for (const record of records) {
      if (
        !record ||
        typeof record.key !== "string" ||
        typeof record.name !== "string" ||
        typeof record.origin !== "string"
      ) {
        continue;
      }

      const value = decodeElectronAuthCookieValue(record);
      if (!value) {
        continue;
      }

      const cookie = {
        name: record.name,
        value,
        origin: record.origin,
        path: typeof record.path === "string" ? record.path : "/",
        expiresAt: Number.isFinite(record.expiresAt) ? record.expiresAt : null,
      };

      if (!isElectronAuthCookieExpired(cookie)) {
        electronAuthCookies.set(record.key, cookie);
      }
    }

    saveElectronAuthCookiesToDisk();
  } catch (error) {
    logToFile("Failed to load persisted Electron auth cookies:", error.message);
  }
}

function clearPersistedElectronAuthCookies() {
  electronAuthCookies.clear();
  try {
    if (fs.existsSync(electronAuthCookiesPath)) {
      fs.rmSync(electronAuthCookiesPath, { force: true });
    }
  } catch (error) {
    logToFile(
      "Failed to clear persisted Electron auth cookies:",
      error.message,
    );
  }
}

function parseSetCookieHeader(header) {
  const [cookiePair, ...attributes] = String(header || "").split(";");
  const separatorIndex = cookiePair.indexOf("=");
  if (separatorIndex <= 0) return null;

  const parsed = {
    name: cookiePair.slice(0, separatorIndex).trim(),
    value: cookiePair.slice(separatorIndex + 1).trim(),
    path: "/",
    maxAge: null,
    expires: null,
  };

  for (const attribute of attributes) {
    const [rawName, ...rawValueParts] = attribute.trim().split("=");
    const attrName = rawName.toLowerCase();
    const attrValue = rawValueParts.join("=");

    if (attrName === "path" && attrValue) {
      parsed.path = attrValue;
    } else if (attrName === "max-age" && attrValue) {
      const maxAge = Number(attrValue);
      parsed.maxAge = Number.isFinite(maxAge) ? maxAge : null;
    } else if (attrName === "expires" && attrValue) {
      const expires = Date.parse(attrValue);
      parsed.expires = Number.isFinite(expires) ? expires : null;
    }
  }

  return parsed;
}

function rememberElectronAuthCookieFromHeader(url, header) {
  const origin = getCookieOrigin(url);
  if (!origin) return;

  const cookie = parseSetCookieHeader(header);
  if (!cookie || cookie.name !== "jwt") return;

  const key = getElectronAuthCookieKey(cookie.name, origin);
  if (!key) return;

  const expired =
    cookie.maxAge === 0 ||
    (cookie.expires !== null && cookie.expires <= Date.now());

  if (expired || !cookie.value) {
    electronAuthCookies.delete(key);
    saveElectronAuthCookiesToDisk();
    return;
  }

  const expiresAt =
    cookie.maxAge !== null ? Date.now() + cookie.maxAge * 1000 : cookie.expires;

  electronAuthCookies.set(key, {
    name: cookie.name,
    value: cookie.value,
    origin,
    path: cookie.path,
    expiresAt,
  });
  saveElectronAuthCookiesToDisk();
}

function getRememberedElectronAuthCookie(name, targetUrl) {
  const target = parseCookieTarget(targetUrl);
  if (!target) return null;

  const exactKey = getElectronAuthCookieKey(name, target.origin);
  const exactCookie = exactKey ? electronAuthCookies.get(exactKey) : null;
  if (exactCookie && !isElectronAuthCookieExpired(exactCookie)) {
    return exactCookie;
  }

  if (target.protocol !== "https:") {
    return null;
  }

  const httpOrigin = `http://${target.host}`;
  const httpKey = getElectronAuthCookieKey(name, httpOrigin);
  const httpCookie = httpKey ? electronAuthCookies.get(httpKey) : null;
  return httpCookie && !isElectronAuthCookieExpired(httpCookie)
    ? httpCookie
    : null;
}

function getHeaderName(headers, name) {
  const lowerName = name.toLowerCase();
  return Object.keys(headers || {}).find(
    (key) => key.toLowerCase() === lowerName,
  );
}

function setCookieHeaderValue(requestHeaders, name, value) {
  const headerName = getHeaderName(requestHeaders, "Cookie") || "Cookie";
  const existing = requestHeaders[headerName];
  const existingValue = Array.isArray(existing)
    ? existing.join("; ")
    : existing;
  const nextCookie = `${name}=${value}`;
  const otherCookies = String(existingValue || "")
    .split(";")
    .map((cookie) => cookie.trim())
    .filter((cookie) => cookie && !cookie.startsWith(`${name}=`));

  requestHeaders[headerName] =
    otherCookies.length > 0
      ? `${otherCookies.join("; ")}; ${nextCookie}`
      : nextCookie;
}

function parseSemver(version) {
  const match = String(version || "").match(/(\d+)\.(\d+)(?:\.(\d+))?/);
  if (!match) return null;

  return [Number(match[1]), Number(match[2]), Number(match[3] || 0)];
}

function compareSemver(a, b) {
  const parsedA = parseSemver(a);
  const parsedB = parseSemver(b);
  if (!parsedA || !parsedB) return null;

  for (let i = 0; i < 3; i += 1) {
    if (parsedA[i] > parsedB[i]) return 1;
    if (parsedA[i] < parsedB[i]) return -1;
  }

  return 0;
}

const INSECURE_MODE_VALUES = new Set(["true", "1", "yes"]);

function isInsecureModeEnabled() {
  return INSECURE_MODE_VALUES.has(
    String(process.env.ENABLE_INSECURE_MODE || "")
      .trim()
      .toLowerCase(),
  );
}

function getServerConfigPath() {
  return path.join(app.getPath("userData"), "server-config.json");
}

function getServerConfigSync() {
  try {
    const configPath = getServerConfigPath();
    if (!fs.existsSync(configPath)) return null;
    return JSON.parse(fs.readFileSync(configPath, "utf8"));
  } catch {
    return null;
  }
}

function getOrigin(url) {
  try {
    return new URL(url).origin;
  } catch {
    return null;
  }
}

function isPrivateNetworkHost(hostname) {
  if (
    hostname === "localhost" ||
    hostname === "127.0.0.1" ||
    hostname === "::1"
  ) {
    return true;
  }

  const ipv4Match = hostname.match(
    /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/,
  );
  if (ipv4Match) {
    const [, a, b] = ipv4Match.map(Number);
    return (
      a === 10 ||
      (a === 172 && b >= 16 && b <= 31) ||
      (a === 192 && b === 168) ||
      (a === 169 && b === 254)
    );
  }

  if (hostname.startsWith("fc") || hostname.startsWith("fd")) {
    return true;
  }

  return false;
}

function isInvalidCertificateAllowedForUrl(url) {
  if (isInsecureModeEnabled()) return true;

  try {
    const { hostname } = new URL(url);
    if (isPrivateNetworkHost(hostname)) return true;
  } catch {
    // fall through
  }

  // The only remaining "connected remote server" a self-signed/invalid
  // certificate could legitimately apply to is the Remote Sync server
  // (also used for C2S tunnel relaying, see getC2SRelayUrl).
  const config = remoteSync.getRemoteSyncConfig();
  if (!config?.allowInvalidCertificate || !config?.serverUrl) return false;

  return getOrigin(url) === getOrigin(config.serverUrl);
}

function getTlsVerificationOptions(url) {
  return {
    rejectUnauthorized: !isInvalidCertificateAllowedForUrl(url),
  };
}

function getWebSocketOptions(url, options = {}) {
  return {
    ...options,
    ...(String(url).startsWith("wss:") ? getTlsVerificationOptions(url) : {}),
  };
}

function httpFetch(url, options = {}) {
  return new Promise((resolve, reject) => {
    const urlObj = new URL(url);
    const isHttps = urlObj.protocol === "https:";
    const client = isHttps ? https : http;

    const requestOptions = {
      method: options.method || "GET",
      headers: options.headers || {},
      timeout: options.timeout || 10000,
      ...(isHttps
        ? options.allowInvalidCertificate
          ? { rejectUnauthorized: false }
          : getTlsVerificationOptions(url)
        : {}),
    };

    const req = client.request(url, requestOptions, (res) => {
      const chunks = [];
      // Reverse proxies (nginx and friends) commonly gzip/deflate/br-compress
      // responses regardless of client Accept-Encoding. Unlike browser fetch,
      // Node's http/https modules never auto-decompress, so an unhandled
      // content-encoding here silently turns the body into garbage bytes.
      let stream = res;
      const encoding = (res.headers["content-encoding"] || "")
        .toLowerCase()
        .trim();
      try {
        if (encoding === "gzip" || encoding === "x-gzip") {
          stream = res.pipe(zlib.createGunzip());
        } else if (encoding === "br") {
          stream = res.pipe(zlib.createBrotliDecompress());
        } else if (encoding === "deflate") {
          stream = res.pipe(zlib.createInflate());
        }
      } catch (decompressError) {
        reject(decompressError);
        return;
      }

      stream.on("data", (chunk) => chunks.push(chunk));
      stream.on("end", () => {
        const data = Buffer.concat(chunks).toString("utf8");
        resolve({
          ok: res.statusCode >= 200 && res.statusCode < 300,
          status: res.statusCode,
          text: () => Promise.resolve(data),
          json: () => Promise.resolve(JSON.parse(data)),
        });
      });
      stream.on("error", reject);
    });

    req.on("error", reject);
    req.on("timeout", () => {
      req.destroy();
      reject(new Error("Request timeout"));
    });

    if (options.body) {
      req.write(options.body);
    }
    req.end();
  });
}

if (process.platform === "linux") {
  app.commandLine.appendSwitch("--ozone-platform-hint=auto");

  app.commandLine.appendSwitch("--enable-features=VaapiVideoDecoder");

  // Fix click-coordinate misalignment with fractional display scaling on KDE/Wayland.
  // Chromium's hit-testing uses unscaled coords while the compositor scales visually,
  // so forcing scale factor 1 keeps them in sync. See: https://github.com/brave/brave-browser/issues/50028
  app.commandLine.appendSwitch("--force-device-scale-factor", "1");

  const passwordStore = selectLinuxPasswordStore(app.commandLine, process.env);
  if (passwordStore) {
    logToFile(
      `[safeStorage] Selected the ${passwordStore} password store for this desktop.`,
    );
  }
}

if (process.platform === "win32") {
  app.disableHardwareAcceleration();
}

if (isInsecureModeEnabled()) {
  logToFile(
    "[security] ENABLE_INSECURE_MODE is enabled; TLS certificate validation is disabled.",
  );
  app.commandLine.appendSwitch("--ignore-certificate-errors");
  app.commandLine.appendSwitch("--ignore-ssl-errors");
  app.commandLine.appendSwitch("--ignore-certificate-errors-spki-list");
}
app.commandLine.appendSwitch("--enable-features=NetworkService");

let mainWindow = null;
let backendProcess = null;
let backendStartFailed = false;
let tray = null;
let isQuitting = false;
const tempFiles = new Map();
const externalEditorSessions = new Map();

const isDev = process.env.NODE_ENV === "development" || !app.isPackaged;
const appRoot = isDev ? process.cwd() : path.join(__dirname, "..");
const windowsAppUserModelId = "com.karmaa.termix";
const electronCacheBuildPath = path.join(
  app.getPath("userData"),
  "client-cache-build.json",
);
const termixSessionPartition = "persist:termix";

function getTempRoot() {
  const tempRoot = path.join(app.getPath("temp"), "termix");
  fs.mkdirSync(tempRoot, { recursive: true });
  return tempRoot;
}

function sanitizeFileName(fileName) {
  const baseName = path.basename(String(fileName || "file"));
  return baseName.replace(/[<>:"/\\|?*\x00-\x1f]/g, "_") || "file";
}

function decodeFileContent(content, encoding) {
  if (encoding === "base64") {
    return Buffer.from(String(content || ""), "base64");
  }
  return Buffer.from(String(content || ""), "utf8");
}

function createManagedTempFile(fileName, content, encoding = "utf8") {
  const tempId = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
  const tempDir = fs.mkdtempSync(path.join(getTempRoot(), `${tempId}-`));
  const filePath = path.join(tempDir, sanitizeFileName(fileName));
  fs.writeFileSync(filePath, decodeFileContent(content, encoding));
  tempFiles.set(tempId, { path: filePath, dir: tempDir });
  return { tempId, path: filePath };
}

function cleanupManagedTempFile(tempId) {
  const temp = tempFiles.get(tempId);
  if (!temp) return;
  try {
    fs.rmSync(temp.dir, { recursive: true, force: true });
  } catch (error) {
    logToFile("Failed to clean up temporary file:", error.message);
  }
  tempFiles.delete(tempId);
}

function closeExternalEditorSession(editId) {
  const session = externalEditorSessions.get(editId);
  if (!session) return;
  if (session.timer) clearTimeout(session.timer);
  try {
    session.watcher.close();
  } catch {
    // watcher may already be closed
  }
  externalEditorSessions.delete(editId);
  cleanupManagedTempFile(editId);
}

function notifyExternalEditorSaved(editId) {
  const session = externalEditorSessions.get(editId);
  if (!session || !mainWindow || mainWindow.isDestroyed()) return;

  try {
    const stat = fs.statSync(session.path);
    if (stat.mtimeMs === session.lastMtimeMs) return;
    session.lastMtimeMs = stat.mtimeMs;

    const content = fs.readFileSync(session.path, "utf8");
    mainWindow.webContents.send("external-editor-saved", {
      editId,
      content,
      encoding: "utf8",
      path: session.path,
    });
  } catch (error) {
    logToFile("Failed to read external editor file:", error.message);
  }
}

function openPathWithEditor(filePath, editorPath) {
  if (!editorPath) {
    return shell.openPath(filePath);
  }

  return new Promise((resolve) => {
    let settled = false;
    const child = spawn(editorPath, [filePath], {
      detached: true,
      stdio: "ignore",
    });

    child.once("error", (error) => {
      if (settled) return;
      settled = true;
      resolve(error.message);
    });

    setTimeout(() => {
      if (settled) return;
      settled = true;
      child.unref();
      resolve("");
    }, 500);
  });
}

app.on(
  "certificate-error",
  (event, _webContents, url, error, certificate, callback) => {
    if (isInvalidCertificateAllowedForUrl(url)) {
      event.preventDefault();
      logToFile("Allowed invalid certificate for configured server", {
        url,
        error,
        issuer: certificate?.issuerName,
        subject: certificate?.subjectName,
      });
      callback(true);
      return;
    }

    callback(false);
  },
);

function getElectronBuildTimestamp() {
  try {
    const buildInfo = require("./build-info.cjs");
    if (Number.isInteger(buildInfo.buildTimestamp)) {
      return buildInfo.buildTimestamp;
    }
  } catch {
    // Development runs may not have generated build metadata yet.
  }

  return 0;
}

async function clearElectronClientCacheIfBuildChanged() {
  const buildTimestamp = getElectronBuildTimestamp();
  let cacheTimestamp = 0;

  try {
    if (fs.existsSync(electronCacheBuildPath)) {
      const data = JSON.parse(fs.readFileSync(electronCacheBuildPath, "utf8"));
      cacheTimestamp = Number.isInteger(data.buildTimestamp)
        ? data.buildTimestamp
        : 0;
    }
  } catch (error) {
    logToFile(
      "Failed to read Electron client cache build info:",
      error.message,
    );
  }

  if (cacheTimestamp === buildTimestamp) {
    return;
  }

  const clearStep = async (label, action) => {
    try {
      await action();
    } catch (error) {
      logToFile(`Failed to clear Electron ${label}:`, error.message);
    }
  };

  try {
    const defaultSession = session.defaultSession;
    await clearStep("HTTP cache", () => defaultSession.clearCache());
    await clearStep("code cache", () =>
      defaultSession.clearCodeCaches({ urls: [] }),
    );
    await clearStep("auth cache", () => defaultSession.clearAuthCache());
    await clearStep("storage data", () =>
      defaultSession.clearStorageData({
        storages: [
          "appcache",
          "cookies",
          "filesystem",
          "shadercache",
          "websql",
          "serviceworkers",
          "cachestorage",
        ],
      }),
    );

    fs.writeFileSync(
      electronCacheBuildPath,
      JSON.stringify(
        {
          buildTimestamp,
          appVersion: app.getVersion(),
          updatedAt: new Date().toISOString(),
        },
        null,
        2,
      ),
    );

    logToFile("Electron client cache cleared for build change", {
      from: cacheTimestamp,
      to: buildTimestamp,
      appVersion: app.getVersion(),
    });
  } catch (error) {
    logToFile("Failed to clear Electron client cache:", error.message);
  }
}

function getCookieRemovalUrl(cookie) {
  const scheme = cookie.secure ? "https" : "http";
  const domain = cookie.domain?.startsWith(".")
    ? cookie.domain.slice(1)
    : cookie.domain || "localhost";
  return `${scheme}://${domain}${cookie.path || "/"}`;
}

async function clearElectronJwtCookiesAtStartup() {
  loadElectronAuthCookiesFromDisk();

  const targetSessions = new Set([
    session.defaultSession,
    session.fromPartition(termixSessionPartition),
  ]);

  for (const targetSession of targetSessions) {
    try {
      const cookies = await targetSession.cookies.get({ name: "jwt" });
      await Promise.all(
        cookies.map((cookie) =>
          targetSession.cookies.remove(
            getCookieRemovalUrl(cookie),
            cookie.name,
          ),
        ),
      );

      if (cookies.length > 0) {
        logToFile("Cleared Electron JWT cookies from cookie store", {
          count: cookies.length,
        });
      }
    } catch (error) {
      logToFile("Failed to clear Electron JWT cookies:", error.message);
    }
  }
}

function getBackendPaths() {
  if (isDev) {
    const backendDir = path.join(appRoot, "dist", "backend", "backend");
    return {
      entryPath: path.join(backendDir, "starter.js"),
      backendCwd: backendDir,
    };
  }
  // fork() does not go through Electron's asar redirector — use the unpacked path.
  // On macOS multi-arch builds (mergeASARs: false), electron-builder names the ASAR
  // app-arm64.asar / app-x64.asar instead of app.asar, so match all variants.
  const unpackedRoot = getUnpackedAppRoot(appRoot);
  const backendDir = path.join(unpackedRoot, "dist", "backend", "backend");
  return {
    entryPath: path.join(backendDir, "starter.js"),
    backendCwd: backendDir,
  };
}

function getBackendDataDir() {
  const userDataPath = app.getPath("userData");
  const dataDir = path.join(userDataPath, "server-data");
  if (!fs.existsSync(dataDir)) {
    fs.mkdirSync(dataDir, { recursive: true });
  }
  return dataDir;
}

function getBackendPidFilePath() {
  return path.join(app.getPath("userData"), "backend.pid");
}

// If the app was previously killed abnormally (crash, force-quit, Task
// Manager) rather than through the normal quit flow, will-quit never fires
// and stopBackendServer() never runs -- the forked backend child is a
// genuinely separate OS process on Windows/mac/Linux, so it keeps running
// and holding every port the backend binds (30001, 30003-30008, 30010,
// 30012...). Every subsequent launch's own backend then fails outright
// with EADDRINUSE and the app is stuck until something manually kills the
// orphan. Reap any such leftover process, identified by PID file, before
// spawning a new one.
function reapOrphanedBackendProcess() {
  const pidFilePath = getBackendPidFilePath();
  let recordedPid;
  try {
    recordedPid = parseInt(fs.readFileSync(pidFilePath, "utf8").trim(), 10);
  } catch {
    return;
  }
  if (!Number.isInteger(recordedPid) || recordedPid <= 0) return;

  try {
    // Signal 0 does not kill the process -- it only checks whether a
    // process with this PID exists and is signalable, throwing ESRCH if
    // not. This avoids killing an unrelated process that happens to have
    // reused the same PID since the last run.
    process.kill(recordedPid, 0);
  } catch {
    // No live process at that PID; nothing to reap.
    try {
      fs.unlinkSync(pidFilePath);
    } catch {
      // already absent
    }
    return;
  }

  logToFile(
    `Found orphaned backend process from a previous session (pid ${recordedPid}), terminating it before starting a new one`,
  );
  try {
    process.kill(recordedPid, "SIGKILL");
  } catch {
    // already gone
  }
  try {
    fs.unlinkSync(pidFilePath);
  } catch {
    // already absent
  }
}

function startBackendServer() {
  reapOrphanedBackendProcess();
  return new Promise((resolve) => {
    const { entryPath, backendCwd } = getBackendPaths();

    logToFile("isDev:", isDev, "appRoot:", appRoot);
    logToFile("app.isPackaged:", app.isPackaged);
    logToFile("process.env.NODE_ENV:", process.env.NODE_ENV);

    if (!fs.existsSync(entryPath)) {
      logToFile("Backend entry not found:", entryPath);
      resolve(false);
      return;
    }

    const dataDir = getBackendDataDir();
    logToFile("Starting embedded backend server...");
    logToFile("Backend entry:", entryPath);
    logToFile("Data directory:", dataDir);
    logToFile("Backend cwd:", backendCwd);

    logToFile("Checking paths...");
    logToFile("  entryPath exists:", fs.existsSync(entryPath));
    logToFile("  dataDir exists:", fs.existsSync(dataDir));
    logToFile("  backendCwd exists:", fs.existsSync(backendCwd));

    backendProcess = fork(entryPath, [], {
      cwd:
        fs.existsSync(backendCwd) && fs.statSync(backendCwd).isDirectory()
          ? backendCwd
          : dataDir,
      env: {
        ...process.env,
        DATA_DIR: dataDir,
        NODE_ENV: "production",
        ELECTRON_EMBEDDED: "true",
        PORT: "30001",
        VERSION: app.getVersion(),
      },
      stdio: ["pipe", "pipe", "pipe", "ipc"],
    });

    logToFile("Backend process spawned, pid:", backendProcess.pid);
    try {
      fs.writeFileSync(getBackendPidFilePath(), String(backendProcess.pid));
    } catch {
      // Non-fatal: only means a future crash won't self-heal via reap.
    }

    let resolved = false;
    const readyTimeout = setTimeout(() => {
      if (!resolved) {
        resolved = true;
        logToFile("Backend ready timeout (15s), proceeding anyway...");
        resolve(true);
      }
    }, 15000);

    backendProcess.stdout.on("data", (data) => {
      const msg = data.toString().trim();
      logToFile("[backend]", msg);
      if (!resolved && msg.includes("started successfully")) {
        resolved = true;
        clearTimeout(readyTimeout);
        logToFile("Backend ready signal received");
        resolve(true);
      }
    });

    backendProcess.stderr.on("data", (data) => {
      logToFile("[backend:stderr]", data.toString().trim());
    });

    backendProcess.on("exit", (code, signal) => {
      logToFile(`Backend process exited with code ${code}, signal ${signal}`);
      if (!resolved && code !== 0) {
        backendStartFailed = true;
      }
      backendProcess = null;
      clearBackendPidFile();
      if (!resolved) {
        resolved = true;
        clearTimeout(readyTimeout);
        resolve(false);
      }
    });

    backendProcess.on("error", (err) => {
      logToFile("Failed to start backend process:", err.message);
      backendProcess = null;
      if (!resolved) {
        resolved = true;
        clearTimeout(readyTimeout);
        resolve(false);
      }
    });
  });
}

function clearBackendPidFile() {
  try {
    fs.unlinkSync(getBackendPidFilePath());
  } catch {
    // already absent
  }
}

function stopBackendServer() {
  if (!backendProcess) return;

  console.log("Stopping embedded backend server...");

  try {
    backendProcess.send({ type: "shutdown" });
  } catch {
    // IPC channel may already be closed
  }

  const forceKillTimeout = setTimeout(() => {
    if (backendProcess) {
      console.log("Force killing backend process...");
      backendProcess.kill("SIGKILL");
      backendProcess = null;
    }
  }, 5000);

  backendProcess.on("exit", () => {
    clearTimeout(forceKillTimeout);
    backendProcess = null;
    clearBackendPidFile();
  });
}

const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
  console.log("Another instance is already running, quitting...");
  app.quit();
  process.exit(0);
} else {
  app.on("second-instance", (event, commandLine, workingDirectory) => {
    if (mainWindow) {
      if (mainWindow.isMinimized()) mainWindow.restore();
      mainWindow.focus();
      mainWindow.show();
    }
  });
}

function createTray() {
  try {
    const { nativeImage } = require("electron");

    // Native APIs (Tray, nativeImage) can't load files from inside app.asar —
    // use the unpacked path so the OS sees a real file.
    const publicRoot = isDev
      ? path.join(appRoot, "public")
      : path.join(
          appRoot.replace(
            /app(-[a-z0-9]+)?\.asar(?!\.unpacked)/,
            "app.asar.unpacked",
          ),
          "public",
        );

    let trayIcon;
    if (process.platform === "darwin") {
      const iconPath = path.join(publicRoot, "icons", "16x16.png");
      trayIcon = nativeImage.createFromPath(iconPath);
      trayIcon.setTemplateImage(true);
    } else if (process.platform === "win32") {
      trayIcon = path.join(publicRoot, "icon.ico");
    } else {
      trayIcon = path.join(publicRoot, "icons", "32x32.png");
    }

    tray = new Tray(trayIcon);
    tray.setToolTip("Termix");

    const contextMenu = Menu.buildFromTemplate([
      {
        label: "Show Window",
        click: () => {
          if (mainWindow) {
            mainWindow.show();
            mainWindow.focus();
          }
        },
      },
      {
        label: "Quit",
        click: () => {
          isQuitting = true;
          quitApp(app, mainWindow);
        },
      },
    ]);

    tray.setContextMenu(contextMenu);

    tray.on("click", () => {
      if (mainWindow) {
        if (mainWindow.isVisible()) {
          mainWindow.hide();
        } else {
          mainWindow.show();
          mainWindow.focus();
        }
      }
    });

    console.log("System tray created successfully");
  } catch (err) {
    console.error("Failed to create system tray:", err);
  }
}

function createWindow() {
  const appVersion = app.getVersion();
  const electronVersion = process.versions.electron;
  const platform =
    process.platform === "win32"
      ? "Windows"
      : process.platform === "darwin"
        ? "macOS"
        : "Linux";

  mainWindow = new BrowserWindow({
    width: 1200,
    height: 800,
    minWidth: 800,
    minHeight: 600,
    title: "Termix",
    icon: path.join(
      appRoot,
      "public",
      process.platform === "win32" ? "icon.ico" : "icon.png",
    ),
    webPreferences: {
      nodeIntegration: false,
      contextIsolation: true,
      webSecurity: false,
      preload: path.join(__dirname, "preload.js"),
      partition: termixSessionPartition,
      allowRunningInsecureContent: true,
      webviewTag: true,
      offscreen: false,
    },
    show: true,
  });

  mainWindow.webContents.session.setPermissionRequestHandler(
    (webContents, permission, callback) => {
      if (
        permission === "clipboard-read" ||
        permission === "clipboard-write" ||
        permission === "clipboard-sanitized-write"
      ) {
        callback(true);
        return;
      }
      callback(false);
    },
  );

  if (process.platform !== "darwin") {
    mainWindow.setMenuBarVisibility(false);
  }

  const customUserAgent = `Termix-Desktop/${appVersion} (${platform}; Electron/${electronVersion})`;
  mainWindow.webContents.setUserAgent(customUserAgent);

  mainWindow.webContents.on("before-input-event", (event, input) => {
    if (process.platform !== "win32" || !isCloseActiveTabInput(input)) return;

    event.preventDefault();
    mainWindow.webContents.send("close-active-tab");
  });

  mainWindow.webContents.session.webRequest.onBeforeSendHeaders(
    (details, callback) => {
      details.requestHeaders["X-Electron-App"] = "true";

      details.requestHeaders["User-Agent"] = customUserAgent;

      const rememberedJwt = getRememberedElectronAuthCookie("jwt", details.url);
      if (rememberedJwt) {
        setCookieHeaderValue(
          details.requestHeaders,
          rememberedJwt.name,
          rememberedJwt.value,
        );
      }

      callback({ requestHeaders: details.requestHeaders });
    },
  );

  if (isDev) {
    mainWindow.loadURL("http://localhost:5173");
    mainWindow.webContents.openDevTools();
  } else {
    const indexPath = path.join(appRoot, "dist", "index.html");
    mainWindow.loadFile(indexPath).catch((err) => {
      console.error("Failed to load file:", err);
    });
  }

  mainWindow.webContents.session.webRequest.onHeadersReceived(
    (details, callback) => {
      const headers = details.responseHeaders;

      if (headers) {
        delete headers["x-frame-options"];
        delete headers["X-Frame-Options"];

        if (headers["content-security-policy"]) {
          headers["content-security-policy"] = headers[
            "content-security-policy"
          ]
            .map((value) => value.replace(/frame-ancestors[^;]*/gi, ""))
            .filter((value) => value.trim().length > 0);

          if (headers["content-security-policy"].length === 0) {
            delete headers["content-security-policy"];
          }
        }
        if (headers["Content-Security-Policy"]) {
          headers["Content-Security-Policy"] = headers[
            "Content-Security-Policy"
          ]
            .map((value) => value.replace(/frame-ancestors[^;]*/gi, ""))
            .filter((value) => value.trim().length > 0);

          if (headers["Content-Security-Policy"].length === 0) {
            delete headers["Content-Security-Policy"];
          }
        }

        const setCookieHeaderName = getHeaderName(headers, "Set-Cookie");
        if (setCookieHeaderName) {
          const setCookieHeaders = Array.isArray(headers[setCookieHeaderName])
            ? headers[setCookieHeaderName]
            : [headers[setCookieHeaderName]];

          setCookieHeaders.forEach((cookie) => {
            rememberElectronAuthCookieFromHeader(details.url, cookie);
          });

          headers[setCookieHeaderName] = setCookieHeaders.map((cookie) => {
            let modified = cookie.replace(
              /;\s*SameSite=Strict/gi,
              "; SameSite=None",
            );
            modified = modified.replace(
              /;\s*SameSite=Lax/gi,
              "; SameSite=None",
            );
            if (!modified.includes("SameSite=")) {
              modified += "; SameSite=None";
            }
            if (
              !modified.includes("Secure") &&
              details.url.startsWith("https")
            ) {
              modified += "; Secure";
            }
            return modified;
          });
        }
      }

      callback({ responseHeaders: headers });
    },
  );

  mainWindow.once("ready-to-show", () => {
    mainWindow.show();
    mainWindow.focus();
  });

  setTimeout(() => {
    if (mainWindow && !mainWindow.isVisible()) {
      mainWindow.show();
    }
  }, 3000);

  mainWindow.webContents.on(
    "did-fail-load",
    (event, errorCode, errorDescription, validatedURL) => {
      console.error(
        "Failed to load:",
        errorCode,
        errorDescription,
        validatedURL,
      );
    },
  );

  mainWindow.webContents.on("did-finish-load", () => {
    console.log("Frontend loaded successfully");
  });

  mainWindow.on("close", (event) => {
    if (!isQuitting && tray && !tray.isDestroyed()) {
      event.preventDefault();
      mainWindow.hide();
    }
  });

  mainWindow.on("closed", () => {
    mainWindow = null;
  });

  mainWindow.webContents.setWindowOpenHandler(({ url }) => {
    try {
      const parsed = new URL(url);
      if (parsed.protocol === "http:" || parsed.protocol === "https:") {
        shell.openExternal(url);
      }
    } catch {
      // invalid URL, ignore
    }
    return { action: "deny" };
  });
}

ipcMain.handle("get-app-version", () => {
  return app.getVersion();
});

const GITHUB_API_BASE = "https://api.github.com";
const REPO_OWNER = "Termix-SSH";
const REPO_NAME = "Termix";

const githubCache = new Map();
const CACHE_DURATION = 30 * 60 * 1000;

async function fetchGitHubAPI(endpoint, cacheKey) {
  const cached = githubCache.get(cacheKey);
  if (cached && Date.now() - cached.timestamp < CACHE_DURATION) {
    return {
      data: cached.data,
      cached: true,
      cache_age: Date.now() - cached.timestamp,
    };
  }

  try {
    const response = await httpFetch(`${GITHUB_API_BASE}${endpoint}`, {
      headers: {
        Accept: "application/vnd.github+json",
        "User-Agent": "TermixElectronUpdateChecker/1.0",
        "X-GitHub-Api-Version": "2022-11-28",
      },
      timeout: 10000,
    });

    if (!response.ok) {
      throw new Error(
        `GitHub API error: ${response.status} ${response.statusText}`,
      );
    }

    const data = await response.json();

    githubCache.set(cacheKey, {
      data,
      timestamp: Date.now(),
    });

    return {
      data: data,
      cached: false,
    };
  } catch (error) {
    console.error("Failed to fetch from GitHub API:", error);
    throw error;
  }
}

ipcMain.handle("check-electron-update", async () => {
  try {
    const localVersion = app.getVersion();

    const releaseData = await fetchGitHubAPI(
      `/repos/${REPO_OWNER}/${REPO_NAME}/releases/latest`,
      "latest_release_electron",
    );

    const rawTag = releaseData.data.tag_name || releaseData.data.name || "";
    const remoteVersionMatch = rawTag.match(/(\d+\.\d+(\.\d+)?)/);
    const remoteVersion = remoteVersionMatch ? remoteVersionMatch[1] : null;

    if (!remoteVersion) {
      return {
        success: false,
        error: "Remote version not found",
        localVersion,
      };
    }

    const versionComparison = compareSemver(localVersion, remoteVersion);
    const status =
      versionComparison === null || versionComparison === 0
        ? "up_to_date"
        : versionComparison > 0
          ? "beta"
          : "requires_update";

    const result = {
      success: true,
      status,
      localVersion: localVersion,
      remoteVersion: remoteVersion,
      latest_release: {
        tag_name: releaseData.data.tag_name,
        name: releaseData.data.name,
        published_at: releaseData.data.published_at,
        html_url: releaseData.data.html_url,
        body: releaseData.data.body,
      },
      cached: releaseData.cached,
      cache_age: releaseData.cache_age,
    };

    return result;
  } catch (error) {
    return {
      success: false,
      error: error.message,
      localVersion: app.getVersion(),
    };
  }
});

ipcMain.handle("get-platform", () => {
  return process.platform;
});

ipcMain.handle("open-native-rdp", (_event, options) =>
  launchNativeRdp(options),
);

ipcMain.handle("get-embedded-server-status", () => {
  return {
    running:
      backendProcess !== null && !backendProcess.killed && !backendStartFailed,
    dataDir: isDev ? null : getBackendDataDir(),
  };
});

// OIDC System Browser Authentication (RFC 8252)
ipcMain.handle(
  "oidc-system-browser-auth",
  async (_event, authUrl, callbackPort) => {
    const http = require("http");

    return new Promise((resolve) => {
      let timeout;
      let settled = false;
      const server = http.createServer((req, res) => {
        const url = new URL(req.url || "/", `http://localhost:${callbackPort}`);
        if (url.pathname === "/oidc-callback") {
          const success = url.searchParams.get("success");
          const error = url.searchParams.get("error");
          const token = url.searchParams.get("token");

          res.writeHead(200, { "Content-Type": "text/html" });
          res.end(
            `<html><body><h2>${success === "true" ? "Authentication successful!" : "Authentication failed."}</h2><p>You can close this tab and return to Termix.</p><script>window.close()</script></body></html>`,
          );

          if (success === "true") {
            finish({ success: true, token });
          } else {
            finish({
              success: false,
              error: error || "Authentication failed",
            });
          }
          return;
        }

        res.writeHead(404, { "Content-Type": "text/plain" });
        res.end("Not found");
      });

      const finish = (result) => {
        if (settled) return;
        settled = true;
        if (timeout) clearTimeout(timeout);
        try {
          server.close();
        } catch {
          // Server may not have started yet.
        }
        resolve(result);
      };

      const fail = (error) => {
        finish({
          success: false,
          error: error instanceof Error ? error.message : String(error),
        });
      };

      server.once("error", fail);

      server.listen(callbackPort, "localhost", async () => {
        try {
          await shell.openExternal(authUrl);
        } catch (error) {
          fail(error);
        }
      });

      // Timeout after 5 minutes
      timeout = setTimeout(
        () => {
          fail(new Error("OIDC authentication timed out"));
        },
        5 * 60 * 1000,
      );
    });
  },
);

ipcMain.handle("get-server-config", () => {
  try {
    return getServerConfigSync();
  } catch (error) {
    console.error("Error reading server config:", error);
    return null;
  }
});

ipcMain.handle("save-server-config", (event, config) => {
  try {
    const userDataPath = app.getPath("userData");
    const configPath = getServerConfigPath();

    if (!fs.existsSync(userDataPath)) {
      fs.mkdirSync(userDataPath, { recursive: true });
    }

    fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
    return { success: true };
  } catch (error) {
    console.error("Error saving server config:", error);
    return { success: false, error: error.message };
  }
});

// --- Remote sync (optional desktop <-> self-hosted server sync) ---

// Surfaces the pre-standalone-rework server-config.json (if a serverUrl was
// ever set in it) so the renderer can prompt upgraded installs to set up
// Remote Sync -- their hosts live on that old server and won't appear
// locally until sync is enabled. A fresh install never had this file, so
// this is naturally false for anyone who never used the old architecture.
ipcMain.handle("get-legacy-server-config", () => {
  const config = getServerConfigSync();
  return { serverUrl: config?.serverUrl || null };
});

ipcMain.handle("get-desktop-settings", () => {
  return remoteSync.getDesktopSettings();
});

ipcMain.handle("save-desktop-settings", (_event, settings) => {
  return remoteSync.saveDesktopSettings(settings);
});

ipcMain.handle("get-remote-sync-config", () => {
  return remoteSync.getRemoteSyncConfig();
});

ipcMain.handle("save-remote-sync-config", (_event, config) => {
  return remoteSync.saveRemoteSyncConfig(config);
});

ipcMain.handle("clear-remote-sync-config", async () => {
  const result = remoteSync.clearRemoteSyncConfig();
  remoteSync.clearRemoteSyncJwt();
  remoteSync.getRemoteSyncEngine()?.updateStatus({
    connected: false,
    syncing: false,
    needsReauth: false,
    lastError: null,
  });
  return result;
});

ipcMain.handle("save-remote-sync-jwt", (_event, token) => {
  const result = remoteSync.saveRemoteSyncJwt(token);
  if (result.success) {
    remoteSync.getRemoteSyncEngine()?.updateStatus({
      connected: true,
      needsReauth: false,
      lastError: null,
    });
    remoteSync.getRemoteSyncEngine()?.syncNow();
  }
  return result;
});

ipcMain.handle("get-remote-sync-jwt", () => {
  return remoteSync.getRemoteSyncJwt();
});

ipcMain.handle("clear-remote-sync-jwt", () => {
  return remoteSync.clearRemoteSyncJwt();
});

ipcMain.handle("get-remote-sync-status", () => {
  return remoteSync.getRemoteSyncEngine()?.status || null;
});

ipcMain.handle("get-remote-sync-user-info", () => {
  return remoteSync.getRemoteSyncUserInfo();
});

ipcMain.handle("remote-sync-now", async () => {
  return (await remoteSync.getRemoteSyncEngine()?.syncNow()) || null;
});

ipcMain.handle("notify-local-login", (_event, token) => {
  remoteSync.getRemoteSyncEngine()?.setLocalJwt(token);
  return { success: true };
});

function getC2STunnelConfigPath() {
  return path.join(app.getPath("userData"), "c2s-tunnels.json");
}

ipcMain.handle("get-c2s-tunnel-config", () => {
  try {
    const configPath = getC2STunnelConfigPath();
    if (!fs.existsSync(configPath)) {
      return [];
    }
    const configData = fs.readFileSync(configPath, "utf8");
    const parsed = JSON.parse(configData);
    return Array.isArray(parsed) ? parsed : [];
  } catch (error) {
    console.error("Error reading C2S tunnel config:", error);
    return [];
  }
});

ipcMain.handle("save-c2s-tunnel-config", async (_event, config) => {
  try {
    if (!Array.isArray(config)) {
      return { success: false, error: "C2S tunnel config must be an array" };
    }
    const autoStartListeners = new Set();
    const autoStartRemoteListeners = new Set();
    for (const tunnel of config) {
      if (!tunnel?.autoStart) continue;
      const mode = tunnel.mode || tunnel.tunnelType || "local";
      if (mode === "remote") {
        const sourceHostId = Number(tunnel.sourceHostId);
        const sourcePort = Number(tunnel.sourcePort);
        if (
          !Number.isInteger(sourceHostId) ||
          sourceHostId < 1 ||
          !Number.isInteger(sourcePort) ||
          sourcePort < 1 ||
          sourcePort > 65535
        ) {
          return {
            success: false,
            error: "Invalid remote client tunnel endpoint or port",
          };
        }
        const listenerKey = `${sourceHostId}:${sourcePort}`;
        if (autoStartRemoteListeners.has(listenerKey)) {
          return {
            success: false,
            error: `Another auto-start client tunnel already uses remote ${listenerKey}`,
          };
        }
        autoStartRemoteListeners.add(listenerKey);
        continue;
      }

      const bindHost = tunnel.bindHost || "127.0.0.1";
      const sourcePort = Number(tunnel.sourcePort);
      const listenerKey = `${bindHost}:${sourcePort}`;
      if (autoStartListeners.has(listenerKey)) {
        return {
          success: false,
          error: `Another auto-start client tunnel already uses ${listenerKey}`,
        };
      }
      autoStartListeners.add(listenerKey);
    }
    for (const listenerKey of autoStartListeners) {
      const [bindHost, sourcePort] = listenerKey.split(":");
      const result = await checkLocalPortAvailable(
        bindHost,
        Number(sourcePort),
      );
      const ownedByClientTunnel = Array.from(c2sTunnelRuntimes.values()).some(
        (runtime) =>
          runtime.bindHost === bindHost &&
          runtime.sourcePort === Number(sourcePort),
      );
      if (!result.available && !ownedByClientTunnel) {
        return {
          success: false,
          error: `Cannot auto-start client tunnel on ${listenerKey}: ${result.error || "port is already in use"}`,
        };
      }
    }
    const userDataPath = app.getPath("userData");
    if (!fs.existsSync(userDataPath)) {
      fs.mkdirSync(userDataPath, { recursive: true });
    }
    fs.writeFileSync(getC2STunnelConfigPath(), JSON.stringify(config, null, 2));
    return { success: true };
  } catch (error) {
    console.error("Error saving C2S tunnel config:", error);
    return { success: false, error: error.message };
  }
});

function checkLocalPortAvailable(host, port) {
  return new Promise((resolve) => {
    const server = net.createServer();
    server.once("error", (error) => {
      resolve({ available: false, error: error.message });
    });
    server.once("listening", () => {
      server.close(() => resolve({ available: true }));
    });
    server.listen({ host, port });
  });
}

function checkTcpConnection(host, port) {
  return new Promise((resolve) => {
    const socket = net.createConnection({ host, port });
    const timer = setTimeout(() => {
      socket.destroy();
      resolve({ success: false, error: "Connection timed out" });
    }, 5000);

    socket.once("connect", () => {
      clearTimeout(timer);
      socket.destroy();
      resolve({ success: true });
    });
    socket.once("error", (error) => {
      clearTimeout(timer);
      socket.destroy();
      resolve({ success: false, error: error.message });
    });
  });
}

const c2sTunnelRuntimes = new Map();
const C2S_WS_HIGH_WATERMARK = 1024 * 1024;
const C2S_WS_LOW_WATERMARK = 256 * 1024;
const C2S_STREAM_WRITE_LIMIT = 8 * 1024 * 1024;

// C2S (client-to-server) tunnels relay through a connected, self-hosted
// Termix server -- the same "remote server" concept Remote Sync connects
// to, not the always-local embedded backend. There's no separate C2S
// server-URL setting in the UI; it has always shared whatever remote
// server the rest of the app was pointed at. Before the standalone-first
// rework that was server-config.json; now it's remote-sync-config.json,
// since that's the only remaining notion of "a connected remote server."
function getC2SRelayUrl() {
  const config = remoteSync.getRemoteSyncConfig();
  const serverUrl = config?.serverUrl;
  if (!serverUrl) {
    throw new Error(
      "No remote Termix server connected -- enable Remote Sync first",
    );
  }

  const base = serverUrl.replace(/\/$/, "");
  const relayHttpUrl = `${base}/ssh/tunnel/c2s/stream`;
  return relayHttpUrl.replace(/^http:/, "ws:").replace(/^https:/, "wss:");
}

async function getC2SRelayHeaders() {
  const jwt = remoteSync.getRemoteSyncJwt();
  if (!jwt) return {};

  return {
    Authorization: `Bearer ${jwt}`,
  };
}

function getC2STunnelName(tunnel, index = 0) {
  if (tunnel.name) return tunnel.name;
  return [
    "c2s",
    index,
    tunnel.sourceHostId || 0,
    tunnel.mode || tunnel.tunnelType || "local",
    tunnel.bindHost || "127.0.0.1",
    tunnel.sourcePort,
    tunnel.endpointPort || 0,
  ].join("::");
}

function getC2STunnelStatus(tunnelName) {
  return (
    c2sTunnelRuntimes.get(tunnelName)?.status || {
      connected: false,
      status: "DISCONNECTED",
    }
  );
}

function getAllC2STunnelStatuses() {
  const statuses = {};
  for (const [tunnelName] of c2sTunnelRuntimes.entries()) {
    statuses[tunnelName] = getC2STunnelStatus(tunnelName);
  }
  return statuses;
}

function emitC2STunnelStatuses() {
  if (!mainWindow || mainWindow.isDestroyed()) return;
  mainWindow.webContents.send("c2s-tunnel-statuses", getAllC2STunnelStatuses());
}

function setC2STunnelStatus(tunnelName, status) {
  const runtime = c2sTunnelRuntimes.get(tunnelName);
  if (runtime) {
    runtime.status = status;
    emitC2STunnelStatuses();
  }
}

function setC2STunnelError(tunnelName, message) {
  logToFile(`[c2s] ${tunnelName} failed:`, message);
  setC2STunnelStatus(tunnelName, {
    connected: false,
    status: "ERROR",
    reason: message,
  });
}

function parseSocks5Target(buffer) {
  if (buffer.length < 7 || buffer[0] !== 0x05 || buffer[1] !== 0x01) {
    return null;
  }

  const addressType = buffer[3];
  let offset = 4;
  let host;

  if (addressType === 0x01) {
    if (buffer.length < offset + 4 + 2) return null;
    host = Array.from(buffer.subarray(offset, offset + 4)).join(".");
    offset += 4;
  } else if (addressType === 0x03) {
    const length = buffer[offset];
    offset += 1;
    if (buffer.length < offset + length + 2) return null;
    host = buffer.subarray(offset, offset + length).toString("utf8");
    offset += length;
  } else if (addressType === 0x04) {
    if (buffer.length < offset + 16 + 2) return null;
    const parts = [];
    for (let i = 0; i < 16; i += 2) {
      parts.push(buffer.readUInt16BE(offset + i).toString(16));
    }
    host = parts.join(":");
    offset += 16;
  } else {
    throw new Error("Unsupported SOCKS5 address type");
  }

  const port = buffer.readUInt16BE(offset);
  return { host, port, bytesRead: offset + 2 };
}

async function openC2SRelay(
  tunnel,
  targetHost,
  targetPort,
  socket,
  initialData,
) {
  const tunnelName = tunnel.name || getC2STunnelName(tunnel);
  const relayUrl = getC2SRelayUrl();
  const headers = await getC2SRelayHeaders();
  logToFile(`[c2s] opening relay for ${tunnelName}`, {
    relayUrl,
    targetHost,
    targetPort,
  });
  setC2STunnelStatus(tunnelName, {
    connected: false,
    status: "CONNECTING",
    reason: `Opening relay to ${targetHost}:${targetPort}`,
  });
  const ws = new WebSocket(
    relayUrl,
    getWebSocketOptions(relayUrl, { headers }),
  );
  const pendingChunks = [];
  let ready = false;
  let closed = false;

  const cleanup = () => {
    if (closed) return;
    closed = true;
    try {
      socket.destroy();
    } catch {
      // expected during shutdown
    }
    try {
      ws.close();
    } catch {
      // expected during shutdown
    }
  };

  const sendChunk = (chunk) => {
    if (ready && ws.readyState === WebSocket.OPEN) {
      ws.send(chunk);
    } else {
      pendingChunks.push(chunk);
    }
  };

  socket.on("data", sendChunk);
  socket.on("close", cleanup);
  socket.on("error", (error) => {
    setC2STunnelError(tunnelName, error.message || "Local socket error");
    cleanup();
  });
  ws.on("close", cleanup);
  ws.on("error", (error) => {
    setC2STunnelError(tunnelName, error.message || "Relay connection failed");
    cleanup();
  });

  ws.on("open", () => {
    logToFile(`[c2s] relay connected for ${tunnelName}`);
    ws.send(
      JSON.stringify({
        type: "open",
        tunnelConfig: tunnel,
        targetHost,
        targetPort,
      }),
    );
  });

  ws.on("message", (data, isBinary) => {
    if (isBinary) {
      socket.write(Buffer.isBuffer(data) ? data : Buffer.from(data));
      return;
    }

    try {
      const message = JSON.parse(data.toString());
      if (message.type === "ready") {
        ready = true;
        logToFile(`[c2s] relay ready for ${tunnelName}`);
        setC2STunnelStatus(tunnelName, {
          connected: true,
          status: "CONNECTED",
        });
        if (initialData?.length) {
          ws.send(initialData);
        }
        while (pendingChunks.length > 0) {
          ws.send(pendingChunks.shift());
        }
      } else if (message.type === "error") {
        logToFile("[c2s] relay error:", message.error);
        setC2STunnelError(
          tunnelName,
          message.error || "Relay rejected the client tunnel",
        );
        cleanup();
      }
    } catch (error) {
      logToFile("[c2s] invalid relay message:", error.message);
      setC2STunnelError(tunnelName, error.message || "Invalid relay response");
      cleanup();
    }
  });
}

async function testC2SRelay(tunnel, targetHost, targetPort) {
  const relayUrl = getC2SRelayUrl();
  const headers = await getC2SRelayHeaders();
  const ws = new WebSocket(
    relayUrl,
    getWebSocketOptions(relayUrl, { headers }),
  );

  return new Promise((resolve) => {
    let settled = false;
    const settle = (result) => {
      if (settled) return;
      settled = true;
      try {
        ws.close();
      } catch {
        // expected during shutdown
      }
      resolve(result);
    };

    const timer = setTimeout(() => {
      settle({ success: false, error: "Tunnel test timed out" });
    }, 15000);

    ws.on("open", () => {
      ws.send(
        JSON.stringify({
          type: "test",
          tunnelConfig: tunnel,
          targetHost,
          targetPort,
        }),
      );
    });
    ws.on("message", (data, isBinary) => {
      if (isBinary) return;

      try {
        const message = JSON.parse(data.toString());
        if (message.type === "ready") {
          clearTimeout(timer);
          settle({ success: true });
        } else if (message.type === "error") {
          clearTimeout(timer);
          settle({
            success: false,
            error: message.error || "Tunnel test failed",
          });
        }
      } catch (error) {
        clearTimeout(timer);
        settle({ success: false, error: error.message });
      }
    });
    ws.on("error", (error) => {
      clearTimeout(timer);
      settle({ success: false, error: error.message });
    });
    ws.on("close", () => {
      clearTimeout(timer);
      settle({ success: false, error: "Tunnel test connection closed" });
    });
  });
}

async function testC2STunnel(tunnel, index = 0) {
  const mode = tunnel.mode || tunnel.tunnelType || "local";
  const testTunnel = {
    ...tunnel,
    name: `${getC2STunnelName(tunnel, index)}::test`,
    mode,
  };
  const bindHost = tunnel.bindHost || "127.0.0.1";
  const sourcePort = Number(tunnel.sourcePort);
  const endpointPort = Number(tunnel.endpointPort);

  if (!tunnel.sourceHostId) {
    return { success: false, error: "Endpoint SSH host is required" };
  }

  if (mode === "remote") {
    const localTarget = await checkTcpConnection(bindHost, endpointPort);
    if (!localTarget.success) {
      return {
        success: false,
        error: `Local target ${bindHost}:${endpointPort} is not reachable: ${localTarget.error}`,
      };
    }

    return testC2SRelay(testTunnel, undefined, undefined);
  }

  if (!Number.isInteger(sourcePort) || sourcePort < 1 || sourcePort > 65535) {
    return { success: false, error: "Invalid local port" };
  }

  const runtime = c2sTunnelRuntimes.get(getC2STunnelName(tunnel, index));
  if (!runtime) {
    const availability = await checkLocalPortAvailable(bindHost, sourcePort);
    if (!availability.available) {
      return {
        success: false,
        error: `Local listener ${bindHost}:${sourcePort} is not available: ${availability.error}`,
      };
    }
  }

  if (mode === "dynamic") {
    return testC2SRelay(testTunnel, undefined, undefined);
  }

  if (!Number.isInteger(endpointPort) || endpointPort < 1) {
    return { success: false, error: "Invalid remote port" };
  }

  return testC2SRelay(
    testTunnel,
    tunnel.targetHost || "127.0.0.1",
    endpointPort,
  );
}

function handleC2SDynamicConnection(tunnel, socket) {
  const tunnelName = tunnel.name || getC2STunnelName(tunnel);
  let buffer = Buffer.alloc(0);
  let stage = "greeting";

  const fail = (code = 0x01, message = "SOCKS5 request failed") => {
    setC2STunnelError(tunnelName, message);
    if (!socket.destroyed) {
      socket.write(Buffer.from([0x05, code, 0x00, 0x01, 0, 0, 0, 0, 0, 0]));
      socket.destroy();
    }
  };

  const onData = (chunk) => {
    buffer = Buffer.concat([buffer, chunk]);

    try {
      if (stage === "greeting") {
        if (buffer.length < 2) return;
        if (buffer[0] !== 0x05) {
          fail(0x01, "Invalid SOCKS5 greeting");
          return;
        }
        const methodsLength = buffer[1];
        if (buffer.length < 2 + methodsLength) return;
        socket.write(Buffer.from([0x05, 0x00]));
        buffer = buffer.subarray(2 + methodsLength);
        stage = "connect";
      }

      if (stage === "connect") {
        const target = parseSocks5Target(buffer);
        if (!target) return;

        stage = "piping";
        socket.off("data", onData);
        const remainder = buffer.subarray(target.bytesRead);
        socket.write(Buffer.from([0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0]));
        openC2SRelay(tunnel, target.host, target.port, socket, remainder).catch(
          (error) => {
            logToFile("[c2s] dynamic relay failed:", error.message);
            fail(0x05, error.message || "Dynamic relay failed");
          },
        );
      }
    } catch (error) {
      logToFile("[c2s] SOCKS5 parse failed:", error.message);
      fail(0x01, error.message || "SOCKS5 parse failed");
    }
  };

  socket.on("data", onData);
  socket.on("error", () => socket.destroy());
}

function handleC2SLocalConnection(tunnel, socket) {
  const tunnelName = tunnel.name || getC2STunnelName(tunnel);
  const targetHost = tunnel.targetHost || "127.0.0.1";
  const targetPort = Number(tunnel.endpointPort);
  openC2SRelay(tunnel, targetHost, targetPort, socket).catch((error) => {
    logToFile("[c2s] local relay failed:", error.message);
    setC2STunnelError(tunnelName, error.message || "Local relay failed");
    socket.destroy();
  });
}

function pauseSourceForC2SWebSocket(ws, source) {
  if (!source?.pause || !source?.resume) return;
  if (ws.bufferedAmount <= C2S_WS_HIGH_WATERMARK) return;

  source.pause();
  const resumeTimer = setInterval(() => {
    if (
      ws.readyState !== WebSocket.OPEN ||
      source.destroyed ||
      ws.bufferedAmount <= C2S_WS_LOW_WATERMARK
    ) {
      clearInterval(resumeTimer);
      if (ws.readyState === WebSocket.OPEN && !source.destroyed) {
        source.resume();
      }
    }
  }, 25);
}

function sendC2SRemoteMessage(ws, message, source) {
  if (ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify(message), (error) => {
      if (error && source?.destroy) {
        source.destroy(error);
      }
    });
    pauseSourceForC2SWebSocket(ws, source);
  }
}

function writeC2SRemoteChunk(target, chunk, ws, closeTarget) {
  if (!target || target.destroyed) return;

  if (target.writableLength > C2S_STREAM_WRITE_LIMIT) {
    closeTarget();
    return;
  }

  const canContinue = target.write(chunk);
  if (!canContinue && typeof ws.pause === "function") {
    ws.pause();
    target.once("drain", () => {
      if (ws.readyState === WebSocket.OPEN && typeof ws.resume === "function") {
        ws.resume();
      }
    });
  }
}

async function startC2SRemoteTunnel(tunnel, index = 0) {
  const tunnelName = getC2STunnelName(tunnel, index);
  const localHost = tunnel.bindHost || "127.0.0.1";
  const localPort = Number(tunnel.endpointPort);
  const remotePort = Number(tunnel.sourcePort);

  if (!tunnel.sourceHostId) {
    return { success: false, error: "Endpoint SSH host is required" };
  }
  if (!Number.isInteger(remotePort) || remotePort < 1 || remotePort > 65535) {
    return { success: false, error: "Invalid remote port" };
  }
  if (!Number.isInteger(localPort) || localPort < 1 || localPort > 65535) {
    return { success: false, error: "Invalid local port" };
  }

  const localTarget = await checkTcpConnection(localHost, localPort);
  if (!localTarget.success) {
    return {
      success: false,
      error: `Local target ${localHost}:${localPort} is not reachable: ${localTarget.error}`,
    };
  }

  const existing = c2sTunnelRuntimes.get(tunnelName);
  if (existing) {
    return { success: true, tunnelName };
  }

  for (const runtime of c2sTunnelRuntimes.values()) {
    if (
      runtime.mode === "remote" &&
      runtime.sourceHostId === Number(tunnel.sourceHostId) &&
      runtime.sourcePort === remotePort
    ) {
      return {
        success: false,
        error: `Another client remote tunnel already uses ${remotePort} on this endpoint`,
      };
    }
  }

  const relayUrl = getC2SRelayUrl();
  const headers = await getC2SRelayHeaders();
  const ws = new WebSocket(
    relayUrl,
    getWebSocketOptions(relayUrl, { headers }),
  );
  const sockets = new Map();
  let closed = false;

  const cleanup = () => {
    if (closed) return;
    closed = true;
    for (const socket of sockets.values()) {
      socket.destroy();
    }
    sockets.clear();
    try {
      ws.close();
    } catch {
      // expected during shutdown
    }
  };

  c2sTunnelRuntimes.set(tunnelName, {
    ws,
    sockets,
    mode: "remote",
    sourceHostId: Number(tunnel.sourceHostId),
    sourcePort: remotePort,
    bindHost: localHost,
    status: { connected: false, status: "CONNECTING" },
    close: cleanup,
  });
  emitC2STunnelStatuses();

  return new Promise((resolve) => {
    let settled = false;
    const settle = (result) => {
      if (settled) return;
      settled = true;
      resolve(result);
    };

    ws.on("open", () => {
      logToFile(`[c2s] opening remote tunnel ${tunnelName}`, {
        relayUrl,
        remotePort,
        localHost,
        localPort,
      });
      ws.send(
        JSON.stringify({
          type: "open",
          tunnelConfig: { ...tunnel, name: tunnelName, mode: "remote" },
        }),
      );
    });

    ws.on("message", (data, isBinary) => {
      if (isBinary) return;

      let message;
      try {
        message = JSON.parse(data.toString());
      } catch (error) {
        setC2STunnelError(tunnelName, error.message || "Invalid relay message");
        cleanup();
        settle({ success: false, error: error.message });
        return;
      }

      if (message.type === "ready") {
        setC2STunnelStatus(tunnelName, {
          connected: true,
          status: "CONNECTED",
        });
        settle({ success: true, tunnelName });
        return;
      }

      if (message.type === "error") {
        const error = message.error || "Relay rejected the client tunnel";
        setC2STunnelError(tunnelName, error);
        cleanup();
        c2sTunnelRuntimes.delete(tunnelName);
        emitC2STunnelStatuses();
        settle({ success: false, error });
        return;
      }

      if (message.type === "connection" && message.streamId) {
        const socket = net.createConnection(
          { host: localHost, port: localPort },
          () => {
            logToFile(`[c2s] remote stream ${message.streamId} connected`, {
              tunnelName,
              localHost,
              localPort,
            });
          },
        );
        sockets.set(message.streamId, socket);
        socket.on("data", (chunk) => {
          sendC2SRemoteMessage(
            ws,
            {
              type: "data",
              streamId: message.streamId,
              data: chunk.toString("base64"),
            },
            socket,
          );
        });
        socket.on("close", () => {
          sockets.delete(message.streamId);
          sendC2SRemoteMessage(ws, {
            type: "close",
            streamId: message.streamId,
          });
        });
        socket.on("error", (error) => {
          logToFile(`[c2s] remote stream ${message.streamId} failed:`, {
            tunnelName,
            error: error.message,
          });
          sockets.delete(message.streamId);
          sendC2SRemoteMessage(ws, {
            type: "close",
            streamId: message.streamId,
            error: error.message,
          });
        });
        return;
      }

      if (message.type === "data" && message.streamId && message.data) {
        const socket = sockets.get(message.streamId);
        writeC2SRemoteChunk(
          socket,
          Buffer.from(message.data, "base64"),
          ws,
          () => {
            if (socket) {
              sockets.delete(message.streamId);
              socket.destroy();
            }
          },
        );
        return;
      }

      if (message.type === "close" && message.streamId) {
        const socket = sockets.get(message.streamId);
        if (socket) {
          sockets.delete(message.streamId);
          socket.destroy();
        }
      }
    });

    ws.on("close", () => {
      cleanup();
      c2sTunnelRuntimes.delete(tunnelName);
      emitC2STunnelStatuses();
      settle({ success: false, error: "Remote tunnel relay closed" });
    });

    ws.on("error", (error) => {
      setC2STunnelError(tunnelName, error.message || "Relay connection failed");
      cleanup();
      c2sTunnelRuntimes.delete(tunnelName);
      emitC2STunnelStatuses();
      settle({ success: false, error: error.message });
    });
  });
}

async function startC2STunnel(tunnel, index = 0) {
  const mode = tunnel.mode || tunnel.tunnelType || "local";
  const tunnelName = getC2STunnelName(tunnel, index);
  const bindHost = tunnel.bindHost || "127.0.0.1";
  const sourcePort = Number(tunnel.sourcePort);
  logToFile(`[c2s] starting tunnel ${tunnelName}`, {
    mode,
    bindHost,
    sourcePort,
    sourceHostId: tunnel.sourceHostId,
    endpointPort: tunnel.endpointPort,
  });

  if (mode === "remote") {
    return startC2SRemoteTunnel(tunnel, index);
  }
  if (!tunnel.sourceHostId) {
    return { success: false, error: "Endpoint SSH host is required" };
  }
  if (!Number.isInteger(sourcePort) || sourcePort < 1 || sourcePort > 65535) {
    return { success: false, error: "Invalid local port" };
  }

  const existing = c2sTunnelRuntimes.get(tunnelName);
  if (existing) {
    return { success: true, tunnelName };
  }

  for (const runtime of c2sTunnelRuntimes.values()) {
    if (
      runtime.mode !== "remote" &&
      runtime.bindHost === bindHost &&
      runtime.sourcePort === sourcePort
    ) {
      return {
        success: false,
        error: `Another client tunnel already uses ${bindHost}:${sourcePort}`,
      };
    }
  }

  const availability = await checkLocalPortAvailable(bindHost, sourcePort);
  if (!availability.available) {
    return {
      success: false,
      error: availability.error || "Port is already in use",
    };
  }

  const sockets = new Set();
  const server = net.createServer((socket) => {
    sockets.add(socket);
    socket.on("close", () => sockets.delete(socket));
    if (mode === "dynamic") {
      handleC2SDynamicConnection({ ...tunnel, name: tunnelName, mode }, socket);
    } else {
      handleC2SLocalConnection({ ...tunnel, name: tunnelName, mode }, socket);
    }
  });

  c2sTunnelRuntimes.set(tunnelName, {
    server,
    sockets,
    bindHost,
    sourcePort,
    status: { connected: false, status: "CONNECTING" },
  });

  return new Promise((resolve) => {
    server.once("error", (error) => {
      c2sTunnelRuntimes.delete(tunnelName);
      logToFile(`[c2s] failed to listen for ${tunnelName}:`, error.message);
      emitC2STunnelStatuses();
      resolve({ success: false, error: error.message });
    });
    server.listen({ host: bindHost, port: sourcePort }, () => {
      logToFile(
        `[c2s] listening for ${tunnelName} on ${bindHost}:${sourcePort}`,
      );
      setC2STunnelStatus(tunnelName, {
        connected: false,
        status: "CONNECTING",
        reason: "Verifying endpoint SSH connection",
      });

      const verifyTunnel =
        mode === "dynamic"
          ? testC2SRelay(
              { ...tunnel, name: `${tunnelName}::verify`, mode },
              undefined,
              undefined,
            )
          : testC2SRelay(
              { ...tunnel, name: `${tunnelName}::verify`, mode },
              tunnel.targetHost || "127.0.0.1",
              Number(tunnel.endpointPort),
            );

      verifyTunnel.then((result) => {
        if (!c2sTunnelRuntimes.has(tunnelName)) return;
        if (result.success) {
          setC2STunnelStatus(tunnelName, {
            connected: true,
            status: "CONNECTED",
          });
        } else {
          setC2STunnelError(
            tunnelName,
            result.error || "Endpoint SSH connection failed",
          );
        }
      });

      resolve({ success: true, tunnelName });
    });
  });
}

async function stopC2STunnel(tunnelName) {
  const runtime = c2sTunnelRuntimes.get(tunnelName);
  if (!runtime) {
    return { success: true };
  }

  setC2STunnelStatus(tunnelName, {
    connected: false,
    status: "DISCONNECTING",
  });

  return new Promise((resolve) => {
    if (typeof runtime.close === "function") {
      runtime.close();
      c2sTunnelRuntimes.delete(tunnelName);
      emitC2STunnelStatuses();
      resolve({ success: true });
      return;
    }

    for (const socket of runtime.sockets || []) {
      socket.destroy();
    }
    runtime.server?.close(() => {
      c2sTunnelRuntimes.delete(tunnelName);
      emitC2STunnelStatuses();
      resolve({ success: true });
    });
  });
}

function stopAllC2STunnels() {
  for (const [tunnelName, runtime] of c2sTunnelRuntimes.entries()) {
    try {
      if (typeof runtime.close === "function") {
        runtime.close();
      } else {
        for (const socket of runtime.sockets || []) {
          socket.destroy();
        }
        runtime.server?.close();
      }
    } catch (error) {
      logToFile(`[c2s] failed to stop tunnel ${tunnelName}:`, error.message);
    }
    c2sTunnelRuntimes.delete(tunnelName);
  }
  emitC2STunnelStatuses();
}

async function startC2SAutoStartTunnels() {
  const configPath = getC2STunnelConfigPath();
  if (!fs.existsSync(configPath)) {
    return { success: true, started: 0, errors: [] };
  }

  const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
  const tunnels = Array.isArray(config) ? config : [];
  const errors = [];
  let started = 0;

  for (let index = 0; index < tunnels.length; index += 1) {
    const tunnel = tunnels[index];
    if (!tunnel?.autoStart) continue;
    const result = await startC2STunnel(tunnel, index);
    if (result.success) {
      started += 1;
    } else {
      errors.push(result.error || "Failed to start client tunnel");
    }
  }

  return { success: errors.length === 0, started, errors };
}

ipcMain.handle("check-local-port-available", async (_event, host, port) => {
  const sourcePort = Number(port);
  if (
    !host ||
    !Number.isInteger(sourcePort) ||
    sourcePort < 1 ||
    sourcePort > 65535
  ) {
    return { available: false, error: "Invalid local bind address or port" };
  }
  return checkLocalPortAvailable(host, sourcePort);
});

ipcMain.handle("start-c2s-tunnel", async (_event, tunnel, index) => {
  try {
    return await startC2STunnel(tunnel, Number(index) || 0);
  } catch (error) {
    return { success: false, error: error.message };
  }
});

ipcMain.handle("test-c2s-tunnel", async (_event, tunnel, index) => {
  try {
    return await testC2STunnel(tunnel, Number(index) || 0);
  } catch (error) {
    return { success: false, error: error.message };
  }
});

ipcMain.handle("stop-c2s-tunnel", async (_event, tunnelName) => {
  try {
    return await stopC2STunnel(tunnelName);
  } catch (error) {
    return { success: false, error: error.message };
  }
});

ipcMain.handle("get-c2s-tunnel-statuses", () => {
  return getAllC2STunnelStatuses();
});

ipcMain.handle("start-c2s-autostart-tunnels", async () => {
  try {
    return await startC2SAutoStartTunnels();
  } catch (error) {
    return { success: false, started: 0, errors: [error.message] };
  }
});

ipcMain.handle("get-c2s-tunnel-preset-default-name", () => {
  const now = new Date();
  const date = now.toISOString().slice(0, 10);
  const platform =
    process.platform === "darwin"
      ? "macOS"
      : process.platform === "win32"
        ? "Windows"
        : "Linux";
  const release = os.release();
  const computerName = os.hostname();
  return `[${date}] ${computerName} (${platform} ${release})`;
});

ipcMain.handle("get-setting", (event, key) => {
  try {
    const userDataPath = app.getPath("userData");
    const settingsPath = path.join(userDataPath, "settings.json");

    if (!fs.existsSync(settingsPath)) {
      return null;
    }

    const settingsData = fs.readFileSync(settingsPath, "utf8");
    const settings = JSON.parse(settingsData);
    return settings[key] !== undefined ? settings[key] : null;
  } catch (error) {
    console.error("Error reading setting:", error);
    return null;
  }
});

ipcMain.handle("set-setting", (event, key, value) => {
  try {
    const userDataPath = app.getPath("userData");
    const settingsPath = path.join(userDataPath, "settings.json");

    if (!fs.existsSync(userDataPath)) {
      fs.mkdirSync(userDataPath, { recursive: true });
    }

    let settings = {};
    if (fs.existsSync(settingsPath)) {
      const settingsData = fs.readFileSync(settingsPath, "utf8");
      settings = JSON.parse(settingsData);
    }

    settings[key] = value;
    fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
    return { success: true };
  } catch (error) {
    console.error("Error saving setting:", error);
    return { success: false, error: error.message };
  }
});

ipcMain.handle("get-session-cookie", async (_event, name, targetUrl) => {
  try {
    const ses = mainWindow?.webContents?.session;
    if (!ses) return null;
    const cookies = await ses.cookies.get({
      name,
      ...(targetUrl ? { url: targetUrl } : {}),
    });
    const cookie = cookies.find((candidate) =>
      cookieMatchesUrl(candidate, targetUrl),
    );
    return (
      cookie?.value ||
      getRememberedElectronAuthCookie(name, targetUrl)?.value ||
      null
    );
  } catch (error) {
    console.error("Failed to get session cookie:", error);
    return getRememberedElectronAuthCookie(name, targetUrl)?.value || null;
  }
});

function cookieMatchesUrl(cookie, targetUrl) {
  if (!targetUrl) return true;

  try {
    const targetHost = new URL(targetUrl).hostname;
    const cookieDomain = (cookie.domain || "").replace(/^\./, "");

    return (
      cookieDomain === targetHost ||
      targetHost.endsWith(`.${cookieDomain}`) ||
      (!cookieDomain && targetHost === "localhost")
    );
  } catch {
    return true;
  }
}

ipcMain.handle(
  "wait-session-cookie",
  async (_event, name, targetUrl, previousValue, timeoutMs = 5000) => {
    const ses = mainWindow?.webContents?.session;
    if (!ses) return { success: false, error: "No Electron session" };

    const existingCookies = await ses.cookies.get({
      name,
      ...(targetUrl ? { url: targetUrl } : {}),
    });
    const existingCookie = existingCookies.find((cookie) =>
      cookieMatchesUrl(cookie, targetUrl),
    );
    if (existingCookie?.value && existingCookie.value !== previousValue) {
      return { success: true, value: existingCookie.value };
    }

    const rememberedCookie = getRememberedElectronAuthCookie(name, targetUrl);
    if (rememberedCookie?.value && rememberedCookie.value !== previousValue) {
      return { success: true, value: rememberedCookie.value };
    }

    return new Promise((resolve) => {
      const timeout = setTimeout(() => {
        ses.cookies.off("changed", onCookieChanged);
        resolve({ success: false, error: "Timed out waiting for cookie" });
      }, timeoutMs);

      function onCookieChanged(_event, cookie, _cause, removed) {
        if (
          removed ||
          cookie.name !== name ||
          !cookie.value ||
          cookie.value === previousValue ||
          !cookieMatchesUrl(cookie, targetUrl)
        ) {
          return;
        }

        clearTimeout(timeout);
        ses.cookies.off("changed", onCookieChanged);
        resolve({ success: true, value: cookie.value });
      }

      ses.cookies.on("changed", onCookieChanged);
    });
  },
);

ipcMain.handle("clear-session-cookies", async () => {
  try {
    clearPersistedElectronAuthCookies();
    const ses = mainWindow?.webContents?.session;
    if (ses) {
      const cookies = await ses.cookies.get({});
      for (const cookie of cookies) {
        await ses.cookies.remove(getCookieRemovalUrl(cookie), cookie.name);
      }
    }
  } catch (error) {
    console.error("Failed to clear session cookies:", error);
  }
});

ipcMain.handle("clipboard-write-text", (_event, text) => {
  clipboard.writeText(typeof text === "string" ? text : String(text ?? ""));
  return true;
});

ipcMain.handle("clipboard-read-text", () => clipboard.readText());

ipcMain.handle("local-terminal-start", (event, dimensions = {}) => {
  const cols = Math.min(500, Math.max(2, Number(dimensions.cols) || 80));
  const rows = Math.min(300, Math.max(1, Number(dimensions.rows) || 24));
  const sessionId = crypto.randomUUID();
  const shellConfig = resolveLocalShell(process.platform, dimensions.shell);
  const child = pty.spawn(shellConfig.file, shellConfig.args, {
    name: "xterm-256color",
    cols,
    rows,
    cwd: os.homedir(),
    env: {
      ...process.env,
      TERM: "xterm-256color",
      COLORTERM: "truecolor",
    },
  });
  const ownerId = event.sender.id;
  const session = { ownerId, process: child, ready: false, buffered: "" };
  localTerminalSessions.set(sessionId, session);
  child.onData((data) => {
    if (!session.ready) {
      session.buffered = (session.buffered + data).slice(-1024 * 1024);
      return;
    }
    if (!event.sender.isDestroyed()) {
      event.sender.send(`local-terminal:data:${sessionId}`, data);
    }
  });
  child.onExit(({ exitCode }) => {
    localTerminalSessions.delete(sessionId);
    if (!event.sender.isDestroyed()) {
      event.sender.send(`local-terminal:exit:${sessionId}`, exitCode);
    }
  });
  event.sender.once("destroyed", () => closeLocalTerminalsFor(ownerId));
  return { sessionId, shell: shellConfig.file };
});

ipcMain.handle("local-terminal-ready", (event, sessionId) => {
  const session = ownedLocalTerminal(event, sessionId);
  if (!session) return false;
  session.ready = true;
  if (session.buffered && !event.sender.isDestroyed()) {
    event.sender.send(`local-terminal:data:${sessionId}`, session.buffered);
    session.buffered = "";
  }
  return true;
});

ipcMain.handle("local-terminal-write", (event, sessionId, data) => {
  const session = ownedLocalTerminal(event, sessionId);
  if (!session || typeof data !== "string" || data.length > 64 * 1024) {
    return false;
  }
  session.process.write(data);
  return true;
});

ipcMain.handle("local-terminal-resize", (event, sessionId, cols, rows) => {
  const session = ownedLocalTerminal(event, sessionId);
  const width = Number(cols);
  const height = Number(rows);
  if (
    !session ||
    !Number.isInteger(width) ||
    !Number.isInteger(height) ||
    width < 2 ||
    width > 500 ||
    height < 1 ||
    height > 300
  ) {
    return false;
  }
  session.process.resize(width, height);
  return true;
});

ipcMain.handle("local-terminal-close", (event, sessionId) => {
  const session = ownedLocalTerminal(event, sessionId);
  if (!session) return false;
  localTerminalSessions.delete(sessionId);
  session.process.kill();
  return true;
});

ipcMain.handle("show-save-dialog", async (_event, options) => {
  return dialog.showSaveDialog(mainWindow, options || {});
});

ipcMain.handle("show-open-dialog", async (_event, options) => {
  return dialog.showOpenDialog(mainWindow, options || {});
});

ipcMain.handle("create-temp-file", async (_event, fileData) => {
  try {
    const result = createManagedTempFile(
      fileData?.fileName,
      fileData?.content,
      fileData?.encoding,
    );
    return { success: true, ...result };
  } catch (error) {
    return { success: false, error: error.message };
  }
});

ipcMain.handle("create-temp-folder", async (_event, folderData) => {
  try {
    const tempId = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
    const tempDir = fs.mkdtempSync(path.join(getTempRoot(), `${tempId}-`));
    const folderPath = path.join(
      tempDir,
      sanitizeFileName(folderData?.folderName || "files"),
    );
    fs.mkdirSync(folderPath, { recursive: true });

    for (const file of folderData?.files || []) {
      const relativePath = String(file.relativePath || "")
        .split(/[\\/]+/)
        .map(sanitizeFileName)
        .filter(Boolean)
        .join(path.sep);
      if (!relativePath) continue;
      const targetPath = path.join(folderPath, relativePath);
      fs.mkdirSync(path.dirname(targetPath), { recursive: true });
      fs.writeFileSync(
        targetPath,
        decodeFileContent(file.content, file.encoding),
      );
    }

    tempFiles.set(tempId, { path: folderPath, dir: tempDir });
    return { success: true, tempId, path: folderPath };
  } catch (error) {
    return { success: false, error: error.message };
  }
});

ipcMain.handle("start-drag-to-desktop", (event, dragData) => {
  try {
    const temp = tempFiles.get(dragData?.tempId);
    if (!temp) return { success: false, error: "Temporary file not found" };

    event.sender.startDrag({
      file: temp.path,
      icon: nativeImage.createEmpty(),
    });
    return { success: true };
  } catch (error) {
    return { success: false, error: error.message };
  }
});

ipcMain.handle("cleanup-temp-file", (_event, tempId) => {
  try {
    cleanupManagedTempFile(tempId);
    return { success: true };
  } catch (error) {
    return { success: false, error: error.message };
  }
});

ipcMain.handle("open-external-editor", async (_event, fileData) => {
  try {
    const result = createManagedTempFile(
      fileData?.fileName,
      fileData?.content,
      fileData?.encoding,
    );
    const editId = result.tempId;
    const stat = fs.statSync(result.path);
    const watcher = fs.watch(result.path, { persistent: false }, () => {
      const session = externalEditorSessions.get(editId);
      if (!session) return;
      if (session.timer) clearTimeout(session.timer);
      session.timer = setTimeout(() => notifyExternalEditorSaved(editId), 500);
    });

    externalEditorSessions.set(editId, {
      path: result.path,
      watcher,
      timer: null,
      lastMtimeMs: stat.mtimeMs,
    });

    const editorPath =
      typeof fileData?.editorPath === "string" && fileData.editorPath.trim()
        ? fileData.editorPath.trim()
        : null;
    const openError = await openPathWithEditor(result.path, editorPath);
    if (openError) {
      closeExternalEditorSession(editId);
      return { success: false, error: openError };
    }

    return { success: true, editId, path: result.path };
  } catch (error) {
    return { success: false, error: error.message };
  }
});

ipcMain.handle("close-external-editor", (_event, editId) => {
  try {
    closeExternalEditorSession(editId);
    return { success: true };
  } catch (error) {
    return { success: false, error: error.message };
  }
});

async function testServerConnection(
  _event,
  serverUrl,
  allowInvalidCertificate = false,
) {
  try {
    const normalizedServerUrl = serverUrl.replace(/\/$/, "");
    const healthUrl = `${normalizedServerUrl}/health`;

    // This is a best-effort reachability probe, not a hard gate: a reverse
    // proxy doing SSO in front of the real server (Pangolin, Authelia,
    // Cloudflare Access, etc.) intercepts this unauthenticated request
    // before it ever reaches Termix's own /health route, and returns its
    // own login page (HTML, or a redirect) instead of {"status":"ok"}.
    // That's a legitimate, working setup -- the login iframe shown right
    // after this check is what actually proves the server is real, by
    // completing an authenticated round-trip. So any response at all here
    // (any status code, any body) means "something is there, let the user
    // proceed"; only a network-level failure (nothing answered at all)
    // blocks continuing.
    try {
      const response = await httpFetch(healthUrl, {
        method: "GET",
        timeout: 10000,
        allowInvalidCertificate,
      });

      const data = await response.text();
      const looksLikeHtml =
        data.includes("<html") ||
        data.includes("<!DOCTYPE") ||
        data.includes("<head>") ||
        data.includes("<body>");

      if (response.ok && !looksLikeHtml) {
        try {
          const healthData = JSON.parse(data);
          if (
            healthData &&
            (healthData.status === "ok" ||
              healthData.status === "healthy" ||
              healthData.healthy === true ||
              healthData.database === "connected")
          ) {
            return {
              success: true,
              status: response.status,
              testedUrl: healthUrl,
            };
          }
        } catch (parseError) {
          console.log("Health endpoint did not return valid JSON");
        }
      }

      // Reachable, but not a recognized Termix health response -- likely a
      // proxy/SSO login page in front of the real server. Let the user
      // proceed; the login step next will fail clearly if this really
      // isn't a Termix server.
      return {
        success: true,
        status: response.status,
        testedUrl: healthUrl,
        warning: looksLikeHtml
          ? "Could not confirm this is a Termix server (the response looked like an HTML page, which can happen behind a login-protected reverse proxy). You can continue, and the next step will fail clearly if this isn't actually a Termix server."
          : "Server responded, but not with the expected health check format. Continuing anyway.",
      };
    } catch (urlError) {
      console.error("Health check failed:", urlError);
      return {
        success: false,
        error:
          "Server is not responding. Please ensure the server is running and accessible.",
      };
    }
  } catch (error) {
    return { success: false, error: error.message };
  }
}

ipcMain.handle("test-server-connection", testServerConnection);

function createMenu() {
  if (process.platform === "darwin") {
    const template = [
      {
        label: app.name,
        submenu: [
          { role: "about" },
          { type: "separator" },
          { role: "services" },
          { type: "separator" },
          { role: "hide" },
          { role: "hideOthers" },
          { role: "unhide" },
          { type: "separator" },
          { role: "quit" },
        ],
      },
      {
        label: "Edit",
        submenu: [
          { role: "undo" },
          { role: "redo" },
          { type: "separator" },
          { role: "cut" },
          { role: "copy" },
          { role: "paste" },
          { role: "selectAll" },
        ],
      },
      {
        label: "View",
        submenu: [
          { role: "reload" },
          { role: "forceReload" },
          { role: "toggleDevTools" },
          { type: "separator" },
          { role: "resetZoom" },
          { role: "zoomIn" },
          { role: "zoomOut" },
          { type: "separator" },
          { role: "togglefullscreen" },
        ],
      },
      {
        label: "Window",
        submenu: [
          { role: "minimize" },
          { role: "zoom" },
          { type: "separator" },
          { role: "front" },
          { type: "separator" },
          { role: "window" },
        ],
      },
    ];
    const menu = Menu.buildFromTemplate(template);
    Menu.setApplicationMenu(menu);
  }
}

app.whenReady().then(async () => {
  logToFile("=== App ready ===");
  logToFile(
    "isDev:",
    isDev,
    "platform:",
    process.platform,
    "arch:",
    process.arch,
  );
  if (process.platform === "win32") {
    app.setAppUserModelId(windowsAppUserModelId);
  }
  createMenu();
  await clearElectronClientCacheIfBuildChanged();
  await clearElectronJwtCookiesAtStartup();

  if (!isDev) {
    const result = await startBackendServer();
    logToFile("startBackendServer result:", result);
  } else {
    logToFile(
      "Skipping embedded backend (isDev=true) - expecting separate dev:backend process",
    );
  }

  createTray();
  createWindow();
  remoteSync.initRemoteSync(() => mainWindow);
  logToFile("=== Startup complete ===");
});

app.on("window-all-closed", () => {
  if (!tray || tray.isDestroyed()) {
    app.quit();
  }
});

app.on("activate", () => {
  if (BrowserWindow.getAllWindows().length === 0) {
    createWindow();
  } else if (mainWindow) {
    mainWindow.show();
    mainWindow.focus();
  }
});

app.on("before-quit", () => {
  isQuitting = true;
});

app.on("will-quit", () => {
  console.log("App will quit...");
  for (const editId of externalEditorSessions.keys()) {
    closeExternalEditorSession(editId);
  }
  for (const tempId of tempFiles.keys()) {
    cleanupManagedTempFile(tempId);
  }
  stopAllC2STunnels();
  stopBackendServer();
});

process.on("uncaughtException", (error) => {
  console.error("Uncaught Exception:", error);
});

process.on("unhandledRejection", (reason, promise) => {
  console.error("Unhandled Rejection at:", promise, "reason:", reason);
});
