// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import {
  createTrouterService,
  ITrouterServiceBase,
  ITrouterServiceConfig,
  TrouterState,
  StateChangedListener,
  UserActivityState,
} from "@skype/tstrouter";
import { toMessageHandler, toLogProvider, toTelemetrySender } from "./TrouterUtils";
import { defaultTelemetrySettings, createSettings } from "./TrouterSettings";
import {
  ChatEventId,
  BaseChatEvent,
  BaseChatMessageEvent,
  ChatMessageReceivedEvent,
  ChatMessageEditedEvent,
  ChatMessageDeletedEvent,
  ReadReceiptReceivedEvent,
  TypingIndicatorReceivedEvent,
  BaseChatThreadEvent,
  ChatParticipant,
  ChatAttachment,
  ChatAttachmentType,
  ChatThreadProperties,
  ChatThreadCreatedEvent,
  ChatThreadDeletedEvent,
  ChatThreadPropertiesUpdatedEvent,
  ParticipantsAddedEvent,
  ParticipantsRemovedEvent,
  ChatRetentionPolicy,
  NoneRetentionPolicy,
  ThreadCreationDateRetentionPolicy,
} from "./events/chat";
import {
  CommunicationIdentifier,
  CommunicationUserIdentifier,
  PhoneNumberIdentifier,
  MicrosoftTeamsUserIdentifier,
  TeamsExtensionUserIdentifier,
  UnknownIdentifier,
  CommunicationIdentifierKind,
  CommunicationUserKind,
  PhoneNumberKind,
  MicrosoftTeamsUserKind,
  TeamsExtensionUserKind,
  MicrosoftTeamsAppKind,
  MicrosoftTeamsAppIdentifier,
  UnknownIdentifierKind,
} from "./events/identifierModels";
import { MAX_NUMBER_OF_TOKEN_FETCH_RETRIES } from "./constants";
import { AzureLogger } from "@azure/logger";
import { AbortSignalLike } from "@azure/abort-controller";
import { AccessToken } from "@azure/core-auth";
import { AdditionalPolicyConfig } from "@azure/core-client";
import { UserAgentPolicyOptions } from "@azure/core-rest-pipeline";

export enum ConnectionState {
  Unknown = 0,
  Connected = 2,
  Disconnected = 3,
  Switching = 9,
}

export interface SignalingClientOptions {
  registrationTimeInMs?: number;
  resourceEndpoint?: string;
  gatewayApiVersion?: string;
  additionalPolicies?: AdditionalPolicyConfig[];
  userAgentOptions?: UserAgentPolicyOptions;
}

export {
  ChatEventId,
  BaseChatEvent,
  BaseChatMessageEvent,
  ChatAttachment,
  ChatAttachmentType,
  ChatMessageReceivedEvent,
  ChatMessageEditedEvent,
  ChatMessageDeletedEvent,
  ReadReceiptReceivedEvent,
  TypingIndicatorReceivedEvent,
  BaseChatThreadEvent,
  ChatParticipant,
  ChatThreadProperties,
  ChatThreadCreatedEvent,
  ChatThreadDeletedEvent,
  ChatThreadPropertiesUpdatedEvent,
  ParticipantsAddedEvent,
  ParticipantsRemovedEvent,
  CommunicationIdentifier,
  CommunicationUserIdentifier,
  PhoneNumberIdentifier,
  MicrosoftTeamsUserIdentifier,
  TeamsExtensionUserIdentifier,
  UnknownIdentifier,
  CommunicationIdentifierKind,
  CommunicationUserKind,
  PhoneNumberKind,
  MicrosoftTeamsUserKind,
  TeamsExtensionUserKind,
  MicrosoftTeamsAppKind,
  MicrosoftTeamsAppIdentifier,
  UnknownIdentifierKind,
  ChatRetentionPolicy,
  NoneRetentionPolicy,
  ThreadCreationDateRetentionPolicy,
};

/**
 * Options for `CommunicationTokenCredential`'s `getToken` function.
 */
export interface CommunicationGetTokenOptions {
  /**
   * An implementation of `AbortSignalLike` to cancel the operation.
   */
  abortSignal?: AbortSignalLike;
}

/**
 * The Azure Communication Services token credential.
 */
export interface CommunicationTokenCredential {
  /**
   * Gets an `AccessToken` for the user. Throws if already disposed.
   * @param options - Additional options.
   */
  getToken(options?: CommunicationGetTokenOptions): Promise<AccessToken>;
}

