import Vue from 'vue'
import * as api from '@/api'
import { cartesian, flatten, once } from '@/util/helpers'
import { Assignment, Subscription } from '@/models'
import { ActionContext, Module } from 'vuex'
import { RootState } from '@/store/store'
import { Authentication } from '@/store/modules/authentication'
import { getStoreBuilder, BareActionContext } from 'vuex-typex'

export interface SubscriptionsState {
  subscriptions: {[pk: string]: Subscription}
  currentAssignment?: Assignment
  loading: boolean
}

function initialState (): SubscriptionsState {
  return {
    subscriptions: {},
    currentAssignment: undefined,
    loading: false
  }
}

const mb = getStoreBuilder<RootState>().module('Subscriptions', initialState())

const stateGetter = mb.state()

const availableTypesGetter = mb.read(function availableTypes (state, getters) {
  let types = [Subscription.QueryTypeEnum.Random, Subscription.QueryTypeEnum.SubmissionType]
  if (Authentication.isReviewer) {
    types.push(Subscription.QueryTypeEnum.Exam)
  }
  return types
})

const availableStagesGetter = mb.read(function availableStages (state, getters) {
  let stages = [Subscription.FeedbackStageEnum.Creation, Subscription.FeedbackStageEnum.Validation]
  if (Authentication.isReviewer) {
    stages.push(Subscription.FeedbackStageEnum.ConflictResolution)
  }
  return stages
})

const availableStagesReadableGetter = mb.read(function availableStagesReadable (state, getters) {
  let stages = ['initial', 'validate']
  if (Authentication.isReviewer) {
    stages.push('conflict')
  }
  return stages
})

const availableSubmissionTypeQueryKeysGetter = mb.read(function availableSubmissionTypeQueryKeys (state, getters, rootState) {
  return Object.values(rootState.submissionTypes).map((subType: any) => subType.pk)
})

const availableExamTypeQueryKeysGetter = mb.read(function availableExamTypeQueryKeys (state, getters, rootState) {
  return Object.values(rootState.examTypes).map((examType: any) => examType.pk)
})

const activeSubscriptionGetter = mb.read(function activeSubscription (state) {
  if (state.currentAssignment && state.currentAssignment.subscription) {
    return state.subscriptions[state.currentAssignment.subscription]
  }

  return undefined
})

const resolveSubscriptionKeyToNameGetter = mb.read(function resolveSubscriptionKeyToName (state, getters, rootState) {
  return (subscription: {queryType: Subscription.QueryTypeEnum, queryKey: string}) => {
    switch (subscription.queryType) {
      case Subscription.QueryTypeEnum.Random:
        return 'Active'
      case Subscription.QueryTypeEnum.Exam:
        return subscription.queryKey
          ? rootState.examTypes[subscription.queryKey].moduleReference : 'Exam'
      case Subscription.QueryTypeEnum.SubmissionType:
        return subscription.queryKey
          ? rootState.submissionTypes[subscription.queryKey].name : 'Submission Type'
      case Subscription.QueryTypeEnum.Student:
        return subscription.queryKey
          ? rootState.students[subscription.queryKey].name : 'Student'
    }
  }
})

