import { NetworkProxy } from "../drivers/network/typings";
import { debug, Debug } from "../logging";

import { v4 as uuidv4 } from "uuid";
import isBrowser from "is-in-browser";

import {
  AppInfoObject,
  DeviceInfoObject,
  Event,
  EventMainObject,
  ExceptionObject,
  LogIngestorOptions,
  MainExceptionBody,
  QueueItem,
} from "./typings";
import {
  getAppDetails,
  getDeviceDetails,
  getNetworkInformation,
  getOperatingSystem,
} from "../drivers/CfSystem";
import { version } from "../../package.json";
import CfExceptionHandler from "./CfExceptionHandler";

export interface ISenderRepository {
  add(userId: string, deviceId: string, event: Event, forceSend: boolean): void;
  flush();
}

export default class CfSender implements ISenderRepository {
  private flushing: boolean = false;
  private flushIntervalId: number = 0;
  private cfNetwork;
  private debug;
  private options;
  private endpointURL;
  private exceptionsEndPoint = "/ingest/sdk/crash";

  private userId: string = "";
  private deviceId: string = "";
  private usingAsInternalWrapper: boolean = false;

  private queue: QueueItem[];

  public constructor(cfNetwork: NetworkProxy, options: LogIngestorOptions) {
    this.queue = [];
    this.cfNetwork = cfNetwork;
    this.options = options;
    this.debug = debug(this.options.debug);

    // to avoid short timeout to save cpu usage
    if (this.options.flushInterval < 1000) {
      this.options.flushInterval = 1000;
    }

    if (
      this.options.cacheEventsInLocalstorage &&
      typeof Storage !== "undefined"
    ) {
      try {
        this.queue = JSON.parse(
          localStorage.getItem(this.options.cacheEventsKey) || "[]"
        ) as QueueItem[];
      } catch (e) {
        this.debug(
          Debug.Error,
          `Error getting queue items from cache ${e.message || ""}`
        );
      }
    }

    this.endpointURL = `${this.options.baseUrl}${this.options.ingestPath}`;

    this.updateQueueCache();

    this.flushIntervalId = +setInterval(() => {
      this.flush();
    }, this.options.flushInterval);
  }

  private removeQueueItemsExceedMaxRetries() {
    this.queue = this.queue.filter(
      (item: QueueItem) => item.retries <= this.options.flushMaxRetries
    );
    this.updateQueueCache();
  }

  private removeFromQueue(uuids: string[]) {
    this.queue = this.queue.filter(
      (item: QueueItem) => uuids.indexOf(item.uuid) === -1
    );
    this.updateQueueCache();
  }

  private removeIndexFromQueue(indexToRemove: number) {
    this.queue.splice(indexToRemove, 1);
    this.updateQueueCache();
  }

  private updateQueueCache() {
    if (
      this.options.cacheEventsInLocalstorage &&
      typeof Storage !== "undefined"
    ) {
      localStorage.setItem(
        this.options.cacheEventsKey,
        JSON.stringify(this.queue)
      );
    }
  }

  private addRetries(uuids: string[]) {
    const time = +new Date();
    this.queue
      .filter((item: QueueItem) => uuids.indexOf(item.uuid) !== -1)
      .forEach((item: QueueItem) => {
        item.retries = +item.retries + 1;
        item.lastRetry = time;
      });
    this.updateQueueCache();
  }

  public flush() {

    if(this.options.pauseSDK){
      return
    }

    const flushItems = [...this.queue];

    if (flushItems.length === 0) {
      this.debug(Debug.Log, "Skip sending empty list of logs ");
      return;
    }

    const uuids: string[] = flushItems.map((item: QueueItem) => item.uuid);
    const data = flushItems.map((item: QueueItem) => item.event);

    this.flushing = true;

    this.send(data, uuids);
  }

