/// <reference types="@applicaster/applicaster-types" />
import * as R from "ramda";
import { getIdResolver, itemIsOfType, SCREEN_TYPES } from "./itemTypes";
import { layoutV2TypeMatcher } from "./layoutV2TypeMatcher";
import { HOOKS_EVENTS } from "../appUtils/HooksManager/constants";
import { HooksManager } from "../appUtils/HooksManager";
import { appStore } from "@applicaster/zapp-react-native-redux/AppStore";
import { HookModalContextT } from "@applicaster/zapp-react-native-ui-components/Contexts/ZappHookModalContext";
import { logger } from "@applicaster/zapp-react-native-utils/logger";
import {
  isGeneralPlugin,
  isLink,
  isPlayable,
  isV2River,
} from "./itemTypeMatchers";

type PathAttribute = {
  screenType: string;
  screenId: string;
};

const navigationUtilsLogger = logger?.addSubsystem("NavigationUtils");

/**
 * returns the type of navigation based on a navigation category
 * @param {String} category to look for. can be either "nav_bar" or "menu"
 * @param {Array} navigations array to look for the type
 * @returns {String} navigation type for the selected category
 */
export function getNavigationType(
  category: "nav_bar" | "menu",
  navigations: ZappRiver["navigations"]
): string {
  if (R.isEmpty(navigations)) {
    navigationUtilsLogger.warning(
      "getNavigationType: navigations data is empty"
    );
  }

  return R.compose(
    R.unless(R.isNil, R.prop("navigation_type")),
    R.defaultTo(undefined),
    R.find(R.propEq("category", category))
  )(navigations);
}

/**
 * returns the navigation plugin for the given category, and the provided plugins
 * Will return the navigation plugin flagged as default
 * @param {Object} options
 * @param {String} options.category to look for. can be either "nav_bar" or menu
 * @param {Array<Object>} navigations array to look for the type
 * @param {Array<Object>} plugins list to search for the plugin to return
 * @returns {Object} plugin to use
 */
export function resolveNavigationPlugin({
  category,
  navigations,
  plugins,
}: {
  category: "nav_bar" | "menu";
  navigations: ZappRiver["navigations"];
  plugins: QuickBrickPlugin[];
}): Nullable<QuickBrickPlugin> {
  const navigationType = getNavigationType(category, navigations);

  if (!navigationType) {
    navigationUtilsLogger.warning(
      `resolveNavigationPlugin: failed to resolve plugin with category ${category}`
    );

    return null;
  }

  const elligiblePlugins = R.filter(
    R.propEq("identifier", navigationType),
    plugins
  );

  if (R.length(elligiblePlugins) === 0) {
    return R.find(R.propEq("type", category), plugins);
  }

  if (R.length(elligiblePlugins) > 1) {
    return R.find(R.compose(R.not, R.prop("default")), elligiblePlugins);
  }

  return R.head(elligiblePlugins);
}

/**
 * returns the navigation props for the provided category, for the given screen
 * @param {Object} navigator to look for navigation props
 * @param {String} category to look for in screen navigations
 * @returns {Object} props
 * @deprecated use getNavigationPropsV2 instead
 */
export function getNavigationProps({
  navigator,
  title = "",
  category,
}: {
  navigator: QuickBrickAppNavigator;
  title: string;
  category: string;
}): {
  selected: string;
  title: string;
  home: boolean;
  styles: unknown;
  assets: ZappNavBarAssets;
  rules: ZappNavBarRules;
  nav_items: NavBarItem[];
} | null {
  const { activeRiver } = navigator;
  const { navigations, id: selected, home } = activeRiver; // use useRiver hook instead

  const navigation: ZappNavigation = R.find(
    R.propEq("category", category),
    navigations
  );

  if (!navigation) {
    return null;
  }

  const { styles, assets, nav_items, rules } = navigation;

  return {
    selected,
    title,
    home,
    styles,
    assets: assets as ZappNavBarAssets,
    nav_items: nav_items as any as NavBarItem[],
    rules: rules as any as ZappNavBarRules,
  };
}

export function getNavigationPropsV2({
  currentRiver,
  title = "",
  category,
}: {
  currentRiver: ZappRiver;
  title: string;
  category: string;
}): {
  selected: string;
  title: string;
  home: boolean;
  styles: unknown;
  assets: ZappNavBarAssets;
  rules: ZappNavBarRules;
  nav_items: NavBarItem[];
} | null {
  const { navigations, id: selected, home } = currentRiver; // use useRiver hook instead
  const navigation = R.find(R.propEq("category", category), navigations);

  if (!navigation) {
    return null;
  }

  const { styles, assets, nav_items, rules } = navigation;

  return {
    selected,
    title,
    home,
    styles,
    assets,
    nav_items,
    rules,
  };
}