type SubscriptionsByStage = {[p in Subscription.FeedbackStageEnum]?: {[k in Subscription.QueryTypeEnum]: Subscription[]}}
// TODO Refactor this monstrosity
const getSubscriptionsGroupedByTypeGetter = mb.read(function getSubscriptionsGroupedByType (state, getters) {
  const subscriptionsByType = () => {
    return {
      [Subscription.QueryTypeEnum.Random]: [],
      [Subscription.QueryTypeEnum.Student]: [],
      [Subscription.QueryTypeEnum.Exam]: [],
      [Subscription.QueryTypeEnum.SubmissionType]: []
    }
  }
  let subscriptionsByStage: SubscriptionsByStage = Subscriptions.availableStages.reduce((acc: SubscriptionsByStage,
    curr: Subscription.FeedbackStageEnum) => {
    acc[curr] = subscriptionsByType()
    return acc
  }, {})
  Object.values(state.subscriptions).forEach((subscription: Subscription) => {
    if (subscriptionsByStage && subscription.feedbackStage && subscription.queryType) {
      subscriptionsByStage[subscription.feedbackStage]![subscription.queryType].push(subscription)
    }
  })
  // sort the resulting arrays in subscriptions lexicographically by their query_keys
  const sortSubscriptions = (subscriptionsByType: {[k: string]: Subscription[]}) => Object.values(subscriptionsByType)
    .forEach((arr: object[]) => {
      if (arr.length > 1 && arr[0].hasOwnProperty('queryKey')) {
        arr.sort((subA, subB) => {
          const subALower = getters.resolveSubscriptionKeyToName(subA).toLowerCase()
          const subBLower = getters.resolveSubscriptionKeyToName(subB).toLowerCase()
          if (subALower < subBLower) {
            return -1
          } else if (subALower > subBLower) {
            return 1
          } else {
            return 0
          }
        })
      }
    })
  Object.values(subscriptionsByStage).forEach((subscriptionsByType: any) => {
    sortSubscriptions(subscriptionsByType)
  })
  return subscriptionsByStage
})

function SET_SUBSCRIPTIONS (state: SubscriptionsState, subscriptions: Array<Subscription>): void {
  state.subscriptions = subscriptions.reduce((acc: {[pk: string]: Subscription}, curr) => {
    acc[curr.pk] = curr
    return acc
  }, {})
}

function SET_SUBSCRIPTION (state: SubscriptionsState, subscription: Subscription): void {
  Vue.set(state.subscriptions, subscription.pk, subscription)
}

function SET_CURRENT_ASSIGNMENT (state: SubscriptionsState, assignment?: Assignment): void {
  state.currentAssignment = assignment
}

function RESET_STATE (state: SubscriptionsState): void {
  Object.assign(state, initialState())
  subscribeToAll.reset()
}

async function subscribeTo (
  context: BareActionContext<SubscriptionsState, RootState>,
  { type, key, stage }:
  {type: Subscription.QueryTypeEnum, key?: string, stage: Subscription.FeedbackStageEnum}): Promise<Subscription> {
  // don't subscribe to type, key, stage combinations if they're already present
  let subscription = Subscriptions.getSubscriptionsGroupedByType[stage]![type].find((elem: Subscription) => {
    if (type === Subscription.QueryTypeEnum.Random) {
      return true
    }
    return elem.queryKey === key
  })
  subscription = subscription || await api.subscribeTo(type, key, stage)
  Subscriptions.SET_SUBSCRIPTION(subscription)
  return subscription
}

async function getSubscriptions () {
  const subscriptions = await api.fetchSubscriptions()
  Subscriptions.SET_SUBSCRIPTIONS(subscriptions)
  return subscriptions
}


async function changeToSubscription({state}: BareActionContext<SubscriptionsState, RootState>, subscriptionPk: string) {
  const currAssignment = state.currentAssignment
  if (currAssignment && currAssignment.subscription == subscriptionPk) {
    return
  }

  if (currAssignment) {
    await api.deleteAssignment({assignment: currAssignment})
  }

  const newAssignment = await api.createAssignment({subscriptionPk})
  Subscriptions.SET_CURRENT_ASSIGNMENT(newAssignment)
}

async function createNextAssignment() {
  const activeSubscription = Subscriptions.activeSubscription
  if (!activeSubscription) {
    throw new Error("There must be an active Subscription before calling createNextAssignment")
  }
  const newAssignment = await api.createAssignment({subscription: activeSubscription})
  Subscriptions.SET_CURRENT_ASSIGNMENT(newAssignment)
}

async function cleanAssignmentsFromSubscriptions
({ state }: BareActionContext<SubscriptionsState, RootState>, excludeActive = true) {
  Object.values(state.subscriptions).forEach(subscription => {
    if (!excludeActive ||
        !Subscriptions.activeSubscription ||
        subscription.pk !== Subscriptions.activeSubscription.pk) {
      if (subscription.assignments) {
        subscription.assignments.forEach(assignment => {
          api.deleteAssignment({ assignment })
        })
      }
    }
  })
}

