import * as fs from "fs";
import * as os from "os";
import * as path from "path";

import * as toolrunner from "@actions/exec/lib/toolrunner";
import { IHeaders } from "@actions/http-client/interfaces";
import * as io from "@actions/io";
import * as actionsToolcache from "@actions/tool-cache";
import * as safeWhich from "@chrisgavin/safe-which";
import * as semver from "semver";
import { v4 as uuidV4 } from "uuid";

import { Logger } from "./logging";
import { isActions } from "./util";

/*
 * This file acts as an interface to the functionality of the actions toolcache.
 * That library is not safe to use outside of actions as it makes assumptions about
 * the state of the filesystem and available environment variables.
 *
 * On actions we can just delegate to the toolcache library, however outside of
 * actions we provide our own implementation.
 */

/**
 * Extract a compressed tar archive.
 *
 * See extractTar function from node_modules/@actions/tool-cache/lib/tool-cache.d.ts
 *
 * @param file           path to the tar
 * @param mode           should run the actions or runner implementation
 * @param tempDir        path to the temporary directory
 * @param logger         logger to use
 * @returns              path to the destination directory
 */
export async function extractTar(
  file: string,
  tempDir: string,
  logger: Logger
): Promise<string> {
  if (isActions()) {
    return await actionsToolcache.extractTar(file);
  } else {
    // Initial implementation copied from node_modules/@actions/tool-cache/lib/tool-cache.js

    if (!file) {
      throw new Error("parameter 'file' is required");
    }
    // Create dest
    const dest = createExtractFolder(tempDir);
    // Determine whether GNU tar
    logger.debug("Checking tar --version");
    let versionOutput = "";
    await new toolrunner.ToolRunner(
      await safeWhich.safeWhich("tar"),
      ["--version"],
      {
        ignoreReturnCode: true,
        silent: true,
        listeners: {
          stdout: (data) => (versionOutput += data.toString()),
          stderr: (data) => (versionOutput += data.toString()),
        },
      }
    ).exec();
    logger.debug(versionOutput.trim());
    const isGnuTar = versionOutput.toUpperCase().includes("GNU TAR");
    // Initialize args
    const args = ["xz"];
    if (logger.isDebug()) {
      args.push("-v");
    }
    let destArg = dest;
    let fileArg = file;
    if (process.platform === "win32" && isGnuTar) {
      args.push("--force-local");
      destArg = dest.replace(/\\/g, "/");
      // Technically only the dest needs to have `/` but for aesthetic consistency
      // convert slashes in the file arg too.
      fileArg = file.replace(/\\/g, "/");
    }
    if (isGnuTar) {
      // Suppress warnings when using GNU tar to extract archives created by BSD tar
      args.push("--warning=no-unknown-keyword");
    }
    args.push("-C", destArg, "-f", fileArg);
    await new toolrunner.ToolRunner(`tar`, args).exec();
    return dest;
  }
}

/**
 * Caches a directory and installs it into the tool cacheDir.
 *
 * Also see cacheDir function from node_modules/@actions/tool-cache/lib/tool-cache.d.ts
 *
 * @param sourceDir    the directory to cache into tools
 * @param tool         tool name
 * @param version      version of the tool.  semver format
 * @param mode           should run the actions or runner implementation
 * @param toolCacheDir   path to the tool cache directory
 * @param logger         logger to use
 */
export async function cacheDir(
  sourceDir: string,
  tool: string,
  version: string,
  toolCacheDir: string,
  logger: Logger
): Promise<string> {
  if (isActions()) {
    return await actionsToolcache.cacheDir(sourceDir, tool, version);
  } else {
    // Initial implementation copied from node_modules/@actions/tool-cache/lib/tool-cache.js

    version = semver.clean(version) || version;
    const arch = os.arch();
    logger.debug(`Caching tool ${tool} ${version} ${arch}`);
    logger.debug(`source dir: ${sourceDir}`);
    if (!fs.statSync(sourceDir).isDirectory()) {
      throw new Error("sourceDir is not a directory");
    }
    // Create the tool dir
    const destPath = createToolPath(tool, version, arch, toolCacheDir, logger);
    // copy each child item. do not move. move can fail on Windows
    // due to anti-virus software having an open handle on a file.
    for (const itemName of fs.readdirSync(sourceDir)) {
      const s = path.join(sourceDir, itemName);
      await io.cp(s, destPath, { recursive: true });
    }
    // write .complete
    completeToolPath(tool, version, arch, toolCacheDir, logger);
    return destPath;
  }
}

/**
 * Finds the path to a tool version in the local installed tool cache.
 *
 * Also see find function from node_modules/@actions/tool-cache/lib/tool-cache.d.ts
 *
 * @param toolName      name of the tool
 * @param versionSpec   version of the tool
 * @param mode           should run the actions or runner implementation
 * @param toolCacheDir   path to the tool cache directory
 * @param logger         logger to use
 */
