import axios, { Canceler } from "axios";
import { orderBy } from "lodash";
import * as React from "react";

import { LoadingPlaceholder } from "@dndbeyond/character-components/es";
import {
  ApiAdapterPromise,
  ApiAdapterRequestConfig,
  ApiAdapterUtils,
  ApiResponse,
  Constants,
  ContainerManager,
  DefinitionPool,
  DefinitionPoolUtils,
  DefinitionUtils,
  EntitledEntity,
  HelperUtils,
  HtmlSelectOption,
  HtmlSelectOptionGroup,
  InfusionChoice,
  InfusionChoiceUtils,
  InfusionDefinitionContract,
  InfusionUtils,
  InventoryManager,
  Item,
  ItemUtils,
  KnownInfusionUtils,
  Modifier,
  RuleData,
  RuleDataUtils,
  TypeValueLookup,
} from "@dndbeyond/character-rules-engine/es";

import { HtmlContent } from "~/components/HtmlContent";
import { Select } from "~/components/Select";

import DataLoadingStatusEnum from "../../constants/DataLoadingStatusEnum";
import { AppLoggerUtils, TypeScriptUtils } from "../../utils";

type OnInfusionChangeFunc = (
  choiceKey: string,
  infusionId: string | null,
  oldInfusionId: string | null,
  accept: () => void,
  reject: () => void
) => void;

interface Props {
  infusionChoices: Array<InfusionChoice>;
  contextLevel: number | null;
  globalModifiers: Array<Modifier>;
  typeValueLookup: TypeValueLookup;
  ruleData: RuleData;
  definitionPool: DefinitionPool;
  knownInfusionLookup: Record<string, InfusionChoice>;
  knownReplicatedItems: Array<string>;
  onInfusionChoiceItemChangePromise?: (
    infusionChoiceKey: string,
    infusionId: string,
    itemDefinitionKey: string,
    itemName: string,
    accept: () => void,
    reject: () => void
  ) => void;
  onInfusionChoiceItemDestroyPromise?: (
    infusionChoiceKey: string,
    accept: () => void,
    reject: () => void
  ) => void;
  onInfusionChoiceChangePromise?: (
    infusionChoiceKey: string,
    infusionId: string,
    accept: () => void,
    reject: () => void
  ) => void;
  onInfusionChoiceDestroyPromise?: (
    infusionChoiceKey: string,
    accept: () => void,
    reject: () => void
  ) => void;
  onInfusionChoiceCreatePromise?: (
    infusionChoiceKey: string,
    infusionId: string,
    accept: () => void,
    reject: () => void
  ) => void;
  loadAvailableInfusions: (
    additionalConfig?: Partial<ApiAdapterRequestConfig>
  ) => ApiAdapterPromise<
    ApiResponse<EntitledEntity<InfusionDefinitionContract>>
  >;
  onDefinitionsLoaded?: (
    definitions: Array<InfusionDefinitionContract>,
    accessTypes: Record<string, number>
  ) => void;
  inventoryManager: InventoryManager;
}
interface State {
  equipment: Array<Item>;
  loadingStatus: DataLoadingStatusEnum;
}
class InfusionChoiceManager extends React.PureComponent<Props, State> {
  static defaultProps = {
    loadAvailableInfusions: null,
  };

  loadItemsCanceler: null | Canceler = null;
  loadInfusionsCanceler: null | Canceler = null;

  constructor(props: Props) {
    super(props);

    this.state = {
      equipment: [],
      loadingStatus: DataLoadingStatusEnum.NOT_LOADED,
    };
  }

  componentDidMount() {
    const { loadingStatus } = this.state;
    const {
      infusionChoices,
      inventoryManager,
      loadAvailableInfusions,
      onDefinitionsLoaded,
    } = this.props;

    if (
      infusionChoices.length &&
      loadingStatus === DataLoadingStatusEnum.NOT_LOADED
    ) {
      this.setState({
        loadingStatus: DataLoadingStatusEnum.LOADING,
      });

      // TODO fix typings "any" once axios fixes how all can have multiple return types
      axios
        .all<any>([
          inventoryManager.getInventoryShoppe({
            onSuccess: (shoppeContainer: ContainerManager) => {
              this.setState({
                equipment: shoppeContainer
                  .getInventoryItems()
                  .items.map((item) => item.getItem()),
                loadingStatus: DataLoadingStatusEnum.LOADED,
              });
            },
            additionalApiConfig: {
              cancelToken: new axios.CancelToken((c) => {
                this.loadItemsCanceler = c;
              }),
            },
          }),
          loadAvailableInfusions({
            cancelToken: new axios.CancelToken((c) => {
              this.loadInfusionsCanceler = c;
            }),
          }),
        ])
        .then(([equipmentResponse, infusionResponse]) => {
          //do nothing with equipmentResponse since we moved converted to the inventoryManager which sets state on onsuccess

          let infusionData =
            ApiAdapterUtils.getResponseData<
              EntitledEntity<InfusionDefinitionContract>
            >(infusionResponse);
          if (
            infusionData &&
            infusionData.definitionData.length > 0 &&
            onDefinitionsLoaded
          ) {
            onDefinitionsLoaded(
              infusionData.definitionData,
              infusionData.accessTypes
            );
          }

          this.loadItemsCanceler = null;
          this.loadInfusionsCanceler = null;
        })
        .catch(AppLoggerUtils.handleAdhocApiError);
    }
  }