  public add(
    userId: string,
    deviceId: string,
    event: Event,
    forceSend: boolean = false,
    isUsingAsInternalWrapper: boolean = false
  ) {

    if(this.options.pauseSDK){
      return
    }

    const item: QueueItem = {
      uuid: uuidv4(),
      event,
      time: +new Date(),
      retries: 0,
    };
    this.userId = userId;
    this.deviceId = deviceId;
    this.usingAsInternalWrapper = isUsingAsInternalWrapper;

    const isDuplicate = this.queue.some(queueItem => queueItem.event === item.event);
    if (!isDuplicate) {
      this.queue.push(item);
      this.updateQueueCache();
      // send immediately. In case of error, it was already enqueued
      // so will be tried again in interval tick
      if (forceSend) {
        this.send([item.event], [item.uuid]);
      }
    }
  }

  private send(dataList: Event[], uuids: string[]) {

    if(this.options.pauseSDK){
      return
    }

    const { downlink, uplink } = getNetworkInformation();

    // Remove duplicates
    const hashEvent = (event: Event) => JSON.stringify(event); // Convert event to string for comparison
    const getUniqueEvents = (data: Event[]): Event[] => {
      const seen = new Set<string>(); // Stores unique event hashes
      return data.filter(event => {
        const hash = hashEvent(event); // Create a unique identifier
        if (seen.has(hash)) return false; // If duplicate, skip it
        seen.add(hash); // Otherwise, add it to seen
        return true;
      });
    };
    const filteredData = getUniqueEvents(dataList);

    const deviceObject: DeviceInfoObject = {
      id: this.deviceId,
      ...getDeviceDetails(),
    };
    const appObject: AppInfoObject = {
      id: isBrowser
        ? document?.location.host || "NO_URL_FOUND"
        : "NO_URL_FOUND",
      min_sdk_version: 0,
      target_sdk_version: 0,
      ...getAppDetails(),
    };

    const eventMainObject: EventMainObject = {
      s_id: this.generateSessionID(this.userId),
      u_id: this.userId,
      app_info: appObject,
      d_info: deviceObject,
      sdk: `js/${version}`,
      up: this.checkDefaultNetworkValue(uplink),
      dn: this.checkDefaultNetworkValue(downlink),
      data: filteredData,
    };

    return this.cfNetwork
      .send(this.endpointURL, eventMainObject)
      .then(() => {
        this.debug(Debug.Info, `Flushed ${filteredData.length} events`);
        this.removeFromQueue(uuids);
      })
      .catch((error) => {
        if (error.response.data.data) {
          // Accessing specific properties from the error response
          this.removeIndexFromQueue(Number(error.response.data.data))
        }

        this.addRetries(uuids);
        this.debug(Debug.Error, error.message || "");
      })
      .finally(() => {
        this.flushing = false;
        this.removeQueueItemsExceedMaxRetries();
      });
  }

  public sendException(exceptionList: ExceptionObject[]) {

    if(this.options.pauseSDK){
      return
    }

    const deviceObject: DeviceInfoObject = {
      id: this.deviceId,
      ...getDeviceDetails(),
    };

    const appObject: AppInfoObject = {
      id: isBrowser
        ? document?.location.host || "NO_URL_FOUND"
        : "NO_URL_FOUND",
      min_sdk_version: 0,
      target_sdk_version: 0,
      ...getAppDetails(),
    };

    const mainExceptionBody: MainExceptionBody = {
      u_id: this.userId,
      d_info: deviceObject,
      app_info: appObject,
      sdk_version: `js/${version}`,
      data: exceptionList,
    };

    return this.cfNetwork
      .send(
        `${this.options.baseUrl}${this.exceptionsEndPoint}`,
        mainExceptionBody
      )
      .then(() => {
        this.debug(Debug.Info, `Flushed ${exceptionList.length} exceptions`);
      })
      .catch((e) => {
        this.debug(Debug.Error, e.message || "");
      });
  }

  private generateSessionID(userId: string) {
    // return `${userId}_${startTimeInMillis}_${endTimeInMillis}`;
    return `${userId}_${Date.now()}`;
  }

  private checkDefaultNetworkValue(defaultValue: number): number {
    if (this.usingAsInternalWrapper) {
      return this.getRandomNumberValue(100, 1024);
    }
    return defaultValue;
  }

  private getRandomNumberValue(min: number, max: number): number {
    return Math.floor(Math.random() * (max - min) + min);
  }
}
