import * as core from '@actions/core';
import semver from 'semver';
import fs from 'fs';
import {OutgoingHttpHeaders} from 'http';
import path from 'path';
import {
  cacheJdkDir,
  convertVersionToSemver,
  extractJdkFile,
  getDownloadArchiveExtension,
  isVersionSatisfies,
  renameWinArchive
} from '../../util.js';
import {JavaBase} from '../base-installer.js';
import {
  JavaDownloadRelease,
  JavaInstallerOptions,
  JavaInstallerResults
} from '../base-models.js';
import {isAlpineLinux} from '../platform-types.js';
import {ISapMachineAllVersions, ISapMachineVersions} from './models.js';

export class SapMachineDistribution extends JavaBase {
  constructor(installerOptions: JavaInstallerOptions) {
    super('SapMachine', installerOptions);
  }

  protected async findPackageForDownload(
    version: string
  ): Promise<JavaDownloadRelease> {
    core.debug(`Only stable versions: ${this.stable}`);

    if (!['jdk', 'jre'].includes(this.packageType)) {
      throw new Error(
        'SapMachine provides only the `jdk` and `jre` package type'
      );
    }

    const availableVersions = await this.getAvailableVersions();

    const matchedVersions = availableVersions
      .filter(item => {
        return isVersionSatisfies(version, item.version);
      })
      .map(item => {
        return {
          version: item.version,
          url: item.downloadLink
        } as JavaDownloadRelease;
      });

    if (!matchedVersions.length) {
      const availableVersionStrings = availableVersions.map(
        item => item.version
      );
      throw this.createVersionNotFoundError(version, availableVersionStrings);
    }

    const resolvedVersion = matchedVersions[0];
    const checksumUrl = resolvedVersion.url.replace(
      /\.(?:tar\.gz|zip)$/,
      '.sha256.txt'
    );
    return {
      ...resolvedVersion,
      checksum: await this.fetchChecksum(checksumUrl, 'sha256')
    };
  }

  private async getAvailableVersions(): Promise<ISapMachineVersions[]> {
    const platform = this.getPlatformOption();
    const arch = this.distributionArchitecture();

    let fetchedReleasesJson = await this.fetchReleasesFromUrl(
      'https://sapmachine.io/assets/data/sapmachine-releases-all.json'
    );

    if (!fetchedReleasesJson) {
      fetchedReleasesJson = await this.fetchReleasesFromUrl(
        'https://sap.github.io/SapMachine/assets/data/sapmachine-releases-all.json'
      );
    }

    if (!fetchedReleasesJson) {
      throw new Error(
        `Couldn't fetch SapMachine versions information from both primary and backup urls`
      );
    }

    core.debug(
      'Successfully fetched information about available SapMachine versions'
    );

    const availableVersions = this.parseVersions(
      platform,
      arch,
      fetchedReleasesJson
    );

    if (core.isDebug()) {
      core.startGroup('Print information about available versions');
      core.debug(availableVersions.map(item => item.version).join(', '));
      core.endGroup();
    }

    return availableVersions;
  }

  protected async downloadTool(
    javaRelease: JavaDownloadRelease
  ): Promise<JavaInstallerResults> {
    core.info(
      `Downloading Java ${javaRelease.version} (${this.distribution}) from ${javaRelease.url} ...`
    );
    let javaArchivePath = await this.downloadAndVerify(javaRelease);

    core.info(`Extracting Java archive...`);
    const extension = getDownloadArchiveExtension();
    if (process.platform === 'win32') {
      javaArchivePath = renameWinArchive(javaArchivePath);
    }
    const extractedJavaPath = await extractJdkFile(javaArchivePath, extension);

    const archiveName = fs.readdirSync(extractedJavaPath)[0];
    const archivePath = path.join(extractedJavaPath, archiveName);
    const version = this.getToolcacheVersionName(javaRelease.version);

    const javaPath = await cacheJdkDir(
      archivePath,
      this.toolcacheFolderName,
      version,
      this.architecture
    );

    return {version: javaRelease.version, path: javaPath};
  }

