import { REPOSITORY_ARCHIVED } from '../../../constants/error-messages';
import { logger } from '../../../logger';
import { GerritHttp } from '../../../util/http/gerrit';
import { regEx } from '../../../util/regex';
import type {
  GerritAccountInfo,
  GerritBranchInfo,
  GerritChange,
  GerritChangeMessageInfo,
  GerritFindPRConfig,
  GerritMergeableInfo,
  GerritProjectInfo,
} from './types';
import { mapPrStateToGerritFilter } from './utils';

const QUOTES_REGEX = regEx('"', 'g');

class GerritClient {
  private requestDetails = [
    'SUBMITTABLE', //include the submittable field in ChangeInfo, which can be used to tell if the change is reviewed and ready for submit.
    'CHECK', // include potential problems with the change.
    'MESSAGES',
    'DETAILED_ACCOUNTS',
    'LABELS',
    'CURRENT_ACTIONS', //to check if current_revision can be "rebased"
    'CURRENT_REVISION', //get RevisionInfo::ref to fetch
    'CURRENT_COMMIT', // to get the commit message
  ] as const;

  private gerritHttp = new GerritHttp();

  async getRepos(): Promise<string[]> {
    const res = await this.gerritHttp.getJson<string[]>(
      'a/projects/?type=CODE&state=ACTIVE',
      {},
    );
    return Object.keys(res.body);
  }

  async getProjectInfo(repository: string): Promise<GerritProjectInfo> {
    const projectInfo = await this.gerritHttp.getJson<GerritProjectInfo>(
      `a/projects/${encodeURIComponent(repository)}`,
    );
    if (projectInfo.body.state !== 'ACTIVE') {
      throw new Error(REPOSITORY_ARCHIVED);
    }
    return projectInfo.body;
  }

  async getBranchInfo(repository: string): Promise<GerritBranchInfo> {
    const branchInfo = await this.gerritHttp.getJson<GerritBranchInfo>(
      `a/projects/${encodeURIComponent(repository)}/branches/HEAD`,
    );
    return branchInfo.body;
  }

  async findChanges(
    repository: string,
    findPRConfig: GerritFindPRConfig,
    refreshCache?: boolean,
  ): Promise<GerritChange[]> {
    const filters = GerritClient.buildSearchFilters(repository, findPRConfig);
    const changes = await this.gerritHttp.getJson<GerritChange[]>(
      `a/changes/?q=` +
        filters.join('+') +
        this.requestDetails.map((det) => `&o=${det}`).join(''),
      { memCache: !refreshCache },
    );
    logger.trace(
      `findChanges(${filters.join(', ')}) => ${changes.body.length}`,
    );
    return changes.body;
  }

  async getChange(changeNumber: number): Promise<GerritChange> {
    const changes = await this.gerritHttp.getJson<GerritChange>(
      `a/changes/${changeNumber}?` +
        this.requestDetails.map((det) => `o=${det}`).join('&'),
    );
    return changes.body;
  }

  async getMergeableInfo(change: GerritChange): Promise<GerritMergeableInfo> {
    const mergeable = await this.gerritHttp.getJson<GerritMergeableInfo>(
      `a/changes/${change._number}/revisions/current/mergeable`,
    );
    return mergeable.body;
  }

  async abandonChange(changeNumber: number): Promise<void> {
    await this.gerritHttp.postJson(`a/changes/${changeNumber}/abandon`);
  }

  async submitChange(changeNumber: number): Promise<GerritChange> {
    const change = await this.gerritHttp.postJson<GerritChange>(
      `a/changes/${changeNumber}/submit`,
    );
    return change.body;
  }

  async setCommitMessage(changeNumber: number, message: string): Promise<void> {
    await this.gerritHttp.putJson(`a/changes/${changeNumber}/message`, {
      body: { message },
    });
  }

  async updateChangeSubject(
    number: number,
    currentMessage: string,
    newSubject: string,
  ): Promise<void> {
    // Replace first line of the commit message with the new subject
    const newMessage = currentMessage.replace(
      new RegExp(`^.*$`, 'm'),
      newSubject,
    );
    await this.setCommitMessage(number, newMessage);
  }

