import { getErrorMessage } from "../../utils/error-message.js";
import type { AuthenticatedRequest } from "../../../types/index.js";
import express, { type Request, type Response } from "express";
import axios from "axios";
import multer from "multer";
import { sshLogger, databaseLogger } from "../../utils/logger.js";
import { AuthManager } from "../../utils/auth-manager.js";
import { PermissionManager } from "../../utils/permission-manager.js";
import { DataCrypto } from "../../utils/data-crypto.js";
import { parseSSHKey } from "../../utils/ssh-key-utils.js";
import {
  pickResolvedPassword,
  pickResolvedUsername,
} from "../../hosts/credential-username.js";
import { notifyAutomationInternalEvent } from "../../hosts/metrics/automation-bridge.js";
import {
  createCurrentCommandHistoryRepository,
  createCurrentCredentialRepository,
  createCurrentFileManagerBookmarkRepository,
  createCurrentOpksshTokenRepository,
  createCurrentRecentActivityRepository,
  createCurrentSshCredentialUsageRepository,
  createCurrentSessionRecordingRepository,
  createCurrentTransferRecentRepository,
  createCurrentRbacAccessRepository,
  createCurrentRoleRepository,
  createCurrentHostResolutionRepository,
  createCurrentHostRepository,
  createCurrentUserRepository,
  createCurrentSyncTombstoneRepository,
} from "../repositories/factory.js";
import {
  containsOwnerPrivateAuthUpdate,
  isNonEmptyString,
  isOptionalBoolean,
  isValidPort,
  OWNER_PRIVATE_AUTH_FIELDS,
  OWNER_PRIVATE_TERMINAL_CONFIG_FIELDS,
  sanitizeHostForRecipient,
  stripSensitiveFields,
  transformHostResponse,
} from "./host-normalizers.js";
import { validateParentHostId } from "./host-parent-validation.js";
import { registerHostOpksshRoutes } from "./host-opkssh-routes.js";
import { registerHostFolderRoutes } from "./host-folder-routes.js";
import { registerHostFileManagerBookmarkRoutes } from "./host-file-manager-bookmark-routes.js";
import { registerHostCommandHistoryRoutes } from "./host-command-history-routes.js";
import { registerHostAutostartRoutes } from "./host-autostart-routes.js";
import { registerHostInternalRoutes } from "./host-internal-routes.js";
import { registerHostNetworkRoutes } from "./host-network-routes.js";
import { registerHostBulkRoutes } from "./host-bulk-routes.js";
import {
  applyHostEnrollmentDefaults,
  requireHostEnrollmentAccessForPath,
} from "./host-enrollment-auth.js";
import {
  logAudit,
  getAuditUsername,
  getRequestMeta,
} from "../../utils/audit-logger.js";
import type {
  HostResolutionCredentialRecord,
  HostResolutionHostRecord,
} from "../repositories/host-resolution-repository.js";
import {
  requiresPersonalHostAuthentication,
  resolveRecipientSharedHostAuthentication,
} from "../../utils/shared-host-auth-resolver.js";

const router = express.Router();

const upload = multer({ storage: multer.memoryStorage() });

const STATS_SERVER_URL = "http://localhost:30005";

function notifyStatsHostUpdated(
  hostId: number,
  headers: Pick<Request["headers"], "authorization" | "cookie">,
  operation: string,
): void {
  axios
    .post(
      `${STATS_SERVER_URL}/host-updated`,
      { hostId },
      {
        headers: {
          Authorization: headers.authorization || "",
          Cookie: headers.cookie || "",
        },
        timeout: 5000,
      },
    )
    .catch((err) => {
      sshLogger.warn("Failed to notify stats server of host update", {
        operation,
        hostId,
        error: err instanceof Error ? err.message : String(err),
      });
    });
}

const authManager = AuthManager.getInstance();
const permissionManager = PermissionManager.getInstance();
const authenticateJWT = authManager.createAuthMiddleware();
const requireDataAccess = authManager.createDataAccessMiddleware();

registerHostInternalRoutes(router);

/**
 * @openapi
 * /host/db/host:
 *   post:
 *     summary: Create SSH host
 *     description: Creates a new SSH host configuration.
 *     tags:
 *       - SSH
 *     responses:
 *       200:
 *         description: Host created successfully.
 *       400:
 *         description: Invalid SSH data.
 *       500:
 *         description: Failed to save SSH data.
 */
