import { EventEmitter } from 'events';

import { SDK as RingCentralSDK } from '@ringcentral/sdk';

import { formatParty } from './formatParty';
import { USER_AGENT } from './userAgent';

export enum Direction {
  inbound = 'Inbound',
  outbound = 'Outbound',
}

export enum PartyStatusCode {
  setup = 'Setup',
  proceeding = 'Proceeding',
  answered = 'Answered',
  disconnected = 'Disconnected',
  gone = 'Gone',
  parked = 'Parked',
  hold = 'Hold',
  voicemail = 'Voicemail',
  faxReceive = 'FaxReceive',
  voicemailScreening = 'VoiceMailScreening',
}

export interface PartyToFrom {
  phoneNumber?: string;
  name?: string;
  extensionId?: string;
  extensionNumber?: string;
}

export interface PartyStatus {
  code?: PartyStatusCode;
}

export interface Recording {
  id: string;
  active: boolean;
}

export interface Party {
  id: string;
  extensionId?: string;
  accountId?: string;
  direction: Direction;
  to: PartyToFrom;
  from: PartyToFrom;
  status: PartyStatus;
  missedCall: boolean;
  standAlone: boolean;
  muted: boolean;
  conferenceRole?: 'Host' | 'Participant';
  ringOutRole?: 'Initiator' | 'Target';
  ringMeRole?: 'Initiator' | 'Target';
  recordings?: Recording[];
}

export interface Origin {
  type:  'Call' | 'RingOut' | 'RingMe' | 'Conference' | 'GreetingsRecording' |
    'VerificationCall' | 'TestCall';
}

export interface SessionData {
  id: string;
  extensionId: string;
  accountId: string;
  parties: Party[];
  sessionId: string;
  creationTime: string;
  voiceCallToken?: string;
  origin: Origin;
}

export interface ForwardParams {
  phoneNumber?: string;
  extensionNumber?: string;
  voicemail?: string;
}

export interface TransferParams extends ForwardParams {
  parkOrbit?: string;
}

export interface FlipParams {
  callFlipId: string;
}

export interface PartyParams {
  muted?: boolean;
  standAlone?: boolean;
}

export interface RecordParams {
  id: string;
  active: boolean;
}

export interface SuperviseParams {
  mode: 'Listen';
  deviceId: string;
  extensionNumber: string;
}

export interface BringInParams {
  partyId: string;
  sessionId: string;
}

export interface AnswerParams {
  deviceId: string;
}

export interface BridgeParams {
  telephonySessionId: string;
  partyId: string;
}

export interface IgnoreParams {
  deviceId: string;
}

export enum ReplyWithPattern {
  willCallYouBack = 'WillCallYouBack',
  callMeBack = 'CallMeBack',
  onMyWay = 'OnMyWay',
  onTheOtherLine = 'OnTheOtherLine',
  willCallYouBackLater = 'WillCallYouBackLater',
  callMeBackLater = 'CallMeBackLater',
  inAMeeting = 'InAMeeting',
  onTheOtherLineNoCall = 'OnTheOtherLineNoCall'
}

export interface ReplyWithPatternParams {
  pattern: ReplyWithPattern,
  time?: number,
  timeUnit?: 'Minute' | 'Hour' | 'Day',
}

export interface ReplyWithTextParams {
  replyWithText?: string;
  replyWithPattern?: ReplyWithPatternParams,
}

export interface PickUpParams {
  deviceId: string;
}

export interface RemovePartyOptions {
  keepConferenceAlive?: boolean;
}

function objectEqual(obj1: any, obj2: any) {
  let equal = true;
  if (!obj1 || !obj2) {
    return false;
  }
  Object.keys(obj2).forEach((key) => {
    if (obj2[key] === obj1[key]) {
      return;
    }
    equal = false;
  });
  Object.keys(obj1).forEach((key) => {
    if (obj1[key] === obj2[key]) {
      return;
    }
    equal = false;
  });
  return equal;
}