  componentWillUnmount(): void {
    if (this.loadItemsCanceler !== null) {
      this.loadItemsCanceler();
    }
    if (this.loadInfusionsCanceler !== null) {
      this.loadInfusionsCanceler();
    }
  }

  getItemKeyParts = (itemKey: string): Array<string> => {
    return itemKey.split("|");
  };

  getItemKey = (id: number, name: string): string => {
    return [
      DefinitionUtils.hack__generateDefinitionKey(
        Constants.FUTURE_ITEM_DEFINITION_TYPE,
        id
      ),
      name,
    ].join("|");
  };

  handleChoiceItemChangePromise = (
    infusionChoice: InfusionChoice,
    itemKey: string | null,
    oldItemKey: string | null,
    accept: () => void,
    reject: () => void
  ): void => {
    const {
      onInfusionChoiceItemChangePromise,
      onInfusionChoiceItemDestroyPromise,
    } = this.props;

    const choiceKey = InfusionChoiceUtils.getKey(infusionChoice);

    if (choiceKey === null) {
      reject();
      return;
    }

    if (itemKey) {
      if (onInfusionChoiceItemChangePromise) {
        const knownInfusion =
          InfusionChoiceUtils.getKnownInfusion(infusionChoice);
        if (knownInfusion === null) {
          reject();
          return;
        }

        const simulatedInfusion =
          KnownInfusionUtils.getSimulatedInfusion(knownInfusion);
        if (simulatedInfusion === null) {
          reject();
          return;
        }

        const infusionId = InfusionUtils.getId(simulatedInfusion);
        if (infusionId === null) {
          reject();
          return;
        }

        const itemKeyParts = this.getItemKeyParts(itemKey);

        onInfusionChoiceItemChangePromise(
          choiceKey,
          infusionId,
          itemKeyParts[0],
          itemKeyParts[1],
          accept,
          reject
        );
      }
    } else {
      if (onInfusionChoiceItemDestroyPromise) {
        onInfusionChoiceItemDestroyPromise(choiceKey, accept, reject);
      }
    }
  };

  handleChoiceChangePromise = (
    choiceKey: string,
    infusionId: string | null,
    oldInfusionId: string | null,
    accept: () => void,
    reject: () => void
  ): void => {
    const { onInfusionChoiceChangePromise, onInfusionChoiceDestroyPromise } =
      this.props;

    if (infusionId) {
      if (onInfusionChoiceChangePromise) {
        onInfusionChoiceChangePromise(choiceKey, infusionId, accept, reject);
      }
    } else {
      if (onInfusionChoiceDestroyPromise) {
        onInfusionChoiceDestroyPromise(choiceKey, accept, reject);
      }
    }
  };

  handleChoiceCreatePromise = (
    choiceKey: string,
    infusionId: string,
    oldInfusionId: string | null,
    accept: () => void,
    reject: () => void
  ) => {
    const { onInfusionChoiceCreatePromise } = this.props;

    if (onInfusionChoiceCreatePromise) {
      onInfusionChoiceCreatePromise(choiceKey, infusionId, accept, reject);
    }
  };

