import { getErrorMessage } from "../../utils/error-message.js";
import express from "express";
import { GuacamoleTokenService } from "./token-service.js";
import { withRecordingSettings } from "./recording-settings.js";
import { guacLogger } from "../../utils/logger.js";
import { AuthManager } from "../../utils/auth-manager.js";
import { PermissionManager } from "../../utils/permission-manager.js";
import net from "net";
import crypto from "crypto";
import path from "path";
import type { AuthenticatedRequest } from "../../../types/index.js";
import {
  createCurrentHostResolutionRepository,
  createCurrentSettingsRepository,
} from "../../database/repositories/factory.js";
import { resolveGuacdOptions } from "../../utils/guacd-config.js";
import { createJumpHostChain } from "../jump-host-chain.js";
import { waitForGuacdOpen } from "./guacamole-server.js";
import {
  logAudit,
  getAuditUsername,
  getRequestMeta,
} from "../../utils/audit-logger.js";
import { resolveJumpTunnelEndpoint } from "./jump-tunnel-endpoint.js";
import { buildRdpSettings, resolveRdpDomain } from "./rdp-settings.js";

const router = express.Router();
const tokenService = GuacamoleTokenService.getInstance();
const authManager = AuthManager.getInstance();
const DATA_DIR = process.env.DATA_DIR || "./db/data";

router.use(authManager.createAuthMiddleware());

/**
 * @openapi
 * /guacamole/token:
 *   post:
 *     summary: Generate an encrypted Guacamole connection token
 *     description: Creates an AES-256-CBC encrypted token for guacamole-lite with the given connection parameters
 *     tags:
 *       - Guacamole
 *     security:
 *       - bearerAuth: []
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             type: object
 *             required:
 *               - type
 *               - hostname
 *             properties:
 *               type:
 *                 type: string
 *                 enum: [rdp, vnc, telnet]
 *               hostname:
 *                 type: string
 *               port:
 *                 type: integer
 *               username:
 *                 type: string
 *               password:
 *                 type: string
 *               domain:
 *                 type: string
 *     responses:
 *       200:
 *         description: Encrypted connection token
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 token:
 *                   type: string
 *       400:
 *         description: Invalid request
 *       500:
 *         description: Server error
 */
router.post("/token", async (req, res) => {
  try {
    const { type, hostname, port, username, password, domain, ...rawOptions } =
      req.body;

    // Strip "auto" sentinel values before forwarding to guacd
    const options: Record<string, unknown> = {};
    for (const [key, value] of Object.entries(rawOptions)) {
      if (value !== "auto") options[key] = value;
    }

    if (!type || !hostname) {
      return res
        .status(400)
        .json({ error: "Missing required fields: type and hostname" });
    }

    if (!["rdp", "vnc", "telnet"].includes(type)) {
      return res.status(400).json({
        error: "Invalid connection type. Must be rdp, vnc, or telnet",
      });
    }

    let token: string;

    switch (type) {
      case "rdp":
        token = tokenService.createRdpToken(
          hostname,
          username || "",
          password || "",
          {
            port: port || 3389,
            domain,
            ...options,
          },
        );
        break;
      case "vnc":
        token = tokenService.createVncToken(
          hostname,
          username || undefined,
          password,
          {
            port: port || 5900,
            ...options,
          },
        );
        break;
      case "telnet":
        token = tokenService.createTelnetToken(hostname, username, password, {
          port: port || 23,
          ...options,
        });
        break;
      default:
        return res.status(400).json({ error: "Invalid connection type" });
    }

    res.json({ token });
  } catch (error) {
    guacLogger.error("Failed to generate guacamole token", error, {
      operation: "guac_token_error",
    });
    res.status(500).json({ error: "Failed to generate connection token" });
  }
});