router.post(
  ["/db/host", "/enroll"],
  authenticateJWT,
  requireDataAccess,
  requireHostEnrollmentAccessForPath,
  upload.single("key"),
  async (req: Request, res: Response) => {
    const userId = (req as AuthenticatedRequest).userId;
    let hostData: Record<string, unknown>;

    if (req.headers["content-type"]?.includes("multipart/form-data")) {
      if (req.body.data) {
        try {
          hostData = JSON.parse(req.body.data);
        } catch (err) {
          sshLogger.warn("Invalid JSON data in multipart request", {
            operation: "host_create",
            userId,
            error: err,
          });
          return res.status(400).json({ error: "Invalid JSON data" });
        }
      } else {
        sshLogger.warn("Missing data field in multipart request", {
          operation: "host_create",
          userId,
        });
        return res.status(400).json({ error: "Missing data field" });
      }

      if (req.file) {
        hostData.key = req.file.buffer.toString("utf8");
      }
    } else {
      hostData = req.body;
    }

    if (req.path === "/enroll") {
      hostData = applyHostEnrollmentDefaults(hostData);
    }

    const {
      connectionType,
      name,
      folder,
      parentHostId,
      tags,
      ip,
      port,
      username,
      password,
      authMethod,
      authType,
      useWarpgate,
      shareSshAuth,
      credentialId,
      vaultProfileId,
      key,
      keyPassword,
      keyType,
      sudoPassword,
      pin,
      enableTerminal,
      enableCommandHistory,
      enableTunnel,
      enableFileManager,
      scpLegacy,
      enableDocker,
      enableProxmox,
      enableTmuxMonitor,
      enableTerminalToolbar,
      allowSessionSharing,
      showTerminalInSidebar,
      showFileManagerInSidebar,
      showTunnelInSidebar,
      showDockerInSidebar,
      showServerStatsInSidebar,
      defaultPath,
      tunnelConnections,
      jumpHosts,
      quickActions,
      statsConfig,
      dockerConfig,
      proxmoxConfig,
      enableProxmoxStats,
      proxmoxStatsConfig,
      terminalConfig,
      forceKeyboardInteractive,
      domain,
      security,
      ignoreCert,
      guacamoleConfig,
      notes,
      useSocks5,
      socks5Host,
      socks5Port,
      socks5Username,
      socks5Password,
      socks5ProxyChain,
      connectionOrigin,
      portKnockSequence,
      overrideCredentialUsername,
      macAddress,
      wolBroadcastAddress,
      enableSsh,
      enableRdp,
      enableVnc,
      enableTelnet,
      sshPort,
      rdpPort,
      vncPort,
      telnetPort,
      rdpAuthType,
      rdpCredentialId,
      rdpUser,
      rdpPassword,
      rdpDomain,
      rdpSecurity,
      rdpIgnoreCert,
      vncAuthType,
      vncCredentialId,
      vncPassword,
      vncUser,
      telnetAuthType,
      telnetCredentialId,
      telnetUser,
      telnetPassword,
    } = hostData;
    databaseLogger.info("Creating SSH host", {
      operation: "host_create",
      userId,
      name,
      ip,
    });

    if (
      !isNonEmptyString(userId) ||
      !isNonEmptyString(ip) ||
      !isValidPort(port) ||
      !isOptionalBoolean(shareSshAuth)
    ) {
      sshLogger.warn("Invalid SSH data input validation failed", {
        operation: "host_create",
        userId,
        hasIp: !!ip,
        port,
        isValidPort: isValidPort(port),
      });
      return res.status(400).json({ error: "Invalid SSH data" });
    }

    let validatedParentHostId: number | null = null;
    if (parentHostId !== undefined && parentHostId !== null) {
      const numericParentHostId = Number(parentHostId);
      if (!Number.isInteger(numericParentHostId)) {
        return res.status(400).json({ error: "Invalid parent host" });
      }
      const parentError = await validateParentHostId(
        userId,
        null,
        numericParentHostId,
      );
      if (parentError) {
        return res.status(400).json({ error: parentError });
      }
      validatedParentHostId = numericParentHostId;
    }

    const effectiveConnectionType = connectionType || "ssh";
    const effectiveAuthType =
      authType ||
      authMethod ||
      (effectiveConnectionType !== "ssh" ? "password" : undefined);
    const effectiveUsername =
      username || rdpUser || vncUser || telnetUser || "";
    const effectiveName =
      name || (effectiveUsername ? `${effectiveUsername}@${ip}` : String(ip));
    const sshDataObj: Record<string, unknown> = {
      userId: userId,
      connectionType: effectiveConnectionType,
      name: effectiveName,
      // A host is either placed in a folder or nested under a parent host,
      // never both -- setting one clears the other.
      folder: validatedParentHostId ? null : folder || null,
      parentHostId: validatedParentHostId,
      tags: Array.isArray(tags) ? tags.join(",") : tags || "",
      ip,
      port,
      username: effectiveUsername,
      authType: effectiveAuthType,
      useWarpgate: useWarpgate ? 1 : 0,
      shareSshAuth: shareSshAuth === true ? 1 : 0,
      credentialId: credentialId || null,
      vaultProfileId:
        effectiveAuthType === "vault" ? vaultProfileId || null : null,
      overrideCredentialUsername: overrideCredentialUsername ? 1 : 0,
      pin: pin ? 1 : 0,
      enableTerminal: enableTerminal ? 1 : 0,
      enableCommandHistory: enableCommandHistory ? 1 : 0,
      enableTunnel: enableTunnel ? 1 : 0,
      tunnelConnections: Array.isArray(tunnelConnections)
        ? JSON.stringify(tunnelConnections)
        : null,
      jumpHosts: Array.isArray(jumpHosts) ? JSON.stringify(jumpHosts) : null,
      quickActions: Array.isArray(quickActions)
        ? JSON.stringify(quickActions)
        : null,
      enableFileManager: enableFileManager ? 1 : 0,
      scpLegacy: scpLegacy ? 1 : 0,
      enableDocker: enableDocker ? 1 : 0,
      enableProxmox: enableProxmox ? 1 : 0,
      enableTmuxMonitor: enableTmuxMonitor ? 1 : 0,
      enableTerminalToolbar: enableTerminalToolbar === false ? 0 : 1,
      allowSessionSharing: allowSessionSharing === false ? 0 : 1,
      showTerminalInSidebar: showTerminalInSidebar ? 1 : 0,
      showFileManagerInSidebar: showFileManagerInSidebar ? 1 : 0,
      showTunnelInSidebar: showTunnelInSidebar ? 1 : 0,
      showDockerInSidebar: showDockerInSidebar ? 1 : 0,
      showServerStatsInSidebar: showServerStatsInSidebar ? 1 : 0,
      defaultPath: defaultPath || null,
      statsConfig: statsConfig
        ? typeof statsConfig === "string"
          ? statsConfig
          : JSON.stringify(statsConfig)
        : null,
      dockerConfig: dockerConfig
        ? typeof dockerConfig === "string"
          ? dockerConfig
          : JSON.stringify(dockerConfig)
        : null,
      proxmoxConfig: proxmoxConfig
        ? typeof proxmoxConfig === "string"
          ? proxmoxConfig
          : JSON.stringify(proxmoxConfig)
        : null,
      enableProxmoxStats: enableProxmoxStats ? 1 : 0,
      proxmoxStatsConfig: proxmoxStatsConfig
        ? typeof proxmoxStatsConfig === "string"
          ? proxmoxStatsConfig
          : JSON.stringify(proxmoxStatsConfig)
        : null,
      terminalConfig: terminalConfig
        ? typeof terminalConfig === "string"
          ? terminalConfig
          : JSON.stringify(terminalConfig)
        : null,
      forceKeyboardInteractive: forceKeyboardInteractive ? "true" : "false",
      domain: domain || null,
      security: security || null,
      ignoreCert: ignoreCert ? 1 : 0,
      guacamoleConfig: guacamoleConfig ? JSON.stringify(guacamoleConfig) : null,
      notes: notes || null,
      sudoPassword: sudoPassword || null,
      useSocks5: useSocks5 ? 1 : 0,
      socks5Host: socks5Host || null,
      socks5Port: socks5Port || null,
      socks5Username: socks5Username || null,
      socks5Password: socks5Password || null,
      socks5ProxyChain: socks5ProxyChain
        ? JSON.stringify(socks5ProxyChain)
        : null,
      connectionOrigin:
        connectionOrigin === "local" || connectionOrigin === "remote"
          ? connectionOrigin
          : null,
      macAddress: macAddress || null,
      wolBroadcastAddress: wolBroadcastAddress || null,
      portKnockSequence: portKnockSequence
        ? JSON.stringify(portKnockSequence)
        : null,
      enableSsh: enableSsh ? 1 : 0,
      enableRdp: enableRdp ? 1 : 0,
      enableVnc: enableVnc ? 1 : 0,
      enableTelnet: enableTelnet ? 1 : 0,
      sshPort: sshPort || port || 22,
      rdpPort: rdpPort || 3389,
      vncPort: vncPort || 5900,
      telnetPort: telnetPort || 23,
      rdpAuthType: enableRdp ? rdpAuthType || null : null,
      rdpCredentialId:
        enableRdp && rdpAuthType === "credential" && rdpCredentialId
          ? rdpCredentialId
          : null,
      rdpUser: rdpUser || null,
      rdpDomain: rdpDomain || null,
      rdpSecurity: rdpSecurity || null,
      rdpIgnoreCert: rdpIgnoreCert ? 1 : 0,
      vncAuthType: enableVnc ? vncAuthType || null : null,
      vncCredentialId:
        enableVnc && vncAuthType === "credential" && vncCredentialId
          ? vncCredentialId
          : null,
      vncUser: vncUser || null,
      telnetAuthType: enableTelnet ? telnetAuthType || null : null,
      telnetCredentialId:
        enableTelnet && telnetAuthType === "credential" && telnetCredentialId
          ? telnetCredentialId
          : null,
      telnetUser: telnetUser || null,
    };

    // For non-SSH hosts (RDP, VNC, Telnet), always save password if provided
    if (effectiveConnectionType !== "ssh") {
      sshDataObj.password = password || null;
      sshDataObj.key = null;
      sshDataObj.keyPassword = null;
      sshDataObj.keyType = null;
    } else if (effectiveAuthType === "password") {
      sshDataObj.password = password || null;
      sshDataObj.key = null;
      sshDataObj.keyPassword = null;
      sshDataObj.keyType = null;
    } else if (effectiveAuthType === "key") {
      if (key && typeof key === "string") {
        const keyValidation = parseSSHKey(
          key,
          typeof keyPassword === "string" ? keyPassword : undefined,
        );
        if (!keyValidation.success) {
          sshLogger.warn("SSH key validation failed", {
            operation: "host_create",
            userId,
            name,
            ip,
            port,
            error: keyValidation.error,
          });
          return res.status(400).json({
            error: `Invalid SSH key: ${keyValidation.error || "Unable to parse key"}`,
          });
        }
      }

      sshDataObj.key = key || null;
      sshDataObj.keyPassword = keyPassword || null;
      sshDataObj.keyType = keyType;
      sshDataObj.password = password || null;
    } else if (effectiveAuthType === "credential") {
      sshDataObj.password = password || null;
      sshDataObj.key = null;
      sshDataObj.keyPassword = null;
      sshDataObj.keyType = null;
    } else if (effectiveAuthType === "agent") {
      sshDataObj.password = null;
      sshDataObj.key = null;
      sshDataObj.keyPassword = null;
      sshDataObj.keyType = null;
    } else {
      sshDataObj.password = null;
      sshDataObj.key = null;
      sshDataObj.keyPassword = null;
      sshDataObj.keyType = null;
    }

    sshDataObj.rdpPassword = rdpPassword || null;
    sshDataObj.vncPassword = vncPassword || null;
    sshDataObj.telnetPassword = telnetPassword || null;

    try {
      const result = await createCurrentHostRepository().createEncryptedForUser(
        userId,
        sshDataObj,
      );

      if (!result) {
        sshLogger.warn("No host returned after creation", {
          operation: "host_create",
          userId,
          name,
          ip,
          port,
        });
        return res.status(500).json({ error: "Failed to create host" });
      }

      const createdHost = result;
      const baseHost = transformHostResponse(createdHost);

      const resolvedHost =
        (await resolveHostCredentials(baseHost, userId)) || baseHost;
      databaseLogger.success("SSH host created", {
        operation: "host_create_success",
        userId,
        hostId: createdHost.id as number,
        name,
      });

      const { ipAddress: chIp, userAgent: chUa } = getRequestMeta(req);
      await logAudit({
        userId,
        username: await getAuditUsername(userId),
        action: "create_host",
        resourceType: "host",
        resourceId: String(createdHost.id),
        resourceName: String(name ?? ip),
        ipAddress: chIp,
        userAgent: chUa,
        success: true,
      });

      notifyAutomationInternalEvent(
        "host_added",
        userId,
        createdHost.id as number,
        { name: String(name ?? ip) },
      );

      res.json(stripSensitiveFields(resolvedHost));
      notifyStatsHostUpdated(
        createdHost.id as number,
        req.headers,
        "host_create",
      );
    } catch (err) {
      sshLogger.error("Failed to save SSH host to database", err, {
        operation: "host_create",
        userId,
        name,
        ip,
        port,
        authType: effectiveAuthType,
      });
      res.status(500).json({ error: "Failed to save SSH data" });
    }
  },
);

/**
 * @openapi
 * /host/enroll:
 *   post:
 *     summary: Enroll a host with an API key
 *     description: Creates a host owned by the user assigned to the API key. The user's encrypted data must be unlocked by an active sign-in.
 *     tags:
 *       - Host Enrollment
 *     security:
 *       - bearerAuth: []
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             type: object
 *             required: [ip]
 *             properties:
 *               name:
 *                 type: string
 *               ip:
 *                 type: string
 *               port:
 *                 type: integer
 *                 minimum: 1
 *                 maximum: 65535
 *                 default: 22
 *               username:
 *                 type: string
 *               authType:
 *                 type: string
 *                 enum: [none, password, key, credential, agent]
 *                 default: none
 *               password:
 *                 type: string
 *               folder:
 *                 type: string
 *               tags:
 *                 oneOf:
 *                   - type: string
 *                   - type: array
 *                     items:
 *                       type: string
 *               enableTerminal:
 *                 type: boolean
 *               enableFileManager:
 *                 type: boolean
 *               enableTunnel:
 *                 type: boolean
 *     responses:
 *       200:
 *         description: Host enrolled successfully.
 *       400:
 *         description: Invalid host data.
 *       401:
 *         description: Missing or invalid API key.
 *       423:
 *         description: The API key user's encrypted data is locked.
 *       500:
 *         description: Failed to enroll the host.
 */
/**
 * @openapi
 * /host/quick-connect:
 *   post:
 *     summary: Create a temporary SSH connection without saving to database
 *     description: Returns a temporary host configuration for immediate use
 *     tags:
 *       - SSH
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             type: object
 *             required:
 *               - ip
 *               - port
 *               - username
 *               - authType
 *             properties:
 *               ip:
 *                 type: string
 *                 description: SSH server IP or hostname
 *               port:
 *                 type: number
 *                 description: SSH server port
 *               username:
 *                 type: string
 *                 description: SSH username
 *               authType:
 *                 type: string
 *                 enum: [password, key, credential]
 *                 description: Authentication method
 *               password:
 *                 type: string
 *                 description: Password (required if authType is password)
 *               key:
 *                 type: string
 *                 description: SSH private key (required if authType is key)
 *               keyPassword:
 *                 type: string
 *                 description: SSH key password (optional)
 *               keyType:
 *                 type: string
 *                 description: SSH key type
 *               credentialId:
 *                 type: number
 *                 description: Credential ID (required if authType is credential)
 *               overrideCredentialUsername:
 *                 type: boolean
 *                 description: Use provided username instead of credential username
 *     responses:
 *       200:
 *         description: Temporary host configuration created successfully
 *       400:
 *         description: Invalid request data
 *       401:
 *         description: Unauthorized
 *       403:
 *         description: Forbidden
 *       404:
 *         description: Credential not found
 *       500:
 *         description: Server error
 */
