import { PublishAnalyticPayload, SubscribeAnalyticPayload } from './stats/interfaces';
import { AdditionalAnalyticsProperties } from './AdditionalAnalyticsProperties';
import AnalyticsEvent from './AnalyticsEvent';
import { AnalyticsEventLevel } from './AnalyticsEventLevel';
import { IAnalyticsPropertiesProvider } from './IAnalyticsPropertiesProvider';
import { HMSException } from '../error/HMSException';
import { DeviceMap, SelectedDevices } from '../interfaces';
import { HMSTrackSettings } from '../media/settings/HMSTrackSettings';
import { HMSRemoteVideoTrack } from '../media/tracks/HMSRemoteVideoTrack';

export default class AnalyticsEventFactory {
  private static KEY_REQUESTED_AT = 'requested_at';
  private static KEY_RESPONDED_AT = 'responded_at';

  static connect(
    error?: Error,
    additionalProperties?: AdditionalAnalyticsProperties,
    requestedAt: Date = new Date(),
    respondedAt: Date = new Date(),
    endpoint?: string,
  ) {
    const name = this.eventNameFor('connect', error === undefined);
    const level = error ? AnalyticsEventLevel.ERROR : AnalyticsEventLevel.INFO;

    const properties = this.getPropertiesWithError(
      {
        ...additionalProperties,
        [this.KEY_REQUESTED_AT]: requestedAt?.getTime(),
        [this.KEY_RESPONDED_AT]: respondedAt?.getTime(),
        endpoint,
      },
      error,
    );

    return new AnalyticsEvent({ name, level, properties });
  }

  static disconnect(error?: Error, additionalProperties?: AdditionalAnalyticsProperties) {
    const name = 'disconnected';
    const level = error ? AnalyticsEventLevel.ERROR : AnalyticsEventLevel.INFO;
    const properties = this.getPropertiesWithError(additionalProperties, error);

    return new AnalyticsEvent({ name, level, properties });
  }

  static preview({
    error,
    ...props
  }: {
    error?: Error;
    time?: number;
    init_response_time?: number;
    ws_connect_time?: number;
    on_policy_change_time?: number;
    local_audio_track_time?: number;
    local_video_track_time?: number;
  }) {
    const name = this.eventNameFor('preview', error === undefined);
    const level = error ? AnalyticsEventLevel.ERROR : AnalyticsEventLevel.INFO;
    const properties = this.getPropertiesWithError(props, error);

    return new AnalyticsEvent({ name, level, properties });
  }

  static join({
    error,
    ...props
  }: {
    error?: Error;
    is_preview_called?: boolean;
    start?: Date;
    end?: Date;
    time?: number;
    init_response_time?: number;
    ws_connect_time?: number;
    on_policy_change_time?: number;
    local_audio_track_time?: number;
    local_video_track_time?: number;
    retries_join?: number;
  }) {
    const name = this.eventNameFor('join', error === undefined);
    const level = error ? AnalyticsEventLevel.ERROR : AnalyticsEventLevel.INFO;

    const properties = this.getPropertiesWithError({ ...props, is_preview_called: !!props.is_preview_called }, error);

    return new AnalyticsEvent({ name, level, properties });
  }

  static publish({ devices, settings, error }: { devices?: DeviceMap; settings?: HMSTrackSettings; error?: Error }) {
    const name = this.eventNameFor('publish', error === undefined);
    const level = error ? AnalyticsEventLevel.ERROR : AnalyticsEventLevel.INFO;
    const properties = this.getPropertiesWithError(
      {
        devices,
        audio: settings?.audio,
        video: settings?.video,
      },
      error,
    );
    return new AnalyticsEvent({
      name,
      level,
      properties,
    });
  }

  static hlsPlayerError(error: HMSException) {
    return new AnalyticsEvent({
      name: 'hlsPlayerError',
      level: AnalyticsEventLevel.ERROR,
      properties: this.getErrorProperties(error),
    });
  }
  static subscribeFail(error: Error) {
    const name = this.eventNameFor('subscribe', false);
    const level = AnalyticsEventLevel.ERROR;
    const properties = this.getErrorProperties(error);

    return new AnalyticsEvent({ name, level, properties });
  }

  static leave() {
    return new AnalyticsEvent({ name: 'leave', level: AnalyticsEventLevel.INFO });
  }

  static autoplayError() {
    return new AnalyticsEvent({ name: 'autoplayError', level: AnalyticsEventLevel.ERROR });
  }

  static audioPlaybackError(error: HMSException) {
    return new AnalyticsEvent({
      name: 'audioPlaybackError',
      level: AnalyticsEventLevel.ERROR,
      properties: this.getErrorProperties(error),
    });
  }

  static audioRecovered(message: string) {
    return new AnalyticsEvent({
      name: 'audioRecovered',
      level: AnalyticsEventLevel.INFO,
      properties: {
        message,
      },
    });
  }

