import {
  AllPublishOptions,
  newError,
  safeStringifyJson,
  BlockMap,
  CURRENT_APP_INSTALLER_FILE_NAME
} from "builder-util-runtime";
import { stat } from "fs-extra";
import { createReadStream } from "fs";
import { AppAdapter } from "./AppAdapter";
import { BaseUpdater } from "./BaseUpdater";
import {
  UpdateDownloadedEvent,
  newUrlFromBase,
  ResolvedUpdateFileInfo
} from "./main";
import { findFile, Provider } from "./providers/Provider";
import AutoUpdater = Electron.AutoUpdater;
import { createServer, IncomingMessage, ServerResponse } from "http";
import { AddressInfo } from "net";
import { DownloadUpdateOptions } from "./AppUpdater";
import { GenericDifferentialDownloader } from "./differentialDownloader/GenericDifferentialDownloader";
import path from "path";
import { gunzipSync } from "zlib";
import electron from "electron";

export class MacUpdater extends BaseUpdater {
  updateAvailable!: boolean;
  protected doInstall(
    options: import("./BaseUpdater").InstallOptions
  ): boolean {
    throw new Error("Method not implemented.");
  }
  private readonly nativeUpdater: AutoUpdater = electron.autoUpdater;

  private updateInfoForPendingUpdateDownloadedEvent: UpdateDownloadedEvent | null = null;