  renderInfusionReplicateItemChoice = (
    infusionChoice: InfusionChoice
  ): React.ReactNode => {
    const { equipment, loadingStatus } = this.state;
    const { contextLevel, knownReplicatedItems } = this.props;

    let knownInfusion = InfusionChoiceUtils.getKnownInfusion(infusionChoice);
    if (knownInfusion === null) {
      return null;
    }

    let simulatedInfusion =
      KnownInfusionUtils.getSimulatedInfusion(knownInfusion);
    if (
      simulatedInfusion === null ||
      InfusionUtils.getType(simulatedInfusion) !==
        Constants.InfusionTypeEnum.REPLICATE
    ) {
      return null;
    }

    let contentNode: React.ReactNode;
    const classNames: Array<string> = [
      "ct-infusion-choice-manager__choice",
      "ct-infusion-choice-manager__choice--child",
    ];

    if (loadingStatus !== DataLoadingStatusEnum.LOADED) {
      contentNode = <LoadingPlaceholder />;
    } else {
      let groups: Array<HtmlSelectOptionGroup> = [];
      let infusableItems: Array<Item> = equipment.filter((item) => {
        const levelInfusionGranted = ItemUtils.getLevelInfusionGranted(item);
        const itemDefinitionKey = ItemUtils.getDefinitionKey(item);
        let chosenItemDefinitionKey: string | null = null;
        if (knownInfusion !== null) {
          chosenItemDefinitionKey =
            KnownInfusionUtils.getItemDefinitionKey(knownInfusion);
        }
        const hasKnownReplicatedItem: boolean =
          knownReplicatedItems.includes(itemDefinitionKey);
        if (
          hasKnownReplicatedItem &&
          chosenItemDefinitionKey === itemDefinitionKey
        ) {
          return true;
        }

        return (
          !hasKnownReplicatedItem &&
          levelInfusionGranted !== null &&
          contextLevel !== null &&
          contextLevel >= levelInfusionGranted
        );
      });

      let orderedInfusableItems = orderBy(infusableItems, (item) =>
        ItemUtils.getName(item)
      );

      orderedInfusableItems.forEach((item) => {
        const levelInfusionGranted = ItemUtils.getLevelInfusionGranted(item);

        if (levelInfusionGranted === null) {
          return;
        }

        if (!groups.hasOwnProperty(levelInfusionGranted)) {
          groups[levelInfusionGranted] = {
            optGroupLabel: `Level ${levelInfusionGranted}`,
            options: [],
          };
        }

        const definitionName = ItemUtils.getDefinitionName(item);
        groups[levelInfusionGranted].options.push({
          label: definitionName,
          value: this.getItemKey(
            ItemUtils.getId(item),
            definitionName === null ? "" : definitionName
          ),
        });
      });

      groups.forEach((group) => {
        group.options = orderBy(
          group.options,
          (option: HtmlSelectOption) => option.label
        );
      });

      let selectedValue: string | null = null;
      let knownItemId = KnownInfusionUtils.getItemId(knownInfusion);
      let knownItemName = KnownInfusionUtils.getItemName(knownInfusion);
      if (knownItemId && knownItemName) {
        selectedValue = this.getItemKey(knownItemId, knownItemName);
      }

      if (selectedValue === null) {
        classNames.push("ct-infusion-choice-manager__choice--todo");
      }

      contentNode = (
        <Select
          options={groups}
          onChangeConfirm={this.handleChoiceItemChangePromise.bind(
            this,
            infusionChoice
          )}
          value={selectedValue}
          name="infusion-replicate-item-select"
        />
      );
    }

    return <div className={classNames.join(" ")}>{contentNode}</div>;
  };