router.post(
  "/quick-connect",
  authenticateJWT,
  requireDataAccess,
  async (req: AuthenticatedRequest, res: Response) => {
    const userId = req.userId;
    const {
      ip,
      port,
      username,
      authType,
      password,
      key,
      keyPassword,
      keyType,
      credentialId,
      overrideCredentialUsername,
    } = req.body;

    if (
      !isNonEmptyString(ip) ||
      !isValidPort(port) ||
      !isNonEmptyString(username) ||
      !authType
    ) {
      return res.status(400).json({ error: "Missing required fields" });
    }

    try {
      let resolvedPassword = password;
      let resolvedKey = key;
      let resolvedKeyPassword = keyPassword;
      let resolvedKeyType = keyType;
      let resolvedAuthType = authType;
      let resolvedUsername = username;

      if (authType === "credential" && credentialId) {
        const cred =
          await createCurrentHostResolutionRepository().findCredentialByIdForUser(
            Number(credentialId),
            userId,
          );

        if (!cred) {
          return res.status(404).json({ error: "Credential not found" });
        }

        resolvedPassword = pickResolvedPassword(password, cred.password) as
          string | undefined;
        resolvedKey = cred.privateKey as string | undefined;
        resolvedKeyPassword = cred.keyPassword as string | undefined;
        resolvedKeyType = cred.keyType as string | undefined;
        resolvedAuthType = cred.authType as string | undefined;

        if (!overrideCredentialUsername) {
          resolvedUsername = cred.username as string;
        }
      }

      const tempHost: Record<string, unknown> = {
        id: -Date.now(),
        userId: userId,
        name: `${resolvedUsername}@${ip}:${port}`,
        ip,
        port: Number(port),
        username: resolvedUsername,
        folder: "",
        tags: [],
        pin: false,
        authType: resolvedAuthType || authType,
        password: resolvedPassword,
        key: resolvedKey,
        keyPassword: resolvedKeyPassword,
        keyType: resolvedKeyType,
        enableTerminal: true,
        enableTunnel: false,
        enableFileManager: true,
        enableDocker: false,
        enableProxmox: false,
        enableProxmoxStats: false,
        enableTmuxMonitor: false,
        enableTerminalToolbar: true,
        showTerminalInSidebar: true,
        showFileManagerInSidebar: false,
        showTunnelInSidebar: false,
        showDockerInSidebar: false,
        showServerStatsInSidebar: false,
        defaultPath: "/",
        tunnelConnections: [],
        jumpHosts: [],
        quickActions: [],
        statsConfig: {},
        notes: "",
        createdAt: new Date().toISOString(),
        updatedAt: new Date().toISOString(),
      };

      return res.status(200).json(tempHost);
    } catch (error) {
      sshLogger.error("Quick connect failed", error, {
        operation: "quick_connect",
        userId,
        ip,
        port,
        authType,
      });
      return res
        .status(500)
        .json({ error: "Failed to create quick connection" });
    }
  },
);

/**
 * @openapi
 * /host/db/host/{id}:
 *   put:
 *     summary: Update SSH host
 *     description: Updates an existing SSH host configuration.
 *     tags:
 *       - SSH
 *     parameters:
 *       - in: path
 *         name: id
 *         required: true
 *         schema:
 *           type: integer
 *     responses:
 *       200:
 *         description: Host updated successfully.
 *       400:
 *         description: Invalid SSH data.
 *       403:
 *         description: Access denied.
 *       404:
 *         description: Host not found.
 *       500:
 *         description: Failed to update SSH data.
 */
