import { EventEmitter } from 'events';

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

import { formatParty } from './formatParty';
import { ringOutInboundLegCheck } from './helper';
import {
  PartyStatusCode,
  Session,
  SessionData,
} from './Session';
import { USER_AGENT } from './userAgent';

export interface SessionsMap {
  [key: string]: any;
}

export interface SessionMessage {
  event: string;
  body: any;
}

export interface Device {
  id: string;
  linePooling: string;
  name: string;
  uri: string;
  type: 'SoftPhone' | 'OtherPhone' | 'HardPhone'
  serial: string;
  computerName: string;
  boxBillingId: Number;
  useAsCommonPhone: boolean;
  inCompanyNet: boolean;
  model: any;
  extension: any;
  emergencyServiceAddress: any;
  phoneLines: any[];
  shipping: any;
  sku: any;
  status: 'Initial' | 'Offline' | 'Online';
  site: any;
  lastLocationReportTime: string;
}

export interface EventSequenceData {
  sequence: number;
  updatedAt: number;
  telephonySessionId: string;
}

export interface EventSequenceMap {
  [key: string]: EventSequenceData;
}

export interface Account {
  id: string;
}

export interface Extension {
  id: string;
  uri: string;
  account: Account;
  contact: any;
  departments: any[];
  extensionNumber: string;
  name: string;
  partnerId: string;
  permissions: any[];
  profileImage: any;
  references: any[];
  roles: any[];
  regionalSettings: any;
  serviceFeatures: any[];
  setupWizardState: string;
  status: string;
  statusInfo: string;
  type: string;
  callQueueExtensionInfo: any;
  hidden: boolean;
}

export interface CallOutToParams {
  phoneNumber?: string;
  extensionNumber?: string;
}

export class RingCentralCallControl extends EventEmitter {
  private _sdk: RingCentralSDK;
  private _sessionsMap: SessionsMap;
  private _devices: Device[];
  private _currentExtension: Extension;
  private _accountLevel: boolean;
  private _ready: boolean;
  private _initializePromise: any;
  private _preloadSessions: boolean;
  private _preloadDevices: boolean;
  private _userAgent: string;
  private _eventSequenceMap: EventSequenceMap = {};

  constructor({
    sdk,
    accountLevel,
    preloadSessions = true,
    preloadDevices = true,
    extensionInfo,
    userAgent,
  } : {
    sdk: RingCentralSDK,
    accountLevel?: boolean,
    preloadSessions?: boolean,
    preloadDevices?: boolean,
    extensionInfo?: Extension,
    userAgent?: string,
  }) {
    super();
    this._accountLevel = !!accountLevel;
    this._sdk = sdk;
    this._userAgent = userAgent;
    this._sessionsMap = new Map;
    this._devices = [];
    this._ready = false;
    this._initializePromise = null;
    this._preloadSessions = preloadSessions;
    this._preloadDevices = preloadDevices;
    this._currentExtension = extensionInfo;
    this.initialize();
  }

  public async initialize() {
    if (this._ready) {
      return;
    }
    if (!this._initializePromise) {
      this._initializePromise = this._initialize();
    }
    await this._initializePromise;
    this._initializePromise = null;
  }

  private async _initialize() {
    if (!this._currentExtension) {
      await this.loadCurrentExtension();
    }
    if (this._preloadSessions) {
      await this.preloadSessions();
    }
    if (this._preloadDevices) {
      await this.loadDevices();
    }
    this._ready = true;
    this.emit('initialized');
  }