  renderInfusionChoice = (infusionChoice: InfusionChoice): React.ReactNode => {
    const { ruleData, knownInfusionLookup, definitionPool, contextLevel } =
      this.props;

    // TODO check typing once definition pool is updated
    let infusionDefinitions = DefinitionPoolUtils.getTypedDefinitionList(
      Constants.DefinitionTypeEnum.INFUSION,
      definitionPool,
      true
    );
    let infusionOptions: Array<HtmlSelectOption> = infusionDefinitions
      .map((infusionDefinition) =>
        InfusionUtils.simulateInfusion(
          DefinitionUtils.getDefinitionKey(infusionDefinition),
          definitionPool
        )
      )
      .filter(TypeScriptUtils.isNotNullOrUndefined)
      .filter((infusion) => {
        // if the infusion allows duplicates just let it through
        if (InfusionUtils.getAllowDuplicates(infusion)) {
          return true;
        }

        const infusionDefinitionKey = InfusionUtils.getDefinitionKey(infusion);
        let knownInfusion =
          InfusionChoiceUtils.getKnownInfusion(infusionChoice);
        // if choice has the infusion, show it in the list
        if (
          knownInfusion !== null &&
          KnownInfusionUtils.getDefinitionKey(knownInfusion) ===
            infusionDefinitionKey
        ) {
          return true;
        }

        // if its already selected somewhere else and is here, it shouldnt be shown
        if (
          infusionDefinitionKey !== null &&
          HelperUtils.lookupDataOrFallback(
            knownInfusionLookup,
            infusionDefinitionKey
          )
        ) {
          return false;
        }

        if (
          !InfusionUtils.validateIsAvailableByContextLevel(
            infusion,
            contextLevel
          )
        ) {
          return false;
        }

        return true;
      })
      .map((infusion) => {
        let label: string | null = InfusionUtils.getName(infusion);

        const sources = InfusionUtils.getSources(infusion);

        const hasAllToggleableSources: boolean = sources.every(
          (sourceMapping) => {
            const sourceDataInfo = RuleDataUtils.getSourceDataInfo(
              sourceMapping.sourceId,
              ruleData
            );
            return sourceDataInfo?.sourceCategory?.isToggleable;
          }
        );

        if (sources.length && hasAllToggleableSources) {
          const calloutSources: Array<string> = sources
            .map((sourceMapping) => {
              const sourceDataInfo = RuleDataUtils.getSourceDataInfo(
                sourceMapping.sourceId,
                ruleData
              );
              return sourceDataInfo?.name ?? null;
            })
            .filter(TypeScriptUtils.isNotNullOrUndefined);

          label = `${calloutSources.join(", ")} • ${label}`;
        }

        const infusionId = InfusionUtils.getId(infusion);
        return {
          label,
          value: infusionId ?? "",
        };
      });

    let knownInfusion = InfusionChoiceUtils.getKnownInfusion(infusionChoice);

    //TODO this typing is weird because of the binding that will happen further down... may need to split this into more components to remove the bind and make more sense
    let onChangePromise: OnInfusionChangeFunc = this.handleChoiceCreatePromise;
    if (knownInfusion !== null) {
      onChangePromise = this.handleChoiceChangePromise;
    }

    let warningNode: React.ReactNode;
    let descriptionNode: React.ReactNode;
    const classNames: Array<string> = ["ct-infusion-choice-manager__choice"];
    if (knownInfusion === null) {
      classNames.push("ct-infusion-choice-manager__choice--todo");
    } else {
      const simulatedInfusion =
        KnownInfusionUtils.getSimulatedInfusion(knownInfusion);
      if (simulatedInfusion !== null) {
        const description = InfusionUtils.getDescription(simulatedInfusion);
        if (description !== null) {
          descriptionNode = (
            <HtmlContent
              className="ct-infusion-choice-manager__choice-description"
              html={description}
              withoutTooltips
            />
          );
        }

        if (
          !InfusionUtils.validateIsAvailableByContextLevel(
            simulatedInfusion,
            contextLevel
          )
        ) {
          classNames.push("ct-infusion-choice-manager__choice--warning");
          warningNode = (
            <div className="ct-infusion-choice-manager__choice-warning">
              <strong>{InfusionUtils.getName(simulatedInfusion)}</strong> is not
              a valid choice because it requires level{" "}
              <strong>{InfusionUtils.getLevel(simulatedInfusion)}</strong>.
            </div>
          );
        }
      }
    }

    let knownInfusionDefinitionKey: string | null = null;
    if (knownInfusion !== null) {
      knownInfusionDefinitionKey =
        KnownInfusionUtils.getDefinitionKey(knownInfusion);
    }

    const choiceKey = InfusionChoiceUtils.getKey(infusionChoice);
    return (
      <div
        className={classNames.join(" ")}
        key={choiceKey === null ? "" : choiceKey}
      >
        <Select
          options={infusionOptions}
          placeholder={`- Choose a Level ${InfusionChoiceUtils.getLevel(
            infusionChoice
          )} Option -`}
          onChangeConfirm={onChangePromise.bind(this, choiceKey)}
          value={
            knownInfusionDefinitionKey === null
              ? null
              : DefinitionUtils.getDefinitionKeyId(knownInfusionDefinitionKey)
          }
          name="infusion-choice-select"
        />
        {this.renderInfusionReplicateItemChoice(infusionChoice)}
        {warningNode}
        {descriptionNode}
      </div>
    );
    //`
  };

  render() {
    const { loadingStatus } = this.state;
    const { infusionChoices } = this.props;

    if (!infusionChoices.length) {
      return null;
    }

    let contentNode: React.ReactNode;
    if (loadingStatus !== DataLoadingStatusEnum.LOADED) {
      contentNode = <LoadingPlaceholder />;
    } else {
      contentNode = (
        <>
          {infusionChoices.map((infusionChoice) =>
            this.renderInfusionChoice(infusionChoice)
          )}
        </>
      );
    }

    return (
      <div className="ct-infusion-choice-manager">
        <div className="ct-infusion-choice-manager__header">
          Infusion Choices
        </div>
        {contentNode}
      </div>
    );
  }
}

export default InfusionChoiceManager;
