import { app, remote } from "electron";
import path from "path";
import fs from "fs";
import { promiseExec } from "./promise-exec";
import logger from "./logger";
import { ensureDir, pathExists } from "fs-extra";
import * as lockFile from "proper-lockfile";
import { helmCli } from "./helm/helm-cli";
import { userStore } from "../common/user-store";
import { customRequest } from "../common/request";
import { getBundledKubectlVersion } from "../common/utils/app-version";
import { isDevelopment, isWindows, isTestEnv } from "../common/vars";

const bundledVersion = getBundledKubectlVersion();
const kubectlMap: Map<string, string> = new Map([
  ["1.7", "1.8.15"],
  ["1.8", "1.9.10"],
  ["1.9", "1.10.13"],
  ["1.10", "1.11.10"],
  ["1.11", "1.12.10"],
  ["1.12", "1.13.12"],
  ["1.13", "1.13.12"],
  ["1.14", "1.14.10"],
  ["1.15", "1.15.11"],
  ["1.16", "1.16.15"],
  ["1.17", bundledVersion],
  ["1.18", "1.18.14"],
  ["1.19", "1.19.5"],
  ["1.20", "1.20.0"]
]);
const packageMirrors: Map<string, string> = new Map([
  ["default", "https://storage.googleapis.com/kubernetes-release/release"],
  ["china", "https://mirror.azure.cn/kubernetes/kubectl"]
]);
let bundledPath: string;
const initScriptVersionString = "# lens-initscript v3\n";

export function bundledKubectlPath(): string {
  if (bundledPath) { return bundledPath; }

  if (isDevelopment || isTestEnv) {
    const platformName = isWindows ? "windows" : process.platform;

    bundledPath = path.join(process.cwd(), "binaries", "client", platformName, process.arch, "kubectl");
  } else {
    bundledPath = path.join(process.resourcesPath, process.arch, "kubectl");
  }

  if (isWindows) {
    bundledPath = `${bundledPath}.exe`;
  }

  return bundledPath;
}

export class Kubectl {
  public kubectlVersion: string;
  protected directory: string;
  protected url: string;
  protected path: string;
  protected dirname: string;

  static get kubectlDir() {
    return path.join((app || remote.app).getPath("userData"), "binaries", "kubectl");
  }

  public static readonly bundledKubectlVersion: string = bundledVersion;
  public static invalidBundle = false;
  private static bundledInstance: Kubectl;

  // Returns the single bundled Kubectl instance
  public static bundled() {
    if (!Kubectl.bundledInstance) Kubectl.bundledInstance = new Kubectl(Kubectl.bundledKubectlVersion);

    return Kubectl.bundledInstance;
  }

  constructor(clusterVersion: string) {
    const versionParts = /^v?(\d+\.\d+)(.*)/.exec(clusterVersion);
    const minorVersion = versionParts[1];

    /* minorVersion is the first two digits of kube server version
       if the version map includes that, use that version, if not, fallback to the exact x.y.z of kube version */
    if (kubectlMap.has(minorVersion)) {
      this.kubectlVersion = kubectlMap.get(minorVersion);
      logger.debug(`Set kubectl version ${this.kubectlVersion} for cluster version ${clusterVersion} using version map`);
    } else {
      this.kubectlVersion = versionParts[1] + versionParts[2];
      logger.debug(`Set kubectl version ${this.kubectlVersion} for cluster version ${clusterVersion} using fallback`);
    }

    let arch = null;

    if (process.arch == "x64") {
      arch = "amd64";
    } else if (process.arch == "x86" || process.arch == "ia32") {
      arch = "386";
    } else {
      arch = process.arch;
    }

    const platformName = isWindows ? "windows" : process.platform;
    const binaryName = isWindows ? "kubectl.exe" : "kubectl";

    this.url = `${this.getDownloadMirror()}/v${this.kubectlVersion}/bin/${platformName}/${arch}/${binaryName}`;

    this.dirname = path.normalize(path.join(this.getDownloadDir(), this.kubectlVersion));
    this.path = path.join(this.dirname, binaryName);
  }

  public getBundledPath() {
    return bundledKubectlPath();
  }

  public getPathFromPreferences() {
    return userStore.preferences?.kubectlBinariesPath || this.getBundledPath();
  }

  protected getDownloadDir() {
    if (userStore.preferences?.downloadBinariesPath) {
      return path.join(userStore.preferences.downloadBinariesPath, "kubectl");
    }

    return Kubectl.kubectlDir;
  }