  public onNotificationEvent(message: SessionMessage) {
    if (message.event.indexOf('/telephony/sessions') === -1) {
      return;
    }
    const { eventTime, telephonySessionId, ...newData } = message.body;
    if (!telephonySessionId) {
      return;
    }
    const validatedSequence = this.checkSequence(message.body);
    if (!validatedSequence) {
      return;
    }
    const existedSession = this._sessionsMap.get(telephonySessionId);
    newData.id = telephonySessionId;
    newData.extensionId = this.extensionId;
    newData.accountId = this.accountId;
    newData.parties = newData.parties.map(p => formatParty(p));
    if (!existedSession) {
      const disconnectedParties = newData.parties.filter(p => p.status.code === 'Disconnected');
      if (disconnectedParties.length === newData.parties.length) {
        return;
      }
      // use first event's eventTime as session creationTime
      newData.creationTime = eventTime;
      // if new session is the inbound leg of ringout call then abandon it
      const checkResult = ringOutInboundLegCheck(newData, this.sessions);
      if (checkResult.isRingOutInboundLeg) {
        return;
      }
      if(!checkResult.isRingOutInboundLeg && checkResult.legSessionId) {
        // if find an inbound leg then remove it from sessions
        this._sessionsMap.delete(checkResult.legSessionId)
      }
      const newSession = new Session(newData, this._sdk, this._accountLevel);
      newSession.on('status', () => {
        this.onSessionStatusUpdated(newSession);
      });
      this._sessionsMap.set(telephonySessionId, newSession);
      if (newSession.party) {
        this.emit('new', newSession);
      }
      return;
    }
    const party = existedSession.party;
    existedSession.onUpdated(newData);
    if (!party && existedSession.party) {
      this.emit('new', existedSession);
    }
  }

  private checkSequence({ sequence, telephonySessionId, parties }) {
    let result = true;
    const partyId = parties[0] && parties[0].id;
    const eventSequenceData = this._eventSequenceMap[partyId];
    if (eventSequenceData && eventSequenceData.sequence > sequence) {
      result = false;
    } else {
      this._eventSequenceMap[partyId] = {
        sequence,
        telephonySessionId,
        updatedAt: Date.now(),
      };
    }
    this.cleanExpiredSequenceData();
    return result;
  }

  private cleanExpiredSequenceData() {
    Object.keys(this._eventSequenceMap).forEach((partyId) => {
      const eventSequenceData = this._eventSequenceMap[partyId];
      const existedSession = this._sessionsMap.get(eventSequenceData.telephonySessionId);
      if (!existedSession && eventSequenceData.updatedAt + 60000 < Date.now()) {
        delete this._eventSequenceMap[partyId];
      }
    });
  }

  get sessions(): Session[] {
    return Array.from(this._sessionsMap.values());
  }

  get sessionsMap() {
    return this._sessionsMap;
  }

  private async loadCurrentExtension() {
    try {
      const response =
        await this._sdk.platform().get('/restapi/v1.0/account/~/extension/~', undefined, this.requestOptions);
      this._currentExtension = await response.json();
    } catch (e) {
      console.error('Fetch extension info error', e);
    }
  }

  private async preloadSessions() {
    const activeCalls = await this.loadActiveCalls();
    await this.loadSessions(activeCalls);
  }

  private async loadActiveCalls() {
    let presenceUrl = '/restapi/v1.0/account/~/extension/~/presence?detailedTelephonyState=true&sipData=true';
    if (this._accountLevel) {
      presenceUrl = '/restapi/v1.0/account/~/presence?detailedTelephonyState=true&sipData=true';
    }
    try {
      const response = await this._sdk.platform().get(presenceUrl, undefined, this.requestOptions);
      const data = await response.json();
      if (this._accountLevel) {
        const presences = data.records;
        let activeCalls = [];
        presences.forEach((presence) => {
          if (presence.activeCalls) {
            activeCalls = activeCalls.concat(presence.activeCalls);
          }
        });
        return activeCalls;
      }

      return data.activeCalls || [];
    } catch (e) {
      console.error('Fetch presence error', e);
      return [];
    }
  }

