import React, { useCallback, useContext, useMemo, useState } from "react";

import {
  CampaignUtils,
  HelperUtils,
  HtmlSelectOption,
  RollResultContract,
  RollValueContract,
} from "@dndbeyond/character-rules-engine/es";
import { Platform } from "@dndbeyond/event-pipeline-lib/shared";
import {
  GameLogContext,
  isMessage,
  useMessageBroker,
} from "@dndbeyond/game-log-components";
import { DiceVisibilities } from "@dndbeyond/pocket-dimension-dice/constants";
import { Context as DiceWorkerContext } from "@dndbeyond/pocket-dimension-dice/context";
import { instrumentDiceRoll } from "@dndbeyond/pocket-dimension-dice/helpers/bi/instrumentDiceRoll";
import { rollDice } from "@dndbeyond/pocket-dimension-dice/helpers/rollDice";
import { useUserDiceDataStore } from "@dndbeyond/pocket-dimension-dice/hooks/useUserDiceDataStore";
import { RollDicePayload } from "@dndbeyond/pocket-dimension-dice/types";

import { useAuth } from "~/contexts/Authentication";
import { useCharacterEngine } from "~/hooks/useCharacterEngine";

import { DataLoadingStatusEnum } from "../componentConstants";
import { Button, RemoveButton } from "../legacy";
import { TypeScriptUtils, DiceComponentUtils } from "../utils";
import { DiceRoll, DiceRollProps } from "./DiceRoll";

