import type { WebSocket } from "ws";
import { createCurrentHostResolutionRepository } from "../database/repositories/factory.js";
import { sshLogger } from "../utils/logger.js";

interface HostKeyVerificationData {
  scenario: "new" | "changed";
  ip: string;
  port: number;
  hostname?: string;
  fingerprint: string;
  oldFingerprint?: string;
  keyType: string;
  oldKeyType?: string;
  algorithm: string;
}

interface VerificationResponse {
  action: "accept" | "reject";
}

export class SSHHostKeyVerifier {
  /**
   * Pre-fetches the host record from the database so the verifier callback
   * during SSH key exchange doesn't need to do an async DB query. This keeps
   * the key exchange critical path fast, avoiding LoginGraceTime expiry on
   * the remote server (especially important for jump-host tunneled connections).
   */
  static async preloadHostData(hostId: number | null): Promise<{
    hostKeyFingerprint: string | null;
    hostKeyType: string | null;
    hostKeyAlgorithm: string | null;
    hostKeyChangedCount: number | null;
    name: string | null;
  } | null> {
    if (!hostId) return null;
    try {
      return await createCurrentHostResolutionRepository().findHostKeyVerificationData(
        hostId,
      );
    } catch {
      return null;
    }
  }

  static async createHostVerifier(
    hostId: number | null,
    ip: string,
    port: number,
    ws: WebSocket | null,
    userId: string,
    isJumpHost: boolean = false,
    preloadedHost?: Awaited<
      ReturnType<typeof SSHHostKeyVerifier.preloadHostData>
    >,
  ): Promise<(hostkey: Buffer, verify: (valid: boolean) => void) => void> {
    return (hostkey: Buffer, verify: (valid: boolean) => void): void => {
      (async () => {
        try {
          const fingerprint = hostkey.toString("hex");
          const keyType = this.getKeyType(hostkey);
          const algorithm = "sha256";

          if (!hostId) {
            sshLogger.info(
              "Host key verification skipped (no hostId - quick connect)",
              {
                operation: "host_key_skip",
                ip,
                port,
                fingerprint,
                keyType,
                userId,
              },
            );
            verify(true);
            return;
          }

          const host =
            preloadedHost !== undefined
              ? preloadedHost
              : await createCurrentHostResolutionRepository().findHostKeyVerificationData(
                  hostId,
                );

          if (!host) {
            sshLogger.warn(
              "Host not found in database during key verification",
              {
                operation: "host_key_no_host",
                hostId,
                ip,
                port,
                userId,
              },
            );
            verify(true);
            return;
          }

          if (!host.hostKeyFingerprint) {
            if (isJumpHost) {
              await this.storeHostKey(hostId, fingerprint, keyType, algorithm);
              sshLogger.info("Jump host key auto-accepted and stored", {
                operation: "host_key_stored",
                hostId,
                ip,
                port,
                fingerprint,
                keyType,
                userId,
                isJumpHost: true,
              });
              verify(true);
              return;
            }

            if (!ws) {
              sshLogger.warn(
                "No WebSocket available for host key verification prompt",
                {
                  operation: "host_key_no_ws",
                  hostId,
                  ip,
                  port,
                  userId,
                },
              );
              verify(true);
              return;
            }

            const accepted = await this.promptUserForNewKey(
              ws,
              ip,
              port,
              host.name || undefined,
              fingerprint,
              keyType,
              algorithm,
            );

            if (accepted) {
              await this.storeHostKey(hostId, fingerprint, keyType, algorithm);
              sshLogger.info("New host key accepted by user and stored", {
                operation: "host_key_stored",
                hostId,
                ip,
                port,
                fingerprint,
                keyType,
                userId,
              });
            } else {
              sshLogger.warn("User rejected new host key", {
                operation: "host_key_rejected",
                hostId,
                ip,
                port,
                fingerprint,
                keyType,
                userId,
              });
            }

            verify(accepted);
            return;
          }

          if (host.hostKeyFingerprint === fingerprint) {
            sshLogger.info("Host key verified successfully", {
              operation: "host_key_verified",
              hostId,
              ip,
              port,
              fingerprint,
              keyType,
              userId,
            });

            // Verify first, then update the timestamp asynchronously so the
            // DB write doesn't delay the SSH key exchange critical path.
            verify(true);
            createCurrentHostResolutionRepository()
              .touchHostKeyLastVerified(hostId)
              .catch((err) => {
                sshLogger.error("Failed to update hostKeyLastVerified", err, {
                  operation: "host_key_update_timestamp",
                  hostId,
                });
              });
            return;
          }

          sshLogger.error("Host key mismatch detected - SECURITY WARNING", {
            operation: "host_key_mismatch",
            hostId,
            ip,
            port,
            oldFingerprint: host.hostKeyFingerprint,
            newFingerprint: fingerprint,
            oldKeyType: host.hostKeyType,
            newKeyType: keyType,
            userId,
            changeCount: host.hostKeyChangedCount || 0,
          });

          if (isJumpHost) {
            await this.updateHostKey(
              hostId,
              fingerprint,
              keyType,
              algorithm,
              host.hostKeyChangedCount || 0,
            );
            sshLogger.warn("Jump host key changed - auto-accepted", {
              operation: "host_key_updated",
              hostId,
              ip,
              port,
              fingerprint,
              keyType,
              userId,
              isJumpHost: true,
            });
            verify(true);
            return;
          }

          if (!ws) {
            sshLogger.error(
              "Host key changed - please connect via Terminal to verify the new key",
              {
                operation: "host_key_no_ws_reject",
                hostId,
                ip,
                port,
                userId,
                message:
                  "SSH host key has changed. For security, please open a Terminal connection to this host first to verify and accept the new key fingerprint.",
              },
            );
            verify(false);
            return;
          }

          const accepted = await this.promptUserForChangedKey(
            ws,
            ip,
            port,
            host.name || undefined,
            fingerprint,
            host.hostKeyFingerprint,
            keyType,
            host.hostKeyType || "unknown",
            algorithm,
          );

          if (accepted) {
            await this.updateHostKey(
              hostId,
              fingerprint,
              keyType,
              algorithm,
              host.hostKeyChangedCount || 0,
            );
            sshLogger.warn("Changed host key accepted by user", {
              operation: "host_key_updated",
              hostId,
              ip,
              port,
              oldFingerprint: host.hostKeyFingerprint,
              newFingerprint: fingerprint,
              userId,
              changeCount: (host.hostKeyChangedCount || 0) + 1,
            });
          } else {
            sshLogger.error("User rejected changed host key", {
              operation: "host_key_change_rejected",
              hostId,
              ip,
              port,
              userId,
            });
          }

          verify(accepted);
        } catch (error) {
          sshLogger.error("Error in host key verification", error, {
            operation: "host_key_error",
            hostId,
            ip,
            port,
            userId,
          });
          verify(false);
        }
      })();
    };
  }