function diffParty(oldParty: Party, updatedParty: Party) {
  const diffs = [];
  updatedParty && Object.keys(updatedParty).forEach((key) => {
    if (updatedParty[key] === oldParty[key]) {
      return;
    }
    if (typeof updatedParty[key] !== 'object') {
      diffs.push({ key, value: updatedParty[key] });
      return;
    }
    if (objectEqual(updatedParty[key], oldParty[key])) {
      return;
    }
    diffs.push({ key, value: updatedParty[key] });
  })
  return diffs;
}

function diffParties(oldParties: Party[], updatedParties: Party[]) {
  const oldMap = {};
  oldParties.forEach((p) => {
    oldMap[p.id] = p;
  });
  const diffs = [];
  updatedParties.forEach((updatedParty) => {
    if (!oldMap[updatedParty.id]) {
      diffs.push({ type: 'new', party: updatedParty });
      return;
    }
    const oldParty = oldMap[updatedParty.id];
    const partyDiffs= diffParty(oldParty, updatedParty);
    if (partyDiffs.length === 0) {
      return;
    }
    diffs.push({ type: 'update', party: updatedParty, partyDiffs });
  });
  return diffs;
}

export class Session extends EventEmitter {
  private _data: any;
  private _sdk: RingCentralSDK;
  private _accountLevel: boolean;
  private _userAgent: string;

  constructor(rawData: SessionData, sdk: RingCentralSDK, accountLevel: boolean, userAgent?: string) {
    super();
    this._data = { ...rawData };
    this._sdk = sdk;
    this._accountLevel = !!accountLevel;
    this._userAgent = userAgent;
  }

  public onUpdated(data: SessionData) {
    const partiesDiff =  diffParties(this.parties, data.parties);
    partiesDiff.forEach((diff) => {
      if (diff.type === 'new') {
        this._data.parties = [].concat(this.parties).concat(diff.party);
        this.emit('status', { party: diff.party });
        return;
      }
      if (diff.type === 'update') {
        const oldPartyIndex = this.parties.findIndex(p => p.id === diff.party.id);
        const parties = this.parties.slice(0);
        parties[oldPartyIndex] = {
          ...parties[oldPartyIndex],
          ...diff.party,
        }
        this._data.parties = parties;
        diff.partyDiffs.forEach((partyDiff) => {
          this.emit(partyDiff.key, { party: diff.party });
        });
        return;
      }
    });
  }

  public restore(data: SessionData) {
    this._data = data;
  }

  get data() {
    return this._data || {};
  }

  get id() {
    return this.data.id;
  }

  get accountId() {
    return this.data.accountId;
  }

  get creationTime() {
    return this.data.creationTime;
  }

  get extensionId() {
    return this.data.extensionId;
  }

  get origin() {
    return this.data.origin;
  }

  get parties() {
    return this.data.parties || [];
  }

  get serverId() {
    return this.data.serverId;
  }

  get sessionId() {
    return this.data.sessionId;
  }

  get party() {
    const extensionId = this.data.extensionId;
    const accountId = this.data.accountId;
    const parties = this.parties.filter(p => {
      if (this._accountLevel) {
        return p.accountId === accountId;
      }
      return p.extensionId === extensionId;
    });
    if (parties.length === 0) {
      return;
    }
    if (parties.length === 1) {
      return parties[0];
    }
    const activeParty = parties.find(p => p.status.code !== PartyStatusCode.disconnected);
    if (activeParty) {
      return activeParty;
    }
    return parties[parties.length - 1];
  }

  get otherParties() {
    if (!this.party) {
      return this.parties;
    }
    const partyId = this.party.id;
    return this.parties.filter(p => p.id !== partyId);
  }

  get recordings() {
    const party = this.party;
    return (party && party.recordings) || [];
  }

  get voiceCallToken() {
    return this.data.voiceCallToken;
  }

  toJSON() {
    return this.data;
  }

  async reload() {
    const response = await this._sdk.platform().get(
      `/restapi/v1.0/account/~/telephony/sessions/${this._data.id}`,
      undefined,
      this.requestOptions
    );
    const data = await response.json();
    data.extensionId = this.data.extensionId;
    data.accountId = this.data.accountId;
    data.parties = data.parties.map(p => formatParty(p));
    this._data = data;
  }