export interface DiceRollGroupProps {
  className: string;
  componentKey: string;
  diceRolls: Array<RollResultContract>;
  exclusiveOptions?: boolean;
  confirmButtonText?: string;
  onConfirm?: (
    groupKey: string,
    rollResults: Array<RollResultContract>
  ) => void;
  onUpdateGroup?: (
    groupKey: string,
    rollResults: Array<RollResultContract>
  ) => void;
  onRollError?: (errorMessage: string) => void;
  groupKey: string;
  nextGroupKey: string | null;
  showRemoveButton?: boolean;
  onRemoveGroup?: (groupKey: string, nextGroupKey: string | null) => void;
  onResetGroup?: (
    groupKey: string,
    rollResults: Array<RollResultContract>
  ) => void;
  onUpdateDiceRoll: (
    rollKey: string,
    properties: Omit<Partial<RollResultContract>, "rollKey">,
    nextRollKey: string | null
  ) => void;
  onSetRollStatus: (rollKey: string, status: DataLoadingStatusEnum) => void;
  rollStatusLookup: Record<string, DataLoadingStatusEnum>;
  options?: DiceRollProps["options"];
  diceRollRequest: RollDicePayload["data"] | Array<RollDicePayload["data"]>;
}
const DiceRollGroup: React.FunctionComponent<DiceRollGroupProps> = ({
  className = "",
  componentKey,
  diceRolls,
  options,
  confirmButtonText = "Confirm",
  showRemoveButton = false,
  exclusiveOptions = false,
  diceRollRequest,
  groupKey,
  nextGroupKey,
  onUpdateGroup,
  onRemoveGroup,
  onRollError,
  onConfirm,
  onResetGroup,
  onSetRollStatus,
  onUpdateDiceRoll,
  rollStatusLookup,
}) => {
  const { env, hasPddErrored, isPddReady } = useContext(DiceWorkerContext);
  const [{ userId }] = useContext(GameLogContext);
  const { campaign } = useCharacterEngine();
  const gameId = campaign ? String(campaign.id) : undefined;
  const dmId = campaign
    ? String(CampaignUtils.getDmUserId(campaign))
    : undefined;
  const [groupRolls, setGroupRolls] = useState<
    Record<
      string,
      {
        rollKey: string;
        properties: Partial<RollResultContract>;
        nextRollKey: string | null;
      }
    >
  >({});
  const user = useAuth();
  const classNames = useMemo<Array<string>>(
    () => ["ddbc-dice-roll-group", className],
    [className]
  );

  const areRollsEmpty = useMemo<boolean>(
    () => !diceRolls.some((roll) => roll.rollTotal !== null),
    [diceRolls]
  );

  const areScoresUnassigned = useMemo<boolean>(
    () =>
      !diceRolls.some(
        (roll) => roll.assignedValue !== null && roll.assignedValue !== ""
      ),
    [diceRolls]
  );

  const assignedValues = useMemo<Array<string>>(() => {
    return diceRolls
      .map((roll) => roll.assignedValue)
      .filter(TypeScriptUtils.isNotNullOrUndefined);
  }, [diceRolls]);

  const generatedOptions = useMemo<Array<Array<HtmlSelectOption>>>(() => {
    if (!options) {
      return [];
    }

    return diceRolls.map((roll): Array<HtmlSelectOption> => {
      if (!exclusiveOptions) {
        return options;
      }

      return options.filter(
        (option) =>
          roll.assignedValue === option.value ||
          !assignedValues.includes(option.value.toString())
      );
    });
  }, [options, diceRolls, exclusiveOptions, assignedValues]);

  const generatedRollRequests = useMemo<Array<RollDicePayload["data"]>>(() => {
    let requests: Array<RollDicePayload["data"]> = [];
    for (let i = 0; i < diceRolls.length; i++) {
      if (!Array.isArray(diceRollRequest)) {
        requests.push(diceRollRequest);
      } else {
        if (diceRollRequest[i]) {
          requests.push(diceRollRequest[i]);
        } else {
          requests.push(diceRollRequest[0]);
        }
      }
    }

    return requests;
  }, [diceRollRequest, diceRolls]);

  const handleRoll = useCallback(
    (
      rollKey: string,
      diceRollRequest: RollDicePayload["data"],
      nextRollKey: string | null
    ): void => {
      const diceNotation = `${diceRollRequest.rolls[0].diceNotation.set
        .map((set) => `${set?.count}${set?.dieType}`)
        .join("")}`;
      const { data } = rollDice(
        {
          action: diceRollRequest.action,
          diceNotation,
          entityId: String(diceRollRequest.context?.entityId),
          entityType: String(diceRollRequest.context?.entityType),
          gameId,
          rollType: diceRollRequest.rolls[0].rollType,
          setId: String(
            useUserDiceDataStore.getState().getUserDiceData(Number(userId))
              ?.setId
          ),
          userId,
          avatarUrl: diceRollRequest.context?.avatarUrl,
          name: diceRollRequest.context?.name,
          operand: diceRollRequest.rolls[0].diceNotation.set[0]?.operand,
          dmId,
        },
        hasPddErrored,
        isPddReady,
        (eventPayload) =>
          instrumentDiceRoll(
            user,
            eventPayload,
            Platform.Web,
            "characterBuilderRollGroup",
            campaign?.id,
            undefined,
            env
          )
      );

      const rollValues = DiceComponentUtils.generateRollValueContracts(data);

      if (!rollValues) {
        throw new Error("error in rollResult values");
      }

      const newGroupRoll = {
        rollKey,
        properties: {
          rollValues,
          rollTotal: data.rolls[0].result?.total,
        },
        nextRollKey,
      };

      setGroupRolls((prevState) => ({
        ...prevState,
        [String(data?.rollId)]: newGroupRoll,
      }));

      if (
        data.rollId &&
        useUserDiceDataStore.getState().getUserDiceData(Number(userId))
          ?.settings.visibility === DiceVisibilities.Disabled
      ) {
        //update status
        onSetRollStatus(newGroupRoll.rollKey, DataLoadingStatusEnum.LOADED);

        //update diceRoll
        onUpdateDiceRoll(
          newGroupRoll.rollKey,
          newGroupRoll.properties,
          newGroupRoll.nextRollKey
        );
      } else {
        onSetRollStatus(rollKey, DataLoadingStatusEnum.LOADING);
      }
    },
    [
      campaign?.id,
      dmId,
      env,
      gameId,
      hasPddErrored,
      isPddReady,
      onSetRollStatus,
      onUpdateDiceRoll,
      user,
      userId,
    ]
  );

  const handleChange = useCallback(
    (
      rollKey: string,
      newValue: string | null,
      prevValue: string | null,
      nextRollKey: string | null,
      rollTotal: number | null,
      rollValues: Array<RollValueContract>
    ): void => {
      if (newValue === prevValue) {
        return;
      }

      onUpdateDiceRoll(
        rollKey,
        { assignedValue: newValue, rollTotal, rollValues },
        nextRollKey
      );
    },
    []
  );

  const handleConfirm = useCallback((): void => {
    if (onConfirm) {
      return onConfirm(groupKey, diceRolls);
    }
  }, [onConfirm, groupKey, diceRolls]);

  const handleResetGroup = useCallback((): void => {
    if (onResetGroup) {
      onResetGroup(groupKey, diceRolls);
    }

    diceRolls.forEach((roll) => {
      onSetRollStatus(roll.rollKey, DataLoadingStatusEnum.NOT_INITIALIZED);
    });
  }, [onResetGroup, groupKey]);

  const handleRemoveGroup = useCallback((): void => {
    if (onRemoveGroup) {
      onRemoveGroup(groupKey, nextGroupKey);
    }
  }, [onRemoveGroup, groupKey, nextGroupKey]);

  useMessageBroker(
    useCallback(
      (message) => {
        if (
          isMessage(message) &&
          message.eventType === "dice/roll/fulfilled" &&
          groupRolls[message.data.rollId]
        ) {
          //update status
          onSetRollStatus(
            groupRolls[message.data.rollId].rollKey,
            DataLoadingStatusEnum.LOADED
          );

          //update diceRoll
          onUpdateDiceRoll(
            groupRolls[message.data.rollId].rollKey,
            groupRolls[message.data.rollId].properties,
            groupRolls[message.data.rollId].nextRollKey
          );
        }
      },
      [groupRolls, onSetRollStatus, onUpdateDiceRoll]
    )
  );

  return (
    <div className={classNames.join(" ")}>
      <div className="ddbc-dice-roll-group__actions">
        {showRemoveButton && (
          <RemoveButton
            size="small"
            onClick={handleRemoveGroup}
            enableConfirm={true}
          >
            Delete group
          </RemoveButton>
        )}
      </div>
      <div className="ddbc-dice-roll-group__rolls">
        {diceRolls.map((diceRoll, idx) => {
          const diceRollRequest = generatedRollRequests[idx];
          if (!diceRollRequest) {
            return null;
          }
          const rollStatus = HelperUtils.lookupDataOrFallback(
            rollStatusLookup,
            diceRoll.rollKey,
            DataLoadingStatusEnum.NOT_INITIALIZED
          );

          return (
            <DiceRoll
              key={diceRoll.rollKey}
              rollKey={diceRoll.rollKey}
              nextRollKey={diceRoll.nextRollKey}
              rollTotal={diceRoll.rollTotal}
              rollValues={diceRoll.rollValues}
              assignedValue={diceRoll.assignedValue}
              options={generatedOptions[idx]}
              onChange={handleChange}
              onRoll={handleRoll}
              diceRollRequest={diceRollRequest}
              rollStatus={rollStatus}
            />
          );
        })}
      </div>
      <div className="ddbc-dice-roll-group__actions">
        <RemoveButton
          size="medium"
          style="filled"
          onClick={handleResetGroup}
          disabled={areRollsEmpty}
          enableConfirm={true}
        >
          Reset Group
        </RemoveButton>
        {onConfirm && (
          <Button
            size="medium"
            onClick={handleConfirm}
            disabled={areScoresUnassigned}
          >
            {confirmButtonText}
          </Button>
        )}
      </div>
    </div>
  );
};

export default React.memo(DiceRollGroup);
