/*
  Performs top-level actions of the CLI. Connects the file paths found in the
  `CurrentWorkingDirectory` to the worker pool.
*/
import envCi from 'env-ci';
import chalk from "chalk";
import { equal } from 'node:assert';
import { Config } from "./Config";
import { CurrentWorkingDirectory } from "./CurrentWorkingDirectory";
import { Flags } from "./cli";
import { Policy } from "./Policy";
import { GREEN_CHECK, logAllResultDetails, logCheckFailedError, logResults } from "./log";
import { clientVersion } from "./clientVersion";
import { commentResults, postMetrics, ResultMap } from "./count";
import { ResultsMap } from './match';
import { actionPolicyModify } from './policyModify';

interface WorkerPool {
  resultsMap: ResultsMap;
  filesChecked: string[];
  processFile(filePath: string): void;
  onFilesDone(): void;
  onResults(): void;
}
export class Runner {
  private policies: Policy[];
  private ran = false;

  constructor(
    public config: Config,
    private flags: Flags,
    private cwd: CurrentWorkingDirectory,
    private workerPool: WorkerPool
  ) {
    this.policies = Object.values(config.policyMap);

    for (const policyName in config.policyMap) {
      this.workerPool.resultsMap[policyName] = {
        policy: config.policyMap[policyName],
        matches: [],
      };
    }
  }

  private run(): Promise<{ resultsMap: ResultsMap, filesChecked: string[] }> {
    equal(this.ran, false, "Runner.run() should only be called once");
    equal(this.policies.length > 0, true, "No policies specified.\nPlease check your config file at " + this.config.filePath);

    return new Promise(resolve => {
      this.workerPool.onResults = () => {
        this.ran = true;
        resolve({
          resultsMap: this.workerPool.resultsMap,
          filesChecked: this.workerPool.filesChecked
        });
      }

      this.cwd.allNonGitIgnoredFiles(
        this.processFile.bind(this),
        this.workerPool.onFilesDone.bind(this.workerPool)
      );
    })
  }

  async check() {
    const { resultsMap, filesChecked } = await this.run();
    const { breaches } = logResults(resultsMap, filesChecked);
    if (breaches.length) {
      logCheckFailedError();
      process.exitCode = 1;
    } else {
      console.log(`\n${GREEN_CHECK} ${chalk.bold(`All policies passed with ${filesChecked.length} matching files checked`)}`);
    }
  }

  async count() {
    const clientVers = clientVersion();
    const start = new Date();

    const { resultsMap, filesChecked } = await this.run();

    const { breaches, successes, all } = logResults(resultsMap, filesChecked);

    const results: ResultMap = {};
    for (const result of all) {
      results[result.policy.name] = {
        measurement: result.matches.length
      };
    }

    const verbose = Boolean(this.flags.verbose);

    if (breaches.length) {
      logCheckFailedError();
    }

    if (!this.flags.record && !this.flags.ci) {
      console.log(chalk.green.bold(`Done in ${Date.now() - start.getTime()} ms.`));
      return;
    }

    console.log(chalk.yellow("sending metrics to server..."));
    verbose &&
      console.log(chalk.cyan(`successes: ${JSON.stringify(successes)}`));
    verbose && console.log(chalk.cyan(`breaches: ${JSON.stringify(breaches)}`));
    const apiKey = process.env.DIFFJAM_API_KEY;
    if (!apiKey) {
      console.error(chalk.red("Missing api key!  Could not post metrics."));
      console.error(
        chalk.red(
          "You must have an api key in an environment variable named DIFFJAM_API_KEY"
        )
      );
      process.exitCode = 1;
      return;
    }
    const configJson = this.config.toJson();
    verbose && console.log("apiKey, config, results: ", apiKey, configJson, results);

    if (this.flags.record) {
      await postMetrics(apiKey, configJson, results, clientVers);
    }

    if (this.flags.ci) {
      if (!envCi().isCi) {
        throw new Error(`could not detect CI environment`);
      }
      await commentResults(apiKey, configJson, results, clientVers);
    }

    console.log(chalk.green.bold(`Done in ${Date.now() - start.getTime()} ms.`));
  }