/**
 * retrieves the type of item
 * @param {object} item
 * @param {'v1' | 'v2'} layoutVersion
 * @param {object} contentTypes content types map
 * @return {String} type of the item
 */
export function getItemType(
  item,
  layoutVersion?: Maybe<ZappLayoutVersions>,
  contentTypes?: any
) {
  if (layoutVersion === "v1") {
    return R.compose(R.find(itemIsOfType(item)), R.values)(SCREEN_TYPES);
  } else if (layoutVersion === "v2") {
    return layoutV2TypeMatcher(item, contentTypes);
  } else {
    throw new Error(`Layout version: ${layoutVersion} is not supported`);
  }
}

/**
 * Finds correct screen_id for item type
 * @param {object} payload ZappEntry or ZappFeed
 * @param {object} contentTypes content types map
 * @return {string} id of the item target
 */
export function getScreenIdForContentType(payload, contentTypes) {
  const itemType = payload?.type?.value;
  const typeTargetScreenId = contentTypes?.[itemType]?.screen_id;

  return typeTargetScreenId;
}

/**
 * retrieves a the Id of the item target based on an item and a type
 * @param {object} item
 * @param {string} screenType type of the screen
 * @param {object} contentTypes content types map
 * @param {object} rivers
 * @return {string} id of the item target
 */
export function getItemTargetId(
  item: ZappEntry | ZappRiver,
  screenType?: string, // todo: creat screen types enum
  contentTypes?: ZappContentTypes | null,
  rivers?: Record<string, ZappRiver>
) {
  // Pipes v2 resolution
  if (contentTypes) {
    const contentTypeScreenId = getScreenIdForContentType(item, contentTypes);

    if (contentTypeScreenId) {
      return contentTypeScreenId;
    }

    // TODO: remove link check when able to open external URL before this func
    if (isV2River(item) || isGeneralPlugin(item) || isLink(item)) {
      return item?.id;
    }

    // Fallback for playable items
    if (isPlayable(item)) {
      const playerScreen = R.compose(
        R.find(R.propEq("plugin_type", "player")),
        R.values
      )(rivers);

      return playerScreen?.id;
    }
  }

  // old logic for pipes v1
  return getIdResolver(screenType)(item);
}

/**
 * returns the target route given the provided payload and pathname
 * @param {object} payload data of the item which triggered the navigation
 * @param {string} pathname current location if it exist
 * @param {object} options current location if it exist
 * @param {object} options.contentTypes content types mapping, required for v2 version
 * @returns {string} targetRoute to navigate to
 */
export function getTargetRoute(
  payload: ZappEntry | ZappRiver,
  pathname = "",
  {
    contentTypes = undefined,
    layoutVersion = "v2",
    rivers = {},
  }: {
    contentTypes?: ZappContentTypes | null;
    layoutVersion?: Maybe<ZappLayoutVersions>;
    rivers?: Record<string, ZappRiver>;
  } = {}
) {
  const screenType = getItemType(payload, layoutVersion, contentTypes);

  const screenId = getItemTargetId(payload, screenType, contentTypes, rivers);

  // Can't create route without screenID or screenType
  if (!screenId || !screenType) {
    return null;
  }

  const routeForScreenType = screenType === "menu_item" ? "river" : screenType;

  return R.replace("//", "/", `${pathname}/${routeForScreenType}/${screenId}`);
}

/**
 * Returns the path attributes for a provided location
 * will transform /screenType1/screenId/foo/bar
 * into
 [
 { screenType: "screenType1", screenId: "screenId"},
 { screenType: "foo", screenId: "bar"}
 ]
 * @param {Object} location object
 * @param {String} location.pathname path to get attributes from
 * @returns {Array} PathAttributes array, of the form [{ screenType: String, screenId: String }]
 */
export function getPathAttributes({
  pathname,
}: {
  pathname: string;
}): PathAttribute[] {
  if (pathname === "/" || pathname === "") {
    return [];
  }

  return R.compose(
    // mapping(["foo", "bar"] => { screenType: "foo", screenId: "bar" })
    R.map(R.zipObj(["screenType", "screenId"])),
    // ["foo", "bar", "baz", "qux"] => [["foo", "bar"], ["baz", "qux"]]
    R.splitEvery(2),
    // omitting the first empty element in the array since pathname starts with /
    R.tail,
    // spliting pathname string into array
    R.split("/")
  )(pathname);
}