router.put(
  "/db/host/:id",
  authenticateJWT,
  requireDataAccess,
  upload.single("key"),
  async (req: Request, res: Response) => {
    const hostId = Array.isArray(req.params.id)
      ? req.params.id[0]
      : req.params.id;
    const userId = (req as AuthenticatedRequest).userId;
    let hostData: Record<string, unknown>;

    if (req.headers["content-type"]?.includes("multipart/form-data")) {
      if (req.body.data) {
        try {
          hostData = JSON.parse(req.body.data);
        } catch (err) {
          sshLogger.warn("Invalid JSON data in multipart request", {
            operation: "host_update",
            hostId: parseInt(hostId),
            userId,
            error: err,
          });
          return res.status(400).json({ error: "Invalid JSON data" });
        }
      } else {
        sshLogger.warn("Missing data field in multipart request", {
          operation: "host_update",
          hostId: parseInt(hostId),
          userId,
        });
        return res.status(400).json({ error: "Missing data field" });
      }

      if (req.file) {
        hostData.key = req.file.buffer.toString("utf8");
      }
    } else {
      hostData = req.body;
    }

    const {
      connectionType,
      name,
      folder,
      parentHostId,
      tags,
      ip,
      port,
      username,
      password,
      authMethod,
      authType,
      useWarpgate,
      shareSshAuth,
      credentialId,
      vaultProfileId,
      key,
      keyPassword,
      keyType,
      sudoPassword,
      pin,
      enableTerminal,
      enableCommandHistory,
      enableTunnel,
      enableFileManager,
      scpLegacy,
      enableDocker,
      enableProxmox,
      enableTmuxMonitor,
      enableTerminalToolbar,
      allowSessionSharing,
      showTerminalInSidebar,
      showFileManagerInSidebar,
      showTunnelInSidebar,
      showDockerInSidebar,
      showServerStatsInSidebar,
      defaultPath,
      tunnelConnections,
      jumpHosts,
      quickActions,
      statsConfig,
      dockerConfig,
      proxmoxConfig,
      enableProxmoxStats,
      proxmoxStatsConfig,
      terminalConfig,
      forceKeyboardInteractive,
      domain,
      security,
      ignoreCert,
      guacamoleConfig,
      notes,
      useSocks5,
      socks5Host,
      socks5Port,
      socks5Username,
      socks5Password,
      socks5ProxyChain,
      connectionOrigin,
      portKnockSequence,
      overrideCredentialUsername,
      macAddress,
      wolBroadcastAddress,
      enableSsh,
      enableRdp,
      enableVnc,
      enableTelnet,
      sshPort,
      rdpPort,
      vncPort,
      telnetPort,
      rdpAuthType,
      rdpCredentialId,
      rdpUser,
      rdpPassword,
      rdpDomain,
      rdpSecurity,
      rdpIgnoreCert,
      vncAuthType,
      vncCredentialId,
      vncPassword,
      vncUser,
      telnetAuthType,
      telnetCredentialId,
      telnetUser,
      telnetPassword,
    } = hostData;
    databaseLogger.info("Updating SSH host", {
      operation: "host_update",
      userId,
      hostId: parseInt(hostId),
      changes: Object.keys(hostData),
    });

    if (
      !isNonEmptyString(userId) ||
      !isNonEmptyString(ip) ||
      !isValidPort(port) ||
      !isOptionalBoolean(shareSshAuth) ||
      !hostId
    ) {
      sshLogger.warn("Invalid SSH data input validation failed for update", {
        operation: "host_update",
        hostId: parseInt(hostId),
        userId,
        hasIp: !!ip,
        port,
        isValidPort: isValidPort(port),
      });
      return res.status(400).json({ error: "Invalid SSH data" });
    }

    let validatedParentHostId: number | null | undefined = undefined;
    if (parentHostId !== undefined) {
      if (parentHostId === null) {
        validatedParentHostId = null;
      } else {
        const numericParentHostId = Number(parentHostId);
        if (!Number.isInteger(numericParentHostId)) {
          return res.status(400).json({ error: "Invalid parent host" });
        }
        const parentError = await validateParentHostId(
          userId,
          Number(hostId),
          numericParentHostId,
        );
        if (parentError) {
          return res.status(400).json({ error: parentError });
        }
        validatedParentHostId = numericParentHostId;
      }
    }

    const effectiveAuthType = authType || authMethod;
    const effectiveUsername =
      username || rdpUser || vncUser || telnetUser || "";
    const effectiveName =
      name || (effectiveUsername ? `${effectiveUsername}@${ip}` : String(ip));
    const sshDataObj: Record<string, unknown> = {
      connectionType: connectionType || "ssh",
      name: effectiveName,
      // A host is either placed in a folder or nested under a parent host,
      // never both. When the caller is assigning a parent, clear folder;
      // when the caller is assigning a folder, clear parentHostId.
      folder: validatedParentHostId ? null : folder,
      tags: Array.isArray(tags) ? tags.join(",") : tags || "",
      ip,
      port,
      username: effectiveUsername,
      authType: effectiveAuthType,
      useWarpgate: useWarpgate ? 1 : 0,
      shareSshAuth: shareSshAuth === true ? 1 : 0,
      credentialId: credentialId || null,
      vaultProfileId:
        effectiveAuthType === "vault" ? vaultProfileId || null : null,
      overrideCredentialUsername: overrideCredentialUsername ? 1 : 0,
      pin: pin ? 1 : 0,
      enableTerminal: enableTerminal ? 1 : 0,
      enableCommandHistory: enableCommandHistory ? 1 : 0,
      enableTunnel: enableTunnel ? 1 : 0,
      tunnelConnections: Array.isArray(tunnelConnections)
        ? JSON.stringify(tunnelConnections)
        : null,
      jumpHosts: Array.isArray(jumpHosts) ? JSON.stringify(jumpHosts) : null,
      quickActions: Array.isArray(quickActions)
        ? JSON.stringify(quickActions)
        : null,
      enableFileManager: enableFileManager ? 1 : 0,
      scpLegacy: scpLegacy ? 1 : 0,
      enableDocker: enableDocker ? 1 : 0,
      enableProxmox: enableProxmox ? 1 : 0,
      enableTmuxMonitor: enableTmuxMonitor ? 1 : 0,
      enableTerminalToolbar: enableTerminalToolbar === false ? 0 : 1,
      allowSessionSharing: allowSessionSharing === false ? 0 : 1,
      showTerminalInSidebar: showTerminalInSidebar ? 1 : 0,
      showFileManagerInSidebar: showFileManagerInSidebar ? 1 : 0,
      showTunnelInSidebar: showTunnelInSidebar ? 1 : 0,
      showDockerInSidebar: showDockerInSidebar ? 1 : 0,
      showServerStatsInSidebar: showServerStatsInSidebar ? 1 : 0,
      defaultPath: defaultPath || null,
      statsConfig: statsConfig
        ? typeof statsConfig === "string"
          ? statsConfig
          : JSON.stringify(statsConfig)
        : null,
      dockerConfig: dockerConfig
        ? typeof dockerConfig === "string"
          ? dockerConfig
          : JSON.stringify(dockerConfig)
        : null,
      proxmoxConfig: proxmoxConfig
        ? typeof proxmoxConfig === "string"
          ? proxmoxConfig
          : JSON.stringify(proxmoxConfig)
        : null,
      enableProxmoxStats: enableProxmoxStats ? 1 : 0,
      proxmoxStatsConfig: proxmoxStatsConfig
        ? typeof proxmoxStatsConfig === "string"
          ? proxmoxStatsConfig
          : JSON.stringify(proxmoxStatsConfig)
        : null,
      terminalConfig: terminalConfig
        ? typeof terminalConfig === "string"
          ? terminalConfig
          : JSON.stringify(terminalConfig)
        : null,
      forceKeyboardInteractive: forceKeyboardInteractive ? "true" : "false",
      domain: domain || null,
      security: security || null,
      ignoreCert: ignoreCert ? 1 : 0,
      guacamoleConfig: guacamoleConfig ? JSON.stringify(guacamoleConfig) : null,
      notes: notes || null,
      sudoPassword: sudoPassword || null,
      useSocks5: useSocks5 ? 1 : 0,
      socks5Host: socks5Host || null,
      socks5Port: socks5Port || null,
      socks5Username: socks5Username || null,
      socks5Password: socks5Password || null,
      socks5ProxyChain: socks5ProxyChain
        ? JSON.stringify(socks5ProxyChain)
        : null,
      connectionOrigin:
        connectionOrigin === "local" || connectionOrigin === "remote"
          ? connectionOrigin
          : null,
      macAddress: macAddress || null,
      wolBroadcastAddress: wolBroadcastAddress || null,
      portKnockSequence: portKnockSequence
        ? JSON.stringify(portKnockSequence)
        : null,
      enableSsh: enableSsh ? 1 : 0,
      enableRdp: enableRdp ? 1 : 0,
      enableVnc: enableVnc ? 1 : 0,
      enableTelnet: enableTelnet ? 1 : 0,
      sshPort: sshPort || port || 22,
      rdpPort: rdpPort || 3389,
      vncPort: vncPort || 5900,
      telnetPort: telnetPort || 23,
      rdpAuthType: enableRdp ? rdpAuthType || null : null,
      rdpCredentialId:
        enableRdp && rdpAuthType === "credential" && rdpCredentialId
          ? rdpCredentialId
          : null,
      rdpUser: rdpUser || null,
      rdpDomain: rdpDomain || null,
      rdpSecurity: rdpSecurity || null,
      rdpIgnoreCert: rdpIgnoreCert ? 1 : 0,
      vncAuthType: enableVnc ? vncAuthType || null : null,
      vncCredentialId:
        enableVnc && vncAuthType === "credential" && vncCredentialId
          ? vncCredentialId
          : null,
      vncUser: vncUser || null,
      telnetAuthType: enableTelnet ? telnetAuthType || null : null,
      telnetCredentialId:
        enableTelnet && telnetAuthType === "credential" && telnetCredentialId
          ? telnetCredentialId
          : null,
      telnetUser: telnetUser || null,
    };

    // For non-SSH hosts (RDP, VNC, Telnet), always save password if provided
    if ((connectionType || "ssh") !== "ssh") {
      if (password) {
        sshDataObj.password = password;
      }
      sshDataObj.key = null;
      sshDataObj.keyPassword = null;
      sshDataObj.keyType = null;
    } else if (effectiveAuthType === "password") {
      if (password) {
        sshDataObj.password = password;
      }
      sshDataObj.key = null;
      sshDataObj.keyPassword = null;
      sshDataObj.keyType = null;
    } else if (effectiveAuthType === "key") {
      if (key && typeof key === "string") {
        const keyValidation = parseSSHKey(
          key,
          typeof keyPassword === "string" ? keyPassword : undefined,
        );
        if (!keyValidation.success) {
          sshLogger.warn("SSH key validation failed", {
            operation: "host_update",
            hostId: parseInt(hostId),
            userId,
            name,
            ip,
            port,
            error: keyValidation.error,
          });
          return res.status(400).json({
            error: `Invalid SSH key: ${keyValidation.error || "Unable to parse key"}`,
          });
        }

        sshDataObj.key = key;
      }
      if (keyPassword !== undefined) {
        sshDataObj.keyPassword = keyPassword || null;
      }
      if (keyType) {
        sshDataObj.keyType = keyType;
      }
      sshDataObj.password = password || null;
    } else if (effectiveAuthType === "credential") {
      sshDataObj.password = password || null;
      sshDataObj.key = null;
      sshDataObj.keyPassword = null;
      sshDataObj.keyType = null;
    } else if (effectiveAuthType === "agent") {
      sshDataObj.password = null;
      sshDataObj.key = null;
      sshDataObj.keyPassword = null;
      sshDataObj.keyType = null;
    } else {
      sshDataObj.password = null;
      sshDataObj.key = null;
      sshDataObj.keyPassword = null;
      sshDataObj.keyType = null;
    }

    if (rdpPassword) sshDataObj.rdpPassword = rdpPassword;
    if (vncPassword) sshDataObj.vncPassword = vncPassword;
    if (telnetPassword) sshDataObj.telnetPassword = telnetPassword;

    if (validatedParentHostId !== undefined) {
      sshDataObj.parentHostId = validatedParentHostId;
    } else if (folder !== undefined) {
      // Caller is assigning a folder (including clearing it back to root)
      // without touching parentHostId -- folder placement replaces
      // parent-host placement either way.
      sshDataObj.parentHostId = null;
    }

    try {
      const accessInfo = await permissionManager.canAccessHost(
        userId,
        Number(hostId),
        "edit",
      );

      if (!accessInfo.hasAccess) {
        sshLogger.warn("User does not have permission to update host", {
          operation: "host_update",
          hostId: parseInt(hostId),
          userId,
        });
        return res.status(403).json({ error: "Access denied" });
      }

      const hostRecord =
        await createCurrentHostResolutionRepository().findHostUpdateState(
          Number(hostId),
        );

      if (!hostRecord) {
        sshLogger.warn("Host not found for update", {
          operation: "host_update",
          hostId: parseInt(hostId),
          userId,
        });
        return res.status(404).json({ error: "Host not found" });
      }

      const ownerId = hostRecord.userId;

      if (!accessInfo.isOwner) {
        // Shared editors work on the owner's real record, but the owner's SSH
        // authentication is private and can only be changed by that owner.
        if (containsOwnerPrivateAuthUpdate(hostData, "ssh")) {
          return res.status(403).json({
            error:
              "Only the host owner can change the host's SSH authentication",
          });
        }

        const parseTerminalConfig = (
          value: unknown,
        ): Record<string, unknown> | null => {
          if (!value) return null;
          if (
            typeof value === "object" &&
            value !== null &&
            !Array.isArray(value)
          ) {
            return { ...(value as Record<string, unknown>) };
          }
          if (typeof value === "string") {
            const parsed = JSON.parse(value) as unknown;
            if (
              typeof parsed === "object" &&
              parsed !== null &&
              !Array.isArray(parsed)
            ) {
              return { ...(parsed as Record<string, unknown>) };
            }
          }
          return null;
        };

        if (hostData.terminalConfig === undefined) {
          delete sshDataObj.terminalConfig;
        } else {
          let incomingTerminalConfig: Record<string, unknown> | null;
          try {
            incomingTerminalConfig = parseTerminalConfig(
              hostData.terminalConfig,
            );
          } catch {
            return res.status(400).json({ error: "Invalid terminal config" });
          }

          if (!incomingTerminalConfig) {
            return res.status(400).json({ error: "Invalid terminal config" });
          }
          const protectedTerminalConfigField =
            OWNER_PRIVATE_TERMINAL_CONFIG_FIELDS.find((field) =>
              Object.prototype.hasOwnProperty.call(
                incomingTerminalConfig,
                field,
              ),
            );
          if (protectedTerminalConfigField) {
            return res.status(403).json({
              error:
                "Only the host owner can change private SSH authentication settings",
            });
          }

          const ownerHost =
            await createCurrentHostResolutionRepository().findHostById(
              Number(hostId),
              ownerId,
            );
          const ownerTerminalConfig = parseTerminalConfig(
            ownerHost?.terminalConfig,
          );
          if (ownerTerminalConfig) {
            for (const field of OWNER_PRIVATE_TERMINAL_CONFIG_FIELDS) {
              if (
                Object.prototype.hasOwnProperty.call(ownerTerminalConfig, field)
              ) {
                incomingTerminalConfig[field] = ownerTerminalConfig[field];
              }
            }
          }
          sshDataObj.terminalConfig = JSON.stringify(incomingTerminalConfig);
        }

        const referenceViolations: Array<[unknown, number | null, string]> = [
          [
            hostData.rdpCredentialId,
            hostRecord.rdpCredentialId,
            "RDP credential",
          ],
          [
            hostData.vncCredentialId,
            hostRecord.vncCredentialId,
            "VNC credential",
          ],
          [
            hostData.telnetCredentialId,
            hostRecord.telnetCredentialId,
            "Telnet credential",
          ],
        ];

        for (const [incoming, current, label] of referenceViolations) {
          if (incoming !== undefined && (incoming ?? null) !== current) {
            return res.status(403).json({
              error: `Only the host owner can change the ${label}`,
            });
          }
        }

        for (const field of OWNER_PRIVATE_AUTH_FIELDS.ssh) {
          delete sshDataObj[field];
        }
      } else if (
        sshDataObj.terminalConfig &&
        (hostData.terminalConfig as Record<string, unknown> | undefined)
          ?.sudoPassword === undefined
      ) {
        // The editor omits sudoPassword entirely when the user hasn't
        // touched the field, so preserve whatever is already stored instead
        // of letting the wholesale terminalConfig replacement below wipe it.
        const existingHost =
          await createCurrentHostResolutionRepository().findHostById(
            Number(hostId),
            ownerId,
          );
        const existingTerminalConfig = existingHost?.terminalConfig
          ? (JSON.parse(existingHost.terminalConfig as string) as Record<
              string,
              unknown
            >)
          : undefined;
        if (existingTerminalConfig?.sudoPassword !== undefined) {
          const incomingTerminalConfig = JSON.parse(
            sshDataObj.terminalConfig as string,
          ) as Record<string, unknown>;
          incomingTerminalConfig.sudoPassword =
            existingTerminalConfig.sudoPassword;
          sshDataObj.terminalConfig = JSON.stringify(incomingTerminalConfig);
        }
      }

      await createCurrentHostRepository().updateEncryptedForUser(
        ownerId,
        Number(hostId),
        sshDataObj,
      );

      // Keep every recipient's re-encrypted secret snapshots in sync with
      // the updated host record.
      try {
        const { SharedHostSecretsManager } =
          await import("../../utils/shared-host-secrets-manager.js");
        await SharedHostSecretsManager.getInstance().resyncHost(Number(hostId));
      } catch (resyncError) {
        sshLogger.warn("Failed to resync shared host secrets after update", {
          operation: "host_update_resync",
          hostId: parseInt(hostId),
          error: getErrorMessage(resyncError),
        });
      }

      const updatedHost =
        await createCurrentHostResolutionRepository().findHostById(
          Number(hostId),
          ownerId,
        );

      if (!updatedHost) {
        sshLogger.warn("Updated host not found after update", {
          operation: "host_update",
          hostId: parseInt(hostId),
          userId,
        });
        return res.status(404).json({ error: "Host not found after update" });
      }

      const baseHost = transformHostResponse(updatedHost);

      const resolvedHost =
        (await resolveHostCredentials(baseHost, userId)) || baseHost;
      databaseLogger.success("SSH host updated", {
        operation: "host_update_success",
        userId,
        hostId: parseInt(hostId),
      });

      const { ipAddress: uhIp, userAgent: uhUa } = getRequestMeta(req);
      await logAudit({
        userId,
        username: await getAuditUsername(userId),
        action: "update_host",
        resourceType: "host",
        resourceId: hostId,
        resourceName: String(name ?? ip),
        ipAddress: uhIp,
        userAgent: uhUa,
        success: true,
      });

      res.json(stripSensitiveFields(resolvedHost));
      notifyStatsHostUpdated(parseInt(hostId), req.headers, "host_update");
    } catch (err) {
      sshLogger.error("Failed to update SSH host in database", err, {
        operation: "host_update",
        hostId: parseInt(hostId),
        userId,
        name,
        ip,
        port,
        authType: effectiveAuthType,
      });
      res.status(500).json({ error: "Failed to update SSH data" });
    }
  },
);