/**
 * @openapi
 * /guacamole/connect-host/{hostId}:
 *   post:
 *     summary: Generate Guacamole connection token from host configuration
 *     description: Fetches host configuration from database and generates a connection token for RDP/VNC/Telnet
 *     tags:
 *       - Guacamole
 *     security:
 *       - bearerAuth: []
 *     parameters:
 *       - in: path
 *         name: hostId
 *         required: true
 *         schema:
 *           type: integer
 *         description: Host ID to connect to
 *     requestBody:
 *       required: false
 *       content:
 *         application/json:
 *           schema:
 *             type: object
 *             properties:
 *               protocol:
 *                 type: string
 *                 enum: [rdp, vnc, telnet]
 *                 description: Override the host's default connection type
 *               promptedUsername:
 *                 type: string
 *                 description: Username for this connection only, used when the host's RDP auth type is "none". Not persisted.
 *               promptedPassword:
 *                 type: string
 *                 description: Password for this connection only, used when the host's RDP auth type is "none". Not persisted.
 *     responses:
 *       200:
 *         description: Connection token generated successfully
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 token:
 *                   type: string
 *                   description: Encrypted connection token
 *                 guacamoleConnectionId:
 *                   type: string
 *                   nullable: true
 *                   description: guacd's own connection id for this session, once the handshake completes. Used to mint session-share join tokens.
 *       400:
 *         description: Invalid request or unsupported connection type
 *       403:
 *         description: Access denied to host
 *       404:
 *         description: Host not found
 *       500:
 *         description: Server error
 */
