import * as yargs from 'yargs';
import { runCommand } from '../tasks-runner/run-command';
import { NxArgs, RawNxArgs, splitArgsIntoNxArgsAndOverrides } from './utils';
import {
  createProjectGraph,
  isWorkspaceProject,
  ProjectGraph,
  ProjectGraphNode,
  withDeps,
} from '../core/project-graph';
import { readEnvironment } from '../core/file-utils';
import { DefaultReporter } from '../tasks-runner/default-reporter';
import { EmptyReporter } from '../tasks-runner/empty-reporter';
import { projectHasTarget } from '../utilities/project-graph-utils';
import { output } from '../utilities/output';
import { connectToNxCloudUsingScan } from './connect-to-nx-cloud';
import { performance } from 'perf_hooks';
import { Environment } from '../core/shared-interfaces';

export async function runMany(parsedArgs: yargs.Arguments & RawNxArgs) {
  performance.mark('command-execution-begins');
  const { nxArgs, overrides } = splitArgsIntoNxArgsAndOverrides(
    parsedArgs,
    'run-many'
  );

  await connectToNxCloudUsingScan(nxArgs.scan);

  const projectGraph = createProjectGraph();
  const projects = projectsToRun(nxArgs, projectGraph);
  const projectsNotExcluded = applyExclude(projects, nxArgs);
  const env = readEnvironment(nxArgs.target, projectsNotExcluded);
  const filteredProjects = applyOnlyFailed(projectsNotExcluded, nxArgs, env);

  runCommand(
    filteredProjects,
    projectGraph,
    env,
    nxArgs,
    overrides,
    nxArgs.hideCachedOutput ? new EmptyReporter() : new DefaultReporter(),
    null
  );
}

function projectsToRun(nxArgs: NxArgs, projectGraph: ProjectGraph) {
  const allProjects = Object.values(projectGraph.nodes);
  if (nxArgs.all) {
    return runnableForTarget(allProjects, nxArgs.target).reduce(
      (m, c) => ((m[c.name] = c), m),
      {}
    );
  } else {
    checkForInvalidProjects(nxArgs, allProjects);
    let selectedProjects = allProjects.filter(
      (p) => nxArgs.projects.indexOf(p.name) > -1
    );
    if (nxArgs.withDeps) {
      selectedProjects = Object.values(
        withDeps(projectGraph, selectedProjects).nodes
      );
    }
    return runnableForTarget(selectedProjects, nxArgs.target, true).reduce(
      (m, c) => ((m[c.name] = c), m),
      {}
    );
  }
}

function applyExclude(
  projects: Record<string, ProjectGraphNode>,
  nxArgs: NxArgs
) {
  return Object.keys(projects)
    .filter((key) => !(nxArgs.exclude || []).includes(key))
    .reduce((p, key) => {
      p[key] = projects[key];
      return p;
    }, {} as Record<string, ProjectGraphNode>);
}

function applyOnlyFailed(
  projectsNotExcluded: Record<string, ProjectGraphNode>,
  nxArgs: NxArgs,
  env: Environment
) {
  return Object.values(projectsNotExcluded).filter(
    (n) => !nxArgs.onlyFailed || !env.workspaceResults.getResult(n.name)
  );
}

function checkForInvalidProjects(
  nxArgs: NxArgs,
  allProjects: ProjectGraphNode[]
) {
  const invalid = nxArgs.projects.filter(
    (name) => !allProjects.find((p) => p.name === name)
  );
  if (invalid.length !== 0) {
    throw new Error(`Invalid projects: ${invalid.join(', ')}`);
  }
}

function runnableForTarget(
  projects: ProjectGraphNode[],
  target: string,
  strict = false
): ProjectGraphNode[] {
  const notRunnable = [] as ProjectGraphNode[];
  const runnable = [] as ProjectGraphNode[];

  for (let project of projects) {
    if (projectHasTarget(project, target)) {
      runnable.push(project);
    } else if (isWorkspaceProject(project)) {
      notRunnable.push(project);
    }
  }

  if (strict && notRunnable.length) {
    output.warn({
      title: `the following do not have configuration for "${target}"`,
      bodyLines: notRunnable.map((p) => `- ${p.name}`),
    });
  }

  return runnable;
}