  static permissionChange(type: 'audio' | 'video', status: PermissionState) {
    return new AnalyticsEvent({
      name: 'permissionChanged',
      level: AnalyticsEventLevel.INFO,
      properties: {
        message: `Perrmission for ${type} changed to ${status}`,
      },
    });
  }

  static deviceChange({
    isUserSelection,
    selection,
    type,
    devices,
    error,
  }: {
    isUserSelection?: boolean;
    selection: Partial<SelectedDevices>;
    type?: 'change' | 'list' | 'audioInput' | 'audioOutput' | 'video';
    devices: DeviceMap;
    error?: Error;
  }) {
    const name = this.eventNameFor(error ? 'publish' : `device.${type}`, error === undefined);
    const level = error ? AnalyticsEventLevel.ERROR : AnalyticsEventLevel.INFO;
    const properties = this.getPropertiesWithError({ selection, devices, isUserSelection }, error);
    return new AnalyticsEvent({
      name,
      level,
      properties,
    });
  }

  static performance(stats: IAnalyticsPropertiesProvider) {
    const name = 'perf.stats';
    const level = AnalyticsEventLevel.INFO;
    const properties = stats.toAnalyticsProperties();

    return new AnalyticsEvent({ name, level, properties });
  }

  static rtcStats(stats: IAnalyticsPropertiesProvider) {
    const name = 'rtc.stats';
    const level = AnalyticsEventLevel.INFO;
    const properties = stats.toAnalyticsProperties();

    return new AnalyticsEvent({ name, level, properties });
  }

  static rtcStatsFailed(error: HMSException) {
    const name = 'rtc.stats.failed';
    const level = AnalyticsEventLevel.ERROR;

    return new AnalyticsEvent({ name, level, properties: this.getErrorProperties(error) });
  }

  /**
   * TODO: remove once everything is switched to server side degradation, this
   * event can be handled on server side as well.
   */
  static degradationStats(track: HMSRemoteVideoTrack, isDegraded: boolean) {
    const name = 'video.degradation.stats';
    const level = AnalyticsEventLevel.INFO;
    let properties: any = {
      degradedAt: track.degradedAt,
      trackId: track.trackId,
    };

    if (!isDegraded && track.degradedAt instanceof Date) {
      // not degraded => restored
      const restoredAt = new Date();
      const duration = restoredAt.valueOf() - track.degradedAt.valueOf();
      properties = { ...properties, duration, restoredAt };
    }

    return new AnalyticsEvent({ name, level, properties });
  }

  static audioDetectionFail(error: Error, device?: MediaDeviceInfo): AnalyticsEvent {
    const properties = this.getPropertiesWithError({ device }, error);
    const level = AnalyticsEventLevel.ERROR;
    const name = 'audiopresence.failed';

    return new AnalyticsEvent({ name, level, properties });
  }

  static previewNetworkQuality(properties: { downLink?: string; score?: number; error?: string }) {
    return new AnalyticsEvent({
      name: 'perf.networkquality.preview',
      level: properties.error ? AnalyticsEventLevel.ERROR : AnalyticsEventLevel.INFO,
      properties,
    });
  }

  static publishStats(properties: PublishAnalyticPayload) {
    return new AnalyticsEvent({
      name: 'publisher.stats',
      level: AnalyticsEventLevel.INFO,
      properties,
    });
  }

  static subscribeStats(properties: SubscribeAnalyticPayload) {
    return new AnalyticsEvent({
      name: 'subscriber.stats',
      level: AnalyticsEventLevel.INFO,
      properties,
    });
  }

  static getKrispUsage(duration: number) {
    return new AnalyticsEvent({
      name: 'krisp.usage',
      level: AnalyticsEventLevel.INFO,
      properties: { duration },
    });
  }

  static krispStart() {
    return new AnalyticsEvent({
      name: 'krisp.start',
      level: AnalyticsEventLevel.INFO,
    });
  }

  static krispStop() {
    return new AnalyticsEvent({
      name: 'krisp.stop',
      level: AnalyticsEventLevel.INFO,
    });
  }

  static interruption({
    started,
    type,
    reason,
    deviceInfo,
  }: {
    started: boolean;
    type: string;
    reason: string;
    deviceInfo: Partial<MediaDeviceInfo>;
  }) {
    return new AnalyticsEvent({
      name: `${started ? 'interruption.start' : 'interruption.stop'}`,
      level: AnalyticsEventLevel.INFO,
      properties: {
        reason,
        type,
        ...deviceInfo,
      },
    });
  }

  private static eventNameFor(name: string, ok: boolean) {
    const suffix = ok ? 'success' : 'failed';
    return `${name}.${suffix}`;
  }

  private static getPropertiesWithError(initialProperties: any, error?: Error) {
    const errorProperties = this.getErrorProperties(error);
    initialProperties = { ...errorProperties, ...initialProperties };
    return initialProperties;
  }

  private static getErrorProperties(error?: Error): Record<string, any> {
    if (error) {
      return error instanceof HMSException
        ? error.toAnalyticsProperties()
        : {
            error_name: error.name,
            error_message: error.message,
            error_description: error.cause,
          };
    } else {
      return {};
    }
  }
}