/**
 * @openapi
 * /host/db/host:
 *   get:
 *     summary: Get all SSH hosts
 *     description: Retrieves all SSH hosts for the authenticated user.
 *     tags:
 *       - SSH
 *     responses:
 *       200:
 *         description: A list of SSH hosts.
 *       400:
 *         description: Invalid userId.
 *       500:
 *         description: Failed to fetch SSH data.
 */
router.get(
  "/db/host",
  authenticateJWT,
  requireDataAccess,
  async (req: Request, res: Response) => {
    const userId = (req as AuthenticatedRequest).userId;
    if (!isNonEmptyString(userId)) {
      sshLogger.warn("Invalid userId for SSH data fetch", {
        operation: "host_fetch",
        userId,
      });
      return res.status(400).json({ error: "Invalid userId" });
    }
    try {
      const now = new Date().toISOString();

      const roleIds =
        await createCurrentRoleRepository().listUserRoleIds(userId);
      const accessEntries =
        await createCurrentRbacAccessRepository().listVisibleHostAccessEntries(
          userId,
          roleIds,
          now,
        );

      const rawData =
        await createCurrentHostResolutionRepository().listHostRowsForAccessList(
          userId,
          accessEntries,
        );

      const ownHosts = rawData.filter((row) => row.userId === userId);
      const sharedHosts = rawData.filter((row) => row.userId !== userId);

      const decryptedOwnHosts: Record<string, unknown>[] = [];
      const userDataKey = DataCrypto.getUserDataKey(userId);
      if (userDataKey) {
        for (const host of ownHosts) {
          try {
            decryptedOwnHosts.push(
              DataCrypto.decryptRecord("ssh_data", host, userId, userDataKey),
            );
          } catch (decryptError) {
            sshLogger.warn("Skipping host with invalid encrypted fields", {
              operation: "host_fetch_own_decrypt_failed",
              userId,
              hostId: host.id,
              error: getErrorMessage(decryptError),
            });
          }
        }
      }

      // One lookup for every owner rather than one per shared host.
      const ownerUsernames = new Map<string, string>();
      const ownerIds = Array.from(
        new Set(sharedHosts.map((host) => host.userId as string)),
      );
      if (ownerIds.length > 0) {
        try {
          const owners =
            await createCurrentUserRepository().listByIds(ownerIds);
          for (const owner of owners) {
            ownerUsernames.set(owner.id, owner.username ?? "");
          }
        } catch {
          // Falls through to an undefined ownerUsername below.
        }
      }

      const data = [...decryptedOwnHosts, ...sharedHosts];

      // Own hosts all resolve against the caller's own credentials, so they can
      // be fetched and decrypted in one batch instead of once per host.
      const ownCredentialIds = decryptedOwnHosts
        .map((host) => host.credentialId)
        .filter((id): id is number => typeof id === "number");
      const credentialsById = await createCurrentHostResolutionRepository()
        .listCredentialsByIdsForUser(ownCredentialIds, userId)
        .catch(() => new Map<number, HostResolutionCredentialRecord>());

      const result = await Promise.all(
        data.map(async (row: Record<string, unknown>) => {
          const baseHost = {
            ...transformHostResponse(row),
            isShared: !!row.isShared,
            permissionLevel: row.permissionLevel || undefined,
            sharedExpiresAt: row.expiresAt || undefined,
            ownerUsername: row.isShared
              ? ownerUsernames.get(row.userId as string) || undefined
              : undefined,
          };

          const resolved =
            (await resolveHostCredentials(baseHost, userId, credentialsById)) ||
            baseHost;
          return resolved;
        }),
      );

      const sanitized = result.map((host) =>
        host.isShared
          ? sanitizeHostForRecipient(
              host,
              host.permissionLevel as string | undefined,
            )
          : stripSensitiveFields(host),
      );
      res.json(sanitized);
    } catch (err) {
      sshLogger.error("Failed to fetch SSH hosts from database", err, {
        operation: "host_fetch",
        userId,
      });
      res.status(500).json({ error: "Failed to fetch SSH data" });
    }
  },
);

/**
 * @openapi
 * /host/db/host/{id}:
 *   get:
 *     summary: Get SSH host by ID
 *     description: Retrieves a specific SSH host by its ID.
 *     tags:
 *       - SSH
 *     parameters:
 *       - in: path
 *         name: id
 *         required: true
 *         schema:
 *           type: integer
 *     responses:
 *       200:
 *         description: The requested SSH host.
 *       400:
 *         description: Invalid userId or hostId.
 *       404:
 *         description: SSH host not found.
 *       500:
 *         description: Failed to fetch SSH host.
 */
router.get(
  "/db/host/:id",
  authenticateJWT,
  requireDataAccess,
  async (req: Request, res: Response) => {
    const hostId = Array.isArray(req.params.id)
      ? req.params.id[0]
      : req.params.id;
    const userId = (req as AuthenticatedRequest).userId;

    if (!isNonEmptyString(userId) || !hostId) {
      sshLogger.warn("Invalid userId or hostId for SSH host fetch by ID", {
        operation: "host_fetch_by_id",
        hostId: parseInt(hostId),
        userId,
      });
      return res.status(400).json({ error: "Invalid userId or hostId" });
    }
    try {
      const hostResolutionRepository = createCurrentHostResolutionRepository();
      const host = await hostResolutionRepository.findHostByIdForUser(
        Number(hostId),
        userId,
      );

      if (host) {
        const result = transformHostResponse(host);
        const resolved =
          (await resolveHostCredentials(result, userId)) || result;

        return res.json(stripSensitiveFields(resolved));
      }

      // Not the owner: shared recipients get a sanitized view of the host.
      const accessInfo = await permissionManager.canAccessHost(
        userId,
        Number(hostId),
        "connect",
      );

      if (!accessInfo.hasAccess) {
        sshLogger.warn("SSH host not found", {
          operation: "host_fetch_by_id",
          hostId: parseInt(hostId),
          userId,
        });
        return res.status(404).json({ error: "SSH host not found" });
      }

      const ownerId = await hostResolutionRepository.findHostOwnerId(
        Number(hostId),
      );
      const sharedHost = ownerId
        ? await hostResolutionRepository.findHostById(Number(hostId), ownerId)
        : null;

      if (!sharedHost) {
        return res.status(404).json({ error: "SSH host not found" });
      }

      let ownerUsername: string | undefined;
      try {
        const owner = ownerId
          ? await createCurrentUserRepository().findById(ownerId)
          : null;
        ownerUsername = owner?.username ?? undefined;
      } catch {
        ownerUsername = undefined;
      }

      const sharedResult = {
        ...transformHostResponse(sharedHost),
        isShared: true,
        permissionLevel: accessInfo.permissionLevel,
        sharedExpiresAt: accessInfo.expiresAt || undefined,
        ownerUsername,
      };
      const resolvedSharedResult =
        (await resolveHostCredentials(sharedResult, userId)) || sharedResult;

      res.json(
        sanitizeHostForRecipient(
          resolvedSharedResult,
          accessInfo.permissionLevel,
        ),
      );
    } catch (err) {
      sshLogger.error("Failed to fetch SSH host by ID from database", err, {
        operation: "host_fetch_by_id",
        hostId: parseInt(hostId),
        userId,
      });
      res.status(500).json({ error: "Failed to fetch SSH host" });
    }
  },
);

/**
 * @openapi
 * /host/db/host/{id}/password:
 *   get:
 *     summary: Get host password for clipboard copy
 *     description: Returns the password for a specific host. Used by the copy-password feature.
 *     tags:
 *       - SSH
 *     parameters:
 *       - in: path
 *         name: id
 *         required: true
 *         schema:
 *           type: integer
 *       - in: query
 *         name: field
 *         schema:
 *           type: string
 *           enum: [password, sudoPassword, rdpPassword, vncPassword, telnetPassword, key, keyPassword]
 *     responses:
 *       200:
 *         description: The requested password value.
 *       404:
 *         description: Host not found or no password set.
 */
