import { getErrorMessage } from "../../utils/error-message.js";
import type {
  AuthenticatedRequest,
  CredentialBackend,
} from "../../../types/index.js";
import type { Request, RequestHandler, Response, Router } from "express";
import ssh2Pkg from "ssh2";
import { createCurrentHostResolutionRepository } from "../repositories/factory.js";
import { preparePrivateKeyForSSH2 } from "../../utils/ssh-key-utils.js";

const { Client } = ssh2Pkg;

async function deploySSHKeyToHost(
  hostConfig: Record<string, unknown>,
  credData: CredentialBackend,
): Promise<{ success: boolean; message?: string; error?: string }> {
  const publicKey = credData.publicKey as string;
  return new Promise((resolve) => {
    const conn = new Client();

    const connectionTimeout = setTimeout(() => {
      conn.destroy();
      resolve({ success: false, error: "Connection timeout" });
    }, 120000);

    conn.on("ready", async () => {
      clearTimeout(connectionTimeout);

      try {
        await new Promise<void>((resolveCmd, rejectCmd) => {
          const cmdTimeout = setTimeout(() => {
            rejectCmd(new Error("mkdir command timeout"));
          }, 10000);

          conn.exec(
            "test -d ~/.ssh || mkdir -p ~/.ssh; chmod 700 ~/.ssh",
            (err, stream) => {
              if (err) {
                clearTimeout(cmdTimeout);
                return rejectCmd(err);
              }

              stream.on("close", (code) => {
                clearTimeout(cmdTimeout);
                if (code === 0) {
                  resolveCmd();
                } else {
                  rejectCmd(
                    new Error(`mkdir command failed with code ${code}`),
                  );
                }
              });

              stream.on("data", () => {
                // Ignore output
              });
            },
          );
        });

        const keyExists = await new Promise<boolean>(
          (resolveCheck, rejectCheck) => {
            const checkTimeout = setTimeout(() => {
              rejectCheck(new Error("Key check timeout"));
            }, 5000);

            let actualPublicKey = publicKey;
            try {
              const parsed = JSON.parse(publicKey);
              if (parsed.data) {
                actualPublicKey = parsed.data;
              }
            } catch {
              // Ignore parse errors
            }

            const keyParts = actualPublicKey.trim().split(" ");
            if (keyParts.length < 2) {
              clearTimeout(checkTimeout);
              return rejectCheck(
                new Error(
                  "Invalid public key format - must contain at least 2 parts",
                ),
              );
            }

            const keyPattern = keyParts[1];
            if (!/^[A-Za-z0-9+/]+={0,2}$/.test(keyPattern)) {
              clearTimeout(checkTimeout);
              return rejectCheck(new Error("Invalid public key data"));
            }

            conn.exec(
              `if [ -f ~/.ssh/authorized_keys ]; then grep -F "${keyPattern}" ~/.ssh/authorized_keys >/dev/null 2>&1; echo $?; else echo 1; fi`,
              (err, stream) => {
                if (err) {
                  clearTimeout(checkTimeout);
                  return rejectCheck(err);
                }

                let output = "";
                stream.on("data", (data) => {
                  output += data.toString();
                });

                stream.on("close", () => {
                  clearTimeout(checkTimeout);
                  const exists = output.trim() === "0";
                  resolveCheck(exists);
                });
              },
            );
          },
        );

        if (keyExists) {
          conn.end();
          resolve({ success: true, message: "SSH key already deployed" });
          return;
        }

        await new Promise<void>((resolveAdd, rejectAdd) => {
          const addTimeout = setTimeout(() => {
            rejectAdd(new Error("Key add timeout"));
          }, 30000);

          let actualPublicKey = publicKey;
          try {
            const parsed = JSON.parse(publicKey);
            if (parsed.data) {
              actualPublicKey = parsed.data;
            }
          } catch {
            // Ignore parse errors
          }

          const escapedKey = actualPublicKey
            .replace(/\\/g, "\\\\")
            .replace(/'/g, "'\\''");
          const escapedName = credData.name
            .replace(/\\/g, "\\\\")
            .replace(/'/g, "'\\''");

          conn.exec(
            `printf '%s\n' '${escapedKey} ${escapedName}@Termix' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys`,
            (err, stream) => {
              if (err) {
                clearTimeout(addTimeout);
                return rejectAdd(err);
              }

              stream.on("data", () => {
                // Consume output
              });

              stream.on("close", (code) => {
                clearTimeout(addTimeout);
                if (code === 0) {
                  resolveAdd();
                } else {
                  rejectAdd(
                    new Error(`Key deployment failed with code ${code}`),
                  );
                }
              });
            },
          );
        });

        const verifySuccess = await new Promise<boolean>(
          (resolveVerify, rejectVerify) => {
            const verifyTimeout = setTimeout(() => {
              rejectVerify(new Error("Key verification timeout"));
            }, 5000);

            let actualPublicKey = publicKey;
            try {
              const parsed = JSON.parse(publicKey);
              if (parsed.data) {
                actualPublicKey = parsed.data;
              }
            } catch {
              // Ignore parse errors
            }

            const keyParts = actualPublicKey.trim().split(" ");
            if (keyParts.length < 2) {
              clearTimeout(verifyTimeout);
              return rejectVerify(
                new Error(
                  "Invalid public key format - must contain at least 2 parts",
                ),
              );
            }

            const keyPattern = keyParts[1];
            if (!/^[A-Za-z0-9+/]+={0,2}$/.test(keyPattern)) {
              clearTimeout(verifyTimeout);
              return rejectVerify(new Error("Invalid public key data"));
            }
            conn.exec(
              `grep -F "${keyPattern}" ~/.ssh/authorized_keys >/dev/null 2>&1; echo $?`,
              (err, stream) => {
                if (err) {
                  clearTimeout(verifyTimeout);
                  return rejectVerify(err);
                }

                let output = "";
                stream.on("data", (data) => {
                  output += data.toString();
                });

                stream.on("close", () => {
                  clearTimeout(verifyTimeout);
                  const verified = output.trim() === "0";
                  resolveVerify(verified);
                });
              },
            );
          },
        );

        conn.end();

        if (verifySuccess) {
          resolve({ success: true, message: "SSH key deployed successfully" });
        } else {
          resolve({
            success: false,
            error: "Key deployment verification failed",
          });
        }
      } catch (error) {
        conn.end();
        resolve({
          success: false,
          error: getErrorMessage(error, "Deployment failed"),
        });
      }
    });

    conn.on("error", (err) => {
      clearTimeout(connectionTimeout);
      let errorMessage = err.message;

      if (
        err.message.includes("All configured authentication methods failed")
      ) {
        errorMessage =
          "Authentication failed. Please check your credentials and ensure the SSH service is running.";
      } else if (
        err.message.includes("ENOTFOUND") ||
        err.message.includes("ENOENT")
      ) {
        errorMessage = "Could not resolve hostname or connect to server.";
      } else if (err.message.includes("ECONNREFUSED")) {
        errorMessage =
          "Connection refused. The server may not be running or the port may be incorrect.";
      } else if (err.message.includes("ETIMEDOUT")) {
        errorMessage =
          "Connection timed out. Check your network connection and server availability.";
      } else if (
        err.message.includes("authentication failed") ||
        err.message.includes("Permission denied")
      ) {
        errorMessage =
          "Authentication failed. Please check your username and password/key.";
      }

      resolve({ success: false, error: errorMessage });
    });

    try {
      const connectionConfig: Record<string, unknown> = {
        host: hostConfig.ip,
        port: hostConfig.port || 22,
        username: hostConfig.username,
        readyTimeout: 60000,
        keepaliveInterval: 30000,
        keepaliveCountMax: 3,
        tcpKeepAlive: true,
        tcpKeepAliveInitialDelay: 30000,
        algorithms: {
          kex: [
            "diffie-hellman-group14-sha256",
            "diffie-hellman-group14-sha1",
            "diffie-hellman-group1-sha1",
            "diffie-hellman-group-exchange-sha256",
            "diffie-hellman-group-exchange-sha1",
            "ecdh-sha2-nistp256",
            "ecdh-sha2-nistp384",
            "ecdh-sha2-nistp521",
          ],
          cipher: [
            "aes128-ctr",
            "aes192-ctr",
            "aes256-ctr",
            "aes128-gcm@openssh.com",
            "aes256-gcm@openssh.com",
            "aes128-cbc",
            "aes192-cbc",
            "aes256-cbc",
            "3des-cbc",
          ],
          hmac: [
            "hmac-sha2-256-etm@openssh.com",
            "hmac-sha2-512-etm@openssh.com",
            "hmac-sha2-256",
            "hmac-sha2-512",
            "hmac-sha1",
            "hmac-md5",
          ],
          compress: ["none", "zlib@openssh.com", "zlib"],
        },
      };

      if (hostConfig.authType === "password" && hostConfig.password) {
        connectionConfig.password = hostConfig.password;
      } else if (hostConfig.authType === "key" && hostConfig.privateKey) {
        try {
          const privateKey = hostConfig.privateKey as string;
          connectionConfig.privateKey = preparePrivateKeyForSSH2(
            privateKey,
            hostConfig.keyPassword as string | undefined,
          );

          if (hostConfig.keyPassword) {
            connectionConfig.passphrase = hostConfig.keyPassword;
          }
        } catch (keyError) {
          clearTimeout(connectionTimeout);
          resolve({
            success: false,
            error: `Invalid SSH key format: ${getErrorMessage(keyError)}`,
          });
          return;
        }
      } else {
        clearTimeout(connectionTimeout);
        resolve({
          success: false,
          error: `Invalid authentication configuration. Auth type: ${hostConfig.authType}, has password: ${!!hostConfig.password}, has key: ${!!hostConfig.privateKey}`,
        });
        return;
      }

      conn.connect(connectionConfig);
    } catch (error) {
      clearTimeout(connectionTimeout);
      resolve({
        success: false,
        error: getErrorMessage(error, "Connection failed"),
      });
    }
  });
}

export function registerCredentialDeployRoutes(
  router: Router,
  authenticateJWT: RequestHandler,
): void {
  /**
   * @openapi
   * /credentials/{id}/deploy-to-host:
   *   post:
   *     summary: Deploy SSH key to a host
   *     description: Deploys an SSH public key to a target host's authorized_keys file.
   *     tags:
   *       - Credentials
   *     parameters:
   *       - in: path
   *         name: id
   *         required: true
   *         schema:
   *           type: integer
   *     requestBody:
   *       required: true
   *       content:
   *         application/json:
   *           schema:
   *             type: object
   *             properties:
   *               targetHostId:
   *                 type: integer
   *     responses:
   *       200:
   *         description: SSH key deployed successfully.
   *       400:
   *         description: Credential ID and target host ID are required.
   *       401:
   *         description: Authentication required.
   *       404:
   *         description: Credential or target host not found.
   *       500:
   *         description: Failed to deploy SSH key.
   */
  router.post(
    "/:id/deploy-to-host",
    authenticateJWT,
    async (req: Request, res: Response) => {
      const id = Array.isArray(req.params.id)
        ? req.params.id[0]
        : req.params.id;
      const credentialId = parseInt(id);
      const { targetHostId } = req.body;

      if (!credentialId || !targetHostId) {
        return res.status(400).json({
          success: false,
          error: "Credential ID and target host ID are required",
        });
      }

      try {
        const userId = (req as AuthenticatedRequest).userId;
        if (!userId) {
          return res.status(401).json({
            success: false,
            error: "Authentication required",
          });
        }

        const repository = createCurrentHostResolutionRepository();
        const credential = await repository.findCredentialByIdForUser(
          credentialId,
          userId,
        );

        if (!credential) {
          return res.status(404).json({
            success: false,
            error: "Credential not found",
          });
        }

        const credData = credential as unknown as CredentialBackend;

        if (credData.authType !== "key") {
          return res.status(400).json({
            success: false,
            error: "Only SSH key-based credentials can be deployed",
          });
        }

        const publicKey = credData.publicKey;
        if (!publicKey) {
          return res.status(400).json({
            success: false,
            error: "Public key is required for deployment",
          });
        }
        const hostData = await repository.findHostByIdForUser(
          targetHostId,
          userId,
        );

        if (!hostData) {
          return res.status(404).json({
            success: false,
            error: "Target host not found",
          });
        }

        const hostConfig = {
          ip: hostData.ip,
          port: hostData.port,
          username: hostData.username,
          authType: hostData.authType,
          password: hostData.password,
          privateKey: hostData.key,
          keyPassword: hostData.keyPassword,
        };

        if (hostData.authType === "credential" && hostData.credentialId) {
          const userId = (req as AuthenticatedRequest).userId;
          if (!userId) {
            return res.status(400).json({
              success: false,
              error: "Authentication required for credential resolution",
            });
          }

          try {
            const hostCredential = await repository.findCredentialByIdForUser(
              hostData.credentialId as number,
              userId,
            );

            if (hostCredential) {
              hostConfig.authType = hostCredential.authType;
              hostConfig.username = hostCredential.username;

              if (hostCredential.authType === "password") {
                hostConfig.password = hostCredential.password;
              } else if (hostCredential.authType === "key") {
                hostConfig.privateKey =
                  hostCredential.privateKey || hostCredential.key;
                hostConfig.keyPassword = hostCredential.keyPassword;
              }
            } else {
              return res.status(400).json({
                success: false,
                error: "Host credential not found",
              });
            }
          } catch {
            return res.status(500).json({
              success: false,
              error: "Failed to resolve host credentials",
            });
          }
        }

        const deployResult = await deploySSHKeyToHost(hostConfig, credData);

        if (deployResult.success) {
          res.json({
            success: true,
            message: deployResult.message || "SSH key deployed successfully",
          });
        } else {
          res.status(500).json({
            success: false,
            error: deployResult.error || "Deployment failed",
          });
        }
      } catch (error) {
        res.status(500).json({
          success: false,
          error: getErrorMessage(error, "Failed to deploy SSH key"),
        });
      }
    },
  );
}
