import notEmpty from '@inject/shared/utils/notEmpty'
import type { Cache } from '@urql/exchange-graphcache'
import { offlineExchange } from '@urql/exchange-graphcache'
import type { DefaultStorage } from '@urql/exchange-graphcache/default-storage'
import { makeDefaultStorage } from '@urql/exchange-graphcache/default-storage'
import type { ResultOf, VariablesOf } from 'gql.tada'
import { reverse, uniq } from 'lodash'
import type { Exchange } from 'urql'
import { sanitizeHtmlCustom } from '../../client/sanitize'
import { ActionLog, Exercise, MilestoneState } from '../../fragments'
import { graphql } from '../../graphql'
import {
  GetDefinitions,
  GetExerciseLoopStatus,
  GetExercises,
} from '../../queries'
import schemaIntrospected from '../../schema.json'
import type { GraphCacheConfig, GraphCacheKeysConfig } from '../cache-typing'
import { processKVSubscription } from '../utils'
import commitActionLogToCache from './cacheFunctions/commitActionLogToCache'
import getTeamMilestones from './cacheFunctions/getTeamMilestones'
import invalidateUserList from './cacheFunctions/invalidateUserList'
import updateEmailThreads from './cacheFunctions/updateEmailThreads'

//@ts-ignore
const storage = makeDefaultStorage({
  idbName: 'graphcache-v3', // The name of the IndexedDB database
  maxAge: 7, // The maximum age of the persisted data in days
})

const makeLocalStorage = () => {
  const cache = {}

  return {
    writeData(delta) {
      return Promise.resolve().then(() => {
        Object.assign(cache, delta)
        localStorage.setItem('data', JSON.stringify(cache))
      })
    },
    readData() {
      return Promise.resolve().then(() => {
        Object.assign(cache, {})
        return cache
      })
    },
    writeMetadata(data) {
      localStorage.setItem('metadata', JSON.stringify(data))
    },
    readMetadata: () => Promise.resolve().then(() => null),
  } as DefaultStorage
}