  private static async storeHostKey(
    hostId: number,
    fingerprint: string,
    keyType: string,
    algorithm: string,
  ): Promise<void> {
    await createCurrentHostResolutionRepository().storeHostKey(
      hostId,
      fingerprint,
      keyType,
      algorithm,
    );
  }

  private static async updateHostKey(
    hostId: number,
    fingerprint: string,
    keyType: string,
    algorithm: string,
    currentChangeCount: number,
  ): Promise<void> {
    await createCurrentHostResolutionRepository().updateHostKey(
      hostId,
      fingerprint,
      keyType,
      algorithm,
      currentChangeCount,
    );
  }

  private static async promptUserForNewKey(
    ws: WebSocket,
    ip: string,
    port: number,
    hostname: string | undefined,
    fingerprint: string,
    keyType: string,
    algorithm: string,
  ): Promise<boolean> {
    return new Promise<boolean>((resolve) => {
      const timeout = setTimeout(() => {
        ws.removeListener("message", messageHandler);
        sshLogger.warn("Host key verification timeout (new key)", {
          operation: "host_key_timeout",
          ip,
          port,
        });
        resolve(false);
      }, 60000);

      const messageHandler = (data: Buffer) => {
        try {
          const message = JSON.parse(data.toString());

          if (message.type === "host_key_verification_response") {
            clearTimeout(timeout);
            ws.removeListener("message", messageHandler);

            const response = message.data as VerificationResponse;
            resolve(response.action === "accept");
          }
        } catch (error) {
          sshLogger.error(
            "Error parsing host key verification response",
            error,
          );
        }
      };

      ws.on("message", messageHandler);

      const verificationData: HostKeyVerificationData = {
        scenario: "new",
        ip,
        port,
        hostname,
        fingerprint,
        keyType,
        algorithm,
      };

      ws.send(
        JSON.stringify({
          type: "host_key_verification_required",
          data: verificationData,
        }),
      );
    });
  }

  private static async promptUserForChangedKey(
    ws: WebSocket,
    ip: string,
    port: number,
    hostname: string | undefined,
    fingerprint: string,
    oldFingerprint: string,
    keyType: string,
    oldKeyType: string,
    algorithm: string,
  ): Promise<boolean> {
    return new Promise<boolean>((resolve) => {
      const timeout = setTimeout(() => {
        ws.removeListener("message", messageHandler);
        sshLogger.error("Host key verification timeout (changed key)", {
          operation: "host_key_timeout",
          ip,
          port,
        });
        resolve(false);
      }, 120000);

      const messageHandler = (data: Buffer) => {
        try {
          const message = JSON.parse(data.toString());

          if (message.type === "host_key_verification_response") {
            clearTimeout(timeout);
            ws.removeListener("message", messageHandler);

            const response = message.data as VerificationResponse;
            resolve(response.action === "accept");
          }
        } catch (error) {
          sshLogger.error(
            "Error parsing host key verification response",
            error,
          );
        }
      };

      ws.on("message", messageHandler);

      const verificationData: HostKeyVerificationData = {
        scenario: "changed",
        ip,
        port,
        hostname,
        fingerprint,
        oldFingerprint,
        keyType,
        oldKeyType,
        algorithm,
      };

      ws.send(
        JSON.stringify({
          type: "host_key_changed",
          data: verificationData,
        }),
      );
    });
  }

  private static getKeyType(key: Buffer): string {
    try {
      if (key.length < 4) {
        return "unknown";
      }

      const typeLength = key.readUInt32BE(0);
      if (typeLength > key.length - 4 || typeLength > 256) {
        return "unknown";
      }

      const keyType = key.toString("utf8", 4, 4 + typeLength);

      if (
        (keyType && keyType.startsWith("ssh-")) ||
        keyType.startsWith("ecdsa-")
      ) {
        return keyType;
      }

      return "unknown";
    } catch (error) {
      sshLogger.error("Error parsing SSH key type", error);
      return "unknown";
    }
  }
}
