import clsx from "clsx";
import { FC, HTMLAttributes, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";

import { PrerequisiteUtils } from "@dndbeyond/character-rules-engine";

import { Accordion } from "~/components/Accordion";
import { HtmlContent } from "~/components/HtmlContent";
import { Link } from "~/components/Link";
import { PreferenceProgressionTypeEnum as progressionType } from "~/constants";
import { orderBy } from "~/helpers/sortUtils";
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
import { useSource } from "~/hooks/useSource";
import { navigationConfig } from "~/tools/js/CharacterBuilder/config";

import { Button } from "../../../../components/Button";
import { ConfirmClassModal } from "../../components/ConfirmClassModal";
import { Listing } from "../../components/Listing";
import { PortraitName } from "../../components/PortraitName";
import { Search } from "../../components/Search";
import { Spinner } from "../../components/Spinner";
import { RouteKey } from "../../constants";
import { useClassContext } from "../../contexts/Class";
import {
  ClassDefinitionContract,
  ClassItems,
  ClassRequirements,
  ListingItem,
  MulticlassAvailability,
} from "../../types";
import styles from "./styles.module.css";

//showHeader and OnQuickSelect are only be used from QuickBuild.tsx and RandomBuild.tsx
export interface ClassChooseProps extends HTMLAttributes<HTMLDivElement> {
  showHeader?: boolean;
  onQuickSelect?: (charClass: ClassDefinitionContract) => void;
}

export interface SourceGroupMapping {
  name: string;
  id: number;
  items: ClassDefinitionContract[];
  sortOrder: number | undefined;
  isOpen: boolean;
}

/**
 * The page which lists all available classes for the user to choose
 * from when building or updating a character. Can be found at
 * `/characters/:characterId/builder/class/choose`.
 */
export const ClassChoose: FC<ClassChooseProps> = ({
  showHeader = true,
  className,
  onQuickSelect,
  ...props
}) => {
  const navigate = useNavigate();
  const {
    classes: charClasses,
    preferences,
    prerequisiteData,
    startingClass,
    totalClassLevel,
    currentLevel,
    characterId,
    classUtils,
    helperUtils,
    prerequisiteUtils: {
      validatePrerequisiteGrouping: validateGrouping,
      getPrerequisiteGroupingFailures: getGroupingFailures,
    },
  } = useCharacterEngine();
  const { allSources, getSourceCategoryDescription, getSourceCategoryGroups } =
    useSource();
  const {
    filteredClasses,
    allClasses,
    handleSelectClass,
    query,
    setQuery,
    isLoading,
    isModalShowing,
    closeModal,
  } = useClassContext();

  const [mappedSourceCategoryGroups, setMappedSourceCategoryGroups] = useState<
    SourceGroupMapping[]
  >([]);

  useEffect(() => {
    const sourceCategoryGroups = getSourceCategoryGroups(filteredClasses);
    setMappedSourceCategoryGroups(
      sourceCategoryGroups.map((group) => ({
        ...group,
        isOpen: true,
      }))
    );
  }, [filteredClasses]);

  // Determine if the character's XP will be adjusted when adding a new class
  const willXpBeAdjusted =
    charClasses.length > 0 &&
    preferences.progressionType === progressionType.XP &&
    totalClassLevel + 1 > currentLevel;

  // Navigate back to the class manage page
  const handleNavigate = (): void =>
    navigate(
      navigationConfig
        .getRouteDefPath(RouteKey.CLASS_MANAGE)
        .replace(":characterId", characterId)
    );

  // Return requirements for multiclassing based on existing classes
  const getMulticlassAvailability = (
    classes: ClassItems
  ): Array<MulticlassAvailability> => {
    const { enforceMulticlassRules } = preferences;
    let canStartingClassMulticlass = false;
    let startingClassRequirements: ClassRequirements = [];

    if (enforceMulticlassRules) {
      if (startingClass) {
        canStartingClassMulticlass = validateGrouping(
          classUtils.getPrerequisites(startingClass),
          prerequisiteData
        );
        startingClassRequirements = getGroupingFailures(
          classUtils.getPrerequisites(startingClass),
          prerequisiteData
        );
      }
    } else {
      canStartingClassMulticlass = true;
    }

    return classes.map(
      ({ id: classId, prerequisites }): MulticlassAvailability => {
        let canMulticlass = false;

        // If there are no prerequisites, the class can always be
        const missingRequirements = getGroupingFailures(
          prerequisites,
          prerequisiteData
        );

        if (enforceMulticlassRules) {
          canMulticlass =
            canStartingClassMulticlass &&
            validateGrouping(prerequisites, prerequisiteData);
        } else {
          canMulticlass = true;
        }

        return {
          classId,
          canStartingClassMulticlass,
          startingClassRequirements,
          canMulticlass,
          missingRequirements,
        };
      }
    );
  };

  const getDisabledIds = (classes: ClassItems): Array<number> => {
    if (startingClass === null || !classes.length) {
      return [];
    }

    // const classDefinitions = classes.map((item) => item.entity);
    const multiclassAvailability = getMulticlassAvailability(classes);

    // disable any current classes
    let currentClassIds = charClasses.map((charClass) =>
      classUtils.getId(charClass)
    );

    // disable any multiclass options that don't meet required prerequisites
    let canMulticlassIds = multiclassAvailability
      .filter((classInfo) => !classInfo.canMulticlass)
      .map((classInfo) => classInfo.classId);

    return [...currentClassIds, ...canMulticlassIds];
  };

  // Transform the class items into a format that listing can use
  const transformClassItems = (data: ClassItems): Array<ListingItem> => {
    let classDefinitions = [...data];

    // Get the multiclass availability for each class
    const availability = getMulticlassAvailability(classDefinitions);

    return classDefinitions.map((classItem: ClassDefinitionContract) => {
      const { id, sources, name, portraitAvatarUrl } = classItem;
      // Find the class in the character's classes
      const existingClass = charClasses.find(
        (cls) => classUtils.getId(cls) === id
      );

      // Find the multiclass info for the class
      const multiclassInfo = availability.find((cls) => cls.classId === id);
      let metaItems: Array<{ type: string; text: string }> = [];

      // Add the source descriptions to the meta items
      if (sources)
        sources.forEach(({ sourceId }) => {
          let source = helperUtils.lookupDataOrFallback(allSources, sourceId);

          // If a source has a description and is toggleable, add it to the meta items
          if (
            source?.description !== null &&
            source?.sourceCategory?.isToggleable
          )
            metaItems.push({
              type: "normal",
              text: source.description,
            });
        });

      // If the character is already multiclassed
      if (charClasses && charClasses.length >= 1) {
        if (existingClass) {
          const { level, isStartingClass } = existingClass;
          const currentLevel = `Level ${level}`;
          const startingClass = isStartingClass ? " • Starting Class" : "";

          // Add the class level to the meta items
          metaItems.push({
            type: "normal",
            text: `${currentLevel}${startingClass}`,
          });
        }
        // If the class is the starting class
        else if (multiclassInfo) {
          const {
            canMulticlass,
            canStartingClassMulticlass,
            missingRequirements,
            startingClassRequirements,
          } = multiclassInfo;
          // Add an error message to the meta items
          if (!canMulticlass || !canStartingClassMulticlass)
            metaItems.push({
              type: "error",
              text: !canStartingClassMulticlass
                ? `Starting class does not meet multiclass prerequisites - ${PrerequisiteUtils.getPrerequisiteFailuresText(
                    startingClassRequirements
                  )}`
                : !canMulticlass
                ? `Prerequisites not met - ${PrerequisiteUtils.getPrerequisiteFailuresText(
                    missingRequirements
                  )}`
                : "",
            });
        }
      }

      return {
        id,
        metaItems,
        entityTypeId: -1,
        heading: name || "",
        image: portraitAvatarUrl || null,
        onClick: handleSelectClass,
        entity: classItem,
        type: "class",
      };
    });
  };

  const handleOverrideClick = (override): void => {
    const updatedGroups = mappedSourceCategoryGroups.map((group) => ({
      ...group,
      isOpen: override,
    }));

    setMappedSourceCategoryGroups(updatedGroups);
  };

  const handleSourceCategoryClick = (id: string, state: boolean): void => {
    const parsedId = parseInt(id);
    const updatedGroups = mappedSourceCategoryGroups.map((group) => ({
      ...group,
      isOpen: group.id === parsedId ? state : group.isOpen,
    }));

    setMappedSourceCategoryGroups(updatedGroups);
  };

  return (
    <div
      className={clsx([styles.page, className])}
      {...props}
      data-testid="chooseClassSection"
    >
      {showHeader && (
        <>
          <PortraitName />
          <hr className={styles.divider} />
          <h2 className={styles.title}>
            Choose a Class{startingClass && " to Multiclass"}
          </h2>
          {startingClass && (
            <>
              <p className={styles.text}>
                Learn more about multiclassing{" "}
                <Link
                  href="/sources/dnd/free-rules/creating-a-character#Multiclassing"
                  target="_blank"
                  rel="noopener noreferrer"
                >
                  here
                </Link>
                .
              </p>
              <p className={styles.multiclassMessage}>
                Classes from the 2024 and 2014 rulebooks are not designed to be
                multiclassed together.
              </p>
            </>
          )}
          {charClasses.length > 0 && (
            <div className={styles.buttons}>
              {willXpBeAdjusted && (
                <p className={styles.error}>
                  Your XP total will be adjusted when you add your new class.
                </p>
              )}
              <Button
                className={styles.navigateButton}
                variant="builder"
                onClick={handleNavigate}
              >
                Cancel Class Add
              </Button>
            </div>
          )}
        </>
      )}
      {isLoading ? (
        <Spinner />
      ) : (
        <>
          <Search
            className={styles.search}
            value={query}
            onChange={(e) => setQuery(e.target.value)}
          />

          <div className={styles.collapseExpand}>
            <Button
              variant="text"
              size="x-small"
              className={styles.collapseExpandButton}
              onClick={() => handleOverrideClick(false)}
              forceThemeMode="light"
            >
              Collapse All
            </Button>
            <Button
              variant="text"
              size="x-small"
              className={styles.collapseExpandButton}
              onClick={() => handleOverrideClick(true)}
              forceThemeMode="light"
            >
              Expand All
            </Button>
          </div>

          {mappedSourceCategoryGroups.length > 0 ? (
            mappedSourceCategoryGroups.map((category) => {
              const description = getSourceCategoryDescription(category.id);
              const metaItems = description
                ? [
                    <HtmlContent
                      html={description}
                      className={styles.accordionText}
                    />,
                  ]
                : [];

              return (
                <Accordion
                  id={category.id.toString()}
                  className={styles.accordion}
                  summary={<h3 className={styles.heading}>{category.name}</h3>}
                  summaryMetaItems={metaItems}
                  variant="text"
                  resetOpen={allClasses.length !== filteredClasses.length}
                  key={category.id}
                  handleIsOpen={handleSourceCategoryClick}
                  override={category.isOpen}
                  forceShow
                >
                  <Listing
                    items={orderBy(
                      transformClassItems(category.items),
                      "heading"
                    )}
                    disabledIds={getDisabledIds(category.items)}
                    onQuickSelect={onQuickSelect}
                  />
                </Accordion>
              );
            })
          ) : (
            <p className={styles.notFound}>
              No Class options available. Return to the <strong>Home</strong>{" "}
              tab to enable more source categories.
            </p>
          )}
        </>
      )}
      <ConfirmClassModal open={isModalShowing} onClose={closeModal} />
    </div>
  );
};