if (typeof window === 'undefined') {
  throw Error('Window is undefined')
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const isEmailThreadArchived = (cache: Cache, emailThread: any) =>
  !!cache.resolve(emailThread, 'archived')

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const isEmailThreadSent = (cache: Cache, emailThread: any, teamId: string) => {
  const emails = cache.resolve(emailThread, 'emails')
  if (!Array.isArray(emails)) {
    return false
  }
  const senders = emails
    .map(email => cache.resolve(email, 'sender'))
    .filter(sender => typeof sender === 'string')
  const teams = senders
    .map(sender => cache.resolve(sender, 'team'))
    .filter(team => typeof team === 'string')
  return teams.some(team => team === `TeamType:${teamId}`)
}

const isEmailThreadReceived = (
  cache: Cache,
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  emailThread: any
) => {
  const emails = cache.resolve(emailThread, 'emails')
  if (!Array.isArray(emails)) {
    return false
  }
  const senders = emails
    .map(email => cache.resolve(email, 'sender'))
    .filter(sender => typeof sender === 'string')
  return senders.some(sender => !cache.resolve(sender, 'team'))
}

const cache: Exchange = offlineExchange<GraphCacheConfig>({
  schema: schemaIntrospected,
  //@ts-ignore
  storage: import.meta.env.MODE === 'production' ? storage : makeLocalStorage(),
  logger: (severity, msg) => {
    switch (severity) {
      case 'debug':
        console.log('Cache Debug:', msg)
        break
      case 'error':
        console.error('Cache Error:', msg)
        break
      case 'warn':
        console.warn('Cache Warning:', msg)
        break
    }
  },
  directives: {
    filterEmails(directiveArgs) {
      const received = !!directiveArgs?.received as boolean
      const sent = !!directiveArgs?.sent as boolean
      const archived = !!directiveArgs?.archived as boolean
      return (_, args, cache, info) => {
        if (info.fieldName === 'emailThreads') {
          // only allow singular op
          if (Number(received) + Number(sent) + Number(archived) !== 1) {
            console.warn(
              'Unsupported selection of received/sent/archived, support is only for one'
            )
            return cache.resolve(info.parentKey, info.fieldName, args)
          }
          const emailThreads = cache.resolve(
            info.parentKey,
            info.fieldName,
            args
          )

          if (!Array.isArray(emailThreads)) {
            return cache.resolve(info.parentKey, info.fieldName, args)
          }

          const teamId = args.teamId as string
          return emailThreads.filter(emailThread => {
            if (received) {
              return (
                !isEmailThreadArchived(cache, emailThread) &&
                isEmailThreadReceived(cache, emailThread)
              )
            }
            if (sent) {
              return (
                !isEmailThreadArchived(cache, emailThread) &&
                isEmailThreadSent(cache, emailThread, teamId)
              )
            }
            if (archived) {
              return isEmailThreadArchived(cache, emailThread)
            }
          })
        } else {
          console.error('Directive for unsupported query', info)

          return cache.resolve(info.parentKey, info.fieldName, args)
        }
      }
    },
  },
  keys: new Proxy<GraphCacheKeysConfig>(
    {
      TeamChannelReceipt: ({ id, teamId }) => {
        if (!id || !teamId) {
          throw Error('Fetching error')
        }
        return `${teamId}.${id}`
      },
      EmailDraftType: ({ emailThreadId, instructor, teamId }) => {
        if (!emailThreadId || !teamId || instructor === undefined) {
          throw Error('Fetching error')
        }
        return `${teamId}.${instructor}.${emailThreadId || 'root'}`
      },
    },
    {
      get(target, prop) {
        const fallback = ({ id }: { id: string }) => {
          if (id === null) {
            console.warn(
              `Fallback for ${prop.toString()} has no existing ID. Is it intentional?`
            )
          }
          return id
        }
        //@ts-ignore
        return target[prop] || fallback
      },
    }
  ),
  updates: {
    Query: {
      teamActionLogs(parent, _, cache) {
        const actionLogs = parent.teamActionLogs || []
        // instead of committing it's more sane to have "kill-first" approach, in short, the refetching of query forces recalculation of all teamChannelLogs :)
        actionLogs.forEach(actionLog => {
          cache.invalidate('Query', 'teamChannelLogs', {
            teamId: actionLog.team?.id || '0',
            channelId: actionLog.channel?.id || '0',
          })
        })
      },
      analyticsActionLogs(parent, { exerciseId, newestFirst }, cache, info) {
        const actionLogs = parent.analyticsActionLogs || []

        const reverseLinks = reverse(
          cache.resolve(info.parentKey, info.parentFieldKey) as string[]
        )
        if (newestFirst) {
          cache.link(
            'Query',
            'analyticsActionLogs',
            {
              exerciseId,
              newestFirst: false,
            },
            reverseLinks
          )
          cache.link(
            'Query',
            'analyticsActionLogs',
            {
              exerciseId,
            },
            reverseLinks
          )
        } else {
          cache.link(
            'Query',
            'analyticsActionLogs',
            {
              exerciseId,
              newestFirst: true,
            },
            reverseLinks
          )
        }

        const teamIdSet = new Set<string | null>()
        actionLogs.forEach(actionLog => {
          cache.invalidate('Query', 'teamChannelLogs', {
            teamId: actionLog.id || '0',
            channelId: actionLog.channel?.id || '0',
          })
          teamIdSet.add(actionLog.team?.id ?? null)
        })
        teamIdSet.forEach(
          id =>
            id &&
            cache.invalidate('Query', 'actionLogs', {
              teamId: id,
            })
        )
      },
      teamMilestones(parent, _, cache) {
        parent.teamMilestones.forEach(teamMilestoneKey => {
          const activity = cache.resolve(
            cache.keyOfEntity(teamMilestoneKey),
            'activity'
          )
          if (activity) {
            const teamObjective = cache.resolve(
              activity.toString(),
              'teamObjective'
            )
            cache.invalidate(activity.toString(), 'reached')
            if (teamObjective) {
              cache.invalidate(activity.toString(), 'reached')
            }
          }
        })
      },
      analyticsMilestones(parent, _, cache) {
        parent.analyticsMilestones.forEach(analyticsMilestoneKey => {
          const activity = cache.resolve(
            cache.keyOfEntity(analyticsMilestoneKey),
            'activity'
          )
          if (activity) {
            const teamObjective = cache.resolve(
              activity.toString(),
              'teamObjective'
            )
            cache.invalidate(activity.toString(), 'reached')
            if (teamObjective) {
              cache.invalidate(activity.toString(), 'reached')
            }
          }
        })
      },
    },
    Mutation: {
      deleteEmailDraft(parent, args, cache) {
        if (parent.deleteEmailDraft?.operationDone) {
          const { emailThreadId, instructor, teamId } = args
          cache.invalidate('Query', 'getEmailDraft', {
            emailThreadId,
            instructor,
            teamId,
          })
          cache.invalidate('Query', 'getEmailDrafts', {
            instructor,
            teamId,
          })
        }
      },
      writeReadReceipt({ writeReadReceipt }) {
        return {
          __typename: 'ActionLogType',
          id: writeReadReceipt?.id,
          readReceipt: writeReadReceipt?.readReceipt,
        }
      },
      writeReadReceiptEmail({ writeReadReceiptEmail }) {
        return {
          __typename: 'EmailType',
          id: writeReadReceiptEmail?.id,
          readReceipt: writeReadReceiptEmail?.readReceipt,
        }
      },
      setEmailDraft(parent, _args, cache) {
        const emailThreadId = parent.setEmailDraft?.emailThreadId
        const instructor = parent.setEmailDraft?.instructor
        const teamId = parent.setEmailDraft?.teamId

        if (emailThreadId && instructor && teamId) {
          cache.invalidate('Query', 'getEmailDraft', {
            emailThreadId,
            instructor,
            teamId,
          })
          cache.invalidate('Query', 'getEmailDrafts', {
            instructor,
            teamId,
          })
        }
      },
      deleteUsers(_parent, args, cache) {
        args.userIds.forEach(id => {
          cache.invalidate({
            __typename: 'UserType',
            id: id,
          })
        })
      },
      setIsUnreadChannel(_, args, cache) {
        const key = cache.keyOfEntity({
          __typename: 'TeamChannelReceipt',
          id: args.channelId,
          teamId: args.teamId,
          isUnread: args.isUnread,
        })

        cache.writeFragment(
          graphql(`
            fragment _ on TeamChannelReceipt {
              id
              teamId
              isUnread
            }
          `),
          {
            id: args.channelId,
            teamId: args.teamId,
            isUnread: args.isUnread,
          }
        )

        const params = [key, 'readReceipt'] as const
        const link = cache.resolve(...params) || []
        if (Array.isArray(link) && !link.includes(key)) {
          cache.link(...params, [...link, key])
        }
      },
      reloadTable(_, __, cache) {
        invalidateUserList(cache)
      },
      reloadDefinitions(_, __, cache) {
        cache.invalidate('Query', 'definitions')
      },
      reloadExercises(_, __, cache) {
        cache.invalidate('Query', 'definitions')
        cache.invalidate('Query', 'exercises')
      },
      createUser(parent, __, cache) {
        //TODO: add cache optim to add tags and user into list instead of invalidating it
        const user = parent.createUser?.newUser
        if (user) {
          invalidateUserList(cache)
        }
      },
      assignUsersByTags(parent, args, cache) {
        if (parent.assignUsersByTags?.operationDone) {
          cache.invalidate({
            __typename: 'ExerciseType',
            id: args.exerciseId,
          })
        }
      },
      assignUsersEqually(parent, args, cache) {
        if (parent.assignUsersEqually?.operationDone) {
          cache.invalidate({
            __typename: 'ExerciseType',
            id: args.exerciseId,
          })
        }
      },
      copyUsersAssignment(parent, args, cache) {
        if (parent.copyUsersAssignment?.operationDone) {
          cache.invalidate({
            __typename: 'ExerciseType',
            id: args.toExerciseId,
          })
        }
      },
      assignInstructorsToExercise(parent, args, cache) {
        // add exercisetype optimistic change
        if (parent.assignInstructorsToExercise?.operationDone) {
          cache.invalidate({
            __typename: 'ExerciseType',
            id: args.exerciseId,
          })
        }
      },
      removeInstructorsFromExercise(parent, args, cache) {
        // add exercisetype optimistic change
        if (parent.removeInstructorsFromExercise?.operationDone) {
          cache.invalidate({
            __typename: 'ExerciseType',
            id: args.exerciseId,
          })
        }
      },
      assignUsersToTeam(parent, args, cache) {
        // add exercisetype optimistic change
        if (parent.assignUsersToTeam?.operationDone) {
          cache.invalidate({
            __typename: 'TeamType',
            id: args.teamId,
          })
          cache.invalidate({
            __typename: 'RestrictedTeam',
            id: args.teamId,
          })
        }
      },
      removeUsersFromTeam(parent, args, cache) {
        // add exercisetype optimistic change
        if (parent.removeUsersFromTeam?.operationDone) {
          cache.invalidate({
            __typename: 'TeamType',
            id: args.teamId,
          })
          cache.invalidate({
            __typename: 'RestrictedTeam',
            id: args.teamId,
          })
        }
      },
      addDefinitionAccess(parent, args, cache) {
        if (parent.addDefinitionAccess?.operationDone) {
          cache.invalidate({
            __typename: 'DefinitionType',
            id: args.definitionId,
          })
        }
      },
      removeDefinitionAccess(parent, args, cache) {
        if (parent.removeDefinitionAccess?.operationDone) {
          cache.invalidate({
            __typename: 'DefinitionType',
            id: args.definitionId,
          })
        }
      },
      startExercise(parent, { exerciseId }, cache) {
        if (parent.startExercise?.exercise) {
          cache.writeFragment(
            graphql(`
              fragment _ on ExerciseType {
                id
                running
              }
            `),
            {
              id: exerciseId,
              running: true,
            }
          )
        }
      },
      stopExercise(parent, { exerciseId }, cache) {
        if (parent.stopExercise?.exercise) {
          cache.writeFragment(
            graphql(`
              fragment _ on ExerciseType {
                id
                running
              }
            `),
            {
              id: exerciseId,
              running: false,
            }
          )
        }
      },
      deleteExercise(parent, args, cache) {
        if (parent.deleteExercise?.operationDone) {
          const key = cache.keyOfEntity({
            __typename: 'ExerciseType',
            id: args.exerciseId,
          })
          const params = ['Query', 'exercises'] as const
          const link = cache.resolve(...params)

          if (Array.isArray(link)) {
            cache.link(
              ...params,
              link.filter(exerciseKey => exerciseKey != key)
            )
          }
          cache.invalidate(key)
        }
      },
      deleteDefinition(parent, args, cache) {
        //TODO@xjuhas: please add to DeleteExerciseMutation [exerciseId] which concern the removal of given definition
        if (parent.deleteDefinition?.operationDone) {
          cache.updateQuery<
            ResultOf<typeof GetDefinitions>,
            VariablesOf<typeof GetDefinitions>
          >(
            {
              query: GetDefinitions,
            },
            prev => ({
              __typename: 'Query',
              definitions:
                prev?.definitions?.filter(
                  definition => definition.id !== args.definitionId
                ) || [],
            })
          )
          cache.updateQuery<
            ResultOf<typeof GetExercises>,
            VariablesOf<typeof GetExercises>
          >(
            {
              query: GetExercises,
            },
            prev => ({
              __typename: 'Query',
              exercises:
                prev?.exercises?.filter(
                  exercise =>
                    exercise?.definition?.id?.toString() !== args.definitionId
                ) || [],
            })
          )
        }
      },
      createTag(parent, _, cache) {
        const newTag = parent.createTag?.newTag
        if (newTag) {
          const params = ['Query', 'tags'] as const
          const link = cache.resolve(...params) || []
          if (!Array.isArray(link)) {
            throw Error('Generic typing error, check for createTag')
          }
          cache.link(...params, [...link, cache.keyOfEntity(newTag)])
        }
      },
      moveTime(_, __, cache) {
        cache.invalidate('Query', 'exerciseTimeLeft')
      },
      modifyMilestone() {
        // TODO: @xjuhas - use milestoneState ID to modify given milestone instead of teamId+milestone.name, I can't even find the entry that way
      },
      setSubscriptionTeam(_, { state, teamId }, cache) {
        cache.writeFragment(
          graphql(`
            fragment _ on TeamType {
              id
              subscribed
            }
          `),
          { id: teamId, subscribed: state }
        )
      },
      setArchiveEmail(_, { emailThreadId, state }, cache) {
        cache.writeFragment(
          graphql(`
            fragment _ on EmailThreadType {
              id
              archived
            }
          `),
          { id: emailThreadId, archived: state }
        )
      },
    },
    Subscription: {
      milestones(parent, _args, cache) {
        parent.milestones?.milestones?.forEach(milestone => {
          cache.writeFragment(MilestoneState, milestone)
        })
      },
      exercisesSubscription(parent, _args, cache) {
        /*
         * Q@xjuhas who wanted a subscription for list of exercises? this is just too dense of an idea that needs NOT to be implemented
         */
        const newExercise = parent.exercisesSubscription?.exercise
        const eventType = parent.exercisesSubscription?.eventType

        if (newExercise && eventType) {
          const params = ['Query', 'exercises'] as const

          // TODO: improve on cache handling when it will be planned to use the arguments like skip, running, etc.
          switch (eventType) {
            case 'CREATE':
              {
                const linkNot = cache.resolve(...params) || []
                if (!Array.isArray(linkNot)) {
                  throw Error('Generic typing error, look up exercisesSub')
                }
                cache.link(...params, [
                  ...linkNot,
                  { __typename: 'ExerciseType', id: newExercise.id },
                ])
              }
              break
            case 'DELETE': {
              const linkNot = cache.resolve(...params) || []

              if (!Array.isArray(linkNot)) {
                throw Error('Generic typing error, look up exercisesSub')
              }

              const needle = cache.keyOfEntity({
                __typename: 'ExerciseType',
                id: newExercise.id,
              })
              cache.link(
                ...params,
                linkNot.filter(exerciseKey => exerciseKey !== needle)
              )
              break
            }
            case 'MODIFY':
              cache.writeFragment(Exercise, newExercise)
              break
            default:
              throw Error('Unimplemented exercise handler in subscription')
          }
        }
      },
      exerciseLoopRunning(parent, args, cache) {
        cache.updateQuery<
          ResultOf<typeof GetExerciseLoopStatus>,
          VariablesOf<typeof GetExerciseLoopStatus>
        >(
          {
            query: GetExerciseLoopStatus,
            variables: {
              exerciseId: args.exerciseId,
            },
          },
          () => ({
            __typename: 'Query',
            exerciseLoopRunning:
              !!parent.exerciseLoopRunning?.exerciseLoopRunning,
          })
        )
      },
      actionLogs(parent, args, cache) {
        const actionLog = parent.actionLogs?.actionLog
        const operation = parent.actionLogs?.eventType

        if (actionLog === null || actionLog === undefined) {
          return
        }

        const params = [
          'Query',
          'teamActionLogs',
          { teamId: args.teamId as string },
        ] as const
        const link = cache.resolve(...params) || []
        if (!Array.isArray(link)) {
          throw Error('Generic typing error, please check actionLogs')
        }

        switch (operation) {
          case 'CREATE':
            {
              cache.writeFragment(ActionLog, actionLog)
              const key = cache.keyOfEntity({
                __typename: 'ActionLogType',
                id: actionLog.id,
              })
              cache.link(...params, [...link, key])
              commitActionLogToCache(cache, actionLog)
            }
            break
          case 'MODIFY':
            cache.writeFragment(ActionLog, actionLog)
            break
          default:
            throw Error('Unimplemented state step')
        }
      },
      analyticsActionLogsSubscription: (parent, args, cache) => {
        const actionLog = parent?.analyticsActionLogsSubscription?.actionLog
        const operation = parent.analyticsActionLogsSubscription?.eventType

        if (actionLog === null || actionLog === undefined) {
          return
        }

        const params = ['Query', 'analyticsActionLogs'] as const
        // protip: don't use analActionLogs query without newestFirst, it's not linked up
        const linkNewest =
          cache.resolve(...params, {
            exerciseId: args.exerciseId as string,
            newestFirst: true,
          }) || []
        if (!Array.isArray(linkNewest)) {
          throw Error(
            'Generic typing error, please check analyticsActionLogsSubscription'
          )
        }

        switch (operation) {
          case 'CREATE':
            {
              const key = cache.keyOfEntity({
                __typename: 'ActionLogType',
                id: actionLog.id,
              })
              const newArr = [
                key,
                ...linkNewest.filter(actionLogKey => actionLogKey !== key),
              ]
              cache.link(
                ...params,
                { exerciseId: args.exerciseId as string, newestFirst: true },
                newArr
              )
              cache.link(
                ...params,
                { exerciseId: args.exerciseId as string, newestFirst: false },
                reverse(newArr)
              )
              cache.link(
                ...params,
                { exerciseId: args.exerciseId as string },
                reverse(newArr)
              )
              commitActionLogToCache(cache, actionLog)
            }
            break
          case 'MODIFY':
            cache.writeFragment(ActionLog, actionLog)
            break
          default:
            throw Error('Unimplemented state step')
        }
      },
      analyticsEmailThreadSubscription: (parent, args, cache) => {
        updateEmailThreads(
          cache,
          [
            'Query',
            'analyticsEmailThreads',
            { exerciseId: args.exerciseId as string },
          ],
          parent.analyticsEmailThreadSubscription?.emailThread
        )
      },
      emailThreads: (parent, args, cache) => {
        updateEmailThreads(
          cache,
          ['Query', 'emailThreads', { exerciseId: args.teamId as string }],
          parent.emailThreads?.emailThread
        )
      },
      analyticsMilestonesSubscription: (parent, _args, cache) => {
        parent.analyticsMilestonesSubscription?.milestones?.forEach(
          milestone => {
            cache.writeFragment(MilestoneState, milestone)
            const activity = cache.resolve(
              cache.keyOfEntity(milestone),
              'activity'
            )
            if (typeof activity === 'string') {
              const teamObjective = cache.resolve(activity, 'teamObjective')
              cache.invalidate(activity, 'reached')
              if (typeof teamObjective === 'string') {
                cache.invalidate(teamObjective, 'reached')
              }
            }
          }
        )
      },
      kvStore({ kvStore }, { exerciseId }) {
        if (!kvStore) {
          return
        }
        if (!kvStore.key || !kvStore.value) {
          return
        }
        const { key, value } = kvStore

        const processed = processKVSubscription({
          exerciseId,
          key,
          value,
        })

        // future: this is meant for more complex sideeffects, processor validates sanity
        if (!processed) {
          return
        }
      },
    },
  },
  optimistic: {
    setTodoActionLog({ actionLogId, state }) {
      return {
        __typename: 'ActionLogType',
        id: actionLogId,
        todo: state,
      }
    },
    setEmailTodo({ emailId, state }) {
      return {
        __typename: 'EmailType',
        id: emailId,
        todo: state,
      }
    },
    setArchiveEmail({ emailThreadId, state }) {
      return {
        __typename: 'EmailThreadType',
        id: emailThreadId,
        archived: state,
      }
    },
    writeReadReceipt({ actionLogId }) {
      return {
        __typename: 'ActionLogType',
        id: actionLogId,
        readReceipt: new Date().toUTCString(),
      }
    },
    writeReadReceiptEmail({ emailId }) {
      return {
        __typename: 'EmailType',
        id: emailId,
        readReceipt: new Date().toUTCString(),
      }
    },
  },
  resolvers: {
    ContentType: {
      rendered(parent) {
        return sanitizeHtmlCustom(parent.rendered || '')
      },
    },
    TeamLearningObjectiveType: {
      reached: (parent, _, cache) => {
        const activities = cache.resolve(
          cache.keyOfEntity({
            __typename: 'TeamLearningObjectiveType',
            id: parent.id as string,
          }),
          'activities'
        ) as string[] | undefined

        return activities?.every(activityKey => {
          const res = cache.resolve(activityKey, 'reached')
          if (res) {
            return res
          } else {
            const milestoneStates = cache.resolve(
              activityKey,
              'milestoneStates'
            ) as string[] | undefined

            return milestoneStates?.every(milestoneStateKey =>
              cache.resolve(milestoneStateKey, 'reached')
            ) as unknown as string
          }
        }) as unknown as string
      },
    },
    TeamLearningActivityType: {
      reached: (parent, _, cache) => {
        const milestoneStates = cache.resolve(
          cache.keyOfEntity({
            __typename: 'TeamLearningActivityType',
            id: parent.id as string,
          }),
          'milestoneStates'
        ) as string[] | undefined

        return milestoneStates?.every(milestoneStateKey =>
          cache.resolve(milestoneStateKey, 'reached')
        ) as unknown as string
      },
    },
    Query: {
      teamLearningObjectives: (_, args, cache, info) => {
        // TODO: optim @xjuhas, it's good to have analyst level call for this... and extend the TLO type to contain TeamId :)
        const milestoneStates =
          cache.resolve('Query', 'teamMilestones', {
            teamId: args.teamId,
            visibleOnly: false,
          }) || getTeamMilestones(cache, args.teamId)

        if (Array.isArray(milestoneStates)) {
          const stateMap = milestoneStates
            .map(state => {
              const activity = cache.resolve(state, 'activity')
              if (typeof activity === 'string') {
                return cache.resolve(activity, 'teamObjective')
              }
              return null
            })
            .filter(notEmpty) as string[]
          return uniq(stateMap)
        }

        info.partial = true
        return cache.resolve('Query', 'teamLearningObjectives', args)
      },
      teamMilestones: (_, args, cache, info) => {
        // this resolver does not handle visibleOnly, it's not used
        const res = getTeamMilestones(cache, args.teamId)
        if (res) {
          return res
        }
        info.partial = true
        return cache.resolve('Query', 'teamMilestones', args)
      },
      analyticsActionLogs: (_, args, cache, info) => {
        if (args.newestFirst) {
          const actionLogs =
            cache.resolve('Query', 'analyticsActionLogs', {
              exerciseId: args.exerciseId,
              newestFirst: false,
            }) ||
            cache.resolve('Query', 'analyticsActionLogs', {
              exerciseId: args.exerciseId,
            })
          if (Array.isArray(actionLogs)) {
            return reverse(actionLogs)
          }
        } else {
          const actionLogs = cache.resolve('Query', 'analyticsActionLogs', {
            exerciseId: args.exerciseId,
            newestFirst: true,
          })
          if (Array.isArray(actionLogs)) {
            return reverse(actionLogs)
          }
        }

        info.partial = true
        return cache.resolve('Query', 'analyticsActionLogs', args)
      },
      emailThreads: (_, args, cache, info) => {
        const { teamId } = args
        const exercise = cache.resolve(
          { __typename: 'TeamType', id: teamId },
          'exercise'
        )
        if (typeof exercise === 'string') {
          const exerciseId = exercise.slice('ExerciseType:'.length)
          const emailThreads = cache.resolve('Query', 'analyticsEmailThreads', {
            exerciseId,
          })
          if (Array.isArray(emailThreads)) {
            return emailThreads.filter(emailThread => {
              const participants = cache.resolve(emailThread, 'participants')
              if (Array.isArray(participants)) {
                return participants.some(
                  participant =>
                    cache.resolve(participant, 'team') === `TeamType:${teamId}`
                )
              }
              return false
            })
          }
        }

        info.partial = true
        return cache.resolve('Query', 'emailThreads', args)
      },
      actionLog(_, { logId }) {
        return { __typename: 'ActionLogType', id: logId }
      },
      emailThread(_, { threadId }) {
        return { __typename: 'EmailThreadType', id: threadId }
      },
      user(_, { userId }) {
        return { __typename: 'UserType', id: userId }
      },
      exerciseId(_, { exerciseId }) {
        return { __typename: 'ExerciseType', id: exerciseId }
      },
      definition(_, { definitionId }) {
        return { __typename: 'DefinitionType', id: definitionId }
      },
      team(_, { teamId }) {
        return { __typename: 'TeamType', id: teamId }
      },
      channel(_, { channelId }) {
        return { __typename: 'DefinitionChannelType', id: channelId }
      },
      teamMilestone(_, { milestoneStateId }) {
        return { __typename: 'MilestoneStateType', id: milestoneStateId }
      },
      fileInfo(_, { fileInfoId }) {
        return { __typename: 'FileInfoType', id: fileInfoId }
      },
      emailContact(_, { participantId }) {
        return { __typename: 'EmailParticipantType', id: participantId }
      },
    },
  },
} as GraphCacheConfig)

export default cache
