import {
  GetCodeRepositoryPassword,
  GetCodeRepositoryUsername,
  GetLocalRepositoryPath,
  GetOneUptimeURL,
  GetRepositorySecretKey,
} from "../Config";
import HTTPErrorResponse from "Common/Types/API/HTTPErrorResponse";
import HTTPResponse from "Common/Types/API/HTTPResponse";
import URL from "Common/Types/API/URL";
import CodeRepositoryType from "Common/Types/CodeRepository/CodeRepositoryType";
import PullRequest from "Common/Types/CodeRepository/PullRequest";
import PullRequestState from "Common/Types/CodeRepository/PullRequestState";
import BadDataException from "Common/Types/Exception/BadDataException";
import { JSONArray, JSONObject } from "Common/Types/JSON";
import API from "Common/Utils/API";
import CodeRepositoryServerUtil from "Common/Server/Utils/CodeRepository/CodeRepository";
import GitHubUtil from "Common/Server/Utils/CodeRepository/GitHub/GitHub";
import LocalFile from "Common/Server/Utils/LocalFile";
import logger from "Common/Server/Utils/Logger";
import CopilotCodeRepository from "Common/Models/DatabaseModels/CopilotCodeRepository";
import ServiceCopilotCodeRepository from "Common/Models/DatabaseModels/ServiceCopilotCodeRepository";
import Text from "Common/Types/Text";
import Execute from "Common/Server/Utils/Execute";
import CopilotPullRequestService from "../Service/CopilotPullRequest";
import CopilotPullRequest from "Common/Models/DatabaseModels/CopilotPullRequest";

export interface CodeRepositoryResult {
  codeRepository: CopilotCodeRepository;
  serviceRepositories: Array<ServiceCopilotCodeRepository>;
}

export interface ServiceToImproveResult {
  serviceRepository: ServiceCopilotCodeRepository;
  numberOfOpenPullRequests: number;
  pullRequests: Array<CopilotPullRequest>;
}

export enum RepoScriptType {
  OnAfterClone = "onAfterClone",
  OnBeforeCommit = "onBeforeCommit",
  OnAfterCommit = "OnAfterCommit",
  OnBeforeCodeChange = "OnBeforeCodeChange",
  OnAfterCodeChange = "OnAfterCodeChange",
}

export default class CodeRepositoryUtil {
  public static codeRepositoryResult: CodeRepositoryResult | null = null;
  public static gitHubUtil: GitHubUtil | null = null;
  public static folderNameOfClonedRepository: string | null = null;

  public static async getCurrentCommitHash(): Promise<string> {
    return await CodeRepositoryServerUtil.getCurrentCommitHash({
      repoPath: this.getLocalRepositoryPath(),
    });
  }

  public static isRepoCloned(): boolean {
    return Boolean(this.folderNameOfClonedRepository);
  }

  public static async getOpenSetupPullRequest(): Promise<CopilotPullRequest | null> {
    const openPullRequests: Array<CopilotPullRequest> =
      await CopilotPullRequestService.getOpenPullRequestsFromDatabase();

    for (const pullRequest of openPullRequests) {
      if (pullRequest.isSetupPullRequest) {
        return pullRequest;
      }
    }

    return null;
  }

  public static getLocalRepositoryPath(): string {
    if (this.folderNameOfClonedRepository) {
      return LocalFile.sanitizeFilePath(
        GetLocalRepositoryPath() + "/" + this.folderNameOfClonedRepository,
      );
    }

    return GetLocalRepositoryPath();
  }

  public static async discardAllChangesOnCurrentBranch(): Promise<void> {
    await CodeRepositoryServerUtil.discardAllChangesOnCurrentBranch({
      repoPath: this.getLocalRepositoryPath(),
    });
  }

  public static async setAuthorIdentity(data: {
    name: string;
    email: string;
  }): Promise<void> {
    await CodeRepositoryServerUtil.setAuthorIdentity({
      repoPath: this.getLocalRepositoryPath(),
      authorName: data.name,
      authorEmail: data.email,
    });
  }