router.post(
  "/connect-host/:hostId",
  async (req: express.Request, res: express.Response) => {
    try {
      const userId = (req as AuthenticatedRequest).userId!;
      const hostId = Number.parseInt(String(req.params.hostId), 10);

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

      const hostResolutionRepository = createCurrentHostResolutionRepository();
      // Decrypt under the owner's DEK; shared hosts carry owner-encrypted fields.
      const hostOwnerId =
        await hostResolutionRepository.findHostOwnerId(hostId);
      const host = hostOwnerId
        ? await hostResolutionRepository.findHostById(hostId, hostOwnerId)
        : null;

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

      if (host.userId !== userId) {
        const permissionManager = PermissionManager.getInstance();
        const accessInfo = await permissionManager.canAccessHost(
          userId,
          hostId,
          "connect",
        );

        if (!accessInfo.hasAccess) {
          guacLogger.warn("User attempted to access host without permission", {
            operation: "guac_access_denied",
            userId,
            hostId,
          });
          return res.status(403).json({ error: "Access denied to this host" });
        }
      }

      const requestedProtocol = req.body?.protocol as string | undefined;
      const connectionType =
        requestedProtocol || (host.connectionType as string);

      if (!["rdp", "vnc", "telnet"].includes(connectionType)) {
        return res.status(400).json({
          error: `Connection type '${connectionType}' is not supported for remote desktop. Only RDP, VNC, and Telnet are supported.`,
        });
      }

      // Old hosts only had connectionType set; enableRdp/enableVnc/enableTelnet defaulted to false.
      // Apply the same migration fallback used in host.ts GET routes.
      const ct = host.connectionType as string;
      const rdpRaw = !!host.enableRdp;
      const vncRaw = !!host.enableVnc;
      const telRaw = !!host.enableTelnet;
      const isMigratedNonSsh =
        !rdpRaw && !vncRaw && !telRaw && ct && ct !== "ssh";
      const protocolEnabledMap: Record<string, boolean> = {
        rdp: isMigratedNonSsh ? ct === "rdp" : rdpRaw,
        vnc: isMigratedNonSsh ? ct === "vnc" : vncRaw,
        telnet: isMigratedNonSsh ? ct === "telnet" : telRaw,
      };
      if (!protocolEnabledMap[connectionType]) {
        return res.status(400).json({
          error: `${connectionType.toUpperCase()} is not enabled for this host.`,
        });
      }

      let guacConfig: Record<string, unknown> = {};
      if (host.guacamoleConfig) {
        try {
          guacConfig =
            typeof host.guacamoleConfig === "string"
              ? JSON.parse(host.guacamoleConfig as string)
              : (host.guacamoleConfig as Record<string, unknown>);
        } catch (error) {
          guacLogger.warn("Failed to parse guacamole config", {
            operation: "guac_config_parse_error",
            hostId,
            error: getErrorMessage(error),
          });
        }
      }

      // Strip "auto" sentinel values — these mean "use guacd default" in the UI
      // but guacd doesn't recognise "auto" as a valid parameter value.
      for (const key of Object.keys(guacConfig)) {
        if (guacConfig[key] === "auto") {
          delete guacConfig[key];
        }
      }

      // Extract per-connection guacd proxy settings before passing the rest as connection settings
      const perConnectionGuacdHost = guacConfig["guacd-hostname"] as
        string | undefined;
      const perConnectionGuacdPortRaw = guacConfig["guacd-port"];
      const perConnectionGuacdPort = perConnectionGuacdPortRaw
        ? parseInt(String(perConnectionGuacdPortRaw), 10) || undefined
        : undefined;
      delete guacConfig["guacd-hostname"];
      delete guacConfig["guacd-port"];

      if (guacConfig.dpi != null) {
        const parsed = parseInt(String(guacConfig.dpi), 10);
        guacConfig.dpi =
          Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
      }

      const hostRecord = host as Record<string, unknown>;
      const hostRepository = hostResolutionRepository;
      const isSharedConnection = host.userId !== userId;

      if (isSharedConnection) {
        // Recipients never read the owner's raw secrets; wipe them and use
        // the per-recipient snapshot for the requested protocol instead.
        host.password = null;
        host.rdpUser = null;
        host.rdpPassword = null;
        host.vncUser = null;
        host.vncPassword = null;
        host.telnetUser = null;
        host.telnetPassword = null;

        try {
          const { SharedHostSecretsManager } =
            await import("../../utils/shared-host-secrets-manager.js");
          const secret =
            await SharedHostSecretsManager.getInstance().getSecretForUser(
              hostId,
              userId,
              connectionType as "rdp" | "vnc" | "telnet",
            );
          if (secret) {
            if (connectionType === "rdp") {
              host.rdpUser = secret.username ?? null;
              host.rdpPassword = secret.password ?? null;
              if (secret.domain) host.rdpDomain = secret.domain;
            } else if (connectionType === "vnc") {
              host.vncUser = secret.username ?? null;
              host.vncPassword = secret.password ?? null;
            } else if (connectionType === "telnet") {
              host.telnetUser = secret.username ?? null;
              host.telnetPassword = secret.password ?? null;
            }
          }
        } catch (e) {
          guacLogger.warn("Failed to resolve shared host secret", {
            operation: "guac_shared_secret_resolve",
            hostId,
            protocol: connectionType,
            error: getErrorMessage(e, "Unknown"),
          });
        }
      } else {
        // Backward compat: if authType is not stored but a credentialId is, treat as credential mode
        const rdpEffectiveAuthType =
          (host.rdpAuthType as string) ||
          (host.rdpCredentialId ? "credential" : "direct");
        const vncEffectiveAuthType =
          (host.vncAuthType as string) ||
          (host.vncCredentialId ? "credential" : "direct");
        const telnetEffectiveAuthType =
          (host.telnetAuthType as string) ||
          (hostRecord.telnetCredentialId ? "credential" : "direct");

        if (rdpEffectiveAuthType === "credential" && host.rdpCredentialId) {
          try {
            const cred = await hostRepository.findCredentialByIdForUser(
              host.rdpCredentialId as number,
              host.userId as string,
            );
            if (cred) {
              if (cred.username) host.rdpUser = cred.username;
              if (cred.password) host.rdpPassword = cred.password;
              // domain is never sourced from credential
            }
          } catch (e) {
            guacLogger.warn("Failed to resolve RDP credential", {
              operation: "guac_rdp_credential_resolve",
              hostId,
              error: getErrorMessage(e, "Unknown"),
            });
          }
        }

        if (vncEffectiveAuthType === "credential" && host.vncCredentialId) {
          try {
            const cred = await hostRepository.findCredentialByIdForUser(
              host.vncCredentialId as number,
              host.userId as string,
            );
            if (cred) {
              if (cred.password) host.vncPassword = cred.password;
              if (cred.username) host.vncUser = cred.username;
            }
          } catch (e) {
            guacLogger.warn("Failed to resolve VNC credential", {
              operation: "guac_vnc_credential_resolve",
              hostId,
              error: getErrorMessage(e, "Unknown"),
            });
          }
        }

        if (
          telnetEffectiveAuthType === "credential" &&
          hostRecord.telnetCredentialId
        ) {
          try {
            const cred = await hostRepository.findCredentialByIdForUser(
              hostRecord.telnetCredentialId as number,
              host.userId as string,
            );
            if (cred) {
              if (cred.username) host.telnetUser = cred.username;
              if (cred.password) host.telnetPassword = cred.password;
            }
          } catch (e) {
            guacLogger.warn("Failed to resolve Telnet credential", {
              operation: "guac_telnet_credential_resolve",
              hostId,
              error: getErrorMessage(e, "Unknown"),
            });
          }
        }
      }

      let token: string;
      let hostname = host.ip as string;
      let port = host.port as number;
      let username: string;
      let password: string;

      const rdpAuthTypeForConnect = isSharedConnection
        ? null
        : (host.rdpAuthType as string) ||
          (host.rdpCredentialId ? "credential" : "direct");

      switch (connectionType) {
        case "rdp":
          if (rdpAuthTypeForConnect === "none") {
            username = String(req.body?.promptedUsername || "");
            password = String(req.body?.promptedPassword || "");
          } else {
            username =
              (host.rdpUser as string) || (host.username as string) || "";
            password =
              (host.rdpPassword as string) || (host.password as string) || "";
          }
          port = (host.rdpPort as number) || port || 3389;
          break;
        case "vnc":
          username = (host.vncUser as string) || "";
          password =
            (host.vncPassword as string) || (host.password as string) || "";
          port = (host.vncPort as number) || port || 5900;
          break;
        case "telnet":
          username = (host.telnetUser as string) || "";
          password =
            (host.telnetPassword as string) || (host.password as string) || "";
          port = (host.telnetPort as number) || port || 23;
          break;
        default:
          username = "";
          password = "";
      }
      const storedDomain =
        (host.rdpDomain as string) || (host.domain as string) || "";
      const domain = resolveRdpDomain(
        rdpAuthTypeForConnect,
        req.body?.promptedDomain,
        storedDomain,
      );

      // Establish SSH tunnel if jump hosts are configured
      let jumpHosts: Array<{ hostId: number }> = [];
      if (host.jumpHosts) {
        try {
          jumpHosts =
            typeof host.jumpHosts === "string"
              ? JSON.parse(host.jumpHosts as string)
              : (host.jumpHosts as Array<{ hostId: number }>);
        } catch {
          jumpHosts = [];
        }
      }

      if (jumpHosts.length > 0) {
        try {
          let guacdUrl: string | undefined;
          try {
            guacdUrl =
              (await createCurrentSettingsRepository().get("guac_url")) ??
              undefined;
          } catch {
            // Environment/default guacd configuration remains available.
          }
          const guacdHost =
            perConnectionGuacdHost || resolveGuacdOptions(guacdUrl).host;
          const tunnelEndpoint = resolveJumpTunnelEndpoint(guacdHost);

          // The chain dials the first hop through that hop's own SOCKS5
          // settings; the target host's proxy config does not apply to it.
          const jumpClient = await createJumpHostChain(jumpHosts, userId);

          if (!jumpClient) {
            guacLogger.error(
              "Failed to establish jump host chain for guacamole",
              undefined,
              { operation: "guac_ssh_tunnel_error", hostId },
            );
            return res.status(500).json({
              error: "Failed to establish SSH tunnel to remote host",
            });
          }

          const targetHostname = hostname;
          const targetPort = port;
          const tunnelPort = await new Promise<number>((resolve, reject) => {
            const server = net.createServer((sock) => {
              jumpClient.forwardOut(
                "127.0.0.1",
                0,
                targetHostname,
                targetPort,
                (err, stream) => {
                  if (err) {
                    sock.destroy();
                    return;
                  }
                  sock.pipe(stream).pipe(sock);
                },
              );
            });
            server.on("error", reject);
            server.listen(0, tunnelEndpoint.bindHost, () => {
              const addr = server.address() as net.AddressInfo;
              // Auto-cleanup after 1 hour
              setTimeout(
                () => {
                  server.close();
                  jumpClient.end();
                },
                60 * 60 * 1000,
              );
              resolve(addr.port);
            });
          });
          hostname = tunnelEndpoint.advertisedHost;
          port = tunnelPort;
          guacLogger.info("SSH tunnel established for guacamole", {
            operation: "guac_ssh_tunnel",
            hostId,
            tunnelPort,
          });
        } catch (tunnelError) {
          guacLogger.error("Failed to establish SSH tunnel", tunnelError, {
            operation: "guac_ssh_tunnel_error",
            hostId,
          });
          return res.status(500).json({
            error: "Failed to establish SSH tunnel to remote host",
          });
        }
      }

      const guacdOverrides = {
        ...(perConnectionGuacdHost
          ? { guacdHost: perConnectionGuacdHost }
          : {}),
        ...(perConnectionGuacdPort
          ? { guacdPort: perConnectionGuacdPort }
          : {}),
      };
      const recordingEnabled =
        connectionType !== "vnc" && host.enableSessionLogging !== false;
      const recordingName = `${crypto.randomUUID()}.guac`;
      const recordingPath =
        process.env.GUACD_RECORDING_PATH ||
        process.env.GUACD_RECORDING_BACKEND_PATH ||
        path.resolve(DATA_DIR, "session_recordings", "guacamole");
      const recordingMetadata = recordingEnabled
        ? {
            hostId,
            userId,
            protocol: connectionType as "rdp" | "vnc" | "telnet",
            path: recordingName,
            guacdPath: recordingPath,
            startedAt: new Date().toISOString(),
          }
        : undefined;
      if (recordingEnabled) {
        guacConfig = withRecordingSettings(
          guacConfig,
          recordingPath,
          recordingName,
        );
      }

      const termixConnectId = crypto.randomUUID();
      const termixMeta = {
        termixConnectId,
        hostId,
        ownerUserId: userId,
        protocol: connectionType as "rdp" | "vnc" | "telnet",
      };

      switch (connectionType) {
        case "rdp":
          if (guacConfig["enable-drive"] && !guacConfig["drive-path"]) {
            guacConfig["drive-path"] = "/drive";
            guacConfig["create-drive-path"] = true;
          }
          token = tokenService.createRdpToken(
            hostname,
            username,
            password,
            buildRdpSettings({
              port,
              domain,
              security:
                (host.rdpSecurity as string) ||
                (host.security as string) ||
                undefined,
              ignoreCert:
                host.rdpIgnoreCert !== undefined
                  ? !!host.rdpIgnoreCert
                  : host.ignoreCert !== undefined
                    ? !!host.ignoreCert
                    : true,
              guacConfig,
              guacdOverrides,
            }),
            recordingMetadata,
            termixMeta,
          );
          break;
        case "vnc":
          token = tokenService.createVncToken(
            hostname,
            username || undefined,
            password,
            {
              port,
              ...guacConfig,
              ...guacdOverrides,
            },
            recordingMetadata,
            termixMeta,
          );
          break;
        case "telnet":
          token = tokenService.createTelnetToken(
            hostname,
            username,
            password,
            {
              port,
              ...guacConfig,
              ...guacdOverrides,
            },
            recordingMetadata,
            termixMeta,
          );
          break;
        default:
          return res.status(400).json({ error: "Invalid connection type" });
      }

      const sessionInfo = await waitForGuacdOpen(termixConnectId, 10000);

      const { ipAddress, userAgent } = getRequestMeta(req);
      await logAudit({
        userId,
        username: await getAuditUsername(userId),
        action: `${connectionType}_connect`,
        resourceType: "host",
        resourceId: String(hostId),
        resourceName: `${hostname}:${port}`,
        ipAddress,
        userAgent,
        success: true,
      });

      res.json({
        token,
        guacamoleConnectionId: sessionInfo?.guacamoleConnectionId ?? null,
      });
    } catch (error) {
      guacLogger.error("Failed to generate guacamole token for host", error, {
        operation: "guac_host_token_error",
      });
      res.status(500).json({ error: "Failed to generate connection token" });
    }
  },
);