  constructor(options?: AllPublishOptions, app?: AppAdapter) {
    super(options, app);
    this.nativeUpdater.on("error", it => {
      this._logger.warn(it);
      this.emit("error", it);
    });
    this.nativeUpdater.on("update-downloaded", () => {
      const updateInfo = this.updateInfoForPendingUpdateDownloadedEvent;
      this.updateInfoForPendingUpdateDownloadedEvent = null;
      this.dispatchUpdateDownloaded(updateInfo!!);
    });
  }
  private async differentialDownloadInstaller(
    fileInfo: ResolvedUpdateFileInfo,
    downloadUpdateOptions: DownloadUpdateOptions,
    installerPath: string,
    provider: Provider<any>
  ): Promise<boolean> {
    try {
      if (
        this._testOnlyOptions != null &&
        !this._testOnlyOptions.isUseDifferentialDownload
      ) {
        return true;
      }

      const newBlockMapUrl = newUrlFromBase(
        `${fileInfo.url.pathname}.blockmap`,
        fileInfo.url
      );
      const oldBlockMapUrl = newUrlFromBase(
        `${fileInfo.url.pathname.replace(
          new RegExp(
            downloadUpdateOptions.updateInfoAndProvider.info.version,
            "g"
          ),
          this.app.version
        )}.blockmap`,
        fileInfo.url
      );
      this._logger.info(
        `Download block maps (old: "${oldBlockMapUrl.href}", new: ${newBlockMapUrl.href})`
      );

      const downloadBlockMap = async (url: URL): Promise<BlockMap> => {
        const data = await this.httpExecutor.downloadToBuffer(url, {
          headers: downloadUpdateOptions.requestHeaders,
          cancellationToken: downloadUpdateOptions.cancellationToken
        });

        if (data == null || data.length === 0) {
          throw new Error(`Blockmap "${url.href}" is empty`);
        }

        try {
          return JSON.parse(gunzipSync(data).toString());
        } catch (e) {
          throw new Error(
            `Cannot parse blockmap "${url.href}", error: ${e}, raw data: ${data}`
          );
        }
      };

      const blockMapDataList = await Promise.all([
        downloadBlockMap(oldBlockMapUrl),
        downloadBlockMap(newBlockMapUrl)
      ]);
      await new GenericDifferentialDownloader(
        fileInfo.info,
        this.httpExecutor,
        {
          newUrl: fileInfo.url,
          oldFile: path.join(
            this.downloadedUpdateHelper!!.cacheDir,
            process.platform === "darwin"
              ? `${this.app.name}-${this.app.version}-mac.zip`
              : CURRENT_APP_INSTALLER_FILE_NAME
          ),
          logger: this._logger,
          newFile: installerPath,
          isUseMultipleRangeRequest: provider.isUseMultipleRangeRequest,
          requestHeaders: downloadUpdateOptions.requestHeaders
        }
      ).download(
        blockMapDataList[0],
        blockMapDataList[1],
        this.emit.bind(this)
      );
      return false;
    } catch (e) {
      this._logger.error(
        `Cannot download differentially, fallback to full download: ${e.stack ||
          e}`
      );
      if (this._testOnlyOptions != null) {
        // test mode
        throw e;
      }
      return true;
    }
  }
  protected doDownloadUpdate(
    downloadUpdateOptions: DownloadUpdateOptions
  ): Promise<Array<string>> {
    this.updateInfoForPendingUpdateDownloadedEvent = null;
    const provider = downloadUpdateOptions.updateInfoAndProvider.provider;
    const files = downloadUpdateOptions.updateInfoAndProvider.provider.resolveFiles(
      downloadUpdateOptions.updateInfoAndProvider.info
    );
    const zipFileInfo = findFile(files, "zip", ["pkg", "dmg"]);
    if (zipFileInfo == null) {
      throw newError(
        `ZIP file not provided: ${safeStringifyJson(files)}`,
        "ERR_UPDATER_ZIP_FILE_NOT_FOUND"
      );
    }
    const server = createServer();
    server.on("close", () => {
      this._logger.info(
        `Proxy server for native Squirrel.Mac is closed (was started to download ${zipFileInfo.url.href})`
      );
    });
    function getServerUrl(): string {
      const address = server.address() as AddressInfo;
      return `http://127.0.0.1:${address.port}`;
    }
    return this.executeDownload({
      fileExtension: "zip",
      fileInfo: zipFileInfo,
      downloadUpdateOptions,
      task: async (destinationFile, downloadOptions) => {
        try {
          if (
            await this.differentialDownloadInstaller(
              zipFileInfo,
              downloadUpdateOptions,
              destinationFile,
              provider
            )
          ) {
            await this.httpExecutor.download(
              zipFileInfo.url,
              destinationFile,
              downloadOptions
            );
          }
        } catch (e) {
          console.log(e);
        }
      },
      done: async event => {
        const downloadedFile = event.downloadedFile;
        this.updateInfoForPendingUpdateDownloadedEvent = event;
        let updateFileSize = zipFileInfo.info.size;
        if (updateFileSize == null) {
          updateFileSize = (await stat(downloadedFile)).size;
        }

        return await new Promise<Array<string>>((resolve, reject) => {
          // insecure random is ok
          const fileUrl =
            "/" + Date.now() + "-" + Math.floor(Math.random() * 9999) + ".zip";
          server.on(
            "request",
            (request: IncomingMessage, response: ServerResponse) => {
              const requestUrl = request.url!!;
              this._logger.info(`${requestUrl} requested`);
              if (requestUrl === "/") {
                const data = Buffer.from(
                  `{ "url": "${getServerUrl()}${fileUrl}" }`
                );
                response.writeHead(200, {
                  "Content-Type": "application/json",
                  "Content-Length": data.length
                });
                response.end(data);
                return;
              }

              if (!requestUrl.startsWith(fileUrl)) {
                this._logger.warn(`${requestUrl} requested, but not supported`);
                response.writeHead(404);
                response.end();
                return;
              }

              this._logger.info(
                `${fileUrl} requested by Squirrel.Mac, pipe ${downloadedFile}`
              );

              let errorOccurred = false;
              response.on("finish", () => {
                try {
                  setImmediate(() => server.close());
                } finally {
                  if (!errorOccurred) {
                    this.nativeUpdater.removeListener("error", reject);
                    resolve([]);
                  }
                }
              });

              const readStream = createReadStream(downloadedFile);
              readStream.on("error", error => {
                try {
                  response.end();
                } catch (e) {
                  this._logger.warn(`cannot end response: ${e}`);
                }
                errorOccurred = true;
                this.nativeUpdater.removeListener("error", reject);
                reject(new Error(`Cannot pipe "${downloadedFile}": ${error}`));
              });

              response.writeHead(200, {
                "Content-Type": "application/zip",
                "Content-Length": updateFileSize
              });
              readStream.pipe(response);
            }
          );
          server.listen(0, "127.0.0.1", () => {
            this.nativeUpdater.setFeedURL({
              url: getServerUrl(),
              headers: { "Cache-Control": "no-cache" }
            });

            this.nativeUpdater.once("error", reject);
            this.nativeUpdater.checkForUpdates();
          });
        });
      }
    });
  }

  quitAndInstall(): void {
    this.nativeUpdater.quitAndInstall();
  }
}