#!/usr/bin/env node
/**
 * Copyright (c) OpenLens Authors. All rights reserved.
 * Licensed under MIT License. See LICENSE in root directory for more information.
 */
import { FileHandle, readFile } from "fs/promises";
import type { WriteStream } from "fs";
import { constants } from "fs";
import { open, mkdir, unlink } from "fs/promises";
import path from "path";
import { promisify } from "util";
import { pipeline as _pipeline, Transform, Writable } from "stream";
import type { SingleBar } from "cli-progress";
import { MultiBar } from "cli-progress";
import { extract } from "tar-stream";
import gunzip from "gunzip-maybe";
import fetch from "node-fetch"
import z from "zod";
import arg from "arg";

const options = arg({
  "--package": String,
  "--base-dir": String,
});

const pathToPackage = options["--package"];
const pathToBaseDir = options["--base-dir"];

if (typeof pathToPackage !== "string") {
  throw new Error("--package is required");
}

if (typeof pathToBaseDir !== "string") {
  throw new Error("--base-dir is required");
}

function setTimeoutFor(controller: AbortController, timeout: number): void {
  const handle = setTimeout(() => controller.abort(), timeout);

  controller.signal.addEventListener("abort", () => clearTimeout(handle));
}

const pipeline = promisify(_pipeline);

const getBinaryName = (binaryName: string, { forPlatform }: { forPlatform : string }) => {
  if (forPlatform === "windows") {
    return `${binaryName}.exe`;
  }

  return binaryName;
};

interface BinaryDownloaderArgs {
  readonly version: string;
  readonly platform: SupportedPlatform;
  readonly downloadArch: string;
  readonly fileArch: string;
  readonly binaryName: string;
  readonly baseDir: string;
}

abstract class BinaryDownloader {
  protected abstract readonly url: string;
  protected readonly bar: SingleBar;
  protected readonly target: string;

  protected getTransformStreams(file: Writable): (NodeJS.ReadWriteStream | NodeJS.WritableStream)[] {
    return [file];
  }

  constructor(public readonly args: BinaryDownloaderArgs, multiBar: MultiBar) {
    this.bar = multiBar.create(1, 0, args);
    this.target = path.join(args.baseDir, args.platform, args.fileArch, args.binaryName);
  }

  async ensureBinary(): Promise<void> {
    if (process.env.LENS_SKIP_DOWNLOAD_BINARIES === "true") {
      return;
    }

    const controller = new AbortController();

    setTimeoutFor(controller, 15 * 60 * 1000);

    const stream = await fetch(this.url, {
      signal: controller.signal,
    });
    const total = Number(stream.headers.get("content-length"));
    const bar = this.bar;
    let fileHandle: FileHandle | undefined = undefined;

    if (isNaN(total)) {
      throw new Error("no content-length header was present");
    }

    bar.setTotal(total);

    await mkdir(path.dirname(this.target), {
      mode: 0o755,
      recursive: true,
    });

    try {
      /**
       * This is necessary because for some reason `createWriteStream({ flags: "wx" })`
       * was throwing someplace else and not here
       */
      const handle = fileHandle = await open(this.target, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL);

      if (!stream.body) {
        throw new Error("no body on stream");
      }

      await pipeline(
        stream.body,
        new Transform({
          transform(chunk, encoding, callback) {
            bar.increment(chunk.length);
            this.push(chunk);
            callback();
          },
        }),
        ...this.getTransformStreams(new Writable({
          write(chunk, encoding, cb) {
            handle.write(chunk)
              .then(() => cb())
              .catch(cb);
          },
        })),
      );
      await fileHandle.chmod(0o755);
      await fileHandle.close();
    } catch (error) {
      await fileHandle?.close();

      if ((error as any)?.code === "EEXIST") {
        bar.increment(total); // mark as finished
        controller.abort(); // stop trying to download
      } else {
        await unlink(this.target);
        throw error;
      }
    }
  }
}

class LensK8sProxyDownloader extends BinaryDownloader {
  protected readonly url: string;

  constructor(args: Omit<BinaryDownloaderArgs, "binaryName">, bar: MultiBar) {
    const binaryName = getBinaryName("lens-k8s-proxy", { forPlatform: args.platform });

    super({ ...args, binaryName }, bar);
    this.url = `https://github.com/lensapp/lens-k8s-proxy/releases/download/v${args.version}/lens-k8s-proxy-${args.platform}-${args.downloadArch}`;
  }
}