  public async loadSessions(activeCalls) {
    if (activeCalls.length === 0) {
      return;
    }
    try {
      await Promise.all(activeCalls.map(async (activeCall) => {
        const response =
          await this._sdk.platform().get(
            `/restapi/v1.0/account/~/telephony/sessions/${activeCall.telephonySessionId}`,
            undefined,
            this.requestOptions,
          );
        const data = await response.json();
        data.extensionId = this.extensionId;
        data.accountId = this.accountId;
        data.parties = data.parties.map(p => formatParty(p));
        // since call session status API not provide the `sessionId`, so pick from presence here.
        data.sessionId = activeCall.sessionId;
        const session = new Session(data, this._sdk, this._accountLevel);
        this._sessionsMap.set(
          activeCall.telephonySessionId,
          session,
        );
        session.on('status', () => {
          this.onSessionStatusUpdated(session);
        });
      }));
    } catch (e) {
      console.error('load sessions error', e);
    }
  }

  public restoreSessions(sessionDatas: SessionData[]) {
    const oldSessionMap = this._sessionsMap;
    this._sessionsMap = new Map();
    sessionDatas.forEach((sessionData) => {
      if (oldSessionMap.get(sessionData.id)) {
        const oldSession = oldSessionMap.get(sessionData.id);
        oldSession.restore(sessionData);
        this._sessionsMap.set(sessionData.id, oldSession);
        return;
      }
      this._sessionsMap.set(
        sessionData.id,
        new Session(sessionData, this._sdk, this._accountLevel)
      );
    });
  }

  private async loadDevices() {
    try {
      const response =
        await this._sdk.platform().get(
          '/restapi/v1.0/account/~/extension/~/device',
          undefined,
          this.requestOptions,
        );
      const data = await response.json();
      this._devices = data.records || [];
    } catch (e) {
      console.error('Fetch presence error', e);
    }
  }

  private onSessionStatusUpdated(session: Session) {
    const party = session.party;
    if (
      party &&
      party.status.code === PartyStatusCode.disconnected &&
      party.status.reason !== 'Pickup' && // don't end when call switched
      party.status.reason !== 'CallSwitch' // don't end when call switched
    ) {
      this._sessionsMap.delete(session.id);
    }
  }

  public async refreshDevices() {
    await this.loadDevices();
  }

  public async createCall(deviceId: string, to: CallOutToParams) {
    const response = await this._sdk.platform().post('/restapi/v1.0/account/~/telephony/call-out', {
      from: { deviceId },
      to,
    }, undefined, this.requestOptions);
    const sessionData = (await response.json()).session;
    sessionData.extensionId = this.extensionId;
    sessionData.accountId = this.accountId;
    sessionData.parties = sessionData.parties.map(p => formatParty(p));
    const session = new Session(sessionData, this._sdk, this._accountLevel);
    this._sessionsMap.set(
      sessionData.id,
      session,
    );
    session.on('status', () => {
      this.onSessionStatusUpdated(session);
    });
    return session;
  }

  // Fucntion to create conference session
  // The session's parties are empty
  // Join as HOST with voice by using webphone sdk to call session.voiceCallToken
  // Then bring in other telephony session into this conference
  public async createConference() {
    const response =
      await this._sdk.platform().post(
        '/restapi/v1.0/account/~/telephony/conference',
        {},
        undefined,
        this.requestOptions
      );
    const sessionData = (await response.json()).session;
    sessionData.extensionId = this.extensionId;
    sessionData.accountId = this.accountId;
    sessionData.parties = (sessionData.parties || []).map(p => formatParty(p));
    const session = new Session(sessionData, this._sdk, this._accountLevel);
    this._sessionsMap.set(
      sessionData.id,
      session,
    );
    session.on('status', () => {
      this.onSessionStatusUpdated(session);
    });
    return session;
  }

  get accountId() {
    return this._currentExtension && String(this._currentExtension.account.id);
  }

  get extensionId() {
    return this._currentExtension && String(this._currentExtension.id);
  }

  get devices() {
    return this._devices;
  }

  get ready() {
    return this._ready;
  }

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

  get eventSequenceMap() {
    return this._eventSequenceMap;
  }
}
