// This assumes an installation of Docker exists on the given host
// and that host is configured to accept our SSH key
import config from "../config.ts";
import { VMManager, type VM } from "./base.ts";
import { imageName } from "./utils.ts";
import fs from "node:fs";
import { homedir } from "node:os";
import { NodeSSH } from "node-ssh";

export class Docker extends VMManager {
  // TODO support multiple Docker providers in the pool config with same region
  size = "";
  largeSize = "";
  minRetries = 0;
  reuseVMs = false;
  id = "Docker";
  ssh: NodeSSH | undefined = undefined;
  imageId = imageName;

  getSSH = async () => {
    if (this.ssh && this.ssh.isConnected()) {
      return this.ssh;
    }
    const sshConfig = {
      username: config.DOCKER_VM_HOST_SSH_USER,
      host: this.hostname,
      // The private key the Docker host is configured to accept
      privateKey: config.DOCKER_VM_HOST_SSH_KEY_BASE64
        ? Buffer.from(config.DOCKER_VM_HOST_SSH_KEY_BASE64, "base64").toString()
        : fs.readFileSync(homedir() + "/.ssh/id_rsa").toString(),
    };
    this.ssh = new NodeSSH();
    await this.ssh.connect(sshConfig);
    return this.ssh;
  };

  startVM = async (name: string) => {
    const tag = this.getTag();
    const conn = await this.getSSH();
    // If in development, have neko share the same SSL cert as the other services
    // If in production, they are probably on different hosts and neko is behind a reverse proxy for SSL termination
    const sslEnv =
      config.NODE_ENV === "development" &&
      config.SSL_KEY_FILE &&
      config.SSL_CRT_FILE
        ? `-e NEKO_KEY="${config.SSL_KEY_FILE}" -e NEKO_CERT="${config.SSL_CRT_FILE}"`
        : "";
    const { stdout, stderr } = await conn.execCommand(
      `
      #!/bin/bash
      set -e
      PORT=$(comm -23 <(seq 5000 5063 | sort) <(ss -Htan | awk '{print $4}' | cut -d':' -f2 | sort -u) | sort -n | head -n 1)
      INDEX=$(($PORT - 5000))
      UDP_START=$((59000+$INDEX*100))
      UDP_END=$((59099+$INDEX*100))
      docker run -d --rm --name=${name} --memory="2g" --cpus="2" -p $PORT:$PORT -p $UDP_START-$UDP_END:$UDP_START-$UDP_END/udp -v /etc/letsencrypt:/etc/letsencrypt -l ${tag} -l index=$INDEX --log-opt max-size=1g --shm-size=1g --cap-add="SYS_ADMIN" ${sslEnv} -e DISPLAY=":99.0" -e NEKO_PASSWORD=${name} -e NEKO_PASSWORD_ADMIN=${name} -e NEKO_ADMIN_KEY=${config.VBROWSER_ADMIN_KEY} -e NEKO_BIND=":$PORT" -e NEKO_EPR=":$UDP_START-$UDP_END" -e NEKO_H264="1" ${imageName}
      `,
    );
    console.log(stdout, stderr);
    return stdout.trim();
  };

  terminateVM = async (id: string) => {
    const conn = await this.getSSH();
    const { stdout, stderr } = await conn.execCommand(`docker rm -fv ${id}`);
    console.log(stdout, stderr);
    return;
  };

  rebootVM = async (id: string) => {
    // Docker containers aren't set to reuse, so do nothing (reset will terminate)
  };

  reimageVM = async (id: string) => {
    const conn = await this.getSSH();
    const { stdout, stderr } = await conn.execCommand(
      `docker pull ${this.imageId}`,
    );
    console.log(stdout, stderr);
    // The container is out of date. Delete it
    this.terminateVMWrapper(id);
    return;
  };

  getVM = async (id: string) => {
    const conn = await this.getSSH();
    const { stdout } = await conn.execCommand(`docker inspect ${id}`);
    let data = null;
    try {
      data = JSON.parse(stdout)[0];
      if (!data) {
        throw new Error("no container with this ID found");
      }
    } catch {
      console.warn(stdout);
      throw new Error("failed to parse json");
    }
    let server = this.mapServerObject(data);
    return server;
  };

  listVMs = async (filter: string) => {
    const conn = await this.getSSH();
    const listCmd = `docker inspect $(docker ps --filter label=${filter} --quiet --no-trunc)`;
    const { stdout } = await conn.execCommand(listCmd);
    if (!stdout) {
      return [];
    }
    let data = [];
    try {
      data = JSON.parse(stdout);
    } catch (e) {
      console.warn(stdout);
      throw new Error("failed to parse json");
    }
    return data.map(this.mapServerObject);
  };

  powerOn = async (id: string) => {};

  attachToNetwork = async (id: string) => {};

  updateSnapshot = async () => {
    return "";
  };

  mapServerObject = (server: any): VM => ({
    id: server.Id,
    host: `${this.hostname}:${5000 + Number(server.Config?.Labels?.index)}`,
    provider: this.id,
    large: this.isLarge,
    region: this.region,
  });
}