export interface SignalingClient {
  /**
   * Start the realtime connection.
   */
  start(): void;
  /**
   * Stop the realtime connection and unsubscribe all event handlers.
   */
  stop(isTokenExpired?: boolean): void;
  /**
   * Listen to connectionChanged events.
   */
  on(event: "connectionChanged", listener: (state: ConnectionState) => void): void;
  /**
   * Listen to chatMessageReceived events.
   */
  on(event: "chatMessageReceived", listener: (payload: ChatMessageReceivedEvent) => void): void;
  /**
   * Listen to typingIndicatorReceived events.
   */
  on(
    event: "typingIndicatorReceived",
    listener: (payload: TypingIndicatorReceivedEvent) => void
  ): void;
  /**
   * Listen to readReceiptReceived events.
   */
  on(event: "readReceiptReceived", listener: (payload: ReadReceiptReceivedEvent) => void): void;
  /**
   * Listen to chatMessageEdited events.
   */
  on(event: "chatMessageEdited", listener: (payload: ChatMessageEditedEvent) => void): void;
  /**
   * Listen to chatMessageDeleted events.
   */
  on(event: "chatMessageDeleted", listener: (payload: ChatMessageDeletedEvent) => void): void;
  /**
   * Listen to chatThreadCreated events.
   */
  on(event: "chatThreadCreated", listener: (payload: ChatThreadCreatedEvent) => void): void;
  /**
   * Listen to chatThreadPropertiesUpdated events.
   */
  on(
    event: "chatThreadPropertiesUpdated",
    listener: (payload: ChatThreadPropertiesUpdatedEvent) => void
  ): void;
  /**
   * Listen to chatThreadDeleted events.
   */
  on(event: "chatThreadDeleted", listener: (payload: ChatThreadDeletedEvent) => void): void;
  /**
   * Listen to participantsAdded events.
   */
  on(event: "participantsAdded", listener: (payload: ParticipantsAddedEvent) => void): void;
  /**
   * Listen to participantsRemoved events.
   */
  on(event: "participantsRemoved", listener: (payload: ParticipantsRemovedEvent) => void): void;
}

export class CommunicationSignalingClient implements SignalingClient {
  private readonly trouter: ITrouterServiceBase;
  private config: ITrouterServiceConfig;
  private stateChangedListener: StateChangedListener = null;
  private tokenFetchRetries: number = 0;
  private resourceEndpoint: string;
  private gatewayApiVersion: string;

  constructor(
    private readonly credential: CommunicationTokenCredential,
    private readonly logger: AzureLogger,
    private readonly options?: SignalingClientOptions
  ) {
    this.trouter = createTrouterService(toLogProvider(logger));
  }

  public async start(): Promise<void> {
    this.resourceEndpoint = this.options?.resourceEndpoint;
    if (this.resourceEndpoint === undefined) {
      throw new Error("'endpoint' cannot be null");
    }

    this.gatewayApiVersion = this.options?.gatewayApiVersion || "2024-03-07";
    if (this.config === undefined) {
      this.config = {
        trouterSettings: await createSettings(this.credential, this.options),
        skypeTokenProvider: async (forceRefresh: boolean) => {
          if (forceRefresh) {
            this.tokenFetchRetries += 1;
            if (this.tokenFetchRetries > MAX_NUMBER_OF_TOKEN_FETCH_RETRIES) {
              await this.stop(true);
              throw new Error(
                `Access token is expired and failed to fetch a valid one after ${MAX_NUMBER_OF_TOKEN_FETCH_RETRIES} retries`
              );
            }
          } else {
            this.tokenFetchRetries = 0;
          }

          return Promise.resolve((await this.credential.getToken()).token);
        },
        telemetryConfig: {
          eventLogger: toTelemetrySender(this.logger),
          settings: defaultTelemetrySettings,
        },
      };
    }

    this.trouter.start(this.config);
    this.trouter.setUserActivityState(UserActivityState.Active);
  }

  public async stop(isTokenExpired?: boolean): Promise<void> {
    this.trouter.offStateChanged(this.stateChangedListener);
    this.trouter.clearMessageHandlers();
    this.trouter.stop(isTokenExpired ?? this.tokenFetchRetries > MAX_NUMBER_OF_TOKEN_FETCH_RETRIES);
  }

  public on(event: "connectionChanged", listener: (state: ConnectionState) => void): void;
  public on(
    event: "chatMessageReceived",
    listener: (payload: ChatMessageReceivedEvent) => void
  ): void;
  public on(
    event: "typingIndicatorReceived",
    listener: (payload: TypingIndicatorReceivedEvent) => void
  ): void;
  public on(
    event: "readReceiptReceived",
    listener: (payload: ReadReceiptReceivedEvent) => void
  ): void;
  public on(event: "chatMessageEdited", listener: (payload: ChatMessageEditedEvent) => void): void;
  public on(
    event: "chatMessageDeleted",
    listener: (payload: ChatMessageDeletedEvent) => void
  ): void;
  public on(event: "chatThreadCreated", listener: (payload: ChatThreadCreatedEvent) => void): void;
  public on(
    event: "chatThreadPropertiesUpdated",
    listener: (payload: ChatThreadPropertiesUpdatedEvent) => void
  ): void;
  public on(event: "chatThreadDeleted", listener: (payload: ChatThreadDeletedEvent) => void): void;
  public on(event: "participantsAdded", listener: (payload: ParticipantsAddedEvent) => void): void;
  public on(
    event: "participantsRemoved",
    listener: (payload: ParticipantsRemovedEvent) => void
  ): void;
  public on(
    event: ChatEventId | "connectionChanged",
    listener: (genericPayload: any) => void
  ): void {
    if (event === "connectionChanged") {
      this.trouter.offStateChanged(this.stateChangedListener);
      this.stateChangedListener = (state: TrouterState, _url: string) => listener(state);
      this.trouter.onStateChanged(this.stateChangedListener);
      return;
    }
    this.trouter.registerMessageHandler(
      toMessageHandler(event, listener, this.resourceEndpoint, this.gatewayApiVersion)
    );
  }
}
