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

import { buildImpress2020Url } from "./impress-2020-config";

// 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;
				}

				const { speciesId, colorId, altStyleId } = args;
				console.debug(
					"[appearanceOn] seeking cached appearance",
					speciesId,
					colorId,
					altStyleId,
					readField("id"),
				);

				// If this is an alt style, don't try to mess with clever caching.
				// (Note that, if it's already in the cache, the first condition will
				// catch that! This won't *always* force a fresh load!)
				if (altStyleId != null) {
					return undefined;
				}

				// 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 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 cache = new InMemoryCache({ typePolicies });

const httpLink = createHttpLink({
	uri: buildImpress2020Url("/api/graphql"),
});

const link = 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 apolloClient = new ApolloClient({
	link,
	cache,
	connectToDevTools: true,
});

export default apolloClient;