import { ApolloClient, createHttpLink, InMemoryCache } from "@apollo/client";
import { loadErrorMessages, loadDevMessages } from "@apollo/client/dev";
import { setContext } from "@apollo/client/link/context";
import { createPersistedQueryLink } from "apollo-link-persisted-queries";

import { getAuthModeFeatureFlag } from "./components/useCurrentUser";

// Use Apollo's error messages in development.
if (process.env["NODE_ENV"] === "development") {
  loadErrorMessages();
  loadDevMessages();
}

// Teach Apollo to load certain fields from the cache, to avoid extra network
// requests. This happens a lot - e.g. reusing data from item search on the
// outfit immediately!
const typePolicies = {
  Query: {
    fields: {
      closetList: (_, { args, toReference }) => {
        return toReference({ __typename: "ClosetList", id: args.id }, true);
      },
      items: (_, { args, toReference }) => {
        return args.ids.map((id) =>
          toReference({ __typename: "Item", id }, true)
        );
      },
      item: (_, { args, toReference }) => {
        return toReference({ __typename: "Item", id: args.id }, true);
      },
      petAppearanceById: (_, { args, toReference }) => {
        return toReference({ __typename: "PetAppearance", id: args.id }, true);
      },
      species: (_, { args, toReference }) => {
        return toReference({ __typename: "Species", id: args.id }, true);
      },
      color: (_, { args, toReference }) => {
        return toReference({ __typename: "Color", id: args.id }, true);
      },
      outfit: (_, { args, toReference }) => {
        return toReference({ __typename: "Outfit", id: args.id }, true);
      },
      user: (_, { args, toReference }) => {
        return toReference({ __typename: "User", id: args.id }, true);
      },
    },
  },

  Item: {
    fields: {
      appearanceOn: (appearance, { args, readField, toReference }) => {
        // If we already have this exact appearance in the cache, serve it!
        if (appearance) {
          return appearance;
        }

        // Otherwise, we're going to see if this is a standard color, in which
        // case we can reuse the standard color appearance if we already have
        // it! This helps for fast loading when switching between standard
        // colors.
        const { speciesId, colorId } = args;
        console.debug(
          "[appearanceOn] seeking cached appearance",
          speciesId,
          colorId,
          readField("id")
        );
        const speciesStandardBodyId = readField(
          "standardBodyId",
          toReference({ __typename: "Species", id: speciesId })
        );
        const colorIsStandard = readField(
          "isStandard",
          toReference({ __typename: "Color", id: colorId })
        );
        if (speciesStandardBodyId == null || colorIsStandard == null) {
          // We haven't loaded all the species/colors into cache yet. We might
          // be loading them, depending on the page? Either way, return
          // `undefined`, meaning we don't know how to serve this from cache.
          // This will cause us to start loading it from the server.
          console.debug("[appearanceOn] species/colors not loaded yet");
          return undefined;
        }

        if (colorIsStandard) {
          const itemId = readField("id");
          console.debug(
            "[appearanceOn] standard color, will read:",
            `item-${itemId}-body-${speciesStandardBodyId}`
          );
          return toReference({
            __typename: "ItemAppearance",
            id: `item-${itemId}-body-${speciesStandardBodyId}`,
          });
        } else {
          console.debug("[appearanceOn] non-standard color, failure");
          // This isn't a standard color, so we don't support special
          // cross-color caching for it. Return `undefined`, meaning we don't
          // know how to serve this from cache. This will cause us to start
          // loading it from the server.
          return undefined;
        }
      },

      currentUserOwnsThis: (cachedValue, { readField }) => {
        if (cachedValue != null) {
          return cachedValue;
        }

        // Do we know what items this user owns? If so, scan for this item.
        const currentUserRef = readField("currentUser", {
          __ref: "ROOT_QUERY",
        });
        if (!currentUserRef) {
          return undefined;
        }
        const thisItemId = readField("id");
        const itemsTheyOwn = readField("itemsTheyOwn", currentUserRef);
        if (!itemsTheyOwn) {
          return undefined;
        }

        const theyOwnThisItem = itemsTheyOwn.some(
          (itemRef) => readField("id", itemRef) === thisItemId
        );
        return theyOwnThisItem;
      },
      currentUserWantsThis: (cachedValue, { readField }) => {
        if (cachedValue != null) {
          return cachedValue;
        }

        // Do we know what items this user owns? If so, scan for this item.
        const currentUserRef = readField("currentUser", {
          __ref: "ROOT_QUERY",
        });
        if (!currentUserRef) {
          return undefined;
        }
        const thisItemId = readField("id");
        const itemsTheyWant = readField("itemsTheyWant", currentUserRef);
        if (!itemsTheyWant) {
          return undefined;
        }

        const theyWantThisItem = itemsTheyWant.some(
          (itemRef) => readField("id", itemRef) === thisItemId
        );
        return theyWantThisItem;
      },
    },
  },

  ClosetList: {
    fields: {
      // When loading the updated contents of a list, replace it entirely.
      items: { merge: false },
    },
  },
};

const httpLink = createHttpLink({
  uri: "https://impress-2020.openneo.net/api/graphql",
});
const buildAuthLink = (getAuth0) =>
  setContext(async (_, { headers = {}, sendAuth = false }) => {
    if (!sendAuth) {
      return;
    }

    const token = await getAccessToken(getAuth0);
    if (token) {
      return {
        headers: {
          ...headers,
          authorization: token ? `Bearer ${token}` : "",
        },
      };
    }
  });

// This is a temporary way to pass the DTIAuthMode feature flag back to the
// server!
const authModeLink = setContext((_, { headers = {} }) => {
  const authMode = getAuthModeFeatureFlag();
  return {
    headers: {
      ...headers,
      "DTI-Auth-Mode": authMode,
    },
  };
});

async function getAccessToken(getAuth0) {
  // Wait for auth0 to stop loading, so we can maybe get a token!
  // We'll do this hackily by checking every 100ms until it's true.
  await new Promise((resolve) => {
    function check() {
      if (getAuth0().isLoading) {
        setTimeout(check, 100);
      } else {
        resolve();
      }
    }
    check();
  });

  const { isAuthenticated, getAccessTokenSilently } = getAuth0();
  if (isAuthenticated) {
    const token = await getAccessTokenSilently();
    return token;
  }
}

const buildLink = (getAuth0) =>
  buildAuthLink(getAuth0)
    .concat(authModeLink)
    .concat(
      createPersistedQueryLink({
        useGETForHashedQueries: true,
      })
    )
    .concat(httpLink);

/**
 * apolloClient is the global Apollo Client instance we use for GraphQL
 * queries. This is how we communicate with the server!
 */
const buildClient = ({ getAuth0, initialCacheState }) => {
  return new ApolloClient({
    link: buildLink(getAuth0),
    cache: new InMemoryCache({ typePolicies }).restore(initialCacheState),
    connectToDevTools: true,
  });
};

export default buildClient;