  public static async getPullRequestState(data: {
    pullRequestId: string;
  }): Promise<PullRequestState> {
    // check if org name and repo name is present.

    if (!this.codeRepositoryResult?.codeRepository.organizationName) {
      throw new BadDataException("Organization Name is required");
    }

    if (!this.codeRepositoryResult?.codeRepository.repositoryName) {
      throw new BadDataException("Repository Name is required");
    }

    const githubUtil: GitHubUtil = this.getGitHubUtil();

    if (!githubUtil) {
      throw new BadDataException("GitHub Util is required");
    }

    const pullRequest: PullRequest | undefined =
      await githubUtil.getPullRequestByNumber({
        organizationName:
          this.codeRepositoryResult.codeRepository.organizationName,
        repositoryName: this.codeRepositoryResult.codeRepository.repositoryName,
        pullRequestId: data.pullRequestId,
      });

    if (!pullRequest) {
      throw new BadDataException("Pull Request not found");
    }

    return pullRequest.state;
  }

  public static async setUpRepo(): Promise<PullRequest> {
    // check if the repository is setup properly.
    const isRepoSetupProperly: boolean = await this.isRepoSetupProperly();

    if (isRepoSetupProperly) {
      throw new BadDataException("Repository is already setup properly.");
    }

    // otherwise, we copy the folder /usr/src/app/Templates/.oneuptime to the repository folder.

    const templateFolderPath: string = LocalFile.sanitizeFilePath(
      "/usr/src/app/Templates/.oneuptime",
    );

    const oneUptimeConfigPath: string = LocalFile.sanitizeFilePath(
      this.getLocalRepositoryPath() + "/.oneuptime",
    );

    // create a new branch called oneuptime-copilot-setup

    const branchName: string = "setup-" + Text.generateRandomText(5);

    await this.createBranch({
      branchName: branchName,
    });

    await LocalFile.makeDirectory(oneUptimeConfigPath);

    await LocalFile.copyDirectory({
      source: templateFolderPath,
      destination: oneUptimeConfigPath,
    });

    // add all the files to the git.

    await this.addAllChangedFilesToGit();

    // commit the changes.

    await this.commitChanges({
      message: "OneUptime Copilot Setup",
    });

    // push changes to the repo.

    await this.pushChanges({
      branchName: branchName,
    });

    // create a pull request.

    const pullRequest: PullRequest = await this.createPullRequest({
      branchName: branchName,
      title: "OneUptime Copilot Setup",
      body: "This pull request is created by OneUptime Copilot to setup the repository.",
    });

    // save this to the database.

    await CopilotPullRequestService.addPullRequestToDatabase({
      pullRequest: pullRequest,
      isSetupPullRequest: true,
    });

    return pullRequest;
  }

  public static async isRepoSetupProperly(): Promise<boolean> {
    // check if .oneuptime folder exists.

    const repoPath: string = this.getLocalRepositoryPath();

    const oneUptimeFolderPath: string = LocalFile.sanitizeFilePath(
      `${repoPath}/.oneuptime`,
    );

    const doesDirectoryExist: boolean =
      await LocalFile.doesDirectoryExist(oneUptimeFolderPath);

    if (!doesDirectoryExist) {
      return false;
    }

    // check if .oneuptime/scripts folder exists.

    const oneuptimeScriptsPath: string = LocalFile.sanitizeFilePath(
      `${oneUptimeFolderPath}/scripts`,
    );

    const doesScriptsDirectoryExist: boolean =
      await LocalFile.doesDirectoryExist(oneuptimeScriptsPath);

    if (!doesScriptsDirectoryExist) {
      return false;
    }

    return true; // return true if all checks pass.
  }

  public static addAllChangedFilesToGit(): Promise<void> {
    return CodeRepositoryServerUtil.addAllChangedFilesToGit({
      repoPath: this.getLocalRepositoryPath(),
    });
  }

