// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

import type * as TApiExtractor from '@microsoft/api-extractor';
import type {
  IHeftTaskPlugin,
  IHeftTaskRunHookOptions,
  IHeftTaskSession,
  HeftConfiguration,
  IHeftTaskRunIncrementalHookOptions
} from '@rushstack/heft';
import { ProjectConfigurationFile } from '@rushstack/heft-config-file';

import { ApiExtractorRunner } from './ApiExtractorRunner';
import apiExtractorConfigSchema from './schemas/api-extractor-task.schema.json';

// eslint-disable-next-line @rushstack/no-new-null
const UNINITIALIZED: null = null;

const PLUGIN_NAME: string = 'api-extractor-plugin';
const TASK_CONFIG_RELATIVE_PATH: string = './config/api-extractor-task.json';
const EXTRACTOR_CONFIG_FILENAME: typeof TApiExtractor.ExtractorConfig.FILENAME = 'api-extractor.json';
const LEGACY_EXTRACTOR_CONFIG_RELATIVE_PATH: string = `./${EXTRACTOR_CONFIG_FILENAME}`;
const EXTRACTOR_CONFIG_RELATIVE_PATH: string = `./config/${EXTRACTOR_CONFIG_FILENAME}`;

export interface IApiExtractorConfigurationResult {
  apiExtractorPackage: typeof TApiExtractor;
  apiExtractorConfiguration: TApiExtractor.ExtractorConfig;
}

export interface IApiExtractorTaskConfiguration {
  /**
   * If set to true, use the project's TypeScript compiler version for API Extractor's
   * analysis. API Extractor's included TypeScript compiler can generally correctly
   * analyze typings generated by older compilers, and referencing the project's compiler
   * can cause issues. If issues are encountered with API Extractor's included compiler,
   * set this option to true.
   *
   * This corresponds to API Extractor's `--typescript-compiler-folder` CLI option and
   * `IExtractorInvokeOptions.typescriptCompilerFolder` API option. This option defaults to false.
   */
  useProjectTypescriptVersion?: boolean;

  /**
   * If set to true, do a full run of api-extractor on every build.
   */
  runInWatchMode?: boolean;
}

export default class ApiExtractorPlugin implements IHeftTaskPlugin {
  private _apiExtractor: typeof TApiExtractor | undefined;
  private _apiExtractorConfigurationFilePath: string | undefined | typeof UNINITIALIZED = UNINITIALIZED;
  private _apiExtractorTaskConfigurationFileLoader:
    | ProjectConfigurationFile<IApiExtractorTaskConfiguration>
    | undefined;
  private _printedWatchWarning: boolean = false;

  public apply(taskSession: IHeftTaskSession, heftConfiguration: HeftConfiguration): void {
    const runAsync = async (
      runOptions: IHeftTaskRunHookOptions & Partial<IHeftTaskRunIncrementalHookOptions>
    ): Promise<void> => {
      const result: IApiExtractorConfigurationResult | undefined =
        await this._getApiExtractorConfigurationAsync(taskSession, heftConfiguration);
      if (result) {
        await this._runApiExtractorAsync(
          taskSession,
          heftConfiguration,
          runOptions,
          result.apiExtractorPackage,
          result.apiExtractorConfiguration
        );
      }
    };

    taskSession.hooks.run.tapPromise(PLUGIN_NAME, runAsync);
    taskSession.hooks.runIncremental.tapPromise(PLUGIN_NAME, runAsync);
  }

  private async _getApiExtractorConfigurationFilePathAsync(
    taskSession: IHeftTaskSession,
    heftConfiguration: HeftConfiguration
  ): Promise<string | undefined> {
    if (this._apiExtractorConfigurationFilePath === UNINITIALIZED) {
      this._apiExtractorConfigurationFilePath =
        await heftConfiguration.rigConfig.tryResolveConfigFilePathAsync(EXTRACTOR_CONFIG_RELATIVE_PATH);
      if (this._apiExtractorConfigurationFilePath === undefined) {
        this._apiExtractorConfigurationFilePath =
          await heftConfiguration.rigConfig.tryResolveConfigFilePathAsync(
            LEGACY_EXTRACTOR_CONFIG_RELATIVE_PATH
          );
        if (this._apiExtractorConfigurationFilePath !== undefined) {
          taskSession.logger.emitWarning(
            new Error(
              `The "${LEGACY_EXTRACTOR_CONFIG_RELATIVE_PATH}" configuration file path is not supported ` +
                `in Heft. Please move it to "${EXTRACTOR_CONFIG_RELATIVE_PATH}".`
            )
          );
        }
      }
    }
    return this._apiExtractorConfigurationFilePath;
  }

  private async _getApiExtractorConfigurationAsync(
    taskSession: IHeftTaskSession,
    heftConfiguration: HeftConfiguration,
    ignoreMissingEntryPoint?: boolean
  ): Promise<IApiExtractorConfigurationResult | undefined> {
    // API Extractor provides an ExtractorConfig.tryLoadForFolder() API that will probe for api-extractor.json
    // including support for rig.json.  However, Heft does not load the @microsoft/api-extractor package at all
    // unless it sees a config/api-extractor.json file.  Thus we need to do our own lookup here.
    const apiExtractorConfigurationFilePath: string | undefined =
      await this._getApiExtractorConfigurationFilePathAsync(taskSession, heftConfiguration);
    if (!apiExtractorConfigurationFilePath) {
      return undefined;
    }

    // Since the config file exists, we can assume that API Extractor is available. Attempt to resolve
    // and import the package. If the resolution fails, a helpful error is thrown.
    const apiExtractorPackage: typeof TApiExtractor = await this._getApiExtractorPackageAsync(
      taskSession,
      heftConfiguration
    );
    const apiExtractorConfigurationObject: TApiExtractor.IConfigFile =
      apiExtractorPackage.ExtractorConfig.loadFile(apiExtractorConfigurationFilePath);

    // Load the configuration file. Always load from scratch.
    const apiExtractorConfiguration: TApiExtractor.ExtractorConfig =
      apiExtractorPackage.ExtractorConfig.prepare({
        ignoreMissingEntryPoint,
        configObject: apiExtractorConfigurationObject,
        configObjectFullPath: apiExtractorConfigurationFilePath,
        packageJsonFullPath: `${heftConfiguration.buildFolderPath}/package.json`,
        projectFolderLookupToken: heftConfiguration.buildFolderPath
      });

    return { apiExtractorPackage, apiExtractorConfiguration };
  }

  private async _getApiExtractorPackageAsync(
    taskSession: IHeftTaskSession,
    heftConfiguration: HeftConfiguration
  ): Promise<typeof TApiExtractor> {
    if (!this._apiExtractor) {
      const apiExtractorPackagePath: string = await heftConfiguration.rigPackageResolver.resolvePackageAsync(
        '@microsoft/api-extractor',
        taskSession.logger.terminal
      );
      this._apiExtractor = (await import(apiExtractorPackagePath)) as typeof TApiExtractor;
    }
    return this._apiExtractor;
  }

  private async _getApiExtractorTaskConfigurationAsync(
    taskSession: IHeftTaskSession,
    heftConfiguration: HeftConfiguration
  ): Promise<IApiExtractorTaskConfiguration | undefined> {
    if (!this._apiExtractorTaskConfigurationFileLoader) {
      this._apiExtractorTaskConfigurationFileLoader =
        new ProjectConfigurationFile<IApiExtractorTaskConfiguration>({
          projectRelativeFilePath: TASK_CONFIG_RELATIVE_PATH,
          jsonSchemaObject: apiExtractorConfigSchema
        });
    }

    return await this._apiExtractorTaskConfigurationFileLoader.tryLoadConfigurationFileForProjectAsync(
      taskSession.logger.terminal,
      heftConfiguration.buildFolderPath,
      heftConfiguration.rigConfig
    );
  }

  private async _runApiExtractorAsync(
    taskSession: IHeftTaskSession,
    heftConfiguration: HeftConfiguration,
    runOptions: IHeftTaskRunHookOptions & Partial<IHeftTaskRunIncrementalHookOptions>,
    apiExtractor: typeof TApiExtractor,
    apiExtractorConfiguration: TApiExtractor.ExtractorConfig
  ): Promise<void> {
    const apiExtractorTaskConfiguration: IApiExtractorTaskConfiguration | undefined =
      await this._getApiExtractorTaskConfigurationAsync(taskSession, heftConfiguration);

    if (runOptions.requestRun) {
      if (!apiExtractorTaskConfiguration?.runInWatchMode) {
        if (!this._printedWatchWarning) {
          this._printedWatchWarning = true;
          taskSession.logger.terminal.writeWarningLine(
            "API Extractor isn't currently enabled in watch mode."
          );
        }
        return;
      }
    }

    let typescriptPackagePath: string | undefined;
    if (apiExtractorTaskConfiguration?.useProjectTypescriptVersion) {
      typescriptPackagePath = await heftConfiguration.rigPackageResolver.resolvePackageAsync(
        'typescript',
        taskSession.logger.terminal
      );
    }

    const apiExtractorRunner: ApiExtractorRunner = new ApiExtractorRunner({
      apiExtractor,
      apiExtractorConfiguration,
      typescriptPackagePath,
      buildFolder: heftConfiguration.buildFolderPath,
      production: taskSession.parameters.production,
      scopedLogger: taskSession.logger
    });

    // Run API Extractor
    await apiExtractorRunner.invokeAsync();
  }
}
