/**
 * @license
 * Copyright 2016 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea';
import '@polymer/iron-input/iron-input';
import '../../../styles/shared-styles';
import '../../shared/gr-autocomplete/gr-autocomplete';
import '../../shared/gr-dialog/gr-dialog';
import {navigationToken} from '../../core/gr-navigation/gr-navigation';
import {getAppContext} from '../../../services/app-context';
import {
  ChangeInfo,
  BranchName,
  RepoName,
  CommitId,
  ChangeInfoId,
} from '../../../types/common';
import {customElement, property, query, state} from 'lit/decorators.js';
import {
  AutocompleteQuery,
  AutocompleteSuggestion,
  GrTypedAutocomplete,
} from '../../shared/gr-autocomplete/gr-autocomplete';
import {
  HttpMethod,
  ChangeStatus,
  ProgressStatus,
} from '../../../constants/constants';
import {fireEvent} from '../../../utils/event-util';
import {css, html, LitElement, PropertyValues} from 'lit';
import {sharedStyles} from '../../../styles/shared-styles';
import {choose} from 'lit/directives/choose.js';
import {when} from 'lit/directives/when.js';
import {BindValueChangeEvent} from '../../../types/events';
import {resolve} from '../../../models/dependency';
import {createSearchUrl} from '../../../models/views/search';

const SUGGESTIONS_LIMIT = 15;
const CHANGE_SUBJECT_LIMIT = 50;
enum CherryPickType {
  SINGLE_CHANGE = 1,
  TOPIC,
}

export type Statuses = {[changeId: string]: Status};

interface Status {
  status: ProgressStatus;
  msg?: string;
}

declare global {
  interface HTMLElementTagNameMap {
    'gr-confirm-cherrypick-dialog': GrConfirmCherrypickDialog;
  }
}

@customElement('gr-confirm-cherrypick-dialog')
export class GrConfirmCherrypickDialog extends LitElement {
  /**
   * Fired when the confirm button is pressed.
   *
   * @event confirm
   */

  /**
   * Fired when the cancel button is pressed.
   *
   * @event cancel
   */

  @property({type: String})
  branch = '' as BranchName;

  @property({type: String})
  baseCommit?: string;

  @property({type: String})
  changeStatus?: ChangeStatus;

  @property({type: String})
  commitMessage?: string;

  @property({type: String})
  commitNum?: CommitId;

  @property({type: String})
  message = '';

  @property({type: String})
  project?: RepoName;

  @property({type: Array})
  changes: ChangeInfo[] = [];

  @state()
  private query: AutocompleteQuery;

  @state()
  private showCherryPickTopic = false;

  @state()
  private changesCount?: number;

  @state()
  cherryPickType = CherryPickType.SINGLE_CHANGE;

  @state()
  private duplicateProjectChanges = false;

  @state()
  // Status of each change that is being cherry picked together
  private statuses: Statuses;

  @state()
  private invalidBranch = false;

  @query('#branchInput')
  branchInput!: GrTypedAutocomplete<BranchName>;

  private selectedChangeIds = new Set<ChangeInfoId>();

  private readonly restApiService = getAppContext().restApiService;

  private readonly reporting = getAppContext().reportingService;

  private readonly getNavigation = resolve(this, navigationToken);

  constructor() {
    super();
    this.statuses = {};
    this.query = (text: string) => this.getProjectBranchesSuggestions(text);
  }

  override willUpdate(changedProperties: PropertyValues) {
    if (changedProperties.has('branch')) {
      this.updateBranch();
    }
    if (
      changedProperties.has('changeStatus') ||
      changedProperties.has('commitNum') ||
      changedProperties.has('commitMessage')
    ) {
      this.computeMessage();
    }
  }

  static override styles = [
    sharedStyles,
    css`
      :host {
        display: block;
      }
      :host([disabled]) {
        opacity: 0.5;
        pointer-events: none;
      }
      label {
        cursor: pointer;
      }
      .main {
        display: flex;
        flex-direction: column;
        width: 100%;
      }
      .main label,
      .main input[type='text'] {
        display: block;
        width: 100%;
      }
      iron-autogrow-textarea {
        font-family: var(--monospace-font-family);
        font-size: var(--font-size-mono);
        line-height: var(--line-height-mono);
        width: 73ch; /* Add a char to account for the border. */
      }
      .cherryPickTopicLayout {
        display: flex;
        align-items: center;
        margin-bottom: var(--spacing-m);
      }
      .cherryPickSingleChange,
      .cherryPickTopic {
        margin-left: var(--spacing-m);
      }
      .cherry-pick-topic-message {
        margin-bottom: var(--spacing-m);
      }
      label[for='messageInput'],
      label[for='baseInput'] {
        margin-top: var(--spacing-m);
      }
      .title {
        font-weight: var(--font-weight-bold);
      }
      tr > td {
        padding: var(--spacing-m);
      }
      th {
        color: var(--deemphasized-text-color);
      }
      table {
        border-collapse: collapse;
      }
      tr {
        border-bottom: 1px solid var(--border-color);
      }
      .error {
        color: var(--error-text-color);
      }
      .error-message {
        color: var(--error-text-color);
        margin: var(--spacing-m) 0 var(--spacing-m) 0;
      }
    `,
  ];

  override render() {
    return html`
      <gr-dialog
        confirm-label="Cherry Pick"
        .cancelLabel=${this.computeCancelLabel()}
        ?disabled=${this.computeDisableCherryPick(
          this.cherryPickType,
          this.duplicateProjectChanges,
          this.statuses,
          this.branch
        )}
        @confirm=${this.handleConfirmTap}
        @cancel=${this.handleCancelTap}
      >
        <div class="header title" slot="header">
          Cherry Pick Change to Another Branch
        </div>
        <div class="main" slot="main">
          <gr-endpoint-decorator name="cherrypick-main">
            <gr-endpoint-param name="changes" .value=${this.changes}>
            </gr-endpoint-param>
            <gr-endpoint-slot name="top"></gr-endpoint-slot>
            ${when(this.showCherryPickTopic, () =>
              this.renderCherrypickTopicLayout()
            )}
            <label for="branchInput"> Cherry Pick to branch </label>
            <gr-autocomplete
              id="branchInput"
              .text=${this.branch}
              .query=${this.query}
              placeholder="Destination branch"
              @text-changed=${(e: BindValueChangeEvent) =>
                (this.branch = e.detail.value as BranchName)}
            >
            </gr-autocomplete>
            ${when(
              this.invalidBranch,
              () => html`
                <span class="error"
                  >Branch name cannot contain space or commas.</span
                >
              `
            )}
            ${choose(this.cherryPickType, [
              [
                CherryPickType.SINGLE_CHANGE,
                () => this.renderCherrypickSingleChangeInputs(),
              ],
              [CherryPickType.TOPIC, () => this.renderCherrypickTopicTable()],
            ])}
            <gr-endpoint-slot name="bottom"></gr-endpoint-slot>
          </gr-endpoint-decorator>
        </div>
      </gr-dialog>
    `;
  }

  private renderCherrypickTopicLayout() {
    return html`
      <div class="cherryPickTopicLayout">
        <input
          name="cherryPickOptions"
          type="radio"
          id="cherryPickSingleChange"
          @change=${this.handlecherryPickSingleChangeClicked}
          checked
        />
        <label for="cherryPickSingleChange" class="cherryPickSingleChange">
          Cherry Pick single change
        </label>
      </div>
      <div class="cherryPickTopicLayout">
        <input
          name="cherryPickOptions"
          type="radio"
          id="cherryPickTopic"
          @change=${this.handlecherryPickTopicClicked}
        />
        <label for="cherryPickTopic" class="cherryPickTopic">
          Cherry Pick entire topic (${this.changesCount} Changes)
        </label>
      </div>
    `;
  }

  private renderCherrypickSingleChangeInputs() {
    return html`
      <label for="baseInput"> Provide base commit sha1 for cherry-pick </label>
      <iron-input
        .bindValue=${this.baseCommit}
        @bind-value-changed=${(e: BindValueChangeEvent) =>
          (this.baseCommit = e.detail.value)}
      >
        <input
          is="iron-input"
          id="baseCommitInput"
          maxlength="40"
          placeholder="(optional)"
        />
      </iron-input>
      <label for="messageInput"> Cherry Pick Commit Message </label>
      <iron-autogrow-textarea
        id="messageInput"
        class="message"
        autocomplete="on"
        rows="4"
        .maxRows=${15}
        .bindValue=${this.message}
        @bind-value-changed=${(e: BindValueChangeEvent) =>
          (this.message = e.detail.value ?? '')}
      ></iron-autogrow-textarea>
    `;
  }

  private renderCherrypickTopicTable() {
    return html`
      <span class="error-message">${this.computeTopicErrorMessage()}</span>
      <span class="cherry-pick-topic-message">
        Commit Message will be auto generated
      </span>
      <table>
        <thead>
          <tr>
            <th></th>
            <th>Change</th>
            <th>Status</th>
            <th>Subject</th>
            <th>Project</th>
            <th>Progress</th>
            <!-- Error Message -->
            <th></th>
          </tr>
        </thead>
        <tbody>
          ${this.changes.map(
            item => html`
              <tr>
                <td>
                  <input
                    type="checkbox"
                    data-item=${item.id as string}
                    @change=${this.toggleChangeSelected}
                    ?checked=${this.isChangeSelected(item.id)}
                  />
                </td>
                <td><span> ${this.getChangeId(item)} </span></td>
                <td><span> ${item.status} </span></td>
                <td>
                  <span> ${this.getTrimmedChangeSubject(item.subject)} </span>
                </td>
                <td><span> ${item.project} </span></td>
                <td>
                  <span class=${this.computeStatusClass(item, this.statuses)}>
                    ${this.computeStatus(item, this.statuses)}
                  </span>
                </td>
                <td>
                  <span class="error">
                    ${this.computeError(item, this.statuses)}
                  </span>
                </td>
              </tr>
            `
          )}
        </tbody>
      </table>
    `;
  }

  containsDuplicateProject(changes: ChangeInfo[]) {
    const projects: {[projectName: string]: boolean} = {};
    for (let i = 0; i < changes.length; i++) {
      const change = changes[i];
      if (projects[change.project]) {
        return true;
      }
      projects[change.project] = true;
    }
    return false;
  }

  updateChanges(changes: ChangeInfo[]) {
    this.changes = changes;
    this.statuses = {};
    changes.forEach(change => {
      this.selectedChangeIds.add(change.id);
    });
    this.duplicateProjectChanges = this.containsDuplicateProject(changes);
    this.changesCount = changes.length;
    this.showCherryPickTopic = changes.length > 1;
  }

  private updateBranch() {
    const invalidChars = [',', ' '];
    this.invalidBranch = !!(
      this.branch && invalidChars.some(c => this.branch.includes(c))
    );
  }

  private isChangeSelected(changeId: ChangeInfoId) {
    return this.selectedChangeIds.has(changeId);
  }

  private toggleChangeSelected(e: Event) {
    const changeId = (e.target as HTMLElement).dataset['item']! as ChangeInfoId;
    if (this.selectedChangeIds.has(changeId))
      this.selectedChangeIds.delete(changeId);
    else this.selectedChangeIds.add(changeId);
    const changes = this.changes.filter(change =>
      this.selectedChangeIds.has(change.id)
    );
    this.duplicateProjectChanges = this.containsDuplicateProject(changes);
  }

  private computeTopicErrorMessage() {
    if (this.duplicateProjectChanges) {
      return 'Two changes cannot be of the same project';
    }
    return '';
  }

  updateStatus(change: ChangeInfo, status: Status) {
    this.statuses = {...this.statuses, [change.id]: status};
  }

  private computeStatus(change: ChangeInfo, statuses: Statuses) {
    if (!change || !statuses || !statuses[change.id])
      return ProgressStatus.NOT_STARTED;
    return statuses[change.id].status;
  }

  computeStatusClass(change: ChangeInfo, statuses: Statuses) {
    if (!change || !statuses || !statuses[change.id]) return '';
    return statuses[change.id].status === ProgressStatus.FAILED ? 'error' : '';
  }

  private computeError(change: ChangeInfo, statuses: Statuses) {
    if (!change || !statuses || !statuses[change.id]) return '';
    if (statuses[change.id].status === ProgressStatus.FAILED) {
      return statuses[change.id].msg;
    }
    return '';
  }

  private getChangeId(change: ChangeInfo) {
    return change.change_id.substring(0, 10);
  }

  private getTrimmedChangeSubject(subject: string) {
    if (!subject) return '';
    if (subject.length < CHANGE_SUBJECT_LIMIT) return subject;
    return subject.substring(0, CHANGE_SUBJECT_LIMIT) + '...';
  }

  private computeCancelLabel() {
    const isRunningChange = Object.values(this.statuses).some(
      v => v.status === ProgressStatus.RUNNING
    );
    return isRunningChange ? 'Close' : 'Cancel';
  }

  private computeDisableCherryPick(
    cherryPickType: CherryPickType,
    duplicateProjectChanges: boolean,
    statuses: Statuses,
    branch: BranchName
  ) {
    if (!branch) return true;
    const duplicateProject =
      cherryPickType === CherryPickType.TOPIC && duplicateProjectChanges;
    if (duplicateProject) return true;
    if (!statuses) return false;
    const isRunningChange = Object.values(statuses).some(
      v => v.status === ProgressStatus.RUNNING
    );
    return isRunningChange;
  }

  private handlecherryPickSingleChangeClicked() {
    this.cherryPickType = CherryPickType.SINGLE_CHANGE;
    fireEvent(this, 'iron-resize');
  }

  private handlecherryPickTopicClicked() {
    this.cherryPickType = CherryPickType.TOPIC;
    fireEvent(this, 'iron-resize');
  }

  private computeMessage() {
    if (
      this.changeStatus === undefined ||
      this.commitNum === undefined ||
      this.commitMessage === undefined
    ) {
      return;
    }

    let newMessage = this.commitMessage;

    if (this.changeStatus === 'MERGED') {
      if (!newMessage.endsWith('\n')) {
        newMessage += '\n';
      }
      newMessage += '(cherry picked from commit ' + this.commitNum + ')';
    }
    this.message = newMessage;
  }

  private generateRandomCherryPickTopic(change: ChangeInfo) {
    const randomString = Math.random().toString(36).substr(2, 10);
    const message = `cherrypick-${change.topic}-${randomString}`;
    return message;
  }

  private handleCherryPickFailed(
    change: ChangeInfo,
    response?: Response | null
  ) {
    if (!response) return;
    response.text().then((errText: string) => {
      this.updateStatus(change, {status: ProgressStatus.FAILED, msg: errText});
    });
  }

  private handleCherryPickTopic() {
    const changes = this.changes.filter(change =>
      this.selectedChangeIds.has(change.id)
    );
    if (!changes.length) {
      const errorSpan = this.shadowRoot?.querySelector('.error-message');
      errorSpan!.innerHTML = 'No change selected';
      return;
    }
    const topic = this.generateRandomCherryPickTopic(changes[0]);
    changes.forEach(change => {
      this.updateStatus(change, {status: ProgressStatus.RUNNING});
      const payload = {
        destination: this.branch,
        base: null,
        topic,
        allow_conflicts: true,
        allow_empty: true,
      };
      const handleError = (response?: Response | null) => {
        this.handleCherryPickFailed(change, response);
      };
      // revisions and current_revision must exist hence casting
      const patchNum = change.revisions![change.current_revision!]._number;
      this.restApiService
        .executeChangeAction(
          change._number,
          HttpMethod.POST,
          '/cherrypick',
          patchNum,
          payload,
          handleError
        )
        .then(() => {
          this.updateStatus(change, {status: ProgressStatus.SUCCESSFUL});
          const failedOrPending = Object.values(this.statuses).find(
            v => v.status !== ProgressStatus.SUCCESSFUL
          );
          if (!failedOrPending) {
            // This needs some more work, as the new topic may not always be
            // created, instead we may end up creating a new patchset */
            const query = `topic: "${topic}"`;
            this.getNavigation().setUrl(createSearchUrl({query}));
          }
        });
    });
  }

  private handleConfirmTap(e: Event) {
    e.preventDefault();
    e.stopPropagation();
    if (this.cherryPickType === CherryPickType.TOPIC) {
      this.reporting.reportInteraction('cherry-pick-topic-clicked', {});
      this.handleCherryPickTopic();
      return;
    }
    // Cherry pick single change
    this.dispatchEvent(
      new CustomEvent('confirm', {
        composed: true,
        bubbles: false,
      })
    );
  }

  private handleCancelTap(e: Event) {
    e.preventDefault();
    e.stopPropagation();
    this.dispatchEvent(
      new CustomEvent('cancel', {
        composed: true,
        bubbles: false,
      })
    );
  }

  resetFocus() {
    this.branchInput.focus();
  }

  async getProjectBranchesSuggestions(
    input: string
  ): Promise<AutocompleteSuggestion[]> {
    if (!this.project) return Promise.reject(new Error('Missing project'));
    if (input.startsWith('refs/heads/')) {
      input = input.substring('refs/heads/'.length);
    }
    return this.restApiService
      .getRepoBranches(input, this.project, SUGGESTIONS_LIMIT)
      .then(response => {
        if (!response) return [];
        const branches: Array<{name: BranchName}> = [];
        for (const branchInfo of response) {
          let name: string = branchInfo.ref;
          if (name.startsWith('refs/heads/')) {
            name = name.substring('refs/heads/'.length);
          }
          branches.push({name: name as BranchName});
        }
        return branches;
      });
  }
}