export function find(
  toolName: string,
  versionSpec: string,
  toolCacheDir: string,
  logger: Logger
): string {
  if (isActions()) {
    return actionsToolcache.find(toolName, versionSpec);
  } else {
    // Initial implementation copied from node_modules/@actions/tool-cache/lib/tool-cache.js

    if (!toolName) {
      throw new Error("toolName parameter is required");
    }
    if (!versionSpec) {
      throw new Error("versionSpec parameter is required");
    }
    const arch = os.arch();
    // attempt to resolve an explicit version
    if (!isExplicitVersion(versionSpec, logger)) {
      const localVersions = findAllVersions(toolName, toolCacheDir, logger);
      const match = evaluateVersions(localVersions, versionSpec, logger);
      versionSpec = match;
    }
    // check for the explicit version in the cache
    let toolPath = "";
    if (versionSpec) {
      versionSpec = semver.clean(versionSpec) || "";
      const cachePath = path.join(toolCacheDir, toolName, versionSpec, arch);
      logger.debug(`checking cache: ${cachePath}`);
      if (fs.existsSync(cachePath) && fs.existsSync(`${cachePath}.complete`)) {
        logger.debug(`Found tool in cache ${toolName} ${versionSpec} ${arch}`);
        toolPath = cachePath;
      } else {
        logger.debug("not found");
      }
    }
    return toolPath;
  }
}

/**
 * Finds the paths to all versions of a tool that are installed in the local tool cache.
 *
 * Also see findAllVersions function from node_modules/@actions/tool-cache/lib/tool-cache.d.ts
 *
 * @param toolName  name of the tool
 * @param toolCacheDir   path to the tool cache directory
 * @param logger         logger to use
 */
export function findAllVersions(
  toolName: string,
  toolCacheDir: string,
  logger: Logger
): string[] {
  if (isActions()) {
    return actionsToolcache.findAllVersions(toolName);
  } else {
    // Initial implementation copied from node_modules/@actions/tool-cache/lib/tool-cache.js

    const versions: string[] = [];
    const arch = os.arch();
    const toolPath = path.join(toolCacheDir, toolName);
    if (fs.existsSync(toolPath)) {
      const children = fs.readdirSync(toolPath);
      for (const child of children) {
        if (isExplicitVersion(child, logger)) {
          const fullPath = path.join(toolPath, child, arch || "");
          if (
            fs.existsSync(fullPath) &&
            fs.existsSync(`${fullPath}.complete`)
          ) {
            versions.push(child);
          }
        }
      }
    }
    return versions;
  }
}

export async function downloadTool(
  url: string,
  tempDir: string,
  headers: IHeaders
): Promise<string> {
  const dest = path.join(tempDir, uuidV4());
  const finalHeaders = Object.assign(
    { "User-Agent": "CodeQL Action" },
    headers
  );
  return await actionsToolcache.downloadTool(
    url,
    dest,
    undefined,
    finalHeaders
  );
}

function createExtractFolder(tempDir: string): string {
  // create a temp dir
  const dest = path.join(tempDir, "toolcache-temp");
  if (!fs.existsSync(dest)) {
    fs.mkdirSync(dest);
  }
  return dest;
}

function createToolPath(
  tool: string,
  version: string,
  arch: string,
  toolCacheDir: string,
  logger: Logger
): string {
  const folderPath = path.join(
    toolCacheDir,
    tool,
    semver.clean(version) || version,
    arch || ""
  );
  logger.debug(`destination ${folderPath}`);
  const markerPath = `${folderPath}.complete`;
  fs.rmdirSync(folderPath, { recursive: true });
  fs.rmdirSync(markerPath, { recursive: true });
  fs.mkdirSync(folderPath, { recursive: true });
  return folderPath;
}

function completeToolPath(
  tool: string,
  version: string,
  arch: string,
  toolCacheDir: string,
  logger: Logger
) {
  const folderPath = path.join(
    toolCacheDir,
    tool,
    semver.clean(version) || version,
    arch || ""
  );
  const markerPath = `${folderPath}.complete`;
  fs.writeFileSync(markerPath, "");
  logger.debug("finished caching tool");
}

function isExplicitVersion(versionSpec: string, logger: Logger) {
  const c = semver.clean(versionSpec) || "";
  logger.debug(`isExplicit: ${c}`);
  const valid = semver.valid(c) != null;
  logger.debug(`explicit? ${valid}`);
  return valid;
}

function evaluateVersions(
  versions: string[],
  versionSpec: string,
  logger: Logger
): string {
  let version = "";
  logger.debug(`evaluating ${versions.length} versions`);
  versions = versions.sort((a, b) => {
    if (semver.gt(a, b)) {
      return 1;
    }
    return -1;
  });
  for (let i = versions.length - 1; i >= 0; i--) {
    const potential = versions[i];
    const satisfied = semver.satisfies(potential, versionSpec);
    if (satisfied) {
      version = potential;
      break;
    }
  }
  if (version) {
    logger.debug(`matched: ${version}`);
  } else {
    logger.debug("match not found");
  }
  return version;
}