import {
  AppProperties,
  CoreCatalogType,
  IdentifyProperties,
  MediaCatalog,
  MediaProperties,
  MediaType,
  ModuleSelectionProperties,
  NavigationTypes,
  PageProperties,
  RateProperties,
  SearchProperties,
  SiteCatalog,
  TrackProperties,
  UserCatalog,
} from "./typings";

import { injectEvent } from "../../core/injector";
import CfExceptionHandler from "../../core/CfExceptionHandler";
import { NudgeResponseProperties } from "../../core/repositories/nudges/typings";
import { toSnakeCase } from "../../core/extFunctions";
import { GlobalSDKEventTypes } from "../../core/typings";
import {
  validateAndSaveMediaCatalog,
  validateAndSaveSiteCatalog,
  validateAndSaveUserCatalog,
} from "./CoreCatalogValidator";
import { logSearchEvent } from "./legacyLoggerFunctions";

/**
 * @internal
 *
 * @param userId
 * @param properties
 * @param sendNow
 */
const logIdentifyEvent = (
  properties: IdentifyProperties,
  sendNow = false,
  eventTime?: string
) => {
  injectEvent(
    NavigationTypes.Identify,
    properties.action,
    properties,
    sendNow,
    "",
    eventTime
  );
};

/**
 * @internal
 *
 * @param name
 * @param property
 * @param ctx
 * @param sendNow
 */

const logIngestEvent = (
  name: NavigationTypes | string,
  ctx?:
    | AppProperties
    | PageProperties
    | MediaProperties
    | SearchProperties
    | RateProperties
    | ModuleSelectionProperties
    | TrackProperties
    | Record<string, string | number | boolean>,
  sendNow = false,
  eventTime?: string
) => {
  let checkProps = true;
  let property: string | undefined;

  switch (name) {
    case NavigationTypes.App:
      checkProps = validateAppEvent(ctx as AppProperties);
      if (checkProps) {
        property = (ctx as AppProperties).action;
      }
      break;
    case NavigationTypes.Page:
      checkProps = validatePageEvent(ctx as PageProperties);
      if (checkProps) {
        property = (ctx as PageProperties).title;
      }
      break;
    case NavigationTypes.Media:
      checkProps = validateMediaEvent(ctx as MediaProperties);
      if (checkProps) {
        property = (ctx as MediaProperties).type;
      }
      break;
    case NavigationTypes.Search: {
      const searchContext = ctx as SearchProperties;
      checkProps = validateSearchEvent(searchContext);
      if (checkProps) {
        property = searchContext.query;
        if (Array.isArray(searchContext.results_list)) {
          (ctx as SearchProperties).results_list = [
            ...new Set(searchContext.results_list),
          ];
        } else {
          (ctx as SearchProperties).results_list = [];
        }
      }
      break;
    }
    case NavigationTypes.Rate:
      checkProps = validateRateEvent(ctx as RateProperties);
      if (checkProps) {
        property = (ctx as RateProperties).type;
      }
      break;
    case NavigationTypes.Track:
      checkProps = validateTrackEvent(ctx as TrackProperties);
      if (checkProps) {
        name = toSnakeCase((ctx as TrackProperties).name);
        property = (ctx as TrackProperties).property;
        ctx = (ctx as TrackProperties).meta;
      }
      break;
    case NavigationTypes.ModuleSelection:
      checkProps = validateModuleSelectionEvent(
        ctx as ModuleSelectionProperties
      );
      if (checkProps) {
        property = (ctx as ModuleSelectionProperties).type;
      }
      break;
  }
  if (!checkProps) {
    return; // Exit if the properties validation failed
  }
  if (
    name === NavigationTypes.Media &&
    (ctx as MediaProperties).type === MediaType.Image
  ) {
    (ctx as MediaProperties).seek_time = 0;
  }
  injectEvent(name, property, ctx, sendNow, "", eventTime);
};

const logCatalogEvent = (
  subjectId: string,
  coreCatalogType: CoreCatalogType,
  catalogProps: UserCatalog | MediaCatalog | SiteCatalog
) => {
  switch (coreCatalogType) {
    case CoreCatalogType.User:
      validateAndSaveUserCatalog(subjectId, catalogProps as UserCatalog);
      break;
    case CoreCatalogType.Site:
      validateAndSaveSiteCatalog(subjectId, catalogProps as SiteCatalog);
      break;
    case CoreCatalogType.Media:
      validateAndSaveMediaCatalog(subjectId, catalogProps as MediaCatalog);
      break;
    case CoreCatalogType.Other:
      break;
  }
};

const validateModuleSelectionEvent = (
  properties: ModuleSelectionProperties
): boolean => {
  if (!properties.type) {
    new CfExceptionHandler().throwAndLogError(
      NavigationTypes.ModuleSelection,
      "module type is required"
    );
    return false;
  }
  return true;
};