  // returns the folder name of the cloned repository.
  public static async cloneRepository(data: {
    codeRepository: CopilotCodeRepository;
  }): Promise<void> {
    // make sure this.getLocalRepositoryPath() is empty.
    const repoLocalPath: string = this.getLocalRepositoryPath();

    await LocalFile.deleteAllDataInDirectory(repoLocalPath);
    await LocalFile.makeDirectory(repoLocalPath);

    // check if the data in the directory eixsts, if it does then delete it.

    if (!data.codeRepository.repositoryHostedAt) {
      throw new BadDataException("Repository Hosted At is required");
    }

    if (!data.codeRepository.mainBranchName) {
      throw new BadDataException("Main Branch Name is required");
    }

    if (!data.codeRepository.organizationName) {
      throw new BadDataException("Organization Name is required");
    }

    if (!data.codeRepository.repositoryName) {
      throw new BadDataException("Repository Name is required");
    }

    const CodeRepositoryUsername: string | null = GetCodeRepositoryUsername();

    if (!CodeRepositoryUsername) {
      throw new BadDataException("Code Repository Username is required");
    }

    const CodeRepositoryPassword: string | null = GetCodeRepositoryPassword();

    if (!CodeRepositoryPassword) {
      throw new BadDataException("Code Repository Password is required");
    }

    const repoUrl: string = `https://${CodeRepositoryUsername}:${CodeRepositoryPassword}@${
      data.codeRepository.repositoryHostedAt === CodeRepositoryType.GitHub
        ? "github.com"
        : ""
    }/${data.codeRepository.organizationName}/${data.codeRepository.repositoryName}.git`;

    const folderName: string = await CodeRepositoryServerUtil.cloneRepository({
      repoUrl: repoUrl,
      repoPath: repoLocalPath,
    });

    this.folderNameOfClonedRepository = folderName;

    logger.debug(`Repository cloned to ${repoLocalPath}/${folderName}`);
  }

  public static async executeScript(data: { script: string }): Promise<string> {
    const commands: Array<string> = data.script
      .split("\n")
      .filter((command: string) => {
        return command.trim() !== "" && !command.startsWith("#");
      });

    const results: Array<string> = [];

    for (const command of commands) {
      logger.info(`Executing command: ${command}`);
      const commandResult: string = await Execute.executeCommand(
        `cd ${this.getLocalRepositoryPath()} && ${command}`,
      );
      if (commandResult) {
        logger.info(`Command result: ${commandResult}`);
        results.push(commandResult);
      }
    }

    return results.join("\n");
  }

  public static async getRepoScript(data: {
    scriptType: RepoScriptType;
  }): Promise<string | null> {
    const repoPath: string = this.getLocalRepositoryPath();

    const oneUptimeFolderPath: string = LocalFile.sanitizeFilePath(
      `${repoPath}/.oneuptime`,
    );

    const doesDirectoryExist: boolean =
      await LocalFile.doesDirectoryExist(oneUptimeFolderPath);

    if (!doesDirectoryExist) {
      return null;
    }

    const oneuptimeScriptsPath: string = LocalFile.sanitizeFilePath(
      `${oneUptimeFolderPath}/scripts`,
    );

    const doesScriptsDirectoryExist: boolean =
      await LocalFile.doesDirectoryExist(oneuptimeScriptsPath);

    if (!doesScriptsDirectoryExist) {
      return null;
    }

    const scriptPath: string = LocalFile.sanitizeFilePath(
      `${oneuptimeScriptsPath}/${Text.fromPascalCaseToDashes(data.scriptType)}.sh`,
    );

    const doesScriptExist: boolean = await LocalFile.doesFileExist(scriptPath);

    if (!doesScriptExist) {
      return null;
    }

    const scriptContent: string = await LocalFile.read(scriptPath);

    return scriptContent.trim() || null;
  }

  public static hasOpenPRForFile(data: {
    filePath: string;
    pullRequests: Array<PullRequest>;
  }): boolean {
    const pullRequests: Array<PullRequest> = this.getOpenPRForFile(data);
    return pullRequests.length > 0;
  }

  public static getOpenPRForFile(data: {
    filePath: string;
    pullRequests: Array<PullRequest>;
  }): Array<PullRequest> {
    const pullRequests: Array<PullRequest> = [];

    for (const pullRequest of data.pullRequests) {
      if (pullRequest.title.includes(data.filePath)) {
        pullRequests.push(pullRequest);
      }
    }

    return pullRequests;
  }

