import {readFileSync, writeFileSync} from 'fs';
import path from 'path';
// @ts-ignore This compiles fine, but Webstorm doesn't like the ESM import in a CJS context.
import {
  NgtscProgram,
  CompilerOptions,
  createCompilerHost,
  DocEntry,
  EntryCollection,
  InterfaceEntry,
  ClassEntry,
} from '@angular/compiler-cli';
import ts from 'typescript';

function main() {
  const [paramFilePath] = process.argv.slice(2);
  const rawParamLines = readFileSync(paramFilePath, {encoding: 'utf8'}).split('\n');

  const [
    moduleName,
    moduleLabel,
    serializedPrivateModules,
    entryPointExecRootRelativePath,
    srcs,
    outputFilenameExecRootRelativePath,
    serializedPathMapWithExecRootRelativePaths,
    extraEntriesSrcs,
  ] = rawParamLines;

  const privateModules = new Set(serializedPrivateModules.split(','));

  // The path map is a serialized JSON map of import path to index.ts file.
  // For example, {'@angular/core': 'path/to/some/index.ts'}
  const pathMap = JSON.parse(serializedPathMapWithExecRootRelativePaths) as Record<string, string>;

  // The tsconfig expects the path map in the form of path -> array of actual locations.
  // We also resolve the exec root relative paths to absolute paths to disambiguate.
  const resolvedPathMap: {[key: string]: string[]} = {};
  for (const [importPath, filePath] of Object.entries(pathMap)) {
    resolvedPathMap[importPath] = [path.resolve(filePath)];

    // In addition to the exact import path,
    // also add wildcard mappings for subdirectories.
    const importPathWithWildcard = path.join(importPath, '*');
    resolvedPathMap[importPathWithWildcard] = [
      path.join(path.resolve(path.dirname(filePath)), '*'),
    ];
  }

  const compilerOptions: CompilerOptions = {
    paths: resolvedPathMap,
    rootDir: '.',
    skipLibCheck: true,
    target: ts.ScriptTarget.ES2022,
    moduleResolution: ts.ModuleResolutionKind.NodeNext,
    experimentalDecorators: true,
  };

  const compilerHost = createCompilerHost({options: compilerOptions});
  const program: NgtscProgram = new NgtscProgram(srcs.split(','), compilerOptions, compilerHost);

  const extraEntries: DocEntry[] = (extraEntriesSrcs ?? '')
    .split(',')
    .filter((path) => !!path)
    .reduce((result: DocEntry[], path) => {
      return result.concat(JSON.parse(readFileSync(path, {encoding: 'utf8'})) as DocEntry[]);
    }, []);

  const apiDoc = program.getApiDocumentation(entryPointExecRootRelativePath, privateModules);
  const extractedEntries = apiDoc.entries;
  const combinedEntries = extractedEntries.concat(extraEntries);

  const normalized = moduleName.replace('@', '').replace(/[\/]/g, '_');

  const output = JSON.stringify({
    moduleLabel: moduleLabel || moduleName,
    moduleName: moduleName,
    normalizedModuleName: normalized,
    entries: combinedEntries,
    symbols: [
      // Symbols referenced, originating from other packages
      ...apiDoc.symbols.entries(),

      // Exported symbols from the current package
      ...apiDoc.entries.map((entry) => [entry.name, moduleName]),

      // Also doing it for every member of classes/interfaces
      ...apiDoc.entries.flatMap((entry) => [
        [entry.name, moduleName],
        ...getEntriesFromMembers(entry).map((member) => [member, moduleName]),
      ]),
    ],
  } as EntryCollection);

  writeFileSync(outputFilenameExecRootRelativePath, output, {encoding: 'utf8'});
}

function getEntriesFromMembers(entry: DocEntry): string[] {
  if (!hasMembers(entry)) {
    return [];
  }

  return entry.members.map((member) => `${entry.name}.${member.name}`);
}

function hasMembers(entry: DocEntry): entry is InterfaceEntry | ClassEntry {
  return 'members' in entry;
}

main();