/**
 * retrieves the river from the given route, or null if route is not a river route
 * @param {string} route
 * @param {object} rivers
 * @returns {object}
 */
export function getRiverFromRoute({ route, rivers }) {
  if (!route) {
    return null;
  }

  const { screenType, screenId } =
    R.compose(
      R.last,
      getPathAttributes,
      R.assoc("pathname", R.__, {})
    )(route) || {};

  if (rivers?.[screenId]) {
    return rivers[screenId];
  }

  const findScreenTypeByKey = (key, type) =>
    R.compose(R.find(R.propEq(key, type)), R.values)(rivers);

  const pluginScreen =
    findScreenTypeByKey("type", screenType) ||
    (screenType === SCREEN_TYPES.PLAYABLE &&
      findScreenTypeByKey("plugin_type", "player"));

  if (pluginScreen) {
    return pluginScreen;
  }

  return screenType && screenId ? { screenType, screenId } : null;
}

/**
 * Function returns true if the previous route (in the react-router history) is a hook plugin
 * @param {*} entries - React-Router History Object
 * @return boolean
 */
export function isPreviousRouteHook(entries: [{ pathname: string }]): boolean {
  const previousRoute = entries[entries.length - 2];
  if (!previousRoute) return false;

  return R.test(/\/hooks\/.*/g, previousRoute.pathname);
}

/**
 * Function returns number of consecutive hooks that are behind the current route
 * @param {*} history - React-Router History Object
 * @return true
 */
export function getPreviousHooksCount(history: {
  entries: [{ pathname: string }];
}): number {
  let hooksCount = 0;
  let entries = R.clone(history.entries);

  while (isPreviousRouteHook(entries)) {
    hooksCount++;
    entries = R.init(entries);
  }

  return hooksCount;
}

export const usesVideoModal = (
  item: ZappEntry,
  rivers: Record<string, ZappRiver>,
  contentTypes: ZappContentTypes
) => {
  const targetScreenId = contentTypes?.[item?.type?.value]?.screen_id;

  const targetScreenConfiguration = rivers?.[targetScreenId] as {
    styles?: { use_video_modal: boolean };
  };

  return targetScreenConfiguration?.styles?.use_video_modal;
};

export const mapContentTypesToRivers = ({
  rivers,
  contentTypes,
}): ZappContentTypesMapped | null => {
  if (!contentTypes) {
    return null;
  }

  return R.compose(
    R.reduce((types, key) => {
      const screen = rivers?.[contentTypes?.[key]?.screen_id];
      const screenType = screen?.plugin_type || screen?.type;

      if (screenType) {
        types[key] = R.assoc("screenType", screenType, contentTypes[key]);
      }

      return types;
    }, {}),
    R.keys
  )(contentTypes);
};

export const runZappHooksForEntry = async (
  entry: HookPluginProps["payload"],
  {
    setState,
    resetState,
    setIsHooksExecutionInProgress,
    setIsPresentingFullScreen,
    setIsRunningInBackground,
  }: Partial<HookModalContextT>
): Promise<{ success: boolean; payload: ZappEntry }> => {
  resetState?.();

  let success;
  let payload;
  const rivers = appStore.get("rivers");
  const plugins = appStore.get("plugins");
  const appData = appStore.get("appData");

  const contentTypes = mapContentTypesToRivers({
    rivers,
    contentTypes: appStore.get("contentTypes"),
  });

  const { layoutVersion } = appData;

  setIsHooksExecutionInProgress?.(true);

  const targetRoute = getTargetRoute(entry as ZappEntry, "", {
    layoutVersion,
    contentTypes,
  });

  const targetScreen = getRiverFromRoute({ route: targetRoute, rivers });

  const hooksOptions = {
    rivers,
    plugins,
    targetScreen,
  };

  const handleHookPresent = (hookProps) => {
    const { route, payload } = hookProps;
    setIsPresentingFullScreen?.();

    setState?.({
      path: route,
      screenData: payload,
    });
  };

  const handleBackgroundHook = (hookProps) => {
    const { route, payload } = hookProps;

    setState?.({
      path: route,
      screenData: payload,
    });

    setIsRunningInBackground?.();
  };

  const onFinished =
    (_success) =>
    ({ payload: _payload }) => {
      setIsHooksExecutionInProgress?.(false);
      success = _success;
      payload = _payload;
    };

  HooksManager(hooksOptions)
    .on(HOOKS_EVENTS.PRESENT_SCREEN_HOOK, handleHookPresent)
    .on(HOOKS_EVENTS.START_BACKGROUND_HOOK, handleBackgroundHook)
    .on(HOOKS_EVENTS.ERROR, onFinished(false))
    .on(HOOKS_EVENTS.CANCEL, onFinished(false))
    .on(HOOKS_EVENTS.COMPLETE, onFinished(true))
    .handleHooks(entry);

  // eslint-disable-next-line no-unmodified-loop-condition
  while (success === undefined) {
    await new Promise((resolve) =>
      setTimeout(() => {
        resolve(undefined);
      }, 125)
    );
  }

  return { success, payload };
};