router.get(
  "/db/host/:id/password",
  authenticateJWT,
  requireDataAccess,
  async (req: Request, res: Response) => {
    const hostId = Number(req.params.id);
    const userId = (req as AuthenticatedRequest).userId;
    const field = (req.query.field as string) || "password";

    if (
      ![
        "password",
        "sudoPassword",
        "rdpPassword",
        "vncPassword",
        "telnetPassword",
        "key",
        "keyPassword",
      ].includes(field)
    ) {
      return res.status(400).json({ error: "Invalid field" });
    }

    try {
      const host =
        await createCurrentHostResolutionRepository().findHostByIdForUser(
          hostId,
          userId,
        );

      if (!host) {
        return res.status(404).json({ error: "Host not found" });
      }

      const resolved = (await resolveHostCredentials(host, userId)) || host;
      let value = resolved[field];

      if (!value && field === "sudoPassword" && resolved.terminalConfig) {
        try {
          const tc =
            typeof resolved.terminalConfig === "string"
              ? JSON.parse(resolved.terminalConfig)
              : resolved.terminalConfig;
          value = tc?.sudoPassword || null;
        } catch {
          // malformed JSON — leave value null
        }
      }

      if (!value) {
        return res.status(404).json({ error: "No password set" });
      }

      res.json({ value });
    } catch (err) {
      sshLogger.error("Failed to fetch host password", err, {
        operation: "host_password_fetch",
        hostId,
        userId,
      });
      res.status(500).json({ error: "Failed to fetch password" });
    }
  },
);

/**
 * @openapi
 * /host/db/host/{id}/export:
 *   get:
 *     summary: Export SSH host
 *     description: Exports a specific SSH host with decrypted credentials.
 *     tags:
 *       - SSH
 *     parameters:
 *       - in: path
 *         name: id
 *         required: true
 *         schema:
 *           type: integer
 *     responses:
 *       200:
 *         description: The exported SSH host.
 *       400:
 *         description: Invalid userId or hostId.
 *       404:
 *         description: SSH host not found.
 *       500:
 *         description: Failed to export SSH host.
 */
router.get(
  "/db/host/:id/export",
  authenticateJWT,
  requireDataAccess,
  async (req: Request, res: Response) => {
    const hostId = Array.isArray(req.params.id)
      ? req.params.id[0]
      : req.params.id;
    const userId = (req as AuthenticatedRequest).userId;

    if (!isNonEmptyString(userId) || !hostId) {
      return res.status(400).json({ error: "Invalid userId or hostId" });
    }

    try {
      const host =
        await createCurrentHostResolutionRepository().findHostByIdForUser(
          Number(hostId),
          userId,
        );

      if (!host) {
        return res.status(404).json({ error: "SSH host not found" });
      }

      const resolvedHost = (await resolveHostCredentials(host, userId)) || host;

      const exportedConnectionType =
        (resolvedHost.connectionType as string) || "ssh";
      const isRemoteDesktop = ["rdp", "vnc", "telnet"].includes(
        exportedConnectionType,
      );

      const baseExportData = {
        connectionType: exportedConnectionType,
        name: resolvedHost.name,
        ip: resolvedHost.ip,
        port: resolvedHost.port,
        username: resolvedHost.username,
        password: resolvedHost.password || null,
        folder: resolvedHost.folder,
        tags:
          typeof resolvedHost.tags === "string"
            ? resolvedHost.tags.split(",").filter(Boolean)
            : resolvedHost.tags || [],
        pin: !!resolvedHost.pin,
        notes: resolvedHost.notes || null,
      };

      const exportData = isRemoteDesktop
        ? {
            ...baseExportData,
            enableRdp: !!resolvedHost.enableRdp,
            enableVnc: !!resolvedHost.enableVnc,
            enableTelnet: !!resolvedHost.enableTelnet,
            rdpPort: resolvedHost.rdpPort || 3389,
            vncPort: resolvedHost.vncPort || 5900,
            telnetPort: resolvedHost.telnetPort || 23,
            rdpAuthType: resolvedHost.rdpAuthType || null,
            rdpCredentialId: resolvedHost.rdpCredentialId || null,
            rdpUser: resolvedHost.rdpUser || null,
            rdpPassword: resolvedHost.rdpPassword || null,
            rdpDomain: resolvedHost.rdpDomain || null,
            rdpSecurity: resolvedHost.rdpSecurity || null,
            rdpIgnoreCert: !!resolvedHost.rdpIgnoreCert,
            vncAuthType: resolvedHost.vncAuthType || null,
            vncCredentialId: resolvedHost.vncCredentialId || null,
            vncUser: resolvedHost.vncUser || null,
            vncPassword: resolvedHost.vncPassword || null,
            telnetAuthType: resolvedHost.telnetAuthType || null,
            telnetCredentialId: resolvedHost.telnetCredentialId || null,
            telnetUser: resolvedHost.telnetUser || null,
            telnetPassword: resolvedHost.telnetPassword || null,
            guacamoleConfig: resolvedHost.guacamoleConfig
              ? JSON.parse(resolvedHost.guacamoleConfig as string)
              : null,
          }
        : {
            ...baseExportData,
            authType: resolvedHost.authType,
            key: resolvedHost.key || null,
            keyPassword: resolvedHost.keyPassword || null,
            keyType: resolvedHost.keyType || null,
            credentialId: resolvedHost.credentialId || null,
            overrideCredentialUsername:
              !!resolvedHost.overrideCredentialUsername,
            enableTerminal: !!resolvedHost.enableTerminal,
            enableTunnel: !!resolvedHost.enableTunnel,
            enableFileManager: resolvedHost.enableFileManager !== false,
            scpLegacy: !!resolvedHost.scpLegacy,
            enableDocker: !!resolvedHost.enableDocker,
            enableProxmox: !!resolvedHost.enableProxmox,
            enableProxmoxStats: !!resolvedHost.enableProxmoxStats,
            enableTmuxMonitor: !!resolvedHost.enableTmuxMonitor,
            enableTerminalToolbar: resolvedHost.enableTerminalToolbar !== false,
            showTerminalInSidebar: !!resolvedHost.showTerminalInSidebar,
            showFileManagerInSidebar: !!resolvedHost.showFileManagerInSidebar,
            showTunnelInSidebar: !!resolvedHost.showTunnelInSidebar,
            showDockerInSidebar: !!resolvedHost.showDockerInSidebar,
            showServerStatsInSidebar: !!resolvedHost.showServerStatsInSidebar,
            defaultPath: resolvedHost.defaultPath,
            sudoPassword: resolvedHost.sudoPassword || null,
            tunnelConnections: resolvedHost.tunnelConnections
              ? JSON.parse(resolvedHost.tunnelConnections as string)
              : [],
            jumpHosts: resolvedHost.jumpHosts
              ? JSON.parse(resolvedHost.jumpHosts as string)
              : null,
            quickActions: resolvedHost.quickActions
              ? JSON.parse(resolvedHost.quickActions as string)
              : null,
            statsConfig: resolvedHost.statsConfig
              ? JSON.parse(resolvedHost.statsConfig as string)
              : null,
            dockerConfig: resolvedHost.dockerConfig
              ? JSON.parse(resolvedHost.dockerConfig as string)
              : null,
            proxmoxConfig: resolvedHost.proxmoxConfig
              ? JSON.parse(resolvedHost.proxmoxConfig as string)
              : null,
            proxmoxStatsConfig: resolvedHost.proxmoxStatsConfig
              ? JSON.parse(resolvedHost.proxmoxStatsConfig as string)
              : null,
            terminalConfig: resolvedHost.terminalConfig
              ? JSON.parse(resolvedHost.terminalConfig as string)
              : null,
            forceKeyboardInteractive:
              resolvedHost.forceKeyboardInteractive === "true",
            useSocks5: !!resolvedHost.useSocks5,
            socks5Host: resolvedHost.socks5Host || null,
            socks5Port: resolvedHost.socks5Port || null,
            socks5Username: resolvedHost.socks5Username || null,
            socks5Password: resolvedHost.socks5Password || null,
            socks5ProxyChain: resolvedHost.socks5ProxyChain
              ? JSON.parse(resolvedHost.socks5ProxyChain as string)
              : null,
            portKnockSequence: resolvedHost.portKnockSequence
              ? JSON.parse(resolvedHost.portKnockSequence as string)
              : null,
          };

      sshLogger.success("Host exported with decrypted credentials", {
        operation: "host_export",
        hostId: parseInt(hostId),
        userId,
      });

      res.json(exportData);
    } catch (err) {
      sshLogger.error("Failed to export SSH host", err, {
        operation: "host_export",
        hostId: parseInt(hostId),
        userId,
      });
      res.status(500).json({ error: "Failed to export SSH host" });
    }
  },
);

/**
 * @openapi
 * /host/db/hosts/export:
 *   get:
 *     summary: Export all SSH hosts
 *     description: Exports all SSH hosts for the current user. By default credentials are decrypted and embedded. With `share=1`, secrets are omitted and credential-authenticated hosts instead reference a scrubbed `credentials` array by alias, suitable for handing off to another user.
 *     tags:
 *       - SSH
 *     parameters:
 *       - in: query
 *         name: share
 *         required: false
 *         schema:
 *           type: string
 *         description: Set to "1" to export without embedded secrets.
 *     responses:
 *       200:
 *         description: All exported SSH hosts.
 *       400:
 *         description: Invalid userId.
 *       500:
 *         description: Failed to export SSH hosts.
 */