  async drop() {
    try {
      await this._sdk.platform().delete(
        `/restapi/v1.0/account/~/telephony/sessions/${this._data.id}`,
        undefined,
        this.requestOptions
      );
    } catch (e) {
      if (e && e.response && e.response.status === 404) {
        // Force drop session at client side
        const disconnectedParty = {
          ...this.party,
          status: {
            ...this.party.status,
            code: PartyStatusCode.disconnected,
          },
        };
        this.saveNewPartyData(disconnectedParty);
        this.emit('status', { party: this.party });
        return;
      }
      throw e;
    }
  }

  private saveNewPartyData(rawParty) {
    const newParty = formatParty(rawParty);
    const newParties = this._data.parties.filter((p) => p.id !== newParty.id);
    newParties.push(newParty);
    this._data.parties = newParties;
  }

  async hold() {
    const oldParty = this.party;
    const response = await this._sdk.platform().post(
      `/restapi/v1.0/account/~/telephony/sessions/${this._data.id}/parties/${oldParty.id}/hold`,
      undefined,
      undefined,
      this.requestOptions,
    );
    const newParty = await response.json();
    this.saveNewPartyData(newParty);
    this.emit('status', { party: this.party });
    return this.party;
  }

  async unhold() {
    const oldParty = this.party;
    const response = await this._sdk.platform().post(
      `/restapi/v1.0/account/~/telephony/sessions/${this._data.id}/parties/${oldParty.id}/unhold`,
      undefined,
      undefined,
      this.requestOptions,
    );
    const newParty = await response.json();
    this.saveNewPartyData(newParty);
    this.emit('status', { party: this.party });
    return this.party;
  }

  async toVoicemail() {
    await this._sdk.platform().post(
      `/restapi/v1.0/account/~/telephony/sessions/${this._data.id}/parties/${this.party.id}/reject`,
      undefined,
      undefined,
      this.requestOptions,
    );
  }

  async ignore(params: IgnoreParams) {
    await this._sdk.platform().post(
      `/restapi/v1.0/account/~/telephony/sessions/${this._data.id}/parties/${this.party.id}/ignore`,
      params,
      undefined,
      this.requestOptions,
    );
  }

  async answer(params: AnswerParams) {
    await this._sdk.platform().post(
      `/restapi/v1.0/account/~/telephony/sessions/${this._data.id}/parties/${this.party.id}/answer`,
      params,
      undefined,
      this.requestOptions,
    );
  }

  async reply(params: ReplyWithTextParams) {
    const response = await this._sdk.platform().post(
      `/restapi/v1.0/account/~/telephony/sessions/${this._data.id}/parties/${this.party.id}/reply`,
      params,
      undefined,
      this.requestOptions,
    );
    const rawParty = await response.json();
    this.saveNewPartyData(rawParty);
    this.emit('status', { party: this.party });
    return this.party;
  }

  async forward(params: ForwardParams) {
    const response = await this._sdk.platform().post(
      `/restapi/v1.0/account/~/telephony/sessions/${this._data.id}/parties/${this.party.id}/forward`,
      params,
      undefined,
      this.requestOptions,
    );
    return response.json();
  }

  async transfer(params: TransferParams) {
    const response = await this._sdk.platform().post(
      `/restapi/v1.0/account/~/telephony/sessions/${this._data.id}/parties/${this.party.id}/transfer`,
      params,
      undefined,
      this.requestOptions,
    );
    return response.json();
  }

  async bridge(params: BridgeParams) {
    const response = await this._sdk.platform().post(
      `/restapi/v1.0/account/~/telephony/sessions/${this._data.id}/parties/${this.party.id}/bridge`,
      params,
      undefined,
      this.requestOptions,
    );
    return response.json();
  }