  public static async listFilesInDirectory(data: {
    directoryPath: string;
  }): Promise<Array<string>> {
    return await CodeRepositoryServerUtil.listFilesInDirectory({
      repoPath: this.getLocalRepositoryPath(),
      directoryPath: data.directoryPath,
    });
  }

  public static getGitHubUtil(): GitHubUtil {
    if (!this.gitHubUtil) {
      const gitHubToken: string | null = GetCodeRepositoryPassword();

      const gitHubUsername: string | null = GetCodeRepositoryUsername();

      if (!gitHubUsername) {
        throw new BadDataException("GitHub Username is required");
      }

      if (!gitHubToken) {
        throw new BadDataException("GitHub Token is required");
      }

      this.gitHubUtil = new GitHubUtil({
        authToken: gitHubToken,
        username: gitHubUsername!,
      });
    }

    return this.gitHubUtil;
  }

  public static async pullChanges(): Promise<void> {
    await CodeRepositoryServerUtil.pullChanges({
      repoPath: this.getLocalRepositoryPath(),
    });
  }

  public static getBranchName(data: { branchName: string }): string {
    return "oneuptime-copilot-" + data.branchName;
  }

  public static async createBranch(data: {
    branchName: string;
  }): Promise<void> {
    const branchName: string = this.getBranchName({
      branchName: data.branchName,
    });

    await CodeRepositoryServerUtil.createBranch({
      repoPath: this.getLocalRepositoryPath(),
      branchName: branchName,
    });
  }

  public static async createOrCheckoutBranch(data: {
    branchName: string;
  }): Promise<void> {
    const branchName: string = this.getBranchName({
      branchName: data.branchName,
    });

    await CodeRepositoryServerUtil.createOrCheckoutBranch({
      repoPath: this.getLocalRepositoryPath(),
      branchName: branchName,
    });
  }

  public static async writeToFile(data: {
    filePath: string;
    content: string;
  }): Promise<void> {
    await CodeRepositoryServerUtil.writeToFile({
      repoPath: this.getLocalRepositoryPath(),
      filePath: data.filePath,
      content: data.content,
    });
  }

  public static async createDirectory(data: {
    directoryPath: string;
  }): Promise<void> {
    await CodeRepositoryServerUtil.createDirectory({
      repoPath: this.getLocalRepositoryPath(),
      directoryPath: data.directoryPath,
    });
  }

  public static async deleteFile(data: { filePath: string }): Promise<void> {
    await CodeRepositoryServerUtil.deleteFile({
      repoPath: this.getLocalRepositoryPath(),
      filePath: data.filePath,
    });
  }

  public static async deleteDirectory(data: {
    directoryPath: string;
  }): Promise<void> {
    await CodeRepositoryServerUtil.deleteDirectory({
      repoPath: this.getLocalRepositoryPath(),
      directoryPath: data.directoryPath,
    });
  }

  public static async discardChanges(): Promise<void> {
    if (this.isRepoCloned()) {
      await CodeRepositoryServerUtil.discardChanges({
        repoPath: this.getLocalRepositoryPath(),
      });
    }
  }

  public static async checkoutBranch(data: {
    branchName: string;
  }): Promise<void> {
    if (this.isRepoCloned()) {
      await CodeRepositoryServerUtil.checkoutBranch({
        repoPath: this.getLocalRepositoryPath(),
        branchName: data.branchName,
      });
    }
  }

  public static async checkoutMainBranch(): Promise<void> {
    if (!this.isRepoCloned()) {
      return;
    }

    const codeRepository: CopilotCodeRepository =
      await this.getCodeRepository();

    if (!codeRepository.mainBranchName) {
      throw new BadDataException("Main Branch Name is required");
    }

    await this.checkoutBranch({
      branchName: codeRepository.mainBranchName!,
    });
  }

  public static async addFilesToGit(data: {
    filePaths: Array<string>;
  }): Promise<void> {
    await CodeRepositoryServerUtil.addFilesToGit({
      repoPath: this.getLocalRepositoryPath(),
      filePaths: data.filePaths,
    });
  }

