import React from "react";
import { useToast } from "@chakra-ui/react";
import { useLocation, useNavigate } from "react-router-dom";
import { useDebounce } from "../util";
import useCurrentUser from "../components/useCurrentUser";
import gql from "graphql-tag";
import { useMutation } from "@apollo/client";
import { outfitStatesAreEqual } from "./useOutfitState";

function useOutfitSaving(outfitState, dispatchToOutfit) {
  const { isLoggedIn, id: currentUserId } = useCurrentUser();
  const { pathname } = useLocation();
  const navigate = useNavigate();
  const toast = useToast();

  // There's not a way to reset an Apollo mutation state to clear out the error
  // when the outfit changes… so we track the error state ourselves!
  const [saveError, setSaveError] = React.useState(null);

  // Whether this outfit is new, i.e. local-only, i.e. has _never_ been saved
  // to the server.
  const isNewOutfit = outfitState.id == null;

  // Whether this outfit's latest local changes have been saved to the server.
  // And log it to the console!
  const latestVersionIsSaved =
    outfitState.savedOutfitState &&
    outfitStatesAreEqual(
      outfitState.outfitStateWithoutExtras,
      outfitState.savedOutfitState
    );
  React.useEffect(() => {
    console.debug(
      "[useOutfitSaving] Latest version is saved? %s\nCurrent: %o\nSaved: %o",
      latestVersionIsSaved,
      outfitState.outfitStateWithoutExtras,
      outfitState.savedOutfitState
    );
  }, [
    latestVersionIsSaved,
    outfitState.outfitStateWithoutExtras,
    outfitState.savedOutfitState,
  ]);

  // Only logged-in users can save outfits - and they can only save new outfits,
  // or outfits they created.
  const canSaveOutfit =
    isLoggedIn && (isNewOutfit || outfitState.creator?.id === currentUserId);

  // Users can delete their own outfits too. The logic is slightly different
  // than for saving, because you can save an outfit that hasn't been saved
  // yet, but you can't delete it.
  const canDeleteOutfit = !isNewOutfit && canSaveOutfit;

  const [sendSaveOutfitMutation, { loading: isSaving }] = useMutation(
    gql`
      mutation UseOutfitSaving_SaveOutfit(
        $id: ID # Optional, is null when saving new outfits.
        $name: String # Optional, server may fill in a placeholder.
        $speciesId: ID!
        $colorId: ID!
        $pose: Pose!
        $wornItemIds: [ID!]!
        $closetedItemIds: [ID!]!
      ) {
        outfit: saveOutfit(
          id: $id
          name: $name
          speciesId: $speciesId
          colorId: $colorId
          pose: $pose
          wornItemIds: $wornItemIds
          closetedItemIds: $closetedItemIds
        ) {
          id
          name
          petAppearance {
            id
            species {
              id
            }
            color {
              id
            }
            pose
          }
          wornItems {
            id
          }
          closetedItems {
            id
          }
          creator {
            id
          }
        }
      }
    `,
    {
      context: { sendAuth: true },
      update: (cache, { data: { outfit } }) => {
        // After save, add this outfit to the current user's outfit list. This
        // will help when navigating back to Your Outfits, to force a refresh.
        // https://www.apollographql.com/docs/react/caching/cache-interaction/#example-updating-the-cache-after-a-mutation
        cache.modify({
          id: cache.identify(outfit.creator),
          fields: {
            outfits: (existingOutfitRefs = [], { readField }) => {
              const isAlreadyInList = existingOutfitRefs.some(
                (ref) => readField("id", ref) === outfit.id
              );
              if (isAlreadyInList) {
                return existingOutfitRefs;
              }

              const newOutfitRef = cache.writeFragment({
                data: outfit,
                fragment: gql`
                  fragment NewOutfit on Outfit {
                    id
                  }
                `,
              });

              return [...existingOutfitRefs, newOutfitRef];
            },
          },
        });

        // Also, send a `rename` action, if this is still the current outfit,
        // and the server renamed it (e.g. "Untitled outfit (1)"). (It's
        // tempting to do a full reset, in case the server knows something we
        // don't, but we don't want to clobber changes the user made since
        // starting the save!)
        if (outfit.id === outfitState.id && outfit.name !== outfitState.name) {
          dispatchToOutfit({
            type: "rename",
            outfitName: outfit.name,
          });
        }
      },
    }
  );

  const saveOutfitFromProvidedState = React.useCallback(
    (outfitState) => {
      sendSaveOutfitMutation({
        variables: {
          id: outfitState.id,
          name: outfitState.name,
          speciesId: outfitState.speciesId,
          colorId: outfitState.colorId,
          pose: outfitState.pose,
          wornItemIds: [...outfitState.wornItemIds],
          closetedItemIds: [...outfitState.closetedItemIds],
        },
      })
        .then(({ data: { outfit } }) => {
          // Navigate to the new saved outfit URL. Our Apollo cache should pick
          // up the data from this mutation response, and combine it with the
          // existing cached data, to make this smooth without any loading UI.
          if (pathname !== `/outfits/[outfitId]`) {
            navigate(`/outfits/${outfit.id}`);
          }
        })
        .catch((e) => {
          console.error(e);
          setSaveError(e);
          toast({
            status: "error",
            title: "Sorry, there was an error saving this outfit!",
            description: "Maybe check your connection and try again.",
          });
        });
    },
    // It's important that this callback _doesn't_ change when the outfit
    // changes, so that the auto-save effect is only responding to the
    // debounced state!
    [sendSaveOutfitMutation, pathname, navigate, toast]
  );

  const saveOutfit = React.useCallback(
    () => saveOutfitFromProvidedState(outfitState.outfitStateWithoutExtras),
    [saveOutfitFromProvidedState, outfitState.outfitStateWithoutExtras]
  );

  // Auto-saving! First, debounce the outfit state. Use `outfitStateWithoutExtras`,
  // which only contains the basic fields, and will keep a stable object
  // identity until actual changes occur. Then, save the outfit after the user
  // has left it alone for long enough, so long as it's actually different
  // than the saved state.
  const debouncedOutfitState = useDebounce(
    outfitState.outfitStateWithoutExtras,
    2000,
    {
      // When the outfit ID changes, update the debounced state immediately!
      forceReset: (debouncedOutfitState, newOutfitState) =>
        debouncedOutfitState.id !== newOutfitState.id,
    }
  );
  // HACK: This prevents us from auto-saving the outfit state that's still
  //       loading. I worry that this might not catch other loading scenarios
  //       though, like if the species/color/pose is in the GQL cache, but the
  //       items are still loading in... not sure where this would happen tho!
  const debouncedOutfitStateIsSaveable =
    debouncedOutfitState.speciesId &&
    debouncedOutfitState.colorId &&
    debouncedOutfitState.pose;
  React.useEffect(() => {
    if (
      !isNewOutfit &&
      canSaveOutfit &&
      debouncedOutfitStateIsSaveable &&
      !outfitStatesAreEqual(debouncedOutfitState, outfitState.savedOutfitState)
    ) {
      console.info(
        "[useOutfitSaving] Auto-saving outfit\nSaved: %o\nCurrent (debounced): %o",
        outfitState.savedOutfitState,
        debouncedOutfitState
      );
      saveOutfitFromProvidedState(debouncedOutfitState);
    }
  }, [
    isNewOutfit,
    canSaveOutfit,
    debouncedOutfitState,
    debouncedOutfitStateIsSaveable,
    outfitState.savedOutfitState,
    saveOutfitFromProvidedState,
  ]);

  // When the outfit changes, clear out the error state from previous saves.
  // We'll send the mutation again after the debounce, and we don't want to
  // show the error UI in the meantime!
  React.useEffect(() => {
    setSaveError(null);
  }, [outfitState.outfitStateWithoutExtras]);

  return {
    canSaveOutfit,
    canDeleteOutfit,
    isNewOutfit,
    isSaving,
    latestVersionIsSaved,
    saveError,
    saveOutfit,
  };
}

export default useOutfitSaving;