/// <reference types="@applicaster/applicaster-types" />
import * as R from "ramda";
import { Platform } from "react-native";
import { applyTransform } from "../transform";
import { ManifestField } from "../types";
import { isNilOrEmpty } from "../reactUtils/helpers";

export type PluginConfiguration = Record<string, string>;

type TBooleanLike = boolean | string | number;
type TAnyValue = unknown;
type GetValue = {
  (key: string): string;
  (key: string): number;
  (key: string): null;
};

declare type StringTransformations =
  | "default"
  | "uppercase"
  | "lowercase"
  | "toUpper"
  | "capitalise";

declare type Configuration = {
  target_screen_switch: boolean;
  target: string;
  display_mode: string;
  display_mode_tab_width: number;
  display_mode_tabs_alignment: string;
  text_label_data_key: string;
  text_label_custom_data_key: string;
  text_label_ios_font_family: string;
  text_label_default_font_color: string;
  text_label_active_font_color: string;
  text_label_selected_default_font_color: string;
  text_label_selected_active_font_color: string;
  text_label_android_font_family: string;
  text_label_font_size: number;
  text_label_line_height: number;
  text_label_ios_letter_spacing: number;
  text_label_android_letter_spacing: number;
  text_label_text_transform: StringTransformations;
  tab_bar_background_color: string;
  tab_bar_elevation: number;
  tab_bar_shadow_color: string;
  tab_bar_shadow_offset_width: number;
  tab_bar_shadow_offset_height: number;
  tab_bar_shadow_radius: number;
  tab_bar_padding_top: number;
  tab_bar_padding_right: number;
  tab_bar_padding_bottom: number;
  tab_bar_padding_left: number;
  tab_bar_gutter: number;
  tab_bar_border_bottom_width: number;
  tab_bar_border_bottom_color: string;
  tab_cell_background_color_default: string;
  tab_cell_background_color_active: string;
  tab_cell_background_color_selected_default: string;
  tab_cell_background_color_selected_active: string;
  tab_cell_padding_top: number;
  tab_cell_padding_right: number;
  tab_cell_padding_bottom: number;
  tab_cell_padding_left: number;
  tab_cell_border_radius: number;
  tab_cell_border_width: number;
  tab_cell_border_color_default: string;
  tab_cell_border_color_default_focused: string;
  tab_cell_border_color_active: string;
  tab_cell_border_color_active_focused: string;
  tab_cell_indicator_height: number;
  tab_cell_indicator_color: string;
  tab_cell_indicator_border_radius: number;
  sticky_tab_bar: boolean;
  components_container_background_color?: string;
  components_container_padding_top?: number;
  components_container_padding_bottom?: number;
  components_container_padding_left?: number;
  components_container_padding_right?: number;
  tablet_theme: boolean;
  tablet_components_container_padding_top?: number;
  tablet_components_container_padding_bottom?: number;
  tablet_components_container_padding_left?: number;
  tablet_components_container_padding_right?: number;
};

export const getBoolFromConfigValue = (value: TBooleanLike): boolean => {
  if (typeof value === "boolean") {
    return value;
  }

  return value === "true" || value === "1" || value === 1;
};

export const requiresAuthentication = (entry: ZappEntry): Nullable<boolean> => {
  const requires_authentication: TBooleanLike | undefined = R.path(
    ["extensions", "requires_authentication"],
    entry
  );

  return !R.isNil(requires_authentication)
    ? getBoolFromConfigValue(requires_authentication)
    : null;
};

/**
 * Flattens the manifest configuration fields which contains group into
 * a flat list of fields
 * @param fields to flatten
 * @returns flatten fields
 */
export function flattenFields(
  fields: ManifestField<TAnyValue>[] = []
): ManifestField<TAnyValue>[] {
  return fields.reduce((acc, field) => {
    if (field.fields) {
      return [...acc, ...flattenFields(field.fields)];
    } else {
      return [...acc, field];
    }
  }, []);
}

/**
 * Retrieves the value of the "src" in the first media_item
 * that has the matching key provided in args.
 * Fallbacks: "image_base" key, or first media_item that has any "src"
 * @param {Object} entry    Single entry from a feed
 * @param {Array} arg      Array with a single element - the key of the media item
 *                          from which the "src" should be retrieved
 * @returns {?String}       Value of "src", usually a URI
 */