async function skipAssignment ({ state }: BareActionContext<SubscriptionsState, RootState>) {
  if (!state.currentAssignment || !state.currentAssignment.subscription) {
    throw new Error("skipAssignment can only be called with active assignment")
  }

  const newAssignment = await api.createAssignment({subscriptionPk: state.currentAssignment.subscription})
  await api.deleteAssignment({assignment: state.currentAssignment })

  Subscriptions.SET_CURRENT_ASSIGNMENT(newAssignment)
}

async function deleteCurrentAssignment ({ state }: BareActionContext<SubscriptionsState, RootState>) {
  if (!state.currentAssignment) {
    throw new Error("No active assignment to delete")
  }
  await api.deleteAssignment({assignment: state.currentAssignment})
  Subscriptions.SET_CURRENT_ASSIGNMENT(undefined)
}

async function subscribeToType
(context: BareActionContext<SubscriptionsState, RootState>, type: Subscription.QueryTypeEnum) {
  switch (type) {
    case Subscription.QueryTypeEnum.Random:
      Subscriptions.availableStages.map((stage: Subscription.FeedbackStageEnum) => {
        Subscriptions.subscribeTo({ type, stage })
      })
      break
    case Subscription.QueryTypeEnum.Exam:
      if (Authentication.isReviewer) {
        const stageKeyCartesian = cartesian(
          Subscriptions.availableStages, Subscriptions.availableExamTypeQueryKeys)
        // @ts-ignore
        stageKeyCartesian.map(([stage, key]: [Subscription.FeedbackStageEnum, string]) => {
          Subscriptions.subscribeTo({ stage, type, key })
        })
      }
      break
    case Subscription.QueryTypeEnum.SubmissionType:
      const stageKeyCartesian = cartesian(
        Subscriptions.availableStages, Subscriptions.availableSubmissionTypeQueryKeys)
      // @ts-ignore
      stageKeyCartesian.map(([stage, key]: [Subscription.FeedbackStageEnum, string]) => {
        Subscriptions.subscribeTo({ stage, type, key })
      })
      break
  }
}

const subscribeToAll = once(async () => {
  return Promise.all(flatten(Subscriptions.availableTypes.map((type) => {
    return Subscriptions.subscribeToType(type)
  })))
})

export const Subscriptions = {
  get state () { return stateGetter() },
  get availableTypes () { return availableTypesGetter() },
  get availableStages () { return availableStagesGetter() },
  get availableStagesReadable () { return availableStagesReadableGetter() },
  get availableSubmissionTypeQueryKeys () { return availableSubmissionTypeQueryKeysGetter() },
  get availableExamTypeQueryKeys () { return availableExamTypeQueryKeysGetter() },
  get activeSubscription () { return activeSubscriptionGetter() },
  get resolveSubscriptionKeyToName () { return resolveSubscriptionKeyToNameGetter() },
  get getSubscriptionsGroupedByType () { return getSubscriptionsGroupedByTypeGetter() },

  SET_SUBSCRIPTIONS: mb.commit(SET_SUBSCRIPTIONS),
  SET_SUBSCRIPTION: mb.commit(SET_SUBSCRIPTION),
  SET_CURRENT_ASSIGNMENT: mb.commit(SET_CURRENT_ASSIGNMENT),
  RESET_STATE: mb.commit(RESET_STATE),

  subscribeTo: mb.dispatch(subscribeTo),
  getSubscriptions: mb.dispatch(getSubscriptions),
  cleanAssignmentsFromSubscriptions: mb.dispatch(cleanAssignmentsFromSubscriptions),
  changeToSubscription: mb.dispatch(changeToSubscription),
  createNextAssignment: mb.dispatch(createNextAssignment),
  skipAssignment: mb.dispatch(skipAssignment),
  deleteCurrentAssignment: mb.dispatch(deleteCurrentAssignment),
  subscribeToType: mb.dispatch(subscribeToType),
  subscribeToAll: mb.dispatch(subscribeToAll, 'subscribeToAll')
}