  async getMessages(changeNumber: number): Promise<GerritChangeMessageInfo[]> {
    const messages = await this.gerritHttp.getJson<GerritChangeMessageInfo[]>(
      `a/changes/${changeNumber}/messages`,
      { memCache: false },
    );
    return messages.body;
  }

  async addMessage(
    changeNumber: number,
    fullMessage: string,
    tag?: string,
  ): Promise<void> {
    const message = this.normalizeMessage(fullMessage);
    await this.gerritHttp.postJson(
      `a/changes/${changeNumber}/revisions/current/review`,
      { body: { message, tag } },
    );
  }

  async checkForExistingMessage(
    changeNumber: number,
    newMessage: string,
    msgType?: string,
  ): Promise<boolean> {
    const messages = await this.getMessages(changeNumber);
    return messages.some(
      (existingMsg) =>
        (msgType === undefined || msgType === existingMsg.tag) &&
        existingMsg.message.includes(newMessage),
    );
  }

  async addMessageIfNotAlreadyExists(
    changeNumber: number,
    message: string,
    tag?: string,
  ): Promise<void> {
    const newMsg = this.normalizeMessage(message);
    if (!(await this.checkForExistingMessage(changeNumber, newMsg, tag))) {
      await this.addMessage(changeNumber, newMsg, tag);
    }
  }

  async setLabel(
    changeNumber: number,
    label: string,
    value: number,
  ): Promise<void> {
    await this.gerritHttp.postJson(
      `a/changes/${changeNumber}/revisions/current/review`,
      { body: { labels: { [label]: value } } },
    );
  }

  async addReviewer(changeNumber: number, reviewer: string): Promise<void> {
    await this.gerritHttp.postJson(`a/changes/${changeNumber}/reviewers`, {
      body: { reviewer },
    });
  }

  async addAssignee(changeNumber: number, assignee: string): Promise<void> {
    await this.gerritHttp.putJson<GerritAccountInfo>(
      `a/changes/${changeNumber}/assignee`,
      {
        body: { assignee },
      },
    );
  }

  async getFile(
    repo: string,
    branch: string,
    fileName: string,
  ): Promise<string> {
    const base64Content = await this.gerritHttp.get(
      `a/projects/${encodeURIComponent(
        repo,
      )}/branches/${encodeURIComponent(branch)}/files/${encodeURIComponent(fileName)}/content`,
    );
    return Buffer.from(base64Content.body, 'base64').toString();
  }

  async approveChange(changeId: number): Promise<void> {
    const isApproved = await this.checkIfApproved(changeId);
    if (!isApproved) {
      await this.setLabel(changeId, 'Code-Review', +2);
    }
  }

  async checkIfApproved(changeId: number): Promise<boolean> {
    const change = await client.getChange(changeId);
    const reviewLabels = change?.labels?.['Code-Review'];
    return reviewLabels === undefined || reviewLabels.approved !== undefined;
  }

  wasApprovedBy(change: GerritChange, username: string): boolean | undefined {
    return (
      change.labels?.['Code-Review'].approved &&
      change.labels['Code-Review'].approved.username === username
    );
  }

  normalizeMessage(message: string): string {
    //the last \n was removed from gerrit after the comment was added...
    return message.substring(0, 0x4000).trim();
  }

  private static buildSearchFilters(
    repository: string,
    searchConfig: GerritFindPRConfig,
  ): string[] {
    const filterState = mapPrStateToGerritFilter(searchConfig.state);
    const filters = ['owner:self', 'project:' + repository, filterState];
    if (searchConfig.branchName) {
      filters.push(
        ...[
          '(',
          `footer:Renovate-Branch=${searchConfig.branchName}`,
          // for backwards compatibility
          'OR',
          `hashtag:sourceBranch-${searchConfig.branchName}`,
          ')',
        ],
      );
    }
    if (searchConfig.targetBranch) {
      filters.push(`branch:${searchConfig.targetBranch}`);
    }
    if (searchConfig.label) {
      filters.push(`label:Code-Review=${searchConfig.label}`);
    }
    if (searchConfig.prTitle) {
      // escaping support in Gerrit is not great, so we need to remove quotes
      // special characters are ignored anyway in the search so it does not create any issues
      filters.push(
        `message:${encodeURIComponent('"' + searchConfig.prTitle.replace(QUOTES_REGEX, '') + '"')}`,
      );
    }
    return filters;
  }
}

export const client = new GerritClient();