import {defineStore, acceptHMRUpdate} from 'pinia'
import cloneDeep from 'lodash.clonedeep'

import {findById, findIndexById} from '@/helpers/utils'
import {i18n} from '@/i18n'
import {success} from '@/message'

import BucketService from '@/services/bucket'
import TaskCollectionService from '@/services/taskCollection'

import {setModuleLoading} from '@/stores/helper'

import type { ITask } from '@/modelTypes/ITask'
import type { IList } from '@/modelTypes/IList'
import type { IBucket } from '@/modelTypes/IBucket'

const TASKS_PER_BUCKET = 25

function getTaskIndicesById(state: KanbanState, taskId: ITask['id']) {
	let taskIndex
	const bucketIndex = state.buckets.findIndex(({ tasks }) => {
		taskIndex = findIndexById(tasks, taskId)
		return taskIndex !== -1
	})

	return {
		bucketIndex: bucketIndex !== -1 ? bucketIndex : null,
		taskIndex: taskIndex !== -1 ? taskIndex : null,
	}	
}

const addTaskToBucketAndSort = (state: KanbanState, task: ITask) => {
	const bucketIndex = findIndexById(state.buckets, task.bucketId)
	if(typeof state.buckets[bucketIndex] === 'undefined') {
		return
	}
	state.buckets[bucketIndex].tasks.push(task)
	state.buckets[bucketIndex].tasks.sort((a, b) => a.kanbanPosition > b.kanbanPosition ? 1 : -1)
}

export interface KanbanState {
	buckets: IBucket[],
	listId: IList['id'],
	bucketLoading: {
		[id: IBucket['id']]: boolean
	},
	taskPagesPerBucket: {
		[id: IBucket['id']]: number
	},
	allTasksLoadedForBucket: {
		[id: IBucket['id']]: boolean
	},
	isLoading: boolean,
}

/**
 * This store is intended to hold the currently active kanban view.
 * It should hold only the current buckets.
 */