router.get(
  "/db/hosts/export",
  authenticateJWT,
  requireDataAccess,
  async (req: Request, res: Response) => {
    const userId = (req as AuthenticatedRequest).userId;
    const shareMode = req.query.share === "1" || req.query.share === "true";

    if (!isNonEmptyString(userId)) {
      return res.status(400).json({ error: "Invalid userId" });
    }

    try {
      const allHosts =
        await createCurrentHostResolutionRepository().findHostsByUserId(userId);

      const exportedHosts = [];
      const usedCredentialIds = new Set<number>();

      for (const host of allHosts) {
        const resolvedHost = shareMode
          ? host
          : (await resolveHostCredentials(host, userId)) || host;

        const exportedConnectionType =
          (resolvedHost.connectionType as string) || "ssh";
        const isRemoteDesktop = ["rdp", "vnc", "telnet"].includes(
          exportedConnectionType,
        );

        const baseExportData = {
          connectionType: exportedConnectionType,
          name: resolvedHost.name,
          ip: resolvedHost.ip,
          port: resolvedHost.port,
          username: resolvedHost.username,
          password: shareMode ? null : resolvedHost.password || null,
          folder: resolvedHost.folder,
          tags:
            typeof resolvedHost.tags === "string"
              ? resolvedHost.tags.split(",").filter(Boolean)
              : resolvedHost.tags || [],
          pin: !!resolvedHost.pin,
          notes: resolvedHost.notes || null,
        };

        const exportData = isRemoteDesktop
          ? {
              ...baseExportData,
              domain: resolvedHost.domain || null,
              security: resolvedHost.security || null,
              ignoreCert: !!resolvedHost.ignoreCert,
              guacamoleConfig: resolvedHost.guacamoleConfig
                ? JSON.parse(resolvedHost.guacamoleConfig as string)
                : null,
            }
          : {
              ...baseExportData,
              authType: resolvedHost.authType,
              key: shareMode ? null : resolvedHost.key || null,
              keyPassword: shareMode ? null : resolvedHost.keyPassword || null,
              keyType: resolvedHost.keyType || null,
              credentialId: resolvedHost.credentialId || null,
              overrideCredentialUsername:
                !!resolvedHost.overrideCredentialUsername,
              enableTerminal: !!resolvedHost.enableTerminal,
              enableTunnel: !!resolvedHost.enableTunnel,
              enableFileManager: resolvedHost.enableFileManager !== false,
              enableDocker: !!resolvedHost.enableDocker,
              enableProxmox: !!resolvedHost.enableProxmox,
              enableTmuxMonitor: !!resolvedHost.enableTmuxMonitor,
              enableTerminalToolbar:
                resolvedHost.enableTerminalToolbar !== false,
              showTerminalInSidebar: !!resolvedHost.showTerminalInSidebar,
              showFileManagerInSidebar: !!resolvedHost.showFileManagerInSidebar,
              showTunnelInSidebar: !!resolvedHost.showTunnelInSidebar,
              showDockerInSidebar: !!resolvedHost.showDockerInSidebar,
              showServerStatsInSidebar: !!resolvedHost.showServerStatsInSidebar,
              defaultPath: resolvedHost.defaultPath,
              sudoPassword: shareMode
                ? null
                : resolvedHost.sudoPassword || null,
              tunnelConnections: resolvedHost.tunnelConnections
                ? JSON.parse(resolvedHost.tunnelConnections as string)
                : [],
              jumpHosts: resolvedHost.jumpHosts
                ? JSON.parse(resolvedHost.jumpHosts as string)
                : null,
              quickActions: resolvedHost.quickActions
                ? JSON.parse(resolvedHost.quickActions as string)
                : null,
              statsConfig: resolvedHost.statsConfig
                ? JSON.parse(resolvedHost.statsConfig as string)
                : null,
              dockerConfig: resolvedHost.dockerConfig
                ? JSON.parse(resolvedHost.dockerConfig as string)
                : null,
              proxmoxConfig: resolvedHost.proxmoxConfig
                ? JSON.parse(resolvedHost.proxmoxConfig as string)
                : null,
              terminalConfig: resolvedHost.terminalConfig
                ? JSON.parse(resolvedHost.terminalConfig as string)
                : null,
              forceKeyboardInteractive:
                resolvedHost.forceKeyboardInteractive === "true",
              useSocks5: !!resolvedHost.useSocks5,
              socks5Host: resolvedHost.socks5Host || null,
              socks5Port: resolvedHost.socks5Port || null,
              socks5Username: resolvedHost.socks5Username || null,
              socks5Password: shareMode
                ? null
                : resolvedHost.socks5Password || null,
              socks5ProxyChain: resolvedHost.socks5ProxyChain
                ? JSON.parse(resolvedHost.socks5ProxyChain as string)
                : null,
            };

        if (
          shareMode &&
          !isRemoteDesktop &&
          resolvedHost.authType === "credential" &&
          resolvedHost.credentialId
        ) {
          usedCredentialIds.add(resolvedHost.credentialId as number);
        }

        exportedHosts.push(exportData);
      }

      if (!shareMode) {
        sshLogger.success("All hosts exported with decrypted credentials", {
          operation: "hosts_export_all",
          count: exportedHosts.length,
          userId,
        });

        return res.json({ hosts: exportedHosts });
      }

      const exportedCredentials: Record<string, unknown>[] = [];
      if (usedCredentialIds.size > 0) {
        const credentialRepository = createCurrentCredentialRepository();
        const ownedCredentials =
          await credentialRepository.listDecryptedByUserId(userId);
        const credentialById = new Map(
          ownedCredentials.map((credential) => [credential.id, credential]),
        );

        for (const host of exportedHosts as Record<string, unknown>[]) {
          const credentialId = host.credentialId as number | null;
          if (!credentialId) continue;
          const credential = credentialById.get(credentialId);
          if (!credential) continue;

          host.credentialAlias = credential.name;

          if (
            !exportedCredentials.some(
              (entry) => entry.alias === credential.name,
            )
          ) {
            exportedCredentials.push({
              alias: credential.name,
              name: credential.name,
              description: credential.description || null,
              folder: credential.folder || null,
              tags:
                typeof credential.tags === "string"
                  ? credential.tags.split(",").filter(Boolean)
                  : [],
              authType: credential.authType,
              username: credential.username || null,
              keyType: credential.keyType || null,
            });
          }
        }
      }

      for (const host of exportedHosts as Record<string, unknown>[]) {
        delete host.credentialId;
      }

      sshLogger.success("All hosts exported for sharing without secrets", {
        operation: "hosts_export_all_share",
        count: exportedHosts.length,
        credentialCount: exportedCredentials.length,
        userId,
      });

      res.json({
        version: "1",
        exportedAt: new Date().toISOString(),
        credentials: exportedCredentials,
        hosts: exportedHosts,
      });
    } catch (err) {
      sshLogger.error("Failed to export all SSH hosts", err, {
        operation: "hosts_export_all",
        userId,
      });
      res.status(500).json({ error: "Failed to export SSH hosts" });
    }
  },
);

/**
 * @openapi
 * /host/db/host/{id}:
 *   delete:
 *     summary: Delete SSH host
 *     description: Deletes an SSH host by its ID.
 *     tags:
 *       - SSH
 *     parameters:
 *       - in: path
 *         name: id
 *         required: true
 *         schema:
 *           type: integer
 *     responses:
 *       200:
 *         description: SSH host deleted successfully.
 *       400:
 *         description: Invalid userId or id.
 *       404:
 *         description: SSH host not found.
 *       500:
 *         description: Failed to delete SSH host.
 */
router.delete(
  "/db/host/:id",
  authenticateJWT,
  requireDataAccess,
  async (req: Request, res: Response) => {
    const userId = (req as AuthenticatedRequest).userId;
    const hostId = Array.isArray(req.params.id)
      ? req.params.id[0]
      : req.params.id;

    if (!isNonEmptyString(userId) || !hostId) {
      sshLogger.warn("Invalid userId or hostId for SSH host delete", {
        operation: "host_delete",
        hostId: parseInt(hostId),
        userId,
      });
      return res.status(400).json({ error: "Invalid userId or id" });
    }
    databaseLogger.info("Deleting SSH host", {
      operation: "host_delete",
      userId,
      hostId: parseInt(hostId),
    });
    try {
      const hostToDelete =
        await createCurrentHostResolutionRepository().findHostByIdForUser(
          Number(hostId),
          userId,
        );

      if (!hostToDelete) {
        sshLogger.warn("SSH host not found for deletion", {
          operation: "host_delete",
          hostId: parseInt(hostId),
          userId,
        });
        return res.status(404).json({ error: "SSH host not found" });
      }

      const numericHostId = Number(hostId);

      await createCurrentFileManagerBookmarkRepository().deleteByHostId(
        numericHostId,
      );

      await createCurrentTransferRecentRepository().deleteByHostId(
        numericHostId,
      );

      await createCurrentCommandHistoryRepository().deleteByHostId(
        numericHostId,
      );

      await createCurrentSshCredentialUsageRepository().deleteByHostId(
        numericHostId,
      );

      await createCurrentRecentActivityRepository().deleteByHostId(
        numericHostId,
      );

      await createCurrentRbacAccessRepository().deleteHostAccessForHost(
        numericHostId,
      );

      await createCurrentSessionRecordingRepository().deleteByHostId(
        numericHostId,
      );

      await createCurrentHostRepository().deleteForUser(userId, numericHostId);
      if (hostToDelete.syncId) {
        await createCurrentSyncTombstoneRepository().record(
          userId,
          "hosts",
          hostToDelete.syncId,
        );
      }

      databaseLogger.success("SSH host deleted", {
        operation: "host_delete_success",
        userId,
        hostId: parseInt(hostId),
      });

      const { ipAddress: dhIp, userAgent: dhUa } = getRequestMeta(req);
      await logAudit({
        userId,
        username: await getAuditUsername(userId),
        action: "delete_host",
        resourceType: "host",
        resourceId: hostId,
        resourceName: hostToDelete.name ?? hostToDelete.ip,
        ipAddress: dhIp,
        userAgent: dhUa,
        success: true,
      });

      notifyAutomationInternalEvent("host_deleted", userId, numericHostId, {
        name: hostToDelete.name ?? hostToDelete.ip,
      });

      try {
        const axios = (await import("axios")).default;
        await axios.post(
          `${STATS_SERVER_URL}/host-deleted`,
          { hostId: numericHostId },
          {
            headers: {
              Authorization: req.headers.authorization || "",
              Cookie: req.headers.cookie || "",
            },
            timeout: 5000,
          },
        );
      } catch (err) {
        sshLogger.warn("Failed to notify stats server of host deletion", {
          operation: "host_delete",
          hostId: numericHostId,
          error: err instanceof Error ? err.message : String(err),
        });
      }

      res.json({ message: "SSH host deleted" });
    } catch (err) {
      sshLogger.error("Failed to delete SSH host from database", err, {
        operation: "host_delete",
        hostId: parseInt(hostId),
        userId,
      });
      res.status(500).json({ error: "Failed to delete SSH host" });
    }
  },
);