/**
 * GET /guacamole/status
 * Check if guacd is reachable
 */
router.get("/status", async (req, res) => {
  try {
    let dbUrl: string | undefined;
    try {
      dbUrl =
        (await createCurrentSettingsRepository().get("guac_url")) ?? undefined;
    } catch {
      // Fall back to env vars
    }
    const { host: guacdHost, port: guacdPort } = resolveGuacdOptions(dbUrl);

    const net = await import("net");

    const checkConnection = (): Promise<boolean> => {
      return new Promise((resolve) => {
        const socket = new net.Socket();
        socket.setTimeout(3000);

        socket.on("connect", () => {
          socket.destroy();
          resolve(true);
        });

        socket.on("timeout", () => {
          socket.destroy();
          resolve(false);
        });

        socket.on("error", () => {
          socket.destroy();
          resolve(false);
        });

        socket.connect(guacdPort, guacdHost);
      });
    };

    const isConnected = await checkConnection();

    res.json({
      guacd: {
        host: guacdHost,
        port: guacdPort,
        status: isConnected ? "connected" : "disconnected",
      },
      websocket: {
        port: 30008,
        status: "running",
      },
    });
  } catch (error) {
    guacLogger.error("Failed to check guacamole status", error, {
      operation: "guac_status_error",
    });
    res.status(500).json({ error: "Failed to check status" });
  }
});

export default router;
