/// <reference types="../../" />
/* eslint-disable @typescript-eslint/no-use-before-define */
import * as React from "react";
import * as R from "ramda";
import { NavigationContext } from "@applicaster/zapp-react-native-ui-components/Contexts/NavigationContext";
import {
  getTargetRoute,
  usesVideoModal,
} from "@applicaster/zapp-react-native-utils/navigationUtils";
import { clone, last } from "@applicaster/zapp-react-native-utils/utils";
import {
  allowedOrientationsForScreen,
  useGetScreenOrientation,
} from "@applicaster/zapp-react-native-utils/appUtils/orientationHelper";
import { focusManager } from "@applicaster/zapp-react-native-utils/focusManager/FocusManager";
import {
  useContentTypes,
  usePickFromState,
} from "@applicaster/zapp-react-native-redux/hooks";

import reducer, {
  ACTIONS,
  back,
  backAndReplacePlayer,
  initialState,
  push,
  replace,
  setBackPlayerNestedContent,
  setNestedContent,
  setReplaceTop,
  setVideoModalClose,
  setVideoModalFullscreen,
  setVideoModalItem,
  setVideoModalMaximized,
  setVideoModalMinimized,
  setVideoModalOpen,
  setVideoPiP,
} from "./navigator/reducer";

import { HooksManager } from "@applicaster/zapp-react-native-utils/appUtils/HooksManager";
import { HOOKS_EVENTS } from "@applicaster/zapp-react-native-utils/appUtils/HooksManager/constants";

import {
  activeRiverSelector,
  homeRiverSelector,
  lastEntrySelector,
} from "./navigator/selectors";

import { coreAppLogger } from "../logger";
import {
  getPlatform,
  isTV,
  isVizioPlatform,
} from "@applicaster/zapp-react-native-utils/reactUtils";
import { useConnectionInfo } from "@applicaster/zapp-react-native-utils/reactHooks/connection";
import {
  getNavigationTarget,
  getTargetScreen,
  legacyScreenData,
  openExternalUrl,
} from "./utils";
import {
  debounce,
  getJsonReplacer,
} from "@applicaster/zapp-react-native-utils/functionUtils";

import { findParent } from "@applicaster/zapp-react-native-ui-components/Contexts/ZappPipesContext/ZappPipesEntryContext";
import { ZappPipesEntryContext } from "@applicaster/zapp-react-native-ui-components/Contexts";
import { PlayNextState } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/OverlayObserver/OverlaysObserver";
import { inflateUrl } from "@applicaster/zapp-react-native-ui-components/Components/LinkHandler/LinkHandler";
import {
  dismissModal,
  modalStore,
  openModal,
} from "@applicaster/zapp-react-native-utils/modalState";
import { useSubscriberFor } from "@applicaster/zapp-react-native-utils/reactHooks/useSubscriberFor";
import { QBUIComponentEvents } from "@applicaster/zapp-react-native-ui-components/events";
import { Hook } from "@applicaster/zapp-react-native-utils/appUtils/HooksManager/Hook";
import { URLSchemeContext } from "../DeepLinking/URLSchemeListener/URLSchemeContextProvider";

import { loadingOverlayManager } from "../LoadingOverlay";

const logger = coreAppLogger.addSubsystem("Navigator");

type Props = {
  children?: React.ReactChild;
  rivers?: Record<string, ZappRiver>;
  plugins?: QuickBrickPlugin[];
};

const navigationActions = {
  [ACTIONS.PUSH]: push,
  [ACTIONS.REPLACE]: replace,
  [ACTIONS.REPLACE_TOP]: setReplaceTop,
  [ACTIONS.BACK_PLAYER_NESTED_CONTENT]: setBackPlayerNestedContent,
};

const platform = getPlatform();