  async park() {
    const response = await this._sdk.platform().post(
      `/restapi/v1.0/account/~/telephony/sessions/${this._data.id}/parties/${this.party.id}/park`,
      undefined,
      undefined,
      this.requestOptions,
    );
    return response.json();
  }

  // async pickup(params: PickUpParams) {
  //   const response = await this._sdk.platform().post(
  //     `/account/~/telephony/sessions/${this._data.id}/parties/${this.party.id}/pickup`,
  //     params,
  //   );
  //   return response.json();
  // }

  // async transferToVoicemail() {
  //   const result = await this.forward({ voicemail: this._data.extensionId });
  //   return result;
  // }

  async flip(params: FlipParams) {
    const response = await this._sdk.platform().post(
      `/restapi/v1.0/account/~/telephony/sessions/${this._data.id}/parties/${this.party.id}/flip`,
      params,
      undefined,
      this.requestOptions,
    );
    return response.json();
  }

  async updateParty(params: PartyParams) {
    const response = await this._sdk.platform().send({
      method: 'PATCH',
      url: `/restapi/v1.0/account/~/telephony/sessions/${this._data.id}/parties/${this.party.id}`,
      query: undefined,
      body: params,
      userAgent: this.requestOptions.userAgent,
    });
    const rawParty = await response.json();
    this.saveNewPartyData(rawParty);
    return rawParty;
  }

  async mute() {
    const result = await this.updateParty({ muted: true });
    this.emit('muted', { party: result })
    return result;
  }

  async unmute() {
    const result = await this.updateParty({ muted: false });
    this.emit('muted', { party: result })
    return result;
  }

  async createRecord() {
    const response = await this._sdk.platform().post(
      `/restapi/v1.0/account/~/telephony/sessions/${this._data.id}/parties/${this.party.id}/recordings`,
      undefined,
      undefined,
      this.requestOptions,
    );
    const recording = await response.json();
    const recordings = (this.party.recordings || []).filter(r => r.id !== recording.id);
    recordings.push(recording);
    this.party.recordings = recordings
    this.emit('recordings', { party: this.party });
    return recording;
  }

  async updateRecord(params: RecordParams) {
    const response = await this._sdk.platform().send({
      method: 'PATCH',
      url: `/restapi/v1.0/account/~/telephony/sessions/${this._data.id}/parties/${this.party.id}/recordings/${params.id}`,
      query: undefined,
      body: {
        active: params.active,
      },
      userAgent: this.requestOptions.userAgent,
    });
    const recording = await response.json();
    const recordings = (this.party.recordings || []).filter(r => r.id !== recording.id);
    recordings.push(recording);
    this.party.recordings = recordings
    this.emit('recordings', { party: this.party });
    return recording;
  }

  async pauseRecord(recordingId: string) {
    const result = await this.updateRecord({ id: recordingId, active: false });
    return result;
  }

  async resumeRecord(recordingId: string) {
    const result = await this.updateRecord({ id: recordingId, active: true });
    return result;
  }

  async supervise(params: SuperviseParams) {
    const response = await this._sdk.platform().post(
      `/restapi/v1.0/account/~/telephony/sessions/${this._data.id}/supervise`,
      params,
      undefined,
      this.requestOptions,
    );
    return response.json();
  }

  async bringInParty(params: BringInParams) {
    const response = await this._sdk.platform().post(
      `/restapi/v1.0/account/~/telephony/sessions/${this._data.id}/parties/bring-in`,
      params,
      undefined,
      this.requestOptions,
    );
    return response.json();
  }

  async removeParty(partyId: string, options?: RemovePartyOptions) {
    const requestOptions: {
      userAgent: string;
      body?: RemovePartyOptions;
    } = {
      ...this.requestOptions,
    };
    if (options) {
      requestOptions.body = options;
    }
    return await this._sdk.platform().delete(
      `/restapi/v1.0/account/~/telephony/sessions/${this._data.id}/parties/${partyId}`,
      undefined,
      requestOptions,
    );
  }

  get requestOptions() {
    return {
      userAgent: this._userAgent ? `${this._userAgent} ${USER_AGENT}` : USER_AGENT,
    };
  }
}