  private parseVersions(
    platform: string,
    arch: string,
    versions: ISapMachineAllVersions
  ): ISapMachineVersions[] {
    const eligibleVersions: ISapMachineVersions[] = [];

    for (const [, majorVersionMap] of Object.entries(versions)) {
      for (const [, jdkVersionMap] of Object.entries(majorVersionMap.updates)) {
        for (const [buildVersion, buildVersionMap] of Object.entries(
          jdkVersionMap
        )) {
          let buildVersionWithoutPrefix = buildVersion.replace(
            'sapmachine-',
            ''
          );
          if (!buildVersionWithoutPrefix.includes('.')) {
            // replace major version with major.minor.patch and keep the remaining build identifier after the + as is with regex
            buildVersionWithoutPrefix = buildVersionWithoutPrefix.replace(
              /(\d+)(\+.*)?/,
              '$1.0.0$2'
            );
          }
          // replace + with . to convert to semver format if we have more than 3 version digits
          if (buildVersionWithoutPrefix.split('.').length > 3) {
            buildVersionWithoutPrefix = buildVersionWithoutPrefix.replace(
              '+',
              '.'
            );
          }
          buildVersionWithoutPrefix = convertVersionToSemver(
            buildVersionWithoutPrefix
          );

          // ignore invalid version
          if (!semver.valid(buildVersionWithoutPrefix)) {
            core.debug(`Invalid version: ${buildVersionWithoutPrefix}`);
            continue;
          }

          const isEarlyAccess =
            buildVersionMap.ea === true || buildVersionMap.ea === 'true';
          if (this.stable === isEarlyAccess) {
            continue;
          }

          for (const [edition, editionAssets] of Object.entries(
            buildVersionMap.assets
          )) {
            if (this.packageType !== edition) {
              continue;
            }
            for (const [archAndPlatForm, archAssets] of Object.entries(
              editionAssets
            )) {
              let expectedArchAndPlatform = `${platform}-${arch}`;
              if (platform === 'linux-musl') {
                expectedArchAndPlatform = `linux-${arch}-musl`;
              }
              if (archAndPlatForm !== expectedArchAndPlatform) {
                continue;
              }
              for (const [contentType, contentTypeAssets] of Object.entries(
                archAssets
              )) {
                // skip if not tar.gz and zip files
                if (contentType !== 'tar.gz' && contentType !== 'zip') {
                  continue;
                }
                eligibleVersions.push({
                  os: platform,
                  architecture: arch,
                  version: buildVersionWithoutPrefix,
                  checksum: contentTypeAssets.checksum,
                  downloadLink: contentTypeAssets.url,
                  packageType: edition
                });
              }
            }
          }
        }
      }
    }

    const sortedVersions = this.sortParsedVersions(eligibleVersions);

    return sortedVersions;
  }

  // Sorts versions in descending order as by default data in JSON isn't sorted
  private sortParsedVersions(
    eligibleVersions: ISapMachineVersions[]
  ): ISapMachineVersions[] {
    const sortedVersions = eligibleVersions.sort((versionObj1, versionObj2) => {
      const version1 = versionObj1.version;
      const version2 = versionObj2.version;
      return semver.compareBuild(version1, version2);
    });
    return sortedVersions.reverse();
  }

  private getPlatformOption(): string {
    switch (process.platform) {
      case 'win32':
        return 'windows';
      case 'darwin':
        return 'macos';
      case 'linux':
        // figure out if alpine/musl
        if (isAlpineLinux()) {
          return 'linux-musl';
        }
        return 'linux';
      default:
        return process.platform;
    }
  }

  private async fetchReleasesFromUrl(
    url: string,
    headers: OutgoingHttpHeaders = {}
  ): Promise<ISapMachineAllVersions | null> {
    try {
      core.debug(
        `Trying to fetch available SapMachine versions info from the primary url: ${url}`
      );
      const releases = (
        await this.http.getJson<ISapMachineAllVersions>(url, headers)
      ).result;
      return releases;
    } catch (err) {
      core.debug(
        `Fetching SapMachine versions info from the link: ${url} ended up with the error: ${
          (err as Error).message
        }`
      );
      return null;
    }
  }
}
