import * as os from 'os';
import * as path from 'path';
import * as core from '@actions/core';
import * as tc from '@actions/tool-cache';
import * as semver from 'semver';
import * as httpm from '@actions/http-client';
import * as ifm from '@actions/http-client/lib/interfaces';
import * as exec from '@actions/exec';

import fs from 'fs';
import * as http from 'http';

import {
  IS_WINDOWS,
  IGraalPyManifestRelease,
  createSymlinkInFolder,
  isNightlyKeyword,
  getNextPageUrl
} from './utils';

const TOKEN = core.getInput('token');
const AUTH = !TOKEN ? undefined : `token ${TOKEN}`;

export async function installGraalPy(
  graalpyVersion: string,
  architecture: string,
  allowPreReleases: boolean,
  releases: IGraalPyManifestRelease[] | undefined
) {
  let downloadDir;

  releases = releases ?? (await getAvailableGraalPyVersions());

  if (!releases || !releases.length) {
    throw new Error('No release was found in GraalPy version.json');
  }

  let releaseData = findRelease(releases, graalpyVersion, architecture, false);

  if (allowPreReleases && (!releaseData || !releaseData.foundAsset)) {
    // check for pre-release
    core.info(
      [
        `Stable GraalPy version ${graalpyVersion} with arch ${architecture} not found`,
        `Trying pre-release versions`
      ].join(os.EOL)
    );
    releaseData = findRelease(releases, graalpyVersion, architecture, true);
  }

  if (!releaseData || !releaseData.foundAsset) {
    throw new Error(
      `GraalPy version ${graalpyVersion} with arch ${architecture} not found`
    );
  }

  const {foundAsset, resolvedGraalPyVersion} = releaseData;
  const downloadUrl = `${foundAsset.browser_download_url}`;

  core.info(`Downloading GraalPy from "${downloadUrl}" ...`);

  try {
    const graalpyPath = await tc.downloadTool(downloadUrl, undefined, AUTH);

    core.info('Extracting downloaded archive...');
    if (IS_WINDOWS) {
      downloadDir = await tc.extractZip(graalpyPath);
    } else {
      downloadDir = await tc.extractTar(graalpyPath);
    }

    // root folder in archive can have unpredictable name so just take the first folder
    // downloadDir is unique folder under TEMP and can't contain any other folders
    const archiveName = fs.readdirSync(downloadDir)[0];

    const toolDir = path.join(downloadDir, archiveName);
    let installDir = toolDir;
    if (!isNightlyKeyword(resolvedGraalPyVersion)) {
      installDir = await tc.cacheDir(
        toolDir,
        'GraalPy',
        resolvedGraalPyVersion,
        architecture
      );
    }

    const binaryPath = path.join(installDir, 'bin');
    await createGraalPySymlink(binaryPath, resolvedGraalPyVersion);
    await installPip(binaryPath);

    return {installDir, resolvedGraalPyVersion};
  } catch (err) {
    if (err instanceof Error) {
      // Rate limit?
      if (
        err instanceof tc.HTTPError &&
        (err.httpStatusCode === 403 || err.httpStatusCode === 429)
      ) {
        core.info(
          `Received HTTP status code ${err.httpStatusCode}.  This usually indicates the rate limit has been exceeded`
        );
      } else {
        core.info(err.message);
      }
      if (err.stack !== undefined) {
        core.debug(err.stack);
      }
    }
    throw err;
  }
}

export async function getAvailableGraalPyVersions() {
  const http: httpm.HttpClient = new httpm.HttpClient('tool-cache');

  const headers: http.OutgoingHttpHeaders = {};
  if (AUTH) {
    headers.authorization = AUTH;
  }

  /*
  Get releases first.
  */
  let url: string | null =
    'https://api.github.com/repos/oracle/graalpython/releases';
  const result: IGraalPyManifestRelease[] = [];
  do {
    const response: ifm.TypedResponse<IGraalPyManifestRelease[]> =
      await http.getJson(url, headers);
    if (!response.result) {
      throw new Error(
        `Unable to retrieve the list of available GraalPy versions from '${url}'`
      );
    }
    result.push(...response.result);
    url = getNextPageUrl(response);
  } while (url);

  /*
  Add pre-release builds.
  */
  url =
    'https://api.github.com/repos/graalvm/graal-languages-ea-builds/releases';
  do {
    const response: ifm.TypedResponse<IGraalPyManifestRelease[]> =
      await http.getJson(url, headers);
    if (!response.result) {
      throw new Error(
        `Unable to retrieve the list of available GraalPy versions from '${url}'`
      );
    }
    result.push(...response.result);
    url = getNextPageUrl(response);
  } while (url);

  return result;
}

async function createGraalPySymlink(
  graalpyBinaryPath: string,
  graalpyVersion: string
) {
  const version = semver.coerce(graalpyVersion)!;
  const pythonBinaryPostfix = semver.major(version);
  const pythonMinor = semver.minor(version);
  const graalpyMajorMinorBinaryPostfix = `${pythonBinaryPostfix}.${pythonMinor}`;
  const binaryExtension = IS_WINDOWS ? '.exe' : '';

  core.info('Creating symlinks...');
  createSymlinkInFolder(
    graalpyBinaryPath,
    `graalpy${binaryExtension}`,
    `python${pythonBinaryPostfix}${binaryExtension}`,
    true
  );

  createSymlinkInFolder(
    graalpyBinaryPath,
    `graalpy${binaryExtension}`,
    `python${binaryExtension}`,
    true
  );

  createSymlinkInFolder(
    graalpyBinaryPath,
    `graalpy${binaryExtension}`,
    `graalpy${graalpyMajorMinorBinaryPostfix}${binaryExtension}`,
    true
  );
}

async function installPip(pythonLocation: string) {
  core.info(
    "Installing pip (GraalPy doesn't update pip because it uses a patched version of pip)"
  );
  const pythonBinary = path.join(pythonLocation, 'python');
  await exec.exec(`${pythonBinary} -m ensurepip --default-pip`);
}

export function graalPyTagToVersion(tag: string) {
  const versionPattern =
    /.*-(\d+\.\d+\.\d+(?:\.\d+)?)(?:-((?:ea|a|b|rc))\.0*(\d+))?/;
  const match = tag.match(versionPattern);
  if (match && match[2]) {
    return `${match[1]}-${match[2]}.${match[3]}`;
  } else if (match) {
    return match[1];
  } else {
    return tag.replace(/.*-/, '');
  }
}

export function findRelease(
  releases: IGraalPyManifestRelease[],
  graalpyVersion: string,
  architecture: string,
  includePrerelease: boolean
) {
  const options = {includePrerelease: includePrerelease};
  const filterReleases = releases.filter(item => {
    const isVersionSatisfied = semver.satisfies(
      graalPyTagToVersion(item.tag_name),
      graalpyVersion,
      options
    );
    return (
      isVersionSatisfied && !!findAsset(item, architecture, process.platform)
    );
  });

  if (!filterReleases.length) {
    return null;
  }

  const sortedReleases = filterReleases.sort((previous, current) =>
    semver.compare(
      semver.coerce(graalPyTagToVersion(current.tag_name))!,
      semver.coerce(graalPyTagToVersion(previous.tag_name))!
    )
  );

  const foundRelease = sortedReleases[0];
  const foundAsset = findAsset(foundRelease, architecture, process.platform);

  return {
    foundAsset,
    resolvedGraalPyVersion: graalPyTagToVersion(foundRelease.tag_name)
  };
}

export function toGraalPyPlatform(platform: string) {
  switch (platform) {
    case 'win32':
      return 'windows';
    case 'darwin':
      return 'macos';
  }
  return platform;
}

export function toGraalPyArchitecture(architecture: string) {
  switch (architecture) {
    case 'x64':
      return 'amd64';
    case 'arm64':
      return 'aarch64';
  }
  return architecture;
}

export function findAsset(
  item: IGraalPyManifestRelease,
  architecture: string,
  platform: string
) {
  const graalpyArch = toGraalPyArchitecture(architecture);
  const graalpyPlatform = toGraalPyPlatform(platform);
  const graalpyExt = platform == 'win32' ? 'zip' : 'tar.gz';
  const found = item.assets.filter(
    file =>
      file.name.startsWith('graalpy') &&
      file.name.endsWith(`-${graalpyPlatform}-${graalpyArch}.${graalpyExt}`)
  );
  /*
  In the future there could be more variants of GraalPy for a single release. Pick the shortest name, that one is the most likely to be the primary variant.
  */
  found.sort((f1, f2) => f1.name.length - f2.name.length);
  return found[0];
}