export function imageSrcFromMediaItem(
  entry: ZappEntry,
  arg: string[] | unknown
) {
  const args: unknown = R.unless(Array.isArray, Array)(arg || []);
  const imageKey: string = args?.[0] || "image_base"; // always a single key in this function
  const mediaGroup = R.path<ZappMediaGroup[]>(["media_group"], entry);

  if (!mediaGroup) {
    return undefined;
  }

  const hasTypeImageOrThumbnail = R.either(
    R.propEq("type", "image"),
    R.propEq("type", "thumbnail")
  );

  const pickMediaItemProp = R.prop<ZappMediaItem>("media_item");

  const mediaItems = R.compose(
    R.flatten,
    R.map(pickMediaItemProp),
    R.filter(hasTypeImageOrThumbnail)
  )(mediaGroup);

  if (!mediaItems) {
    return undefined;
  }

  const src = R.compose(
    R.prop("src"),
    R.defaultTo(R.head(mediaItems)),
    R.when(R.isNil, () => R.find(R.propEq("key", "image_base"), mediaItems)),
    R.find(R.propEq("key", imageKey))
  )(mediaItems);

  // Special case for react native - uri cannot be an empty string (yellow warning).
  // R.isEmpty is tailored specifically for checks like these,
  // it returns false for undefined values.
  return R.isEmpty(src) ? undefined : src;
}

/**
 * map of type checks to apply to each manifest field in order to check if
 * the provided value is valid for that given manifest type
 */
const typeChecks = {
  switch: R.is(Boolean),
  checkbox: R.is(Boolean),
  number_input: R.is(Number),
  text_input: R.is(String),
};

/**
 * This function checks if a provided value is valid for the provided type
 * curried function of the form isInvalidForType(type)(value);
 * returns true if the value is invalid, and false if the value is ok.
 * @param {*} type
 * @returns {Function}
 * @param {Any} value to check
 * @returns {boolean}
 */
function isInvalidForType(type: string): (arg0: any) => boolean {
  const typeCheck = typeChecks[type] || (() => true);

  return function (value: TAnyValue): boolean {
    return !typeCheck(value);
  };
}

/**
 * Checks if a value is valid for a provided type. Checks if the the type is correct,
 * but also if the value is not empty, not null, and not NaN
 * curried function of the form isInvalidValue(type)(value). Returns true if the value
 * is invalid, and false otherwise
 * @param {String} type
 * @returns {Function}
 * @param {Any} value to check for
 * @returns {boolean}
 */
const isInvalid = (type: string, value: TAnyValue): boolean => {
  const validators = [R.isEmpty, R.isNil, Number.isNaN, isInvalidForType(type)];

  return validators.some((valid) => valid(value));
};

function isInvalidValue(type: string): (arg0: TAnyValue) => boolean {
  return (value) => isInvalid(type, value);
}

/**
 * This function will sanitize a value from a provided configuration, by trying
 * to apply the relevant transform, and resolving to the default value
 * if the resulting value is invalid (null, empty, NaN, or incorrect type)
 * Curried function of the form castOrDefault(field)(configuration)
 * @param {Object} field plugin manifest field
 * @param {String} field.type type of the manifest field
 * @param {Any} field.initial_value default value provided in the manifest for that field
 * @param {String} field.key key of the manifest field
 * @returns {Function}
 * @param {Object} configuration retrieved from the server
 */
function castOrDefault<T>({
  type,
  initial_value,
  key,
}: ManifestField<T>): (
  configuration: PluginConfiguration,
  skipDefaults: boolean
) => T {
  const transform = applyTransform(type);

  return function (
    configuration: PluginConfiguration,
    skipDefaults: boolean
  ): T {
    const value = transform(configuration?.[key]);

    if (isInvalidValue(type)(value) && !skipDefaults) {
      return transform(initial_value);
    }

    return value;
  };
}

/**
 * This function takes a section of configuration and checks if the relevant section
 * in the manifest has remapped keys. if it does, it updates to the new expected configuration
 * This helps not breaking the layout when a plugin configuration changes its keys
 * @param {Object} config key value pair of configuration for a section in a plugin
 * @param {Object} remappedKeys dictionary of keys to be remapped
 * @returns
 */
const applyRemappedKeys = (
  config: PluginConfiguration,
  remappedKeys: PluginConfiguration
) => {
  const oldKeys = R.keys(remappedKeys || {});
  const configKeys = R.keys(config);

  if (
    !R.compose(R.length, R.intersection(oldKeys))(configKeys) ||
    !R.length(oldKeys)
  ) {
    return config;
  }

  // need to update keys
  return R.reduce(
    (updatedConfig, oldKey: string) => {
      if (R.has(oldKey, config) && !R.has(remappedKeys[oldKey], config)) {
        return R.assoc(remappedKeys[oldKey], config[oldKey], updatedConfig);
      }

      return updatedConfig;
    },
    config,
    oldKeys
  );
};