  public static async commitChanges(data: { message: string }): Promise<void> {
    let username: string | null = null;

    if (
      this.codeRepositoryResult?.codeRepository.repositoryHostedAt ===
      CodeRepositoryType.GitHub
    ) {
      username = GetCodeRepositoryUsername();
    }

    if (!username) {
      throw new BadDataException("Username is required");
    }

    await CodeRepositoryServerUtil.commitChanges({
      repoPath: this.getLocalRepositoryPath(),
      message: data.message,
    });
  }

  public static async pushChanges(data: { branchName: string }): Promise<void> {
    const branchName: string = this.getBranchName({
      branchName: data.branchName,
    });

    const codeRepository: CopilotCodeRepository =
      await this.getCodeRepository();

    if (!codeRepository.mainBranchName) {
      throw new BadDataException("Main Branch Name is required");
    }

    if (!codeRepository.organizationName) {
      throw new BadDataException("Organization Name is required");
    }

    if (!codeRepository.repositoryName) {
      throw new BadDataException("Repository Name is required");
    }

    if (codeRepository.repositoryHostedAt === CodeRepositoryType.GitHub) {
      return await this.getGitHubUtil().pushChanges({
        repoPath: this.getLocalRepositoryPath(),
        branchName: branchName,
        organizationName: codeRepository.organizationName,
        repositoryName: codeRepository.repositoryName,
      });
    }
  }

  public static async switchToMainBranch(): Promise<void> {
    const codeRepository: CopilotCodeRepository =
      await this.getCodeRepository();

    if (!codeRepository.mainBranchName) {
      throw new BadDataException("Main Branch Name is required");
    }

    await this.checkoutBranch({
      branchName: codeRepository.mainBranchName!,
    });
  }

  public static async createPullRequest(data: {
    branchName: string;
    title: string;
    body: string;
  }): Promise<PullRequest> {
    const branchName: string = this.getBranchName({
      branchName: data.branchName,
    });

    const codeRepository: CopilotCodeRepository =
      await this.getCodeRepository();

    if (!codeRepository.mainBranchName) {
      throw new BadDataException("Main Branch Name is required");
    }

    if (!codeRepository.organizationName) {
      throw new BadDataException("Organization Name is required");
    }

    if (!codeRepository.repositoryName) {
      throw new BadDataException("Repository Name is required");
    }

    if (codeRepository.repositoryHostedAt === CodeRepositoryType.GitHub) {
      return await this.getGitHubUtil().createPullRequest({
        headBranchName: branchName,
        baseBranchName: codeRepository.mainBranchName,
        organizationName: codeRepository.organizationName,
        repositoryName: codeRepository.repositoryName,
        title: data.title,
        body: data.body,
      });
    }
    throw new BadDataException("Code Repository type not supported");
  }

  public static async getServicesToImproveCode(data: {
    codeRepository: CopilotCodeRepository;
    serviceRepositories: Array<ServiceCopilotCodeRepository>;
    openPullRequests: Array<CopilotPullRequest>;
  }): Promise<Array<ServiceToImproveResult>> {
    const servicesToImproveCode: Array<ServiceToImproveResult> = [];

    for (const service of data.serviceRepositories) {
      if (!data.codeRepository.mainBranchName) {
        throw new BadDataException("Main Branch Name is required");
      }

      if (!data.codeRepository.organizationName) {
        throw new BadDataException("Organization Name is required");
      }

      if (!data.codeRepository.repositoryName) {
        throw new BadDataException("Repository Name is required");
      }

      if (!service.limitNumberOfOpenPullRequestsCount) {
        throw new BadDataException(
          "Limit Number Of Open Pull Requests Count is required",
        );
      }

      if (
        data.codeRepository.repositoryHostedAt === CodeRepositoryType.GitHub
      ) {
        const gitHuhbToken: string | null = GetCodeRepositoryPassword();

        if (!gitHuhbToken) {
          throw new BadDataException("GitHub Token is required");
        }

        const pullRequestByService: Array<CopilotPullRequest> =
          data.openPullRequests.filter((pullRequest: CopilotPullRequest) => {
            return (
              pullRequest.serviceRepositoryId?.toString() ===
              service.id?.toString()
            );
          });

        const numberOfPullRequestForThisService: number =
          pullRequestByService.length;

        if (
          numberOfPullRequestForThisService <
          service.limitNumberOfOpenPullRequestsCount
        ) {
          servicesToImproveCode.push({
            serviceRepository: service,
            numberOfOpenPullRequests: numberOfPullRequestForThisService,
            pullRequests: pullRequestByService,
          });
          logger.info(
            `Service ${service.serviceCatalog?.name} has ${numberOfPullRequestForThisService} open pull requests. Limit is ${service.limitNumberOfOpenPullRequestsCount}. Adding to the list to improve code...`,
          );
        } else {
          logger.warn(
            `Service ${service.serviceCatalog?.name} has ${numberOfPullRequestForThisService} open pull requests. Limit is ${service.limitNumberOfOpenPullRequestsCount}. Skipping...`,
          );
        }
      }
    }

    return servicesToImproveCode;
  }