registerHostFileManagerBookmarkRoutes(router, authenticateJWT);

router.get(
  "/transfer/recent",
  authenticateJWT,
  async (req: Request, res: Response) => {
    const userId = (req as AuthenticatedRequest).userId;
    const sourceHostIdQuery = Array.isArray(req.query.sourceHostId)
      ? req.query.sourceHostId[0]
      : req.query.sourceHostId;
    const sourceHostId = sourceHostIdQuery
      ? parseInt(sourceHostIdQuery as string)
      : null;

    if (!isNonEmptyString(userId)) {
      return res.status(400).json({ error: "Invalid userId" });
    }

    if (!sourceHostId) {
      return res.status(400).json({ error: "Source host ID is required" });
    }

    try {
      const recent =
        await createCurrentTransferRecentRepository().listBySourceHost(
          userId,
          sourceHostId,
          10,
        );

      res.json(recent);
    } catch (err) {
      sshLogger.error("Failed to fetch transfer recent destinations", err);
      res.status(500).json({ error: "Failed to fetch recent destinations" });
    }
  },
);

router.post(
  "/transfer/recent",
  authenticateJWT,
  async (req: Request, res: Response) => {
    const userId = (req as AuthenticatedRequest).userId;
    const { sourceHostId, destHostId, destPath, destPathLabel } = req.body;

    if (
      !isNonEmptyString(userId) ||
      !sourceHostId ||
      !destHostId ||
      !destPath
    ) {
      return res.status(400).json({ error: "Invalid data" });
    }

    try {
      const transferRecentRepository = createCurrentTransferRecentRepository();
      await transferRecentRepository.upsertForDestination(userId, {
        sourceHostId,
        destHostId,
        destPath,
        destPathLabel,
      });

      await transferRecentRepository.pruneSourceHost(userId, sourceHostId, 10);

      res.json({ message: "Recent destination saved" });
    } catch (err) {
      sshLogger.error("Failed to save transfer recent destination", err);
      res.status(500).json({ error: "Failed to save recent destination" });
    }
  },
);
registerHostCommandHistoryRoutes(router, authenticateJWT);

async function resolveHostCredentials(
  host: Record<string, unknown>,
  requestingUserId?: string,
  /**
   * Credentials already fetched for this request, keyed by id. The host list
   * preloads them in one query; single-host callers omit it and fall back to
   * fetching the one credential they need.
   */
  preloadedCredentials?: Map<number, HostResolutionCredentialRecord>,
): Promise<Record<string, unknown>> {
  try {
    const ownerId = (host.ownerId || host.userId) as string | undefined;
    if (
      requestingUserId &&
      ownerId &&
      requestingUserId !== ownerId &&
      typeof host.id === "number"
    ) {
      const authHost = host as unknown as HostResolutionHostRecord;
      const needsPersonalCredential = requiresPersonalHostAuthentication(
        authHost,
        "ssh",
      );
      const baseSshOverrideState = {
        required: needsPersonalCredential,
        ownerAuthShared: !!host.shareSshAuth,
      };
      const recipientHost: Record<string, unknown> = {
        ...host,
        credentialId: null,
        password: null,
        key: null,
        keyPassword: null,
        keyType: null,
        authOverrides: {
          ssh: baseSshOverrideState,
        },
      };

      try {
        const resolution = await resolveRecipientSharedHostAuthentication(
          authHost,
          host.id,
          requestingUserId,
          "ssh",
        );

        if (resolution.source === "personal-override") {
          const credential = resolution.credential;
          return {
            ...recipientHost,
            authOverrides: {
              ssh: {
                credentialId: resolution.credentialId,
                required: false,
                ownerAuthShared: !!host.shareSshAuth,
              },
            },
            authType:
              credential.key || credential.privateKey
                ? "key"
                : credential.password
                  ? "password"
                  : "none",
            username: credential.username || recipientHost.username,
            password: credential.password,
            key: credential.privateKey || credential.key,
            keyPassword: credential.keyPassword,
            keyType: credential.keyType,
          };
        }

        if (resolution.source === "owner-shared") {
          if (resolution.authType === "agent") {
            return {
              ...recipientHost,
              authOverrides: {
                ssh: {
                  required: false,
                  ownerAuthShared: true,
                },
              },
              authType: "agent",
            };
          }

          const sharedAuth = resolution.secret;
          if (sharedAuth) {
            const resolvedUsername = pickResolvedUsername(
              recipientHost.username,
              sharedAuth.username,
              host.overrideCredentialUsername,
            );
            return {
              ...recipientHost,
              authOverrides: {
                ssh: {
                  required: false,
                  ownerAuthShared: true,
                },
              },
              authType: sharedAuth.key
                ? "key"
                : sharedAuth.password
                  ? "password"
                  : "none",
              username: resolvedUsername,
              password: sharedAuth.password,
              key: sharedAuth.key,
              keyPassword: sharedAuth.keyPassword,
              keyType: sharedAuth.keyType,
            };
          }
        }

        if (resolution.source === "secretless") {
          return {
            ...recipientHost,
            authOverrides: {
              ssh: {
                required: false,
                ownerAuthShared: !!host.shareSshAuth,
              },
            },
          };
        }
      } catch {
        // A missing/deleted override or snapshot behaves like unavailable auth.
      }

      return recipientHost;
    }

    if (host.credentialId && (host.userId || host.ownerId)) {
      const credentialId = host.credentialId as number;
      const credentialOwnerId = (host.ownerId || host.userId) as string;

      const credential =
        preloadedCredentials?.get(credentialId) ??
        (await createCurrentHostResolutionRepository().findCredentialByIdForUser(
          credentialId,
          credentialOwnerId,
        ));

      if (credential) {
        const resolvedHost: Record<string, unknown> = {
          ...host,
          password: pickResolvedPassword(host.password, credential.password),
          key: credential.key,
          keyPassword: credential.keyPassword,
          keyType: credential.keyType,
        };

        const resolvedUsername = pickResolvedUsername(
          host.username,
          credential.username,
          host.overrideCredentialUsername,
        );
        if (resolvedUsername !== undefined) {
          resolvedHost.username = resolvedUsername;
        }

        return resolvedHost;
      }
    }

    return { ...host };
  } catch (error) {
    sshLogger.warn(
      `Failed to resolve credentials for host ${host.id}: ${getErrorMessage(error)}`,
    );
    return host;
  }
}

registerHostFolderRoutes(router, {
  authenticateJWT,
  statsServerUrl: STATS_SERVER_URL,
});

registerHostBulkRoutes(router, authenticateJWT);

registerHostAutostartRoutes(router, {
  authenticateJWT,
  requireDataAccess,
});

/**
 * @openapi
 * /host/opkssh/token/{hostId}:
 *   get:
 *     summary: Get OPKSSH token status for a host
 *     tags: [SSH]
 *     security:
 *       - bearerAuth: []
 *     parameters:
 *       - name: hostId
 *         in: path
 *         required: true
 *         schema:
 *           type: integer
 *         description: Host ID
 *     responses:
 *       200:
 *         description: Token status retrieved successfully
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 exists:
 *                   type: boolean
 *                   description: Whether a valid token exists
 *                 expiresAt:
 *                   type: string
 *                   format: date-time
 *                   description: Token expiration timestamp
 *                 email:
 *                   type: string
 *                   description: User email from OIDC identity
 *       404:
 *         description: No valid token found
 *       500:
 *         description: Internal server error
 */
router.get(
  "/ssh/opkssh/token/:hostId",
  authenticateJWT,
  requireDataAccess,
  async (req: AuthenticatedRequest, res: Response) => {
    const userId = req.userId;
    const hostId = parseInt(
      Array.isArray(req.params.hostId)
        ? req.params.hostId[0]
        : req.params.hostId,
    );

    if (!userId || isNaN(hostId)) {
      return res.status(400).json({ error: "Invalid request" });
    }

    try {
      const opksshTokenRepository = createCurrentOpksshTokenRepository();
      const tokenData = await opksshTokenRepository.findByUserAndHost(
        userId,
        hostId,
      );

      if (!tokenData) {
        return res.status(404).json({ exists: false });
      }

      const expiresAt = new Date(tokenData.expiresAt);

      if (expiresAt < new Date()) {
        await opksshTokenRepository.deleteByUserAndHost(userId, hostId);
        return res.status(404).json({ exists: false });
      }

      res.json({
        exists: true,
        expiresAt: tokenData.expiresAt,
        email: tokenData.email,
      });
    } catch (error) {
      sshLogger.error("Error retrieving OPKSSH token status", error, {
        operation: "opkssh_token_status_error",
        userId,
        hostId,
      });
      res.status(500).json({ error: "Internal server error" });
    }
  },
);

/**
 * @openapi
 * /host/opkssh/token/{hostId}:
 *   delete:
 *     summary: Delete OPKSSH token for a host
 *     tags: [SSH]
 *     security:
 *       - bearerAuth: []
 *     parameters:
 *       - name: hostId
 *         in: path
 *         required: true
 *         schema:
 *           type: integer
 *         description: Host ID
 *     responses:
 *       200:
 *         description: Token deleted successfully
 *       500:
 *         description: Internal server error
 */
router.delete(
  "/ssh/opkssh/token/:hostId",
  authenticateJWT,
  requireDataAccess,
  async (req: AuthenticatedRequest, res: Response) => {
    const userId = req.userId;
    const hostId = parseInt(
      Array.isArray(req.params.hostId)
        ? req.params.hostId[0]
        : req.params.hostId,
    );

    if (!userId || isNaN(hostId)) {
      return res.status(400).json({ error: "Invalid request" });
    }

    try {
      const { deleteOPKSSHToken } = await import("../../hosts/opkssh-auth.js");
      await deleteOPKSSHToken(userId, hostId);
      res.json({ success: true });
    } catch (error) {
      sshLogger.error("Error deleting OPKSSH token", error, {
        operation: "opkssh_token_delete_error",
        userId,
        hostId,
      });
      res.status(500).json({ error: "Internal server error" });
    }
  },
);

registerHostOpksshRoutes(router);

registerHostNetworkRoutes(router, {
  authenticateJWT,
  requireDataAccess,
});

export default router;