export const useKanbanStore = defineStore('kanban', {
	state: () : KanbanState => ({
		buckets: [],
		listId: 0,
		bucketLoading: {},
		taskPagesPerBucket: {},
		allTasksLoadedForBucket: {},
		isLoading: false,
	}),

	getters: {
		getBucketById(state) {
			return (bucketId: IBucket['id']) => findById(state.buckets, bucketId)
		},

		getTaskById(state) {
			return (id: ITask['id']) => {
				const { bucketIndex, taskIndex } = getTaskIndicesById(state, id)
				
				return {
					bucketIndex,
					taskIndex,
					task: bucketIndex !== null && taskIndex !== null && state.buckets[bucketIndex]?.tasks?.[taskIndex] || null,
				}
			}
		},
	},

	actions: {
		setIsLoading(isLoading: boolean) {
			this.isLoading = isLoading
		},

		setListId(listId: IList['id']) {
			this.listId = Number(listId)
		},

		setBuckets(buckets: IBucket[]) {
			this.buckets = buckets
			buckets.forEach(b => {
				this.taskPagesPerBucket[b.id] = 1
				this.allTasksLoadedForBucket[b.id] = false
			})
		},

		addBucket(bucket: IBucket) {
			this.buckets.push(bucket)
		},

		removeBucket(bucket: IBucket) {
			const bucketIndex = findIndexById(this.buckets, bucket.id)
			this.buckets.splice(bucketIndex, 1)
		},

		setBucketById(bucket: IBucket) {
			const bucketIndex = findIndexById(this.buckets, bucket.id)
			this.buckets[bucketIndex] = bucket
		},

		setBucketByIndex({
			bucketIndex,
			bucket,
		} : {
			bucketIndex: number,
			bucket: IBucket
		}) {
			this.buckets[bucketIndex] = bucket
		},

		setTaskInBucketByIndex({
			bucketIndex,
			taskIndex,
			task,
		} : {
			bucketIndex: number,
			taskIndex: number,
			task: ITask
		}) {
			const bucket = this.buckets[bucketIndex]
			bucket.tasks[taskIndex] = task
			this.buckets[bucketIndex] = bucket
		},

		setTasksInBucketByBucketId({
			bucketId,
			tasks,
		} : {
			bucketId: IBucket['id'],
			tasks: ITask[],
		}) {
			const bucketIndex = findIndexById(this.buckets, bucketId)
			this.buckets[bucketIndex] = {
				...this.buckets[bucketIndex],
				tasks,
			}
		},
		
		setTaskInBucket(task: ITask) {
			// If this gets invoked without any tasks actually loaded, we can save the hassle of finding the task
			if (this.buckets.length === 0) {
				return
			}

			let found = false

			const findAndUpdate = b => {
				for (const t in this.buckets[b].tasks) {
					if (this.buckets[b].tasks[t].id === task.id) {
						const bucket = this.buckets[b]
						bucket.tasks[t] = task

						if (bucket.id !== task.bucketId) {
							bucket.tasks.splice(t, 1)
							addTaskToBucketAndSort(this, task)
						}

						this.buckets[b] = bucket

						found = true
						return
					}
				}
			}

			for (const b in this.buckets) {
				if (this.buckets[b].id === task.bucketId) {
					findAndUpdate(b)
					if (found) {
						return
					}
				}
			}

			for (const b in this.buckets) {
				findAndUpdate(b)
				if (found) {
					return
				}
			}
		},

		addTaskToBucket(task: ITask) {
			const bucketIndex = findIndexById(this.buckets, task.bucketId)
			const oldBucket = this.buckets[bucketIndex]
			const newBucket = {
				...oldBucket,
				tasks: [
					...oldBucket.tasks,
					task,
				],
			}
			this.buckets[bucketIndex] = newBucket
		},

		addTasksToBucket({tasks, bucketId}: {
			tasks: ITask[];
			bucketId: IBucket['id'];
		}) {
			const bucketIndex = findIndexById(this.buckets, bucketId)
			const oldBucket = this.buckets[bucketIndex]
			const newBucket = {
				...oldBucket,
				tasks: [
					...oldBucket.tasks,
					...tasks,
				],
			}
			this.buckets[bucketIndex] = newBucket
		},

		removeTaskInBucket(task: ITask) {
			// If this gets invoked without any tasks actually loaded, we can save the hassle of finding the task
			if (this.buckets.length === 0) {
				return
			}

			const { bucketIndex, taskIndex } = getTaskIndicesById(this, task.id)

			if (
				!bucketIndex || 
				this.buckets[bucketIndex]?.id !== task.bucketId ||
				!taskIndex ||
				(this.buckets[bucketIndex]?.tasks[taskIndex]?.id !== task.id)
			) {
				return
			}
			
			this.buckets[bucketIndex].tasks.splice(taskIndex, 1)
		},

		setBucketLoading({bucketId, loading}: {bucketId: IBucket['id'], loading: boolean}) {
			this.bucketLoading[bucketId] = loading
		},

		setTasksLoadedForBucketPage({bucketId, page}: {bucketId: IBucket['id'], page: number}) {
			this.taskPagesPerBucket[bucketId] = page
		},

		setAllTasksLoadedForBucket(bucketId: IBucket['id']) {
			this.allTasksLoadedForBucket[bucketId] = true
		},

		async loadBucketsForList({listId, params}: {listId: IList['id'], params}) {
			const cancel = setModuleLoading(this)

			// Clear everything to prevent having old buckets in the list if loading the buckets from this list takes a few moments
			this.setBuckets([])

			params.per_page = TASKS_PER_BUCKET

			const bucketService = new BucketService()
			try {
				const buckets = await bucketService.getAll({listId}, params)
				this.setBuckets(buckets)
				this.setListId(listId)
				return buckets
			} finally {
				cancel()
			}
		},

		async loadNextTasksForBucket(
			{listId, ps = {}, bucketId} :
			{listId: IList['id'], ps, bucketId: IBucket['id']},
		) {
			const isLoading = this.bucketLoading[bucketId] ?? false
			if (isLoading) {
				return
			}

			const page = (this.taskPagesPerBucket[bucketId] ?? 1) + 1

			const alreadyLoaded = this.allTasksLoadedForBucket[bucketId] ?? false
			if (alreadyLoaded) {
				return
			}

			const cancel = setModuleLoading(this)
			this.setBucketLoading({bucketId: bucketId, loading: true})

			const params = JSON.parse(JSON.stringify(ps))

			params.sort_by = 'kanban_position'
			params.order_by = 'asc'

			let hasBucketFilter = false
			for (const f in params.filter_by) {
				if (params.filter_by[f] === 'bucket_id') {
					hasBucketFilter = true
					if (params.filter_value[f] !== bucketId) {
						params.filter_value[f] = bucketId
					}
					break
				}
			}

			if (!hasBucketFilter) {
				params.filter_by = [...(params.filter_by ?? []), 'bucket_id']
				params.filter_value = [...(params.filter_value ?? []), bucketId]
				params.filter_comparator = [...(params.filter_comparator ?? []), 'equals']
			}

			params.per_page = TASKS_PER_BUCKET

			const taskService = new TaskCollectionService()
			try {
				const tasks = await taskService.getAll({listId}, params, page)
				this.addTasksToBucket({tasks, bucketId: bucketId})
				this.setTasksLoadedForBucketPage({bucketId, page})
				if (taskService.totalPages <= page) {
					this.setAllTasksLoadedForBucket(bucketId)
				}
				return tasks
			} finally {
				cancel()
				this.setBucketLoading({bucketId, loading: false})
			}
		},

		async createBucket(bucket: IBucket) {
			const cancel = setModuleLoading(this)

			const bucketService = new BucketService()
			try {
				const createdBucket = await bucketService.create(bucket)
				this.addBucket(createdBucket)
				return createdBucket
			} finally {
				cancel()
			}
		},

		async deleteBucket({bucket, params}: {bucket: IBucket, params}) {
			const cancel = setModuleLoading(this)

			const bucketService = new BucketService()
			try {
				const response = await bucketService.delete(bucket)
				this.removeBucket(bucket)
				// We reload all buckets because tasks are being moved from the deleted bucket
				this.loadBucketsForList({listId: bucket.listId, params})
				return response
			} finally {
				cancel()
			}
		},

		async updateBucket(updatedBucketData: IBucket) {
			const cancel = setModuleLoading(this)

			const bucketIndex = findIndexById(this.buckets, updatedBucketData.id)
			const oldBucket = cloneDeep(this.buckets[bucketIndex])

			const updatedBucket = {
				...oldBucket,
				...updatedBucketData,
			}

			this.setBucketByIndex({bucketIndex, bucket: updatedBucket})
			
			const bucketService = new BucketService()
			try {
				const returnedBucket = await bucketService.update(updatedBucket)
				this.setBucketByIndex({bucketIndex, bucket: returnedBucket})
				return returnedBucket
			} catch(e) {
				// restore original state
				this.setBucketByIndex({bucketIndex, bucket: oldBucket})

				throw e
			} finally {
				cancel()
			}
		},

		async updateBucketTitle({ id, title }: { id: IBucket['id'], title: IBucket['title'] }) {
			const bucket = findById(this.buckets, id)

			if (bucket?.title === title) {
				// bucket title has not changed
				return
			}

			await this.updateBucket({ id, title })
			success({message: i18n.global.t('list.kanban.bucketTitleSavedSuccess')})
		},
	},
})

// support hot reloading
if (import.meta.hot) {
  import.meta.hot.accept(acceptHMRUpdate(useKanbanStore, import.meta.hot))
}