import * as c from './constants'
import * as core from '@actions/core'
import * as semver from 'semver'
import {
  downloadAndExtractJDK,
  downloadExtractAndCacheJDK,
  getContents,
  getLatestRelease,
  getMatchingTags,
  getTaggedRelease
} from './utils'
import { downloadGraalVM, downloadGraalVMEELegacy } from './gds'
import { downloadTool } from '@actions/tool-cache'
import { basename } from 'path'

const GRAALVM_DL_BASE = 'https://download.oracle.com/graalvm'
const GRAALVM_CE_DL_BASE = `https://github.com/graalvm/${c.GRAALVM_RELEASES_REPO}/releases/download`
const ORACLE_GRAALVM_REPO_EA_BUILDS = 'oracle-graalvm-ea-builds'
const ORACLE_GRAALVM_REPO_EA_BUILDS_LATEST_SYMBOL = 'latest-ea'
const GRAALVM_REPO_DEV_BUILDS = 'graalvm-ce-dev-builds'
const GRAALVM_JDK_TAG_PREFIX = 'jdk-'
const GRAALVM_TAG_PREFIX = 'vm-'

// Support for GraalVM for JDK 17 and later

export async function setUpGraalVMJDK(javaVersionOrDev: string, gdsToken: string): Promise<string> {
  if (javaVersionOrDev === c.VERSION_DEV) {
    return setUpGraalVMJDKDevBuild()
  }
  const isTokenProvided = gdsToken.length > 0
  let javaVersion = javaVersionOrDev
  const toolName = determineToolName(javaVersion, false)
  if (javaVersionOrDev === '17' && !isTokenProvided) {
    core.warning(
      'This build uses the last update of Oracle GraalVM for JDK 17 under the GFTC. More details: https://github.com/marketplace/actions/github-action-for-graalvm#notes-on-oracle-graalvm-for-jdk-17'
    )
    return setUpGraalVMJDK('17.0.12', gdsToken)
  }
  if (isTokenProvided) {
    // Download from GDS
    const downloader = async () => downloadGraalVM(gdsToken, javaVersion)
    return downloadExtractAndCacheJDK(downloader, toolName, javaVersion)
  }
  // Download from oracle.com
  let downloadName = toolName
  let downloadUrl: string
  if (javaVersion.endsWith('-ea')) {
    downloadUrl = await findLatestEABuildDownloadUrl(javaVersion)
    const filename = basename(downloadUrl)
    const resolvedVersion = semver.valid(semver.coerce(filename))
    if (!resolvedVersion) {
      throw new Error(`Unable to determine resolved version based on '${filename}'. ${c.ERROR_REQUEST}`)
    }
    javaVersion = resolvedVersion
  } else if (javaVersion.includes('.')) {
    if (semver.valid(javaVersion)) {
      const majorJavaVersion = semver.major(javaVersion)
      const minorJavaVersion = semver.minor(javaVersion)
      const patchJavaVersion = semver.patch(javaVersion)
      const isGARelease = minorJavaVersion === 0 && patchJavaVersion === 0
      if (isGARelease) {
        // For GA versions of JDKs, /archive/ does not use minor and patch version (see https://www.oracle.com/java/technologies/jdk-script-friendly-urls/)
        downloadName = determineToolName(majorJavaVersion.toString(), false)
      }
      downloadUrl = `${GRAALVM_DL_BASE}/${majorJavaVersion}/archive/${downloadName}${c.GRAALVM_FILE_EXTENSION}`
    } else {
      throw new Error(
        `java-version set to '${javaVersion}'. Please make sure the java-version is set correctly. ${c.ERROR_HINT}`
      )
    }
  } else {
    downloadUrl = `${GRAALVM_DL_BASE}/${javaVersion}/latest/${downloadName}${c.GRAALVM_FILE_EXTENSION}`
  }
  const downloader = async () => downloadGraalVMJDK(downloadUrl, javaVersion)
  return downloadExtractAndCacheJDK(downloader, toolName, javaVersion)
}

export async function findLatestEABuildDownloadUrl(javaEaVersion: string): Promise<string> {
  const filePath = `versions/${javaEaVersion}.json`
  let response
  try {
    response = await getContents(ORACLE_GRAALVM_REPO_EA_BUILDS, filePath)
  } catch (error) {
    throw new Error(
      `Unable to resolve download URL for '${javaEaVersion}' (reason: ${error}). Please make sure the java-version is set correctly. ${c.ERROR_HINT}`
    )
  }
  if (Array.isArray(response) || response.type !== 'file' || !response.content) {
    throw new Error(`Unexpected response when resolving download URL for '${javaEaVersion}'. ${c.ERROR_REQUEST}`)
  }
  const versionData = JSON.parse(Buffer.from(response.content, 'base64').toString('utf-8'))
  let latestVersion
  if (javaEaVersion === ORACLE_GRAALVM_REPO_EA_BUILDS_LATEST_SYMBOL) {
    latestVersion = versionData as c.OracleGraalVMEAVersion
  } else {
    latestVersion = (versionData as c.OracleGraalVMEAVersion[]).find((v) => v.latest)
    if (!latestVersion) {
      throw new Error(`Unable to find latest version for '${javaEaVersion}'. ${c.ERROR_REQUEST}`)
    }
  }
  const file = latestVersion.files.find((f) => f.arch === c.JDK_ARCH && f.platform === c.GRAALVM_PLATFORM)
  if (!file || !file.filename.startsWith('graalvm-jdk-')) {
    throw new Error(`Unable to find file metadata for '${javaEaVersion}'. ${c.ERROR_REQUEST}`)
  }
  return `${latestVersion.download_base_url}${file.filename}`
}

export async function setUpGraalVMJDKCE(javaVersionOrDev: string): Promise<string> {
  if (javaVersionOrDev === c.VERSION_DEV) {
    return setUpGraalVMJDKDevBuild()
  }
  let javaVersion = javaVersionOrDev
  if (!javaVersion.includes('.')) {
    javaVersion = await findLatestGraalVMJDKCEJavaVersion(javaVersion)
  }
  if (javaVersion.split('.').length != 3) {
    throw new Error(
      `java-version set to '${javaVersionOrDev}', which was resolved to '${javaVersion}'. Please make sure the java-version is set correctly. ${c.ERROR_HINT}`
    )
  }
  const toolName = determineToolName(javaVersion, true)
  const downloadUrl = `${GRAALVM_CE_DL_BASE}/jdk-${javaVersion}/${toolName}${c.GRAALVM_FILE_EXTENSION}`
  const downloader = async () => downloadGraalVMJDK(downloadUrl, javaVersion)
  return downloadExtractAndCacheJDK(downloader, toolName, javaVersion)
}

export async function findLatestGraalVMJDKCEJavaVersion(majorJavaVersion: string): Promise<string> {
  const matchingRefs = await getMatchingTags(
    c.GRAALVM_GH_USER,
    c.GRAALVM_RELEASES_REPO,
    `${GRAALVM_JDK_TAG_PREFIX}${majorJavaVersion}`
  )
  const lowestNonExistingVersion = '0.0.1'
  let highestVersion = lowestNonExistingVersion
  const versionNumberStartIndex = `refs/tags/${GRAALVM_JDK_TAG_PREFIX}`.length
  for (const matchingRef of matchingRefs) {
    const currentVersion = matchingRef.ref.substring(versionNumberStartIndex)
    if (semver.valid(currentVersion) && semver.gt(currentVersion, highestVersion)) {
      highestVersion = currentVersion
    }
  }
  if (highestVersion === lowestNonExistingVersion) {
    throw new Error(
      `Unable to find the latest Java version for '${majorJavaVersion}'. Please make sure the java-version is set correctly. ${c.ERROR_HINT}`
    )
  }
  return highestVersion
}

function determineToolName(javaVersion: string, isCommunity: boolean) {
  return `graalvm${isCommunity ? '-community' : ''}-jdk-${javaVersion}_${c.JDK_PLATFORM}-${c.JDK_ARCH}_bin`
}

async function downloadGraalVMJDK(downloadUrl: string, javaVersion: string): Promise<string> {
  try {
    return await downloadTool(downloadUrl)
  } catch (error) {
    if (error instanceof Error && error.message.includes('404')) {
      // Not Found
      throw new Error(
        `Failed to download ${basename(downloadUrl)}. Are you sure java-version: '${javaVersion}' is correct?`
      )
    }
    throw new Error(`Failed to download ${basename(downloadUrl)} (error: ${error}).`)
  }
}

// Support for GraalVM dev builds

export async function setUpGraalVMJDKDevBuild(): Promise<string> {
  const latestDevBuild = await getLatestRelease(GRAALVM_REPO_DEV_BUILDS)
  const resolvedJavaVersion = findHighestJavaVersion(latestDevBuild, c.VERSION_DEV)
  const downloadUrl = findDownloadUrl(latestDevBuild, resolvedJavaVersion)
  return downloadAndExtractJDK(downloadUrl)
}

