import {
  apply,
  chain,
  MergeStrategy,
  mergeWith,
  move,
  Rule,
  schematic,
  Source,
  template,
  Tree,
  url,
} from '@angular-devkit/schematics';
import {
  getProjectGraphFromHost,
  getWorkspace,
  readJsonInTree,
  readWorkspace,
} from '@nrwl/workspace';
import { join, normalize } from '@angular-devkit/core';
import {
  ProjectGraph,
  ProjectGraphNode,
} from '@nrwl/workspace/src/core/project-graph';
import { rulesNodeJSSha, rulesNodeJSVersion } from '../utils/versions';
import { TargetDefinition } from '@angular-devkit/core/src/workspace';

const buildBuilders = {
  '@angular-devkit/build-angular:browser': 'outputPath',
  '@angular-devkit/build-angular:server': 'outputPath',
  '@angular-devkit/build-angular:ng-packagr': 'outputPath',
  '@angular-devkit/build-webpack:webpack': 'outputPath',
  '@nrwl/web:build': 'outputPath',
};

const testBuilders = new Set([
  '@angular-devkit/build-angular:karma',
  '@angular-devkit/build-angular:protractor',
  '@angular-devkit/build-angular:tslint',
  '@nrwl/jest:jest',
  '@nrwl/cypress:cypress',
  '@nrwl/linter:lint',
]);

function createBuildFile(
  project: ProjectGraphNode,
  projectGraph: ProjectGraph,
  labelsMetadata: Array<{
    name: string;
    configurations: string[];
    target: TargetDefinition;
  }>
): Source {
  const labels: {
    cliTarget: string;
    bazelLabel: string;
    isBuildTarget: boolean;
    outputArgument: string;
  }[] = [];
  labelsMetadata
    .map((metadata) =>
      metadata.configurations.map((config) => {
        const isTestTarget = testBuilders.has(metadata.target.builder);
        const isBuildTarget = !!buildBuilders[metadata.target.builder];
        const outputArgument = buildBuilders[metadata.target.builder];
        return {
          bazelRuleName: isTestTarget ? 'nx_test' : 'nx',
          cliTarget: `${project.name}:${metadata.name}${
            config === '__nx_default__' ? '' : `:${config}`
          }`,
          bazelLabel: `${metadata.name}${
            config === '__nx_default__' ? '' : `__${config}`
          }`,
          isBuildTarget,
          outputArgument,
        };
      })
    )
    .forEach((arr) => {
      arr.forEach((label) => labels.push(label));
    });

  return apply(url('./files/build-file'), [
    template({
      tmpl: '',
      project,
      projectGraph,
      dependencies: projectGraph.dependencies[project.name]
        ? projectGraph.dependencies[project.name].map(
            (dep) =>
              `//${normalize(projectGraph.nodes[dep.target].data.root)}:${
                dep.target
              }`
          )
        : [],
      labels,
    }),
  ]);
}

function updateBuildFile(
  project: ProjectGraphNode,
  projectGraph: ProjectGraph
): Rule {
  return async (host, context) => {
    const workspace = await getWorkspace(host);
    const labelsMetadata = Array.from(
      workspace.projects.get(project.name).targets.entries()
    ).map(([name, target]) => ({
      name,
      target,
      configurations: [
        '__nx_default__',
        ...Object.keys(target.configurations || {}),
      ],
    }));
    const buildFile = createBuildFile(project, projectGraph, labelsMetadata);
    const buildFilePath = join(normalize(project.data.root), 'BUILD.bazel');

    return mergeWith(
      apply(buildFile, [
        (sourceHost) => {
          if (host.exists(buildFilePath)) {
            const contents = sourceHost.read('BUILD.bazel').toString();
            const customPart = host
              .read(buildFilePath)
              .toString()
              .split('# ==== Generated by Nx')[0];
            host.delete(buildFilePath);
            sourceHost.overwrite('BUILD.bazel', customPart + contents);
          }
        },
        move(project.data.root),
      ]),
      MergeStrategy.Overwrite
    );
  };
}

function createWorkspaceFile() {
  return (host) => {
    return mergeWith(
      apply(url('./files/workspace-file'), [
        template({
          tmpl: '',
          name: readJsonInTree(host, '/package.json').name.replace('-', '_'),
          rulesNodeJSVersion,
          rulesNodeJSSha,
        }),
        () => {
          if (host.exists('WORKSPACE')) {
            host.delete('WORKSPACE');
          }
        },
      ]),
      MergeStrategy.Overwrite
    );
  };
}

const ignoredFromRootBuildFile = ['WORKSPACE', '.bazelrc', 'BUILD.bazel'];

function createRootBuildFile() {
  return (host) => {
    return mergeWith(
      apply(url('./files/root-build-file'), [
        template({
          tmpl: '',
          rootFiles: host
            .getDir('/')
            .subfiles.filter((f) => !ignoredFromRootBuildFile.includes(f)),
        }),
        () => {
          if (host.exists('BUILD.bazel')) {
            host.delete('BUILD.bazel');
          }
        },
      ]),
      MergeStrategy.Overwrite
    );
  };
}

const runInit = schematic<{}>('init', {});

export default (): Rule => {
  return (host: Tree) => {
    const projectGraph = getProjectGraphFromHost(host);

    return chain([
      runInit,
      createWorkspaceFile(),
      createRootBuildFile(),
      ...Object.values(projectGraph.nodes).map((project) =>
        updateBuildFile(project, projectGraph)
      ),
    ]);
  };
};
