// Copyright IBM Corp. and LoopBack contributors 2019,2020. All Rights Reserved.
// Node module: @loopback/tsdocs
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {
  CompilerState,
  ConsoleMessageId,
  Extractor,
  ExtractorConfig,
  ExtractorLogLevel,
  ExtractorMessage,
  ExtractorMessageId,
  ExtractorResult,
  IConfigFile,
} from '@microsoft/api-extractor';
import debugFactory from 'debug';
import fs from 'fs-extra';
import path from 'path';
import {
  DEFAULT_APIDOCS_EXTRACTION_PATH,
  ExtractorOptions,
  LernaPackage,
  getPackagesWithTsDocs,
  typeScriptPath,
} from './helper';
const debug = debugFactory('loopback:tsdocs');

/**
 * Run api-extractor for a lerna-managed monrepo
 *
 * @remarks
 * The function performs the following steps:
 * 1. Discover packages with tsdocs from the monorepo
 * 2. Iterate through each package to run `api-extractor`
 *
 * @param options - Options for running api-extractor
 */
export async function runExtractorForMonorepo(options: ExtractorOptions = {}) {
  debug('Extractor options:', options);

  options = Object.assign(
    {
      rootDir: process.cwd(),
      apiDocsExtractionPath: DEFAULT_APIDOCS_EXTRACTION_PATH,
      typescriptCompilerFolder: typeScriptPath,
      tsconfigFilePath: 'tsconfig.json',
      mainEntryPointFilePath: 'dist/index.d.ts',
    },
    options,
  );

  const packages = await getPackagesWithTsDocs(options.rootDir);

  /* istanbul ignore if  */
  if (!packages.length) return;

  const lernaRootDir = packages[0].rootPath;

  /* istanbul ignore if  */
  if (!options.silent) {
    console.log('Running api-extractor for lerna repo: %s', lernaRootDir);
  }

  setupApiDocsDirs(lernaRootDir, options);

  const errors: Record<string, unknown> = {};

  for (const pkg of packages) {
    // TODO: api-extractor failed to generate apidocs for the repos below.
    // Excluding them for now
    // https://github.com/loopbackio/loopback-next/issues/10205
    if (
      pkg.name === '@loopback/typeorm' ||
      pkg.name === '@loopback/boot' ||
      pkg.name === '@loopback/express' ||
      pkg.name === '@loopback/repository' ||
      pkg.name === '@loopback/service-proxy'
    )
      continue;
    /* istanbul ignore if  */
    const err = invokeExtractorForPackage(pkg, options);
    if (err != null) {
      if (options.ignoreErrors) {
        errors[pkg.name] = err;
      } else {
        throw err;
      }
    }
  }
  if (Object.keys(errors).length === 0) return;
  console.error(
    '****************************************' +
      '****************************************',
  );
  for (const p in errors) {
    const err = errors[p] as {message: string};
    console.error('%s: %s', p, err?.message ?? err);
  }
  console.error(
    '****************************************' +
      '****************************************',
  );
}

export function runExtractorForPackage(
  pkgDir: string = process.cwd(),
  options: ExtractorOptions = {},
) {
  options = Object.assign(
    {
      rootDir: pkgDir,
      apiDocsExtractionPath: DEFAULT_APIDOCS_EXTRACTION_PATH,
      typescriptCompilerFolder: typeScriptPath,
      tsconfigFilePath: 'tsconfig.json',
      mainEntryPointFilePath: 'dist/index.d.ts',
    },
    options,
  );
  const pkgJson = require(path.join(pkgDir, 'package.json'));
  setupApiDocsDirs(pkgDir, options);
  const pkg: LernaPackage = {
    private: pkgJson.private,
    name: pkgJson.name,
    location: pkgDir,
    manifestLocation: path.join(pkgDir, 'package.json'),
    rootPath: pkgDir,
  };
  const err = invokeExtractorForPackage(pkg, options);
  if (err == null) return;
  if (!options.ignoreErrors) {
    throw err;
  }
  console.error(err);
}

/**
 * Run `api-extractor` on a given package
 * @param pkg - Package descriptor
 * @param options - Options for api extraction
 */