  public async getPath(bundled = false): Promise<string> {
    if (bundled) {
      return this.getBundledPath();
    }

    if (userStore.preferences?.downloadKubectlBinaries === false) {
      return this.getPathFromPreferences();
    }

    // return binary name if bundled path is not functional
    if (!await this.checkBinary(this.getBundledPath(), false)) {
      Kubectl.invalidBundle = true;

      return path.basename(this.getBundledPath());
    }

    try {
      if (!await this.ensureKubectl()) {
        logger.error("Failed to ensure kubectl, fallback to the bundled version");

        return this.getBundledPath();
      }

      return this.path;
    } catch (err) {
      logger.error("Failed to ensure kubectl, fallback to the bundled version");
      logger.error(err);

      return this.getBundledPath();
    }
  }

  public async binDir() {
    try {
      await this.ensureKubectl();
      await this.writeInitScripts();

      return this.dirname;
    } catch (err) {
      logger.error(err);

      return "";
    }
  }

  public async checkBinary(path: string, checkVersion = true) {
    const exists = await pathExists(path);

    if (exists) {
      try {
        const { stdout } = await promiseExec(`"${path}" version --client=true -o json`);
        const output = JSON.parse(stdout);

        if (!checkVersion) {
          return true;
        }
        let version: string = output.clientVersion.gitVersion;

        if (version[0] === "v") {
          version = version.slice(1);
        }

        if (version === this.kubectlVersion) {
          logger.debug(`Local kubectl is version ${this.kubectlVersion}`);

          return true;
        }
        logger.error(`Local kubectl is version ${version}, expected ${this.kubectlVersion}, unlinking`);
      } catch (err) {
        logger.error(`Local kubectl failed to run properly (${err.message}), unlinking`);
      }
      await fs.promises.unlink(this.path);
    }

    return false;
  }

  protected async checkBundled(): Promise<boolean> {
    if (this.kubectlVersion === Kubectl.bundledKubectlVersion) {
      try {
        const exist = await pathExists(this.path);

        if (!exist) {
          await fs.promises.copyFile(this.getBundledPath(), this.path);
          await fs.promises.chmod(this.path, 0o755);
        }

        return true;
      } catch (err) {
        logger.error(`Could not copy the bundled kubectl to app-data: ${err}`);

        return false;
      }
    } else {
      return false;
    }
  }

  public async ensureKubectl(): Promise<boolean> {
    if (userStore.preferences?.downloadKubectlBinaries === false) {
      return true;
    }

    if (Kubectl.invalidBundle) {
      logger.error(`Detected invalid bundle binary, returning ...`);

      return false;
    }
    await ensureDir(this.dirname, 0o755);

    return lockFile.lock(this.dirname).then(async (release) => {
      logger.debug(`Acquired a lock for ${this.kubectlVersion}`);
      const bundled = await this.checkBundled();
      let isValid = await this.checkBinary(this.path, !bundled);

      if (!isValid && !bundled) {
        await this.downloadKubectl().catch((error) => {
          logger.error(error);
          logger.debug(`Releasing lock for ${this.kubectlVersion}`);
          release();

          return false;
        });
        isValid = !await this.checkBinary(this.path, false);
      }

      if (!isValid) {
        logger.debug(`Releasing lock for ${this.kubectlVersion}`);
        release();

        return false;
      }
      logger.debug(`Releasing lock for ${this.kubectlVersion}`);
      release();

      return true;
    }).catch((e) => {
      logger.error(`Failed to get a lock for ${this.kubectlVersion}`);
      logger.error(e);

      return false;
    });
  }

  public async downloadKubectl() {
    await ensureDir(path.dirname(this.path), 0o755);

    logger.info(`Downloading kubectl ${this.kubectlVersion} from ${this.url} to ${this.path}`);

    return new Promise((resolve, reject) => {
      const stream = customRequest({
        url: this.url,
        gzip: true,
      });
      const file = fs.createWriteStream(this.path);

      stream.on("complete", () => {
        logger.debug("kubectl binary download finished");
        file.end();
      });
      stream.on("error", (error) => {
        logger.error(error);
        fs.unlink(this.path, () => {
          // do nothing
        });
        reject(error);
      });
      file.on("close", () => {
        logger.debug("kubectl binary download closed");
        fs.chmod(this.path, 0o755, (err) => {
          if (err) reject(err);
        });
        resolve();
      });
      stream.pipe(file);
    });
  }

