import * as core from '@actions/core';
import * as tc from '@actions/tool-cache';
import path from 'path';

import BaseDistribution from '../base-distribution';
import {NodeInputs, INodeVersion, INodeVersionInfo} from '../base-models';

interface INodeRelease extends tc.IToolRelease {
  lts?: string;
}

export default class OfficialBuilds extends BaseDistribution {
  constructor(nodeInfo: NodeInputs) {
    super(nodeInfo);
  }

  public async setupNodeJs() {
    if (this.nodeInfo.mirrorURL) {
      if (this.nodeInfo.mirrorURL === '') {
        throw new Error('Mirror URL is empty. Please provide a valid mirror URL.');
    }
      let downloadPath = '';
      let toolPath = '';
      try {
        core.info(`Attempting to download using mirror URL...`);
        downloadPath = await this.downloadFromMirrorURL(); // Attempt to download from the mirror
        core.info('downloadPath from downloadFromMirrorURL() '+ downloadPath);
        if (downloadPath) {
          toolPath = downloadPath;
        }
      } catch (err) {
        core.info((err as Error).message);
        core.debug((err as Error).stack ?? 'empty stack');
      }
    } else {
      let manifest: tc.IToolRelease[] | undefined;
      let nodeJsVersions: INodeVersion[] | undefined;
      const osArch = this.translateArchToDistUrl(this.nodeInfo.arch);

      if (this.isLtsAlias(this.nodeInfo.versionSpec)) {
        core.info('Attempt to resolve LTS alias from manifest...');

        // No try-catch since it's not possible to resolve LTS alias without manifest
        manifest = await this.getManifest();

        this.nodeInfo.versionSpec = this.resolveLtsAliasFromManifest(
          this.nodeInfo.versionSpec,
          this.nodeInfo.stable,
          manifest
        );
      }

      if (this.isLatestSyntax(this.nodeInfo.versionSpec)) {
        nodeJsVersions = await this.getNodeJsVersions();
        const versions = this.filterVersions(nodeJsVersions);
        this.nodeInfo.versionSpec = this.evaluateVersions(versions);

        core.info('getting latest node version...');
      }

      if (this.nodeInfo.checkLatest) {
        core.info('Attempt to resolve the latest version from manifest...');
        const resolvedVersion = await this.resolveVersionFromManifest(
          this.nodeInfo.versionSpec,
          this.nodeInfo.stable,
          osArch,
          manifest
        );
        if (resolvedVersion) {
          this.nodeInfo.versionSpec = resolvedVersion;
          core.info(`Resolved as '${resolvedVersion}'`);
        } else {
          core.info(
            `Failed to resolve version ${this.nodeInfo.versionSpec} from manifest`
          );
        }
      }

      let toolPath = this.findVersionInHostedToolCacheDirectory();

      if (toolPath) {
        core.info(`Found in cache @ ${toolPath}`);
        this.addToolPath(toolPath);
        return;
      }

      let downloadPath = '';
      try {
        core.info(`Attempting to download ${this.nodeInfo.versionSpec}...`);

        const versionInfo = await this.getInfoFromManifest(
          this.nodeInfo.versionSpec,
          this.nodeInfo.stable,
          osArch,
          manifest
        );

        if (versionInfo) {
          core.info(
            `Acquiring ${versionInfo.resolvedVersion} - ${versionInfo.arch} from ${versionInfo.downloadUrl}`
          );
          downloadPath = await tc.downloadTool(
            versionInfo.downloadUrl,
            undefined,
            this.nodeInfo.auth
          );

          if (downloadPath) {
            toolPath = await this.extractArchive(
              downloadPath,
              versionInfo,
              false
            );
          }
        } else {
          core.info(
            'Not found in manifest. Falling back to download directly from Node'
          );
        }
      } catch (err) {
        // 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 as Error).message);
        }
        core.debug((err as Error).stack ?? 'empty stack');
        core.info('Falling back to download directly from Node');
      }

      if (!toolPath) {
        toolPath = await this.downloadDirectlyFromNode();
      }

      if (this.osPlat != 'win32') {
        toolPath = path.join(toolPath, 'bin');
      }

      core.addPath(toolPath);
    }
  }

  protected addToolPath(toolPath: string) {
    if (this.osPlat != 'win32') {
      toolPath = path.join(toolPath, 'bin');
    }

    core.addPath(toolPath);
  }

  protected async downloadDirectlyFromNode() {
    const nodeJsVersions = await this.getNodeJsVersions();
    const versions = this.filterVersions(nodeJsVersions);
    const evaluatedVersion = this.evaluateVersions(versions);

    if (!evaluatedVersion) {
      throw new Error(
        `Unable to find Node version '${this.nodeInfo.versionSpec}' for platform ${this.osPlat} and architecture ${this.nodeInfo.arch}.`
      );
    }

    const toolName = this.getNodejsDistInfo(evaluatedVersion);

    try {
      const toolPath = await this.downloadNodejs(toolName);
      return toolPath;
    } catch (error) {
      if (error instanceof tc.HTTPError && error.httpStatusCode === 404) {
        core.warning(
          `Node version ${this.nodeInfo.versionSpec} for platform ${this.osPlat} and architecture ${this.nodeInfo.arch} was found but failed to download. ` +
            'This usually happens when downloadable binaries are not fully updated at https://nodejs.org/. ' +
            'To resolve this issue you may either fall back to the older version or try again later.'
        );
      }

      throw error;
    }
  }

  protected evaluateVersions(versions: string[]): string {
    let version = '';

    if (this.isLatestSyntax(this.nodeInfo.versionSpec)) {
      core.info(`getting latest node version...`);
      return versions[0];
    }

    version = super.evaluateVersions(versions);

    return version;
  }

  protected getDistributionUrl(): string {
    if (this.nodeInfo.mirrorURL) {
      return this.nodeInfo.mirrorURL;
    }
    return `https://nodejs.org/dist`;
  }
  

  private getManifest(): Promise<tc.IToolRelease[]> {
    core.debug('Getting manifest from actions/node-versions@main');
    return tc.getManifestFromRepo(
      'actions',
      'node-versions',
      this.nodeInfo.auth,
      'main'
    );
  }

  private resolveLtsAliasFromManifest(
    versionSpec: string,
    stable: boolean,
    manifest: INodeRelease[]
  ): string {
    const alias = versionSpec.split('lts/')[1]?.toLowerCase();

    if (!alias) {
      throw new Error(
        `Unable to parse LTS alias for Node version '${versionSpec}'`
      );
    }

    core.debug(`LTS alias '${alias}' for Node version '${versionSpec}'`);

    // Supported formats are `lts/<alias>`, `lts/*`, and `lts/-n`. Where asterisk means highest possible LTS and -n means the nth-highest.
    const n = Number(alias);
    const aliases = Object.fromEntries(
      manifest
        .filter(x => x.lts && x.stable === stable)
        .map(x => [x.lts!.toLowerCase(), x])
        .reverse()
    );
    const numbered = Object.values(aliases);
    const release =
      alias === '*'
        ? numbered[numbered.length - 1]
        : n < 0
        ? numbered[numbered.length - 1 + n]
        : aliases[alias];

    if (!release) {
      throw new Error(
        `Unable to find LTS release '${alias}' for Node version '${versionSpec}'.`
      );
    }

    core.debug(
      `Found LTS release '${release.version}' for Node version '${versionSpec}'`
    );

    return release.version.split('.')[0];
  }

  private async resolveVersionFromManifest(
    versionSpec: string,
    stable: boolean,
    osArch: string,
    manifest: tc.IToolRelease[] | undefined
  ): Promise<string | undefined> {
    try {
      const info = await this.getInfoFromManifest(
        versionSpec,
        stable,
        osArch,
        manifest
      );
      return info?.resolvedVersion;
    } catch (err) {
      core.info('Unable to resolve version from manifest...');
      core.debug((err as Error).message);
    }
  }

  private async getInfoFromManifest(
    versionSpec: string,
    stable: boolean,
    osArch: string,
    manifest: tc.IToolRelease[] | undefined
  ): Promise<INodeVersionInfo | null> {
    let info: INodeVersionInfo | null = null;
    if (!manifest) {
      core.debug('No manifest cached');
      manifest = await this.getManifest();
    }

    const rel = await tc.findFromManifest(
      versionSpec,
      stable,
      manifest,
      osArch
    );

    if (rel && rel.files.length > 0) {
      info = <INodeVersionInfo>{};
      info.resolvedVersion = rel.version;
      info.arch = rel.files[0].arch;
      info.downloadUrl = rel.files[0].download_url;
      info.fileName = rel.files[0].filename;
    }

    return info;
  }

  private isLtsAlias(versionSpec: string): boolean {
    return versionSpec.startsWith('lts/');
  }

  private isLatestSyntax(versionSpec): boolean {
    return ['current', 'latest', 'node'].includes(versionSpec);
  }

  protected async downloadFromMirrorURL() {
    const nodeJsVersions = await this.getMirrorUrlVersions();
    const versions = this.filterVersions(nodeJsVersions);


    const evaluatedVersion = this.evaluateVersions(versions);



    if (!evaluatedVersion) {
      throw new Error(
        `Unable to find Node version '${this.nodeInfo.versionSpec}' for platform ${this.osPlat} and architecture ${this.nodeInfo.arch} from the provided mirror-url ${this.nodeInfo.mirrorURL}. Please check the mirror-url`
      );
    }

    const toolName = this.getNodejsMirrorURLInfo(evaluatedVersion);



    try {
      const toolPath = await this.downloadNodejs(toolName);

      return toolPath;
    } catch (error) {
      if (error instanceof tc.HTTPError && error.httpStatusCode === 404) {
        core.error(
          `Node version ${this.nodeInfo.versionSpec} for platform ${this.osPlat} and architecture ${this.nodeInfo.arch} was found but failed to download. ` +
            'This usually happens when downloadable binaries are not fully updated at https://nodejs.org/. ' +
            'To resolve this issue you may either fall back to the older version or try again later.'
        );
      } else {
        // For any other error type, you can log the error message.
        core.error(`An unexpected error occurred like url might not correct`);
      }

      throw error;
    }
  }
}