function invokeExtractorForPackage(
  pkg: LernaPackage,
  options: ExtractorOptions,
) {
  if (!options.silent) {
    console.log('> %s', pkg.name);
  }
  debug('Package: %s (%s)', pkg.name, pkg.location);
  process.chdir(pkg.location);
  const extractorConfig = buildExtractorConfig(pkg, options);
  debug('Resolved extractor config:', extractorConfig);
  try {
    invokeExtractor(extractorConfig, options);
  } catch (err) {
    debug('Error in extracting API docs for %s', pkg.name, err);
    return err;
  }
}

/**
 * Set up dirs for apidocs
 *
 * @param lernaRootDir - Root dir of the monorepo
 * @param options - Extractor options
 */
function setupApiDocsDirs(lernaRootDir: string, options: ExtractorOptions) {
  /* istanbul ignore if  */
  if (options.dryRun) return;
  const apiDocsExtractionPath = options.apiDocsExtractionPath!;

  fs.emptyDirSync(path.join(lernaRootDir, `${apiDocsExtractionPath}/models`));

  if (!options.apiReportEnabled) return;

  fs.ensureDirSync(path.join(lernaRootDir, `${apiDocsExtractionPath}/reports`));
  fs.emptyDirSync(
    path.join(lernaRootDir, `${apiDocsExtractionPath}/reports-temp`),
  );
}

/**
 * Build extractor configuration object for the given package
 *
 * @param pkg - Lerna managed package
 * @param options - Extractor options
 */
function createRawExtractorConfig(
  pkg: LernaPackage,
  options: ExtractorOptions,
) {
  const entryPoint = path.join(pkg.location, options.mainEntryPointFilePath!);
  const apiDocsExtractionPath = options.apiDocsExtractionPath!;
  let configObj: IConfigFile = {
    projectFolder: pkg.location,
    mainEntryPointFilePath: entryPoint,
    apiReport: {
      enabled: !!options.apiReportEnabled,
      reportFolder: path.join(pkg.rootPath, `${apiDocsExtractionPath}/reports`),
      reportTempFolder: path.join(
        pkg.rootPath,
        `${apiDocsExtractionPath}/reports-temp`,
      ),
      reportFileName: '<unscopedPackageName>.api.md',
    },
    docModel: {
      enabled: true,
      apiJsonFilePath: path.join(
        pkg.rootPath,
        `${apiDocsExtractionPath}/models/<unscopedPackageName>.api.json`,
      ),
    },
    messages: {
      extractorMessageReporting: {
        [ExtractorMessageId.MissingReleaseTag]: {
          logLevel: ExtractorLogLevel.None,
          addToApiReportFile: false,
        },
      },
    },
    compiler: {
      tsconfigFilePath: options.tsconfigFilePath!,
    },
  };
  /* istanbul ignore if  */
  if (options.config) {
    configObj = Object.assign(configObj, options.config);
  }
  debug('Extractor config options:', configObj);
  return configObj;
}

/**
 * Create and prepare the extractor config for invocation
 *
 * @param pkg - Lerna package
 * @param options - Extractor options
 */
function buildExtractorConfig(pkg: LernaPackage, options: ExtractorOptions) {
  const configObj: IConfigFile = createRawExtractorConfig(pkg, options);
  const extractorConfig = ExtractorConfig.prepare({
    configObject: configObj,
    configObjectFullPath: '',
    packageJsonFullPath: pkg.manifestLocation,
  });
  return extractorConfig;
}

/**
 * Invoke the extractor
 *
 * @param extractorConfig - Resolved config
 * @param options - Extractor options
 */
function invokeExtractor(
  extractorConfig: ExtractorConfig,
  options: ExtractorOptions,
) {
  const compilerState = CompilerState.create(extractorConfig, {
    // typescriptCompilerFolder: options.typescriptCompilerFolder,
  });

  /* istanbul ignore if  */
  if (options.dryRun) return;

  const extractorResult: ExtractorResult = Extractor.invoke(extractorConfig, {
    typescriptCompilerFolder: options.typescriptCompilerFolder,
    localBuild: true,
    showVerboseMessages: !options.silent,
    messageCallback: (message: ExtractorMessage) => {
      if (message.messageId === ConsoleMessageId.ApiReportCreated) {
        // This script deletes the outputs for a clean build,
        // so don't issue a warning if the file gets created
        message.logLevel = ExtractorLogLevel.None;
      }
    },
    compilerState,
  });
  debug(extractorResult);
}