/**
 * Function which tells if the provided nav items have focusable buttons
 * @param {NavBarItem[]} navItems
 * @return boolean
 */
export const hasButtonItems = (navItems: NavBarItem[]): boolean =>
  navItems?.filter(R.propSatisfies(R.contains, "button"))?.length > 0;

/**
 * Function which tells if a given nav item is a menu item or a nav bar item
 * (button or image)
 * @param {NavBarItem} navItem
 * @return boolean
 */
export const isNavBarItem: (NavBarItem) => boolean = R.anyPass([
  R.propEq("type", "tv_right_button"),
  R.propEq("type", "tv_left_button"),
  R.propEq("type", "tv_left_image"),
  R.propEq("type", "tv_right_image"),
]);

/**
 * This function sorts nav items in a Zapp navigation configuration
 * and returns left items, menu items and right items. returned arrays can be empty
 * @param {NavBarItem[]} - nav items to sort
 * @return {[leftItems: NavBarItem[], menuItems: NavBarItem[], rightItems: NavBarItem[]]}
 */
export const getNavItems = (
  navItems: NavBarItem[]
): [NavBarItem[], NavBarItem[], NavBarItem[]] => {
  const sortedNavItems = R.sortBy(R.prop("position"), navItems);
  const [navBarItems, menuItems] = R.partition(isNavBarItem, sortedNavItems);

  const firstMenuItemPosition = R.compose(
    R.prop("position"),
    R.find(R.propEq("type", "label"))
  )(sortedNavItems);

  const [leftItems, rightItems] = R.compose(
    R.partition((item) => Number(item.position) < firstMenuItemPosition),
    R.sortBy(R.prop("position")),
    R.reject(R.propEq("type", "label"))
  )(navBarItems);

  return [leftItems, menuItems, rightItems];
};

/**
 * This function tells if the menu should be a valid focusable target or not
 * Currently only used on Android TV. It will check if there are enabled left or right
 * items, and if they have focusable elements
 * @param {NavigationProps} Props sent to the Menu plugin
 * @param {QuickBrickAppNavigator}
 * @returns boolean;
 */
export function isMenuDisabled(
  { navigationProps },
  navigator: QuickBrickAppNavigator
): boolean {
  const secondaryLevel = navigator.canGoBack();

  if (!secondaryLevel) return false;

  const { styles, nav_items } = navigationProps;
  const showButtons = styles.show_buttons;
  const showImage = styles.show_left_image;

  const [leftItems, _, rightItems] = getNavItems(nav_items);

  const hasLeftButtons = hasButtonItems(leftItems) && showImage;
  const hasRightButtons = hasButtonItems(rightItems) && showButtons;

  const menuDisabled = !hasLeftButtons && !hasRightButtons;

  return menuDisabled;
}

export function routeIsPlayerScreen(currentRoute) {
  return currentRoute?.includes("/playable");
}

export const getNavBarProps =
  (currentRiver: ZappRiver, pathname: string, title: string) => () => {
    const props = getNavigationPropsV2({
      currentRiver,
      title,
      category: "nav_bar",
    });

    if (props) {
      return {
        ...props,
        id: pathname,
        pathname: pathname,
      };
    }

    return null;
  };

export const findMenuPlugin = (
  navigations: ZappNavigation[],
  plugins
): QuickBrickPlugin => {
  return resolveNavigationPlugin({
    category: "menu",
    navigations,
    plugins,
  }) as QuickBrickPlugin;
};

export const shouldNavBarDisplayMenu = (currentRiver: ZappRiver, plugins) =>
  findMenuPlugin(currentRiver.navigations, plugins)?.identifier ===
    "quick_brick_side_menu" ||
  currentRiver?.navigations.findIndex(
    (nav) => nav.category === "nav_bar" && nav.general?.override_menu_target
  ) > -1;

export const getScreenId = (
  screenData: LegacyNavigationScreenData,
  fallbackRiver: ZappRiver
) => {
  return screenData?.targetScreen
    ? (screenData?.targetScreen?.id ?? fallbackRiver.id)
    : "screen_id" in screenData
      ? screenData.screen_id
      : fallbackRiver.id;
};