class KubectlDownloader extends BinaryDownloader {
  protected readonly url: string;

  constructor(args: Omit<BinaryDownloaderArgs, "binaryName">, bar: MultiBar) {
    const binaryName = getBinaryName("kubectl", { forPlatform: args.platform });

    super({ ...args, binaryName }, bar);
    this.url = `https://storage.googleapis.com/kubernetes-release/release/v${args.version}/bin/${args.platform}/${args.downloadArch}/${binaryName}`;
  }
}

class HelmDownloader extends BinaryDownloader {
  protected readonly url: string;

  constructor(args: Omit<BinaryDownloaderArgs, "binaryName">, bar: MultiBar) {
    const binaryName = getBinaryName("helm", { forPlatform: args.platform });

    super({ ...args, binaryName }, bar);
    this.url = `https://get.helm.sh/helm-v${args.version}-${args.platform}-${args.downloadArch}.tar.gz`;
  }

  protected getTransformStreams(file: WriteStream) {
    const extracting = extract({
      allowUnknownFormat: false,
    });

    extracting.on("entry", (headers, stream, next) => {
      if (headers.name.endsWith(this.args.binaryName)) {
        stream
          .pipe(file)
          .once("finish", () => next())
          .once("error", next);
      } else {
        stream.resume();
        next();
      }
    });

    return [gunzip(3), extracting];
  }
}

type SupportedPlatform = "darwin" | "linux" | "windows";

const PackageInfo = z.object({
  config: z.object({
    k8sProxyVersion: z.string().min(1),
    bundledKubectlVersion: z.string().min(1),
    bundledHelmVersion: z.string().min(1),
  })
})

const packageInfoRaw = await readFile(pathToPackage, "utf-8");
const packageInfo = PackageInfo.parse(JSON.parse(packageInfoRaw));

const normalizedPlatform = (() => {
  switch (process.platform) {
    case "darwin":
      return "darwin";
    case "linux":
      return "linux";
    case "win32":
      return "windows";
    default:
      throw new Error(`platform=${process.platform} is unsupported`);
  }
})();
const multiBar = new MultiBar({
  align: "left",
  clearOnComplete: false,
  hideCursor: true,
  autopadding: true,
  noTTYOutput: true,
  format: "[{bar}] {percentage}% | {downloadArch} {binaryName}",
});
const downloaders: BinaryDownloader[] = [
  new LensK8sProxyDownloader({
    version: packageInfo.config.k8sProxyVersion,
    platform: normalizedPlatform,
    downloadArch: "amd64",
    fileArch: "x64",
    baseDir: pathToBaseDir,
  }, multiBar),
  new KubectlDownloader({
    version: packageInfo.config.bundledKubectlVersion,
    platform: normalizedPlatform,
    downloadArch: "amd64",
    fileArch: "x64",
    baseDir: pathToBaseDir,
  }, multiBar),
  new HelmDownloader({
    version: packageInfo.config.bundledHelmVersion,
    platform: normalizedPlatform,
    downloadArch: "amd64",
    fileArch: "x64",
    baseDir: pathToBaseDir,
  }, multiBar),
];

if (normalizedPlatform !== "windows") {
  downloaders.push(
    new LensK8sProxyDownloader({
      version: packageInfo.config.k8sProxyVersion,
      platform: normalizedPlatform,
      downloadArch: "arm64",
      fileArch: "arm64",
      baseDir: pathToBaseDir,
    }, multiBar),
    new KubectlDownloader({
      version: packageInfo.config.bundledKubectlVersion,
      platform: normalizedPlatform,
      downloadArch: "arm64",
      fileArch: "arm64",
      baseDir: pathToBaseDir,
    }, multiBar),
    new HelmDownloader({
      version: packageInfo.config.bundledHelmVersion,
      platform: normalizedPlatform,
      downloadArch: "arm64",
      fileArch: "arm64",
      baseDir: pathToBaseDir,
    }, multiBar),
  );
}

const settledResults = await Promise.allSettled(downloaders.map(downloader => (
  downloader.ensureBinary()
    .catch(error => {
      throw new Error(`Failed to download ${downloader.args.binaryName} for ${downloader.args.platform}/${downloader.args.downloadArch}: ${error}`);
    })
)));

multiBar.stop();
const errorResult = settledResults.find(res => res.status === "rejected") as PromiseRejectedResult | undefined;

if (errorResult) {
  console.error("234", String(errorResult.reason));
  process.exit(1);
}

process.exit(0);