export function NavigationProvider({ children }: Props) {
  const { url: deepLinkURL } = React.useContext(URLSchemeContext);

  const {
    appData: { layoutVersion },
    rivers,
    plugins,
    pluginConfigurations,
    appState: { appLaunched },
  } = usePickFromState([
    "appData",
    "rivers",
    "plugins",
    "pluginConfigurations",
    "appState",
    "contentTypes",
  ]);

  const contentTypes = useContentTypes();

  const [state, dispatch] = React.useReducer<
    NavigationReducer,
    NavigationReducerState
  >(reducer, initialState, (x) => x);

  const topStackRoute = last(state?.stack?.mainStack);

  /** TODO: temporary variable, before we remove need for a "current".
   There is no current as we are using stack navigator. Route data should be contextualised per screen
   There are some cases to use a route data from top of the stack but it should be separate method.
   */

  const currentLocation = {
    ...topStackRoute,
    route: topStackRoute?.route || "/", // Fallback for initial state.
  };

  const stateRef = React.useRef<NavigationReducerState>(state);
  const pathname = currentLocation?.route; // TODO: remove. Pathname is part of the route, not a global.
  const pathnameRef = React.useRef(pathname);

  const { context } = React.useContext(ZappPipesEntryContext.Context);

  const playNextOverlayState = React.useRef();

  const online = useConnectionInfo(true);

  const replaceEvent = useSubscriberFor(
    QBUIComponentEvents.navigatorReplaceItem
  );

  React.useEffect(() => {
    stateRef.current = state;
    pathnameRef.current = pathname;
  }, [state.stack.mainStack.length, pathname]);

  React.useLayoutEffect(() => {
    if (platform === "android_tv") {
      focusManager.resetFocus();
    }
  }, [pathname]);

  // TODO remove.
  // There is no concept of active river. Multiple rivers can be rendered at the same time.
  const activeRiver = React.useMemo(
    () => activeRiverSelector({ pathname, rivers }),
    [pathname, rivers]
  );

  const getModalState: () => ModalState = () => {
    const { modalState } = modalStore.getState();

    return modalState;
  };

  const homeRiver = React.useMemo(
    () => homeRiverSelector({ rivers }),
    [rivers]
  );

  const homeRiverOffline = React.useMemo(
    () =>
      R.compose(
        R.defaultTo(homeRiver),
        R.find(R.propEq("home_offline", true)),
        R.values
      )(rivers),
    [rivers, homeRiver]
  );

  const getStack = () => [...state?.stack?.mainStack];

  const openVideoModal = (
    item: ZappEntry,
    options: Record<string, unknown> = {}
  ) => {
    const screen = getTargetScreen({ entry: item, rivers, contentTypes });
    dispatch(setVideoModalOpen(item, options, screen));
  };

  // Source screen Orientation will be used when modal is closed.
  // TODO: Move in proper place
  const modalPresenterScreenOrientation = useGetScreenOrientation(activeRiver);

  // TODO: Move modal state to separate store
  const closeVideoModal = () => {
    if (navigator.modalData) {
      allowedOrientationsForScreen(modalPresenterScreenOrientation);
    }

    dispatch(setVideoModalClose());
  };

  const fullscreenVideoModal = () => {
    dispatch(setVideoModalFullscreen());
  };

  const maximiseVideoModal = () => {
    dispatch(setVideoModalMaximized());
  };

  const minimiseVideoModal = () => {
    dispatch(setVideoModalMinimized());
  };

  const _setVideoModalItem = (item) => {
    dispatch(setVideoModalItem(item));
  };

  const isVideoModalDocked = () =>
    state?.options.videoModal.mode === "MINIMIZED";

  // TODO: Remove as it's using a concept of "current" route.
  // Route is contextualised and shouldn't be used this way.
  const routeData = () => {
    // eslint-disable-next-line no-console
    console.warn(`you are retrieving screen data from navigator.routeData()
    This function will be removed in a later version of quick brick.
    You can retrieve this data from navigator.screenData`);

    return legacyScreenData(currentLocation?.state as NavigationScreenData);
  };

  // TODO: remove it by refactoring back buttons handlers
  const setPlayNextOverlay = (state) => {
    playNextOverlayState.current = state;
  };

  const setNestedScreenContent = (entry: ZappEntry, screenId: string) => {
    const screen = rivers[screenId];
    const parent = findParent(context, navigator.currentRoute);

    const entryClone = clone(entry);

    if (!screen) {
      logger.warn({
        message: `Cannot resolve type mapping for ${screenId} id`,
        data: { entry: entryClone, screenId },
      });

      return;
    }

    if (parent) {
      entryClone.parent = parent?.data;

      entryClone.parentId = parent?.id;
    }

    dispatch(setNestedContent(pathname, entryClone, screen));
  };

  const pushItem = (item, options = {}) => {
    navigateTo(item, ACTIONS.PUSH, options);
  };

  const replaceItem = (item, options = {}) => {
    const targetRoute = getTargetRoute(item, undefined, {
      layoutVersion,
      contentTypes,
    });

    if (
      targetRoute !== pathnameRef.current ||
      item?.id !==
        legacyScreenData(currentLocation?.state as NavigationScreenData)?.id
    ) {
      replaceEvent();
      navigateTo(item, ACTIONS.REPLACE, options);
    }
  };

  const canGoBack = (fromHook = false, pathname?: string) => {
    const stack = fromHook ? stateRef.current.stack : state.stack;

    const filteredStack = state.stack.mainStack.filter(
      (stackItem) => !stackItem?.route?.includes("/hooks")
    );

    if (pathname) {
      const pathnameStackIndex = state.stack.mainStack.findIndex(({ route }) =>
        route.includes(pathname)
      );

      return pathnameStackIndex >= 1;
    }

    if (!Number.isNaN(filteredStack?.length)) {
      return Math.min(filteredStack.length, stack.mainStack.length) > 1;
    }

    return stack.mainStack.length > 1;
  };

  const goHome = React.useCallback(
    (initialLaunch = false) => {
      const targetRiver =
        initialLaunch && online === false && !isTV()
          ? homeRiverOffline
          : homeRiver;

      navigateTo(targetRiver, ACTIONS.REPLACE);
    },
    [online, homeRiver]
  );

  const replaceTop = (item, options = {}) => {
    navigateTo(item, ACTIONS.REPLACE_TOP, options);
  };

  const goBack = (
    fallbackToHome = true,
    fromHook = false,
    backToTop = false
  ) => {
    logger.debug("Attempting to go back", {
      fallbackToHome,
      fromHook,
      backToTop,
    });

    if (fallbackToHome && !canGoBack(fromHook)) {
      goHome();
    } else {
      if (backToTop) {
        const firstRoute = fromHook
          ? stateRef.current.stack.mainStack[0]
          : state.stack.mainStack[0];

        const stateForBack = {
          route: firstRoute.route,
          state: firstRoute.state,
        };

        if (
          stateForBack?.state?.entry &&
          "home" in stateForBack?.state?.entry
        ) {
          goHome();
        } else {
          dispatch(back(stateForBack));
        }
      } else {
        const isRouteNestedInPlayer =
          navigator.currentRoute.includes("playable") &&
          !navigator.data.entry?.content?.src;

        const mainStack = stateRef.current.stack.mainStack;
        const previousIndex = Math.max(mainStack.length - 2, 0); // Avoids negative indices

        const { src: previousEntryContentSrc, type: previousEntryContentType } =
          mainStack[previousIndex]?.state?.entry?.content || {};

        const isPreviousLocationPlayable =
          previousEntryContentSrc &&
          ["video", "audio"].includes(previousEntryContentType);

        const isPlayNextVisible: PlayNextState = playNextOverlayState.current;

        if (isPlayNextVisible) {
          isPlayNextVisible.handleUserCancelPlayNext();

          return;
        }

        if (isRouteNestedInPlayer && isPreviousLocationPlayable) {
          const findPlayableEntry = (stack) => {
            return (
              R.find(
                R.pathSatisfies(R.includes("playable"), ["route"]),
                stack
              ) || null
            );
          };

          const playerEntry = findPlayableEntry(state.stack.mainStack);

          navigateTo(
            playerEntry?.state?.entry,
            ACTIONS.BACK_PLAYER_NESTED_CONTENT
          );
        } else {
          dispatch(back());
        }
      }
    }
  };

  const handleHookPresent = (
    {
      hookPlugin,
      route,
      payload,
    }: {
      hookPlugin: Hook;
      route: string;
      payload: object;
    },
    dispose
  ) => {
    if (hookPlugin?.lastHook) {
      dispose();
    }

    if (hookPlugin.isModalHook()) {
      openModal({
        item: payload,
        props: { screenType: "hooks" },
        options: { ModalContainer: hookPlugin.getModalContainer() },
      });
    } else {
      dispatch(push(route, { entry: payload, screen: hookPlugin }));
    }

    closeLoadingOverlay();
  };

  const handleHookError = ({ error, hookPlugin, callback }, dispose) => {
    logger.error(error);

    if (hookPlugin?.lastHook) {
      dispose();
    }

    if (hookPlugin.isModalHook()) {
      dismissModal();
    } else {
      goBack(true, true);
    }

    if (callback && typeof callback === "function") {
      callback();
    }

    closeLoadingOverlay();
  };

  const handleHookCancel = ({ hookPlugin, callback, abort }, dispose) => {
    dispose();
    setStartUpHooks(false);

    if (callback && typeof callback === "function") {
      callback();
    }

    if (!abort) {
      if (hookPlugin.isModalHook()) {
        dismissModal();
      } else {
        goBack(true, true);
      }
    }

    closeLoadingOverlay();
  };

  const handleHookSuccess = ({ hookPlugin, callback }) => {
    callback?.();

    if (hookPlugin.isModalHook()) {
      dismissModal();
    }
  };

  const handleBackgroundHook = () => {
    openLoadingOverlay();
  };

  const checkStartUpHooks = React.useMemo(() => {
    if (!pluginConfigurations) return false;

    const startUpHooksIdentifiers = R.compose(
      R.map((item) => item.plugin.identifier),
      R.filter(
        R.allPass([
          R.pathEq(["plugin", "api", "require_startup_execution"], true),
          R.pathEq(["plugin", "react_native"], true),
          R.pathEq(["plugin", "preload"], true),
        ])
      ),
      R.values
    )(pluginConfigurations);

    if (startUpHooksIdentifiers?.length) {
      const startUpHooks = R.filter(
        (plugin) => R.includes(plugin.identifier, startUpHooksIdentifiers),
        plugins
      );

      const preparedStartUpHooks = R.map((hook) => {
        const riversByIdentifier = R.compose(
          R.filter(R.propEq("type", hook.identifier)),
          R.values
        )(rivers);

        if (riversByIdentifier.length > 1) {
          const river = R.find(
            R.pathEq(["general", "startup_execution_hook"], true)
          )(riversByIdentifier);

          hook.screen_id = river?.id;
        } else {
          hook.screen_id = riversByIdentifier?.[0]?.id;
        }

        hook.call_type = "startup";

        return hook;
      }, startUpHooks);

      return preparedStartUpHooks;
    }

    return false;
  }, [pluginConfigurations, plugins, rivers]);

  const [startUpHooks, _setStartUpHooks] = React.useState(checkStartUpHooks);
  const currentStartUpHooks = React.useRef(checkStartUpHooks);

  const setStartUpHooks = (value) => {
    _setStartUpHooks(value);
    currentStartUpHooks.current = value;
  };

  const handleHookComplete =
    (navigationAction, targetRoute, { screen, options }) =>
    ({ hookPlugin, payload, callback }, dispose) => {
      if (hookPlugin?.lastHook) {
        dispose({ disposeAll: true });
        currentStartUpHooks.current && setStartUpHooks(false);
      }

      dispatch(
        navigationAction(targetRoute, { screen, entry: payload, options })
      );

      callback?.();

      closeLoadingOverlay();
    };

  const offlinePlugin = React.useMemo(() => {
    return R.compose(
      R.prop("module"),
      R.defaultTo({}),
      R.find(R.propEq("identifier", "offline-experience"))
    )(plugins);
  }, [plugins]);

  const isValidNavigationTarget = React.useRef(null);
  const offlineAlert = React.useRef(null);

  isValidNavigationTarget.current =
    offlinePlugin?.useOfflineNavigation?.(activeRiver);

  offlineAlert.current = offlinePlugin?.useOfflineAlert?.();

  const navigateTo = debounce({
    wait: 200,
    fn: (
      item: ZappEntry | ZappRiver,
      action:
        | ACTIONS.PUSH
        | ACTIONS.REPLACE_TOP
        | ACTIONS.REPLACE
        | ACTIONS.BACK_PLAYER_NESTED_CONTENT,
      options = {}
    ) => {
      const { entry, screen, targetRoute, externalUrl } = getNavigationTarget(
        item,
        action === ACTIONS.REPLACE ? "" : pathnameRef.current,
        contentTypes,
        layoutVersion,
        rivers
      );

      if (externalUrl) {
        const openURL = async (url) => {
          const inflatedURL = await inflateUrl(url);
          openExternalUrl(inflatedURL);
        };

        void openURL(externalUrl);

        return;
      }

      // Skip navigation if the entry type is empty
      if (layoutVersion === "v2" && entry && !entry.type?.value) {
        logger.warn({
          message: "Empty content type",
          data: { entry },
        });

        return;
      }

      if (entry && usesVideoModal(entry, rivers, contentTypes)) {
        openVideoModal(entry, options);

        return;
      }

      if (!targetRoute || !screen) {
        logger.warn({
          message: "Cannot resolve navigation target",
          data: { item, action, options },
        });

        return;
      }

      if (isValidNavigationTarget.current) {
        // @ts-ignore - FIXME
        const validTarget = isValidNavigationTarget.current?.(item, action);

        if (!validTarget) {
          // @ts-ignore - FIXME
          offlineAlert.current?.(() => navigateTo(item, action));

          return;
        }
      }

      if (state.options.videoModal.visible && !isVideoModalDocked()) {
        minimiseVideoModal();
      }

      const navigationAction:
        | typeof push
        | typeof replace
        | typeof setReplaceTop
        | typeof backAndReplacePlayer = navigationActions[action] || replace;

      // TV don't handle offline mode so the hooks execution should never be skipped
      if (online || isTV()) {
        const hooksOptions = {
          targetScreen: screen,
          rivers,
          plugins,
          startUpHooksData: {
            startUpHooks: currentStartUpHooks.current,
            setStartUpHooks,
          },
        };

        const hooksManager = HooksManager(hooksOptions);

        hooksManager.subscriber
          .on(HOOKS_EVENTS.PRESENT_SCREEN_HOOK, handleHookPresent)
          .on(HOOKS_EVENTS.ERROR, handleHookError)
          .on(HOOKS_EVENTS.CANCEL, handleHookCancel)
          .on(HOOKS_EVENTS.SUCCESS, handleHookSuccess)
          .on(HOOKS_EVENTS.START_BACKGROUND_HOOK, handleBackgroundHook)
          .on(
            HOOKS_EVENTS.COMPLETE,
            handleHookComplete(navigationAction, targetRoute, {
              screen,
              options,
            })
          );

        hooksManager.handleHooks(legacyScreenData({ entry, screen }));
      } else {
        dispatch(navigationAction(targetRoute, { screen, entry, options }));
      }
    },
  });

  const openLoadingOverlay = React.useCallback(() => {
    deepLinkURL && isVizioPlatform() && loadingOverlayManager.show();
  }, [deepLinkURL]);

  const closeLoadingOverlay = React.useCallback(() => {
    loadingOverlayManager.hide();
  }, []);

  const openPiP = React.useCallback(() => {
    dispatch(setVideoPiP());
  }, []);

  const closePiP = React.useCallback(() => {
    switch (state?.options?.videoModal?.previousMode) {
      case "MAXIMIZED":
        maximiseVideoModal();
        break;
      case "MINIMIZED":
        minimiseVideoModal();
        break;
      case "FULLSCREEN":
        fullscreenVideoModal();
        break;
      default:
        fullscreenVideoModal();
    }
  }, [state?.options?.videoModal?.previousMode]);

  // TODO: remove. This shouldn't be part of navigator
  const getNestedEntry = () => currentLocation?.state?.nested?.entry ?? null;

  // It will only work for the regular stack and pathname, It won't work for modals
  // TODO: integrate video/hook and regular modal into stack.
  const getStackForPathname = (
    pathname: string
  ): NavigationScreenState | undefined => {
    const stack = [...state?.stack?.mainStack];

    const currentStack = stack.find(({ route }) => route === pathname);

    return currentStack;
  };

  const navigator: QuickBrickAppNavigator = React.useMemo(
    () => ({
      activeRiver,
      getPathname: () => pathnameRef.current, // hack use to fix issue causing broken navigation as currentRoute is unreliable
      currentRoute: pathname, // TODO: remove. Current route shouldn't be needed
      previousAction: lastEntrySelector(state)
        ?.action as QuickBrickNavigationActionType,
      push: pushItem,
      replace: replaceItem,
      setNestedScreenContent,
      setPlayNextOverlay,
      goHome,
      canGoBack,
      goBack,
      routeData, // TODO: remove
      screenData: legacyScreenData(
        // TODO: remove
        currentLocation?.state as NavigationScreenData,
        plugins
      ),
      data: currentLocation?.state as NavigationScreenData,
      getNestedEntry,
      key: currentLocation?.key, // TODO: remove
      modalData: state.stack.modal?.state ?? null,
      openModal,
      dismissModal,
      getModalState,
      videoModalState: state.options.videoModal,
      openVideoModal,
      closeVideoModal,
      openPiP,
      closePiP,
      fullscreenVideoModal,
      maximiseVideoModal,
      minimiseVideoModal,
      isVideoModalDocked,
      setVideoModalItem: _setVideoModalItem,
      startUpHooks,
      replaceTop,
      getStack,
      getStackForPathname,
      mainStack: state.stack.mainStack,
    }),
    [
      rivers,
      plugins,
      // TODO: Compare entire screen data from state must be removed
      // State size could be large ~150kb and take 80ms on android tv on 8 rails
      JSON.stringify(state, getJsonReplacer()),
      appLaunched,
      startUpHooks,
    ]
  );

  // when running the tests, we pass the dispatch function
  // in order to be able to trigger state changes
  if (process.env.NODE_ENV === "test") {
    navigator.dispatch = dispatch;
    navigator.state = state;
  }

  React.useLayoutEffect(() => {
    if (pathname === "/" && appLaunched) {
      goHome(true);
    }
  }, [pathname, appLaunched]);

  return (
    <NavigationContext.Provider value={navigator}>
      {children}
    </NavigationContext.Provider>
  );
}