  async cinch() {
    const { resultsMap, filesChecked } = await this.run();
    const { breaches, successes } = logResults(resultsMap, filesChecked);
    console.log();

    if (breaches.length > 0) {
      console.error(
        chalk.bold(
          "Cannot cinch a policy that doesn't even meet the baseline. \n"
        )
      );
      process.exitCode = 1;
      return;
    }

    let anyCinched = false;
    for (const success of successes) {
      if (success.policy.isCountCinchable(success.matches)) {
        anyCinched = true;
        const before = success.policy.baseline;
        success.policy.baseline = success.matches.length;
        console.log(
          `${GREEN_CHECK} cinching ${chalk.bold(success.policy.name)} from ${before
          } to ${chalk.bold(success.matches.length.toString())}!`
        );
      }
    }

    if (anyCinched) {
      this.config.write();
    } else {
      console.log(`${GREEN_CHECK} ${chalk.bold("All policies are already exactly at their baseline, so none were cinched")}`);
    }
  }

  async bump() {
    const { resultsMap, filesChecked } = await this.run();
    const { breaches } = logResults(resultsMap, filesChecked);
    console.log();

    for (const breach of breaches) {
      const before = breach.policy.baseline;
      breach.policy.baseline = breach.matches.length;
      console.log(
        `🎚 bumping ${chalk.bold(breach.policy.name)} from ${before
        } to ${chalk.bold(breach.matches.length.toString())}!`
      );
    }

    if (breaches.length) {
      this.config.write();
    } else {
      console.log(`${GREEN_CHECK} ${chalk.bold("All policies are already exactly at their baseline, so none were bumped")}`);
    }
  }

  async addPolicy() {
    // Requiring ui inline as it adds a fair bit of time to startup
    const ui = require("./ui");

    const name = await ui.textInput("Enter a name for this policy: ");

    const isRegex = await ui.confirm("Is this a regex search?");

    let search: string;
    const negativeSearchTerms = [];

    if (isRegex) {
      const regex = await ui.textInput(
        "Enter the regex to search for: "
      );
      search = `regex:${regex}`;
    } else {
      search = await ui.textInput(
        "Enter the string to match : "
      );

      while (true) {
        const negativeSearchTerm = await ui.textInput(
          "Enter any string to negate (or leave blank to continue): "
        );

        if (negativeSearchTerm.trim()) {
          negativeSearchTerms.push(negativeSearchTerm);
        } else {
          break;
        }
      }
    }

    const filePattern = await ui.textInput(
      "Enter the filePattern to search for this policy: "
    );

    const ignoreFilePatterns = []
    while (true) {
      const ignoreFilePattern = await ui.textInput(
        "Enter any filePatterns to ignore (or leave blank to continue): "
      );

      if (ignoreFilePattern.trim()) {
        ignoreFilePatterns.push(ignoreFilePattern);
      } else {
        break;
      }
    }

    const description = await ui.textInput(
      "Give a description for this policy: "
    );

    const policy = new Policy(name, description, filePattern, [search, ...negativeSearchTerms], 0, ignoreFilePatterns);

    this.config.setPolicy(policy);
    this.policies = [policy];
    this.workerPool.resultsMap[policy.name] = {
      policy,
      matches: []
    };

    const { resultsMap, filesChecked } = await this.run();

    const { matches } = resultsMap[policy.name];
    policy.baseline = matches.length;

    if (
      await ui.confirm(
        `There are currently ${policy.baseline} matches for that configuration after checking ${filesChecked.length} files. Save it?`
      )
    ) {
      this.config.write();
      console.log("Saved!");
    } else {
      console.log("Cancelled save.");
    }
  }

  async runSinglePolicy(policyName: string) {
    const policy = this.config.getPolicy(policyName);
    if (!policy) {
      throw new Error(`Could not find policy ${policyName}`);
    }

    const { resultsMap } = await this.run();
    return resultsMap[policyName];
  }

  async removePolicy() {
    const ui = require("./ui");
    const policy = await ui.select("Select a policy to remove: ", this.config.policyMap);
    await this.config.deletePolicy(policy.name);
    await this.config.write();
    console.log(`Removed ${policy.name}`);
  }

  modifyPolicy() {
    return actionPolicyModify(this);
  }

  async checkBreachesForPolicy() {
    const ui = require("./ui");
    const policy = await ui.select("Select a policy to check for breaches: ", this.config.policyMap);
    const result = await this.runSinglePolicy(policy.name);
    logAllResultDetails(result);
  }

  private processFile(filePath: string) {
    const isUnderPolicy = this.policies.some(policy => policy.isFileUnderPolicy(filePath));
    if (!isUnderPolicy) return;
    this.workerPool.processFile(filePath);
  }
}
