import type {ITask, ITasksWithLabels} from '@/models/ITask'
import {labelsTable, taskLabelTable, tasksTable} from '@/db/schema'
import {db} from '@/db/database'
import {and, eq, notInArray} from 'drizzle-orm'
import {not} from 'drizzle-orm/sql/expressions/conditions'
import type {UseSuspenseQueryResult} from '@tanstack/react-query'
import {useMutation, type UseMutationResult, useQueryClient, useSuspenseQuery} from '@tanstack/react-query'
import type {IOnMutateResult} from '@/models/IOnMutateResult'
import type {ILabel} from '@/models/ILabel'

//region Mutations & queries

/**
 * ---------------------------------------------------------------------------------------------------------------------
 *                                          MUTATIONS & QUERIES
 * ---------------------------------------------------------------------------------------------------------------------
 */

export const useGetTasks = (): UseSuspenseQueryResult<ITask[], Error> => {
  return useSuspenseQuery({
    queryKey: ['tasks'],
    queryFn: getTasks,
  })
}

export const useGetTask = (id?: number): UseSuspenseQueryResult<ITask | null, Error> => {
  return useSuspenseQuery({
    queryKey: ['tasks', id],
    queryFn: () => (id ? getTask(id) : null),
  })
}

export const useCreateTask = (): UseMutationResult<ITask, Error, CreateTasksParams, IOnMutateResult<ITask[]>> => {
  const queryClient = useQueryClient()
  return useMutation({
    mutationFn: createTask,
    onMutate: newTask => {
      const queryKey = ['tasks']
      const oldData = queryClient.getQueryData<ITask[]>(queryKey) ?? []
      queryClient.setQueryData<ITask[]>(queryKey, [...oldData, {...newTask, completed: false, id: -1, labels: []}])
      return {queryKey, oldData}
    },
    onSuccess: (data, _, context) => {
      if (context) {
        queryClient.setQueryData<ITask[]>(context.queryKey, [...context.oldData, data])
      }
    },
  })
}

export const useDeleteTask = (): UseMutationResult<void, Error, DeleteTasksParams, void> => {
  const queryClient = useQueryClient()
  return useMutation({
    mutationFn: deleteTask,
    onMutate: ({id}) => {
      queryClient.setQueryData<ITask[]>(['tasks'], old => (old ?? []).filter(task => task.id !== id))
      queryClient.setQueryData<ITask>(['tasks', id], undefined)
    },
  })
}

export const useUpdateTask = (): UseMutationResult<ITask, Error, ITask, IOnMutateResult<ITask>> => {
  const queryClient = useQueryClient()
  return useMutation({
    mutationFn: updateTask,
    onMutate: updatedTask => {
      const queryKey = ['tasks', updatedTask.id]
      const oldData = queryClient.getQueryData<ITask>(queryKey)!
      queryClient.setQueryData<ITask>(queryKey, updatedTask)
      queryClient.setQueryData<ITask[]>(['tasks'], old =>
        (old ?? []).map(t => (t.id === updatedTask.id ? updatedTask : t)),
      )
      return {queryKey, oldData}
    },
    onSuccess: (data, _, context) => {
      if (context) {
        queryClient.setQueryData<ITask>(context.queryKey, data)
        queryClient.setQueryData<ITask[]>(['tasks'], old => (old ?? []).map(x => (x.id === data.id ? data : x)))
      }
    },
  })
}

export const useToggleTaskStatus = (): UseMutationResult<
  ITask,
  Error,
  ToggleStatusParams,
  IOnMutateResult<ITask[]>
> => {
  const queryClient = useQueryClient()
  return useMutation({
    mutationFn: toggleTaskStatus,
    onMutate: ({id}) => {
      const queryKey = ['tasks']
      const oldData = queryClient.getQueryData<ITask[]>(queryKey) ?? []
      queryClient.setQueryData<ITask[]>(
        queryKey,
        oldData.map(t => {
          if (t.id === id) {
            return {...t, completed: !t.completed}
          }
          return t
        }),
      )
      return {queryKey, oldData}
    },
    onError: (_, __, context) => {
      if (context) {
        queryClient.setQueryData<ITask[]>(context.queryKey, context.oldData)
      }
    },
  })
}

//endregion

//region API functions

/**
 * ---------------------------------------------------------------------------------------------------------------------
 *                                          API functions
 * ---------------------------------------------------------------------------------------------------------------------
 */

function mapTasks(rows: ITasksWithLabels): ITask[] {
  // Map taskId to the task
  const tasks: Record<number, ITask> = {}

  for (const row of rows) {
    if (!tasks[row.Task.id]) {
      tasks[row.Task.id] = {...row.Task, labels: []}
    }

    if (!row.Label) continue
    tasks[row.Task.id].labels.push(row.Label)
  }

  return Object.values(tasks)
}

async function getTasks(): Promise<ITask[]> {
  const tasks = await db
    .select()
    .from(tasksTable)
    .leftJoin(taskLabelTable, eq(taskLabelTable.taskId, tasksTable.id))
    .leftJoin(labelsTable, eq(labelsTable.id, taskLabelTable.labelId))
  return mapTasks(tasks)
}

async function getTask(id: number): Promise<ITask> {
  const tasks = await db
    .select()
    .from(tasksTable)
    .where(eq(tasksTable.id, id))
    .leftJoin(taskLabelTable, eq(taskLabelTable.taskId, tasksTable.id))
    .leftJoin(labelsTable, eq(labelsTable.id, taskLabelTable.labelId))

  return mapTasks(tasks)[0]
}

interface CreateTasksParams {
  name: string
  labels: ILabel[]
}

async function createTask({name, labels}: CreateTasksParams): Promise<ITask> {
  // Via returning wordt de aangemaakte rij teruggegeven.
  const [task] = await db.insert(tasksTable).values({name}).returning()

  // Voeg de labels toe aan de task.
  if (labels.length > 0) {
    await db.insert(taskLabelTable).values(labels.map(l => ({labelId: l.id, taskId: task.id})))
  }

  return {...task, labels: labels}
}

interface DeleteTasksParams {
  id: number
}

async function deleteTask({id}: DeleteTasksParams): Promise<void> {
  await db.delete(tasksTable).where(eq(tasksTable.id, id))
}

async function updateTask({id, name, completed, labels}: ITask): Promise<ITask> {
  // Verwijder de oude labels die niet langer gekoppeld zijn.
  await db.delete(taskLabelTable).where(
    and(
      notInArray(
        taskLabelTable.labelId,
        labels.map(l => l.id),
      ),
      eq(taskLabelTable.taskId, id),
    ),
  )
  // Voeg eventuele nieuwe labels toe.
  if (labels.length > 0) {
    await db
      .insert(taskLabelTable)
      .values(labels.map(l => ({labelId: l.id, taskId: id})))
      .onConflictDoNothing()
  }

  // Via returning wordt de aangepast rij teruggegeven.
  const [task] = await db.update(tasksTable).set({name, completed}).where(eq(tasksTable.id, id)).returning()

  return {...task, labels: labels}
}

interface ToggleStatusParams {
  id: number
}

async function toggleTaskStatus({id}: ToggleStatusParams): Promise<ITask> {
  const [task] = await db
    .update(tasksTable)
    .set({completed: not(tasksTable.completed)})
    .where(eq(tasksTable.id, id))
    .returning()
  return {...task, labels: []}
}

//endregion