  protected async writeInitScripts() {
    const kubectlPath = userStore.preferences?.downloadKubectlBinaries ? this.dirname : path.dirname(this.getPathFromPreferences());
    const helmPath = helmCli.getBinaryDir();
    const fsPromises = fs.promises;
    const bashScriptPath = path.join(this.dirname, ".bash_set_path");
    let bashScript = `${initScriptVersionString}`;

    bashScript += "tempkubeconfig=\"$KUBECONFIG\"\n";
    bashScript += "test -f \"/etc/profile\" && . \"/etc/profile\"\n";
    bashScript += "if test -f \"$HOME/.bash_profile\"; then\n";
    bashScript += "  . \"$HOME/.bash_profile\"\n";
    bashScript += "elif test -f \"$HOME/.bash_login\"; then\n";
    bashScript += "  . \"$HOME/.bash_login\"\n";
    bashScript += "elif test -f \"$HOME/.profile\"; then\n";
    bashScript += "  . \"$HOME/.profile\"\n";
    bashScript += "fi\n";
    bashScript += `export PATH="${helmPath}:${kubectlPath}:$PATH"\n`;
    bashScript += "export KUBECONFIG=\"$tempkubeconfig\"\n";

    bashScript += "NO_PROXY=\",${NO_PROXY:-localhost},\"\n";
    bashScript += "NO_PROXY=\"${NO_PROXY//,localhost,/,}\"\n";
    bashScript += "NO_PROXY=\"${NO_PROXY//,127.0.0.1,/,}\"\n";
    bashScript += "NO_PROXY=\"localhost,127.0.0.1${NO_PROXY%,}\"\n";
    bashScript += "export NO_PROXY\n";
    bashScript += "unset tempkubeconfig\n";
    await fsPromises.writeFile(bashScriptPath, bashScript.toString(), { mode: 0o644 });

    const zshScriptPath = path.join(this.dirname, ".zlogin");
    let zshScript = `${initScriptVersionString}`;

    zshScript += "tempkubeconfig=\"$KUBECONFIG\"\n";
    // restore previous ZDOTDIR
    zshScript += "export ZDOTDIR=\"$OLD_ZDOTDIR\"\n";
    // source all the files
    zshScript += "test -f \"$OLD_ZDOTDIR/.zshenv\" && . \"$OLD_ZDOTDIR/.zshenv\"\n";
    zshScript += "test -f \"$OLD_ZDOTDIR/.zprofile\" && . \"$OLD_ZDOTDIR/.zprofile\"\n";
    zshScript += "test -f \"$OLD_ZDOTDIR/.zlogin\" && . \"$OLD_ZDOTDIR/.zlogin\"\n";
    zshScript += "test -f \"$OLD_ZDOTDIR/.zshrc\" && . \"$OLD_ZDOTDIR/.zshrc\"\n";

    // voodoo to replace any previous occurrences of kubectl path in the PATH
    zshScript += `kubectlpath=\"${kubectlPath}"\n`;
    zshScript += `helmpath=\"${helmPath}"\n`;
    zshScript += "p=\":$kubectlpath:\"\n";
    zshScript += "d=\":$PATH:\"\n";
    zshScript += "d=${d//$p/:}\n";
    zshScript += "d=${d/#:/}\n";
    zshScript += "export PATH=\"$helmpath:$kubectlpath:${d/%:/}\"\n";
    zshScript += "export KUBECONFIG=\"$tempkubeconfig\"\n";
    zshScript += "NO_PROXY=\",${NO_PROXY:-localhost},\"\n";
    zshScript += "NO_PROXY=\"${NO_PROXY//,localhost,/,}\"\n";
    zshScript += "NO_PROXY=\"${NO_PROXY//,127.0.0.1,/,}\"\n";
    zshScript += "NO_PROXY=\"localhost,127.0.0.1${NO_PROXY%,}\"\n";
    zshScript += "export NO_PROXY\n";
    zshScript += "unset tempkubeconfig\n";
    zshScript += "unset OLD_ZDOTDIR\n";
    await fsPromises.writeFile(zshScriptPath, zshScript.toString(), { mode: 0o644 });
  }

  protected getDownloadMirror() {
    const mirror = packageMirrors.get(userStore.preferences?.downloadMirror);

    if (mirror) {
      return mirror;
    }

    return packageMirrors.get("default"); // MacOS packages are only available from default
  }
}