  public static async getCodeRepositoryResult(): Promise<CodeRepositoryResult> {
    if (this.codeRepositoryResult) {
      return this.codeRepositoryResult;
    }

    logger.info("Fetching Code Repository...");

    const repositorySecretKey: string | null = GetRepositorySecretKey();

    if (!repositorySecretKey) {
      throw new BadDataException("Repository Secret Key is required");
    }

    const url: URL = URL.fromString(
      GetOneUptimeURL().toString() + "/api",
    ).addRoute(
      `${new CopilotCodeRepository()
        .getCrudApiPath()
        ?.toString()}/get-code-repository/${repositorySecretKey}`,
    );

    const codeRepositoryResult: HTTPErrorResponse | HTTPResponse<JSONObject> =
      await API.get(url);

    if (codeRepositoryResult instanceof HTTPErrorResponse) {
      throw codeRepositoryResult;
    }

    const codeRepository: CopilotCodeRepository =
      CopilotCodeRepository.fromJSON(
        codeRepositoryResult.data["codeRepository"] as JSONObject,
        CopilotCodeRepository,
      ) as CopilotCodeRepository;

    const servicesRepository: Array<ServiceCopilotCodeRepository> = (
      codeRepositoryResult.data["servicesRepository"] as JSONArray
    ).map((serviceRepository: JSONObject) => {
      return ServiceCopilotCodeRepository.fromJSON(
        serviceRepository,
        ServiceCopilotCodeRepository,
      ) as ServiceCopilotCodeRepository;
    });

    if (!codeRepository) {
      throw new BadDataException(
        "Code Repository not found with the secret key provided.",
      );
    }

    if (!servicesRepository || servicesRepository.length === 0) {
      throw new BadDataException(
        "No services attached to this repository. Please attach services to this repository on OneUptime Dashboard.",
      );
    }

    logger.info(`Code Repository found: ${codeRepository.name}`);

    logger.info("Services found in the repository:");

    servicesRepository.forEach(
      (serviceRepository: ServiceCopilotCodeRepository) => {
        logger.info(`- ${serviceRepository.serviceCatalog?.name}`);
      },
    );

    this.codeRepositoryResult = {
      codeRepository,
      serviceRepositories: servicesRepository,
    };

    return this.codeRepositoryResult;
  }

  public static async getCodeRepository(): Promise<CopilotCodeRepository> {
    if (!this.codeRepositoryResult) {
      const result: CodeRepositoryResult = await this.getCodeRepositoryResult();
      return result.codeRepository;
    }

    return this.codeRepositoryResult.codeRepository;
  }

  public static getCodeFileExtentions(): Array<string> {
    const extensions: Array<string> = [
      "ts",
      "js",
      "tsx",
      "jsx",
      "py",
      "go",
      "java",
      "c",
      "cpp",
      "cs",
      "swift",
      "php",
      "rb",
      "rs",
      "kt",
      "dart",
      "sh",
      "pl",
      "lua",
      "r",
      "scala",
      "ts",
      "js",
      "tsx",
      "jsx",
    ];

    return extensions;
  }

  public static getReadmeFileExtentions(): Array<string> {
    return ["md"];
  }
}