/**
 * This function updates locally the configuration for a cell style with
 * the updated keys if needed
 * @param {Object} manifest plugin manifest
 * @param {Object} config plugin configuration for the given cell style
 * @returns {Object} remapped config
 */

type TRemapUpdatedKeys = (
  manifest: ManifestField<TAnyValue>,
  config: PluginConfiguration
) => PluginConfiguration;

export const remapUpdatedKeys = R.curry<TRemapUpdatedKeys>(
  (manifest, config) => {
    return R.reduce(
      (updatedConfig, configSection: string) => {
        const remappedKeys = R.pathOr<string[] | null>(
          null,
          [configSection, "updated_keys"],
          manifest
        );

        updatedConfig[configSection] = remappedKeys
          ? applyRemappedKeys(config[configSection], remappedKeys)
          : config[configSection];

        return updatedConfig;
      },
      {},
      R.keys(config)
    );
  }
);

/**
 * Flattens the manifest configuration fields which contains group into
 * a flat list of fields and applies defaults/casts
 * @param fields
 * @param configuration
 * @param skipDefaults
 */
export function flattenAndPopulateFields(
  fields: ManifestField<TAnyValue>[],
  configuration: PluginConfiguration,
  skipDefaults?: boolean
): { [key: string]: any } {
  return fields.reduce((acc, field) => {
    if (field.fields) {
      Object.assign(
        acc,
        flattenAndPopulateFields(field.fields, configuration, skipDefaults)
      );
    } else {
      acc[field.key] = castOrDefault(field)(configuration, skipDefaults);
    }

    return acc;
  }, {});
}

/**
 * This function is casting default styles to expected format
 * @param {object} fields - array of configuration fields form manifest
 * @returns array of {key, value, mapper:(function)}
 */
type TPopulateConfigurationValues = (
  fields: ManifestField<TAnyValue>[],
  configuration: PluginConfiguration,
  skipDefaults?: boolean
) => PluginConfiguration;

export const populateConfigurationValues =
  R.curry<TPopulateConfigurationValues>(
    (fields, configuration, skipDefaults = false) =>
      flattenAndPopulateFields(fields, configuration, skipDefaults)
  );

export const getAccesabilityProps = (item: ZappEntry) => ({
  accessible: item?.extensions?.accessibility,
  accessibilityLabel: item?.extensions?.accessibility?.label || item?.title,
  accessibilityHint: item?.extensions?.accessibility?.hint,
});

export const getPlayerControlsAccessibilityProps = (
  icon: string,
  value: GetValue
) => {
  return {
    accessible: true,
    accessibilityLabel: value(`accessibility_${icon}_label`),
    accessibilityHint: value(`accessibility_${icon}_hint`),
  };
};

export const getAllAccessibilityProps = R.pickBy((_, key) =>
  key.includes("accessibility")
);

const os = R.compose(R.toLower, R.prop("OS"))(Platform);

export const castOrDefaultValue = (mapper, defaultValue) =>
  R.ifElse(isNilOrEmpty, R.always(defaultValue), mapper);

export const castIfDefined = (mapper) => R.unless(R.isNil, mapper);

export const getStyleForPlatform = R.curry(
  (
    configuration: Partial<Configuration>,
    [label, key]: [string, string]
  ): any => R.prop(`${label}_${os}_${key}`)(configuration)
);

export const capitalise = R.replace(/^./, R.toUpper);

export const applyStringTransformation = (
  text: string,
  transformation: StringTransformations
) => {
  const transformationMethod = R.cond([
    [R.equals("default"), R.always(R.identity)],
    [R.equals("uppercase"), R.always(R.toUpper)],
    [R.equals("lowercase"), R.always(R.toLower)],
    [R.equals("capitalise"), R.always(capitalise)],
  ])(transformation);

  return transformationMethod(text);
};

const allBetweenParenthesesRegex = /(\()(.*?)(?=\))/g;

export const getOpacityFromHexColor = R.compose(
  parseFloat,
  R.trim,
  R.last,
  R.split(","),
  // Positive Lookbehinds are not supported in Android JS env therefor we need to remove "(" manually.
  R.replace("(", ""),
  R.head,
  R.match(allBetweenParenthesesRegex)
);