export function findHighestJavaVersion(release: c.LatestReleaseResponse['data'], version: string): string {
  const graalVMIdentifierPattern = determineGraalVMLegacyIdentifier(false, version, '(\\d+)')
  const expectedFileNameRegExp = new RegExp(
    `^${graalVMIdentifierPattern}${c.GRAALVM_FILE_EXTENSION.replace(/\./g, '\\.')}$`
  )
  let highestJavaVersion = 0
  for (const asset of release.assets) {
    const matches = asset.name.match(expectedFileNameRegExp)
    if (matches) {
      const javaVersion = +matches[1]
      if (javaVersion > highestJavaVersion) {
        highestJavaVersion = javaVersion
      }
    }
  }
  if (highestJavaVersion > 0) {
    return String(highestJavaVersion)
  } else {
    throw new Error(
      'Could not find highest Java version. Please file an issue at: https://github.com/graalvm/setup-graalvm/issues.'
    )
  }
}

// Support for GraalVM 22.X releases and earlier

export async function setUpGraalVMLatest_22_X(gdsToken: string, javaVersion: string): Promise<string> {
  const lockedVersion = javaVersion === '19' ? '22.3.1' : '22.3.3'
  if (gdsToken.length > 0) {
    return setUpGraalVMRelease(gdsToken, lockedVersion, javaVersion)
  }
  const latestRelease = await getTaggedRelease(
    c.GRAALVM_GH_USER,
    c.GRAALVM_RELEASES_REPO,
    GRAALVM_TAG_PREFIX + lockedVersion
  )
  const version = findGraalVMVersion(latestRelease)
  return setUpGraalVMRelease(gdsToken, version, javaVersion)
}

export function findGraalVMVersion(release: c.LatestReleaseResponse['data']) {
  const tag_name = release.tag_name
  if (!tag_name.startsWith(GRAALVM_TAG_PREFIX)) {
    throw new Error(`Could not find latest GraalVM release: ${tag_name}`)
  }
  return tag_name.substring(GRAALVM_TAG_PREFIX.length, tag_name.length)
}

export async function setUpGraalVMRelease(gdsToken: string, version: string, javaVersion: string): Promise<string> {
  const isEE = gdsToken.length > 0
  const toolName = determineLegacyToolName(isEE, version, javaVersion)
  let downloader: () => Promise<string>
  if (isEE) {
    downloader = async () => downloadGraalVMEELegacy(gdsToken, version, javaVersion)
  } else {
    downloader = async () => downloadGraalVMCELegacy(version, javaVersion)
  }
  return downloadExtractAndCacheJDK(downloader, toolName, version)
}

function findDownloadUrl(release: c.LatestReleaseResponse['data'], javaVersion: string): string {
  const graalVMIdentifier = determineGraalVMLegacyIdentifier(false, c.VERSION_DEV, javaVersion)
  const expectedFileName = `${graalVMIdentifier}${c.GRAALVM_FILE_EXTENSION}`
  for (const asset of release.assets) {
    if (asset.name === expectedFileName) {
      return asset.browser_download_url
    }
  }
  throw new Error(
    `Could not find GraalVM dev build for Java ${javaVersion}. It may no longer be available, so please consider upgrading the Java version. ${c.ERROR_HINT}`
  )
}

function determineGraalVMLegacyIdentifier(isEE: boolean, version: string, javaVersion: string): string {
  return `${determineLegacyToolName(isEE, version, javaVersion)}-${c.GRAALVM_ARCH}-${version}`
}

function determineLegacyToolName(isEE: boolean, version: string, javaVersion: string): string {
  const infix = isEE ? 'ee' : version === c.VERSION_DEV ? 'community' : 'ce'
  return `graalvm-${infix}-java${javaVersion}-${c.GRAALVM_PLATFORM}`
}

async function downloadGraalVMCELegacy(version: string, javaVersion: string): Promise<string> {
  const graalVMIdentifier = determineGraalVMLegacyIdentifier(false, version, javaVersion)
  const downloadUrl = `${GRAALVM_CE_DL_BASE}/${GRAALVM_TAG_PREFIX}${version}/${graalVMIdentifier}${c.GRAALVM_FILE_EXTENSION}`
  try {
    return await downloadTool(downloadUrl)
  } catch (error) {
    if (error instanceof Error && error.message.includes('404')) {
      // Not Found
      throw new Error(
        `Failed to download ${graalVMIdentifier}. Are you sure version: '${version}' and java-version: '${javaVersion}' are correct?`
      )
    }
    throw new Error(`Failed to download ${graalVMIdentifier} (error: ${error}).`)
  }
}