/**
 * Whenever the user interacts with the App, log that action with this method
 */
const validateAppEvent = (properties: AppProperties): boolean => {
  if (properties.action == undefined) {
    new CfExceptionHandler().throwAndLogError(
      NavigationTypes.App,
      "app action is required"
    );
    return false;
  }
  return true;
};

/**
 * SDK tracks automatically page changes by inspecting the URL. However,
 * depending on the app implementation, it may happen that the URL does not
 * change when a new view is presented to the user. For these cases, use this method
 */
const validatePageEvent = (properties: PageProperties): boolean => {
  return properties.duration !== undefined && properties.duration >= 0;
};

/**
 * Whenever the user interacts with a media element (Video, Audio, Image), log that
 * action with this method
 */
const validateMediaEvent = (properties: MediaProperties): boolean => {
  properties.seek_time =
    properties.type === MediaType.Image ? 0 : properties.seek_time;

  if (properties.id == "") {
    new CfExceptionHandler().throwAndLogError(
      NavigationTypes.Media,
      "empty media id"
    );
    return false;
  } else if (properties.type == undefined) {
    new CfExceptionHandler().throwAndLogError(
      NavigationTypes.Media,
      "media type is required"
    );
    return false;
  } else if (properties.action == undefined) {
    new CfExceptionHandler().throwAndLogError(
      NavigationTypes.Media,
      "media action is required"
    );
    return false;
  } else if (properties.seek_time && properties.seek_time < 0) {
    new CfExceptionHandler().throwAndLogError(
      NavigationTypes.Media,
      "negative media seek time"
    );
    return false;
  }
  return true;
};

/**
 * This function logs into the platform a new search performed by the user,
 * including both, the parameters and the resulting ids
 */
const validateSearchEvent = (properties: SearchProperties): boolean => {
  if (properties.page < 0) {
    new CfExceptionHandler().throwAndLogError(
      NavigationTypes.Search,
      "negative search page"
    );
    return false;
  } else if (properties.results_list.length !== 0) {
    properties.results_list.every((item) => {
      if (item == "") {
        new CfExceptionHandler().throwAndLogError(
          NavigationTypes.Search,
          "empty search result item id"
        );
        return false;
      }
    });
  }
  return true;
};

/**
 * This function logs rate value provided by the user
 */
const validateRateEvent = (properties: RateProperties): boolean => {
  const RATE_MIN_VALUE = 0;
  const RATE_MAX_VALUE = 5;

  if (properties.rate_value < 0) {
    new CfExceptionHandler().throwAndLogError(
      NavigationTypes.Rate,
      "negative rate value"
    );
    return false;
  } else if (
    properties.rate_value < RATE_MIN_VALUE ||
    properties.rate_value > RATE_MAX_VALUE
  ) {
    new CfExceptionHandler().throwAndLogError(
      NavigationTypes.Rate,
      "invalid rate range 0-5"
    );
    return false;
  } else if (properties.type == undefined) {
    new CfExceptionHandler().throwAndLogError(
      NavigationTypes.Rate,
      "rate type is required"
    );
    return false;
  } else if (
    properties.subject_id == undefined ||
    properties.subject_id == ""
  ) {
    new CfExceptionHandler().throwAndLogError(
      NavigationTypes.Rate,
      "rate subject id is required"
    );
    return false;
  }

  return true;
};

const validateTrackEvent = (properties: TrackProperties): boolean => {
  const snakeCaseName = toSnakeCase(properties.name);
  // 1. Empty check
  if (!properties.name || properties.name.trim() === "") {
    new CfExceptionHandler().throwAndLogError(
      NavigationTypes.Track,
      "name for track event is required"
    );
    return false;
  }

  // 2. Collision check with GlobalSDKEventTypes
  const reservedNames = new Set<string>(Object.values(GlobalSDKEventTypes));
  if (reservedNames.has(snakeCaseName)) {
    new CfExceptionHandler().throwAndLogError(
      NavigationTypes.Track,
      `The event name "${properties.name}" is reserved and cannot be used.`
    );
    return false;
  }
  return true;
};

/**
 * @internal
 *
 * @param properties
 * @param sendNow
 */
const logPushNotificationEvent = (
  properties: NudgeResponseProperties,
  sendNow = true,
  eventTime?: string
): void => {
  injectEvent(
    NavigationTypes.ActionResponse,
    properties.response,
    properties,
    sendNow,
    "",
    eventTime
  );
};

export default {
  logIdentifyEvent,
  logIngestEvent,
  logCatalogEvent,
  logPushNotificationEvent,

  // Legacy functions
  logSearchEvent,
};
