import {
  getRewardTransactions,
  getTargetedWidgetIdAndKind,
  addProgramListener,
  removeProgramListener,
  IProgramListenerCallbackArgs,
  WidgetCreatedEvent,
  getPostedWidgets,
  getWidgetsInteractions,
  IWidgetPayload,
  IWidgetKindInteractionsMap,
  IWidgetInteraction,
  IRewardTransaction,
  IWidgetEarnableReward,
  IPostedWidgetsPayload,
  PREDICTION_FOLLOW_UP_WIDGET_KIND,
  PREDICTION_WIDGET_ID_PROP,
  getWidgetInteractions,
} from '@livelike/javascript';
import { useCallback, useEffect, useRef, useState } from 'react';
import {
  getSelectedOptionIndex,
  getWidgeUIPhase,
  getWidgetResultState,
  getWidgetRewardsFromRewardTransactions,
  widgetStoreActions,
  timelineWidgetStoreActions,
} from '../store';
import { useApi } from './useApi';
import { WidgetMode } from '../types';

export type UseLoadTimelineWidgetEffectArg = {
  programId: string;
  mode: WidgetMode;
};

export function useLoadTimelineWidgetEffect({
  programId,
  mode,
}: UseLoadTimelineWidgetEffectArg) {
  const [moreWidgets, setMoreWidgets] = useState(false);
  const nextIteratorRef =
    useRef<() => Promise<IteratorResult<IWidgetPayload[]>>>();

  const { onApi, isLoading, error, data } = useApi(() =>
    getPostedWidgets({ programId })
      .then((widgetsPayload) => {
        if (!widgetsPayload.done) {
          setMoreWidgets(true);
          nextIteratorRef.current = widgetsPayload.next;
        }
        return widgetsPayload;
      })
      .then(getWidgetsDetails)
  );

  useEffect(() => {
    if (mode === WidgetMode.POPUP) {
      timelineWidgetStoreActions.updateTimelineWidgetStateAction({
        programId,
        widgetTimelineState: {
          widgets: [],
        },
      });
      return;
    }
    onApi().then((apiData) => updateWidgetsState(apiData, programId));
  }, [programId, mode]);

  useEffect(() => {
    if (!data && mode === WidgetMode.INTERACTIVE_TIMELINE) {
      return;
    }
    function onProgramListener({
      event,
      message,
    }: IProgramListenerCallbackArgs) {
      if (Object.values(WidgetCreatedEvent).includes(event)) {
        const { id: widgetId, kind: widgetKind } = message;
        const widgetPayload = message;
        (PREDICTION_FOLLOW_UP_WIDGET_KIND.includes(widgetKind)
          ? getWidgetInteractions({
              widgetId,
              widgetKind,
              interactionUrl: widgetPayload.widget_interactions_url_template,
            })
          : Promise.resolve([])
        ).then((widgetInteractions) => {
          widgetStoreActions.updateWidgetStateAction({
            widgetId,
            widgetState: {
              widgetPayload,
              widgetInteractions,
              widgetResultState: getWidgetResultState({
                widgetPayload,
                widgetInteractions,
              }),
              widgetUIPhase: getWidgeUIPhase({
                widgetPayload,
                widgetInteractions,
              }),
              selectedOptionIndex: getSelectedOptionIndex({
                widgetPayload,
                widgetInteractions,
              }),
              isTimelineWidget: true,
            },
          });
          timelineWidgetStoreActions.updateTimelineWidgetsAction({
            programId,
            widgets: [{ widgetId, widgetKind }],
            prepend: true,
          });
        });
      }
    }
    addProgramListener({ programId }, onProgramListener);
    return () => {
      removeProgramListener({ programId }, onProgramListener);
    };
  }, [data, programId, mode]);

  const onLoadMore = useCallback(() => {
    if (nextIteratorRef.current) {
      return nextIteratorRef
        .current()
        .then((res) => {
          if (res.done) {
            setMoreWidgets(false);
            nextIteratorRef.current = null;
          }
          return res.value;
        })
        .then(getWidgetsDetails)
        .then((widgetsData) => updateWidgetsState(widgetsData, programId));
    }
  }, [nextIteratorRef.current, programId]);

  return {
    onApi,
    isLoading,
    error,
    data,
    onLoadMore: moreWidgets ? onLoadMore : null,
  };
}

type IWidgetDetails = [
  IWidgetPayload[],
  IWidgetKindInteractionsMap,
  IRewardTransaction[],
];

const getWidgetsDetails = (
  widgetsPayload: IPostedWidgetsPayload
): Promise<IWidgetDetails> => {
  return Promise.all([
    getWidgetsInteractions({
      interactionUrl: widgetsPayload.widget_interactions_url_template,
    }),
    Promise.all(
      widgetsPayload.widgets.map((widget) =>
        getTargetedWidgetIdAndKind({ widget })
      )
    ).then((widgetIdsAndKinds) =>
      getAllRewardTransasctions(
        widgetIdsAndKinds.map(({ widgetId }) => widgetId)
      )
    ),
  ]).then(([widgetInteractions, widgetRewards]) => [
    widgetsPayload.widgets,
    widgetInteractions,
    widgetRewards,
  ]);
};

const updateWidgetsState = (
  [widgets, widgetInteractions, widgetRewards]: IWidgetDetails,
  programId: string
) => {
  const { widgetRecords: widgetsPayloadRecord, followUpWidgetRecords } =
    getWidgetPayloadRecord(widgets);
  const widgetsInteractionsRecord = getWidgetsInteractionsRecord(
    widgetInteractions,
    followUpWidgetRecords
  );
  const widgetsRewardsRecord = getWidgetsRewardsRecord(
    widgetRewards,
    followUpWidgetRecords
  );
  // set individual widget state so that widget details are not reloaded
  // This is done to avoid sending multiple widget details request for every widget rendered
  // that would give same response.
  Object.keys(widgetsPayloadRecord).forEach((widgetId) => {
    const widgetPayload = widgetsPayloadRecord[widgetId];
    const widgetInteractions = widgetsInteractionsRecord[widgetId] ?? [];
    widgetStoreActions.updateWidgetStateAction({
      widgetId,
      widgetState: {
        widgetPayload,
        widgetInteractions,
        widgetRewards: widgetsRewardsRecord[widgetId],
        widgetResultState: getWidgetResultState({
          widgetPayload,
          widgetInteractions,
        }),
        widgetUIPhase: getWidgeUIPhase({
          widgetPayload,
          widgetInteractions,
        }),
        selectedOptionIndex: getSelectedOptionIndex({
          widgetPayload,
          widgetInteractions,
        }),
        isTimelineWidget: true,
      },
    });
  });
  timelineWidgetStoreActions.updateTimelineWidgetsAction({
    programId,
    widgets: widgets.map(({ id: widgetId, kind: widgetKind }) => ({
      widgetId,
      widgetKind,
    })),
  });
};

// get all reward transactions incase there are multiple pages of
// reward transactions
const getAllRewardTransasctions = async (widgetIds: string[]) => {
  const response = await getRewardTransactions({
    widgetIds,
  });
  let results = response.results;
  let done = response.done;
  while (!done) {
    const res = await response.next();
    results = results.concat(res.value.results);
    done = res.done;
  }
  return results;
};

// create a record of widgetId vs widgetpayload
function getWidgetPayloadRecord(widgets: IWidgetPayload[]) {
  const followUpWidgetRecords = {};
  const widgetRecords = widgets.reduce((record, widgetPayload) => {
    record[widgetPayload.id] = widgetPayload;
    if (PREDICTION_FOLLOW_UP_WIDGET_KIND.includes(widgetPayload.kind)) {
      followUpWidgetRecords[widgetPayload.id] = widgetPayload;
    }
    return record;
  }, {});
  return { widgetRecords, followUpWidgetRecords };
}

// create a record of widgetId vs widgetInteractions
function getWidgetsInteractionsRecord(
  widgetsInteractions: IWidgetKindInteractionsMap,
  followUpWidgetRecords: Record<string, IWidgetPayload>
): Record<string, IWidgetInteraction[]> {
  const record = {};
  for (const interactions of Object.values(widgetsInteractions)) {
    for (const interaction of interactions) {
      record[interaction.widget_id] = [
        ...(record[interaction.widget_id] ?? []),
        interaction,
      ];
    }
  }

  // set followup widget interaction using its corresponding prediction widget id
  for (const predictionWidget of Object.values(followUpWidgetRecords)) {
    const widgetId =
      predictionWidget[PREDICTION_WIDGET_ID_PROP[predictionWidget.kind]];
    if (widgetId && record[widgetId]) {
      record[predictionWidget.id] = record[widgetId];
    }
  }

  return record;
}

// create a record of widgetId vs widgetRewards
function getWidgetsRewardsRecord(
  widgetsRewards: IRewardTransaction[],
  followUpWidgetRecords: Record<string, IWidgetPayload>
): Record<string, IWidgetEarnableReward[]> {
  const record = {};
  for (const reward of widgetsRewards) {
    record[reward.widget_id] = [
      ...(record[reward.widget_id] ?? []),
      ...getWidgetRewardsFromRewardTransactions([reward]),
    ];
  }

  // set followup widget rewards using its corresponding prediction widget id
  for (const predictionWidget of Object.values(followUpWidgetRecords)) {
    const widgetId =
      predictionWidget[PREDICTION_WIDGET_ID_PROP[predictionWidget.kind]];
    if (widgetId && record[widgetId]) {
      record[predictionWidget.id] = record[widgetId];
    }
  }

  return record;
}
