"use client";

import { createContext, useContext, useState, useCallback } from "react";
import { toast } from "sonner";

import type { CategoryDTO, GroupDTO, TodoDTO } from "@/types/board";
import {
  createCategory,
  deleteCategory as deleteCategoryAction,
  reorderCategories,
} from "@/lib/actions/categories";
import {
  createGroup,
  updateGroup as updateGroupAction,
  deleteGroup as deleteGroupAction,
  archiveGroup as archiveGroupAction,
  reorderGroupsInCategory,
  moveGroupToCategory,
} from "@/lib/actions/groups";
import {
  createTodo,
  createTodos,
  updateTodo as updateTodoAction,
  toggleTodo as toggleTodoAction,
  deleteTodo as deleteTodoAction,
} from "@/lib/actions/todos";
import { updateGroupNote } from "@/lib/actions/notes";

interface BoardContextValue {
  categories: CategoryDTO[];
  // Whether the admin-configured AI provider has an API URL + model saved
  // -- gates AI-powered actions like the "Add using AI" to-do button.
  aiConfigured: boolean;
  addCategory: (name: string) => Promise<void>;
  removeCategory: (categoryId: string) => Promise<void>;
  reorderLanes: (orderedIds: string[]) => Promise<void>;
  addGroup: (categoryId: string, title: string, color: string) => Promise<GroupDTO | undefined>;
  editGroup: (
    groupId: string,
    categoryId: string,
    data: { title?: string; color?: string }
  ) => Promise<void>;
  removeGroup: (groupId: string, categoryId: string) => Promise<void>;
  archiveGroup: (groupId: string, categoryId: string) => Promise<void>;
  reorderGroups: (categoryId: string, orderedIds: string[]) => Promise<void>;
  moveGroup: (
    groupId: string,
    fromCategoryId: string,
    toCategoryId: string,
    orderedTarget: string[],
    orderedSource: string[]
  ) => Promise<void>;
  saveNote: (groupId: string, categoryId: string, noteContent: string) => Promise<boolean>;
  addTodo: (
    groupId: string,
    categoryId: string,
    title: string,
    details?: string
  ) => Promise<boolean>;
  addTodos: (
    groupId: string,
    categoryId: string,
    todos: { title: string; details?: string }[]
  ) => Promise<boolean>;
  editTodo: (
    todoId: string,
    groupId: string,
    categoryId: string,
    data: { title?: string; details?: string | null }
  ) => Promise<boolean>;
  toggleTodoDone: (
    todoId: string,
    groupId: string,
    categoryId: string,
    completed: boolean
  ) => Promise<void>;
  removeTodo: (todoId: string, groupId: string, categoryId: string) => Promise<void>;
}

const BoardContext = createContext<BoardContextValue | null>(null);

export function BoardProvider({
  initialCategories,
  projectId,
  aiConfigured,
  children,
}: {
  initialCategories: CategoryDTO[];
  // Undefined means Home. The Project-scoped board reuses every bit of
  // this provider -- the only calls that need to know which board they're
  // on are the two that operate over "all of this board's categories"
  // rather than a specific existing category/group/todo id.
  projectId?: string;
  aiConfigured: boolean;
  children: React.ReactNode;
}) {
  const [categories, setCategories] = useState(initialCategories);

  const updateCategory = useCallback(
    (categoryId: string, updater: (c: CategoryDTO) => CategoryDTO) => {
      setCategories((prev) => prev.map((c) => (c.id === categoryId ? updater(c) : c)));
    },
    []
  );

  const updateGroupInState = useCallback(
    (categoryId: string, groupId: string, updater: (g: GroupDTO) => GroupDTO) => {
      updateCategory(categoryId, (c) => ({
        ...c,
        groups: c.groups.map((g) => (g.id === groupId ? updater(g) : g)),
      }));
    },
    [updateCategory]
  );

  const addCategory = useCallback(
    async (name: string) => {
      try {
        const category = await createCategory(name, projectId);
        setCategories((prev) => [...prev, category]);
      } catch {
        toast.error("Couldn't create category. Try again.");
      }
    },
    [projectId]
  );

  const removeCategory = useCallback(async (categoryId: string) => {
    let prevState: CategoryDTO[] = [];
    setCategories((prev) => {
      prevState = prev;
      return prev.filter((c) => c.id !== categoryId);
    });
    try {
      const result = await deleteCategoryAction(categoryId);
      if (result?.error) {
        setCategories(prevState);
        toast.error(result.error);
      }
    } catch {
      setCategories(prevState);
      toast.error("Couldn't delete category. Try again.");
    }
  }, []);

  const reorderLanes = useCallback(
    async (orderedIds: string[]) => {
      let prevState: CategoryDTO[] = [];
      setCategories((prev) => {
        prevState = prev;
        const byId = new Map(prev.map((c) => [c.id, c]));
        return orderedIds.map((id, order) => ({ ...byId.get(id)!, order }));
      });
      try {
        await reorderCategories(orderedIds, projectId);
      } catch {
        setCategories(prevState);
        toast.error("Couldn't reorder categories. Try again.");
      }
    },
    [projectId]
  );

  const addGroup = useCallback(async (categoryId: string, title: string, color: string) => {
    try {
      const group = await createGroup(categoryId, title, color);
      updateCategory(categoryId, (c) => ({ ...c, groups: [...c.groups, group] }));
      return group;
    } catch {
      toast.error("Couldn't create group. Try again.");
      return undefined;
    }
  }, [updateCategory]);

  const editGroup = useCallback(
    async (groupId: string, categoryId: string, data: { title?: string; color?: string }) => {
      try {
        await updateGroupAction(groupId, data);
        updateGroupInState(categoryId, groupId, (g) => ({ ...g, ...data }));
      } catch {
        toast.error("Couldn't update group. Try again.");
      }
    },
    [updateGroupInState]
  );

  const removeGroup = useCallback(async (groupId: string, categoryId: string) => {
    let prevState: CategoryDTO[] = [];
    setCategories((prev) => {
      prevState = prev;
      return prev.map((c) =>
        c.id === categoryId ? { ...c, groups: c.groups.filter((g) => g.id !== groupId) } : c
      );
    });
    try {
      await deleteGroupAction(groupId);
    } catch {
      setCategories(prevState);
      toast.error("Couldn't delete group. Try again.");
    }
  }, []);

  const archiveGroup = useCallback(async (groupId: string, categoryId: string) => {
    let prevState: CategoryDTO[] = [];
    setCategories((prev) => {
      prevState = prev;
      return prev.map((c) =>
        c.id === categoryId ? { ...c, groups: c.groups.filter((g) => g.id !== groupId) } : c
      );
    });
    try {
      await archiveGroupAction(groupId);
    } catch {
      setCategories(prevState);
      toast.error("Couldn't archive group. Try again.");
    }
  }, []);

  const reorderGroups = useCallback(async (categoryId: string, orderedIds: string[]) => {
    let prevState: CategoryDTO[] = [];
    setCategories((prev) => {
      prevState = prev;
      return prev.map((c) => {
        if (c.id !== categoryId) return c;
        const byId = new Map(c.groups.map((g) => [g.id, g]));
        return { ...c, groups: orderedIds.map((id, order) => ({ ...byId.get(id)!, order })) };
      });
    });
    try {
      await reorderGroupsInCategory(categoryId, orderedIds);
    } catch {
      setCategories(prevState);
      toast.error("Couldn't reorder groups. Try again.");
    }
  }, []);

  const moveGroup = useCallback(
    async (
      groupId: string,
      fromCategoryId: string,
      toCategoryId: string,
      orderedTarget: string[],
      orderedSource: string[]
    ) => {
      let prevState: CategoryDTO[] = [];
      setCategories((prev) => {
        prevState = prev;
        const sourceCat = prev.find((c) => c.id === fromCategoryId);
        const movedGroup = sourceCat?.groups.find((g) => g.id === groupId);
        if (!movedGroup) return prev;

        return prev.map((c) => {
          if (c.id === fromCategoryId) {
            const byId = new Map(c.groups.map((g) => [g.id, g]));
            return {
              ...c,
              groups: orderedSource.map((id, order) => ({ ...byId.get(id)!, order })),
            };
          }
          if (c.id === toCategoryId) {
            const byId = new Map(c.groups.map((g) => [g.id, g]));
            byId.set(groupId, movedGroup);
            return {
              ...c,
              groups: orderedTarget.map((id, order) => ({
                ...byId.get(id)!,
                categoryId: toCategoryId,
                order,
              })),
            };
          }
          return c;
        });
      });
      try {
        await moveGroupToCategory(groupId, toCategoryId, orderedTarget, orderedSource);
      } catch {
        setCategories(prevState);
        toast.error("Couldn't move group. Try again.");
      }
    },
    []
  );

  const saveNote = useCallback(
    async (groupId: string, categoryId: string, noteContent: string) => {
      try {
        await updateGroupNote(groupId, noteContent);
        updateGroupInState(categoryId, groupId, (g) => ({ ...g, noteContent }));
        return true;
      } catch {
        toast.error("Couldn't save note. Try again.");
        return false;
      }
    },
    [updateGroupInState]
  );

  const addTodo = useCallback(
    async (groupId: string, categoryId: string, title: string, details?: string) => {
      try {
        const todo = await createTodo(groupId, title, details);
        updateGroupInState(categoryId, groupId, (g) => ({ ...g, todos: [...g.todos, todo] }));
        return true;
      } catch {
        toast.error("Couldn't add to-do. Try again.");
        return false;
      }
    },
    [updateGroupInState]
  );

  const addTodos = useCallback(
    async (groupId: string, categoryId: string, todos: { title: string; details?: string }[]) => {
      try {
        const created = await createTodos(groupId, todos);
        updateGroupInState(categoryId, groupId, (g) => ({ ...g, todos: [...g.todos, ...created] }));
        return true;
      } catch {
        toast.error("Couldn't add to-dos. Try again.");
        return false;
      }
    },
    [updateGroupInState]
  );

  const editTodo = useCallback(
    async (
      todoId: string,
      groupId: string,
      categoryId: string,
      data: { title?: string; details?: string | null }
    ) => {
      try {
        await updateTodoAction(todoId, data);
        // Approximates the server's own `updatedAt` (set by the same write,
        // a moment later) closely enough for display purposes, without
        // waiting on a round trip just to read it back.
        const updatedAt = new Date().toISOString();
        updateGroupInState(categoryId, groupId, (g) => ({
          ...g,
          todos: g.todos.map((t) => (t.id === todoId ? { ...t, ...data, updatedAt } : t)),
        }));
        return true;
      } catch {
        toast.error("Couldn't update to-do. Try again.");
        return false;
      }
    },
    [updateGroupInState]
  );

  const toggleTodoDone = useCallback(
    async (todoId: string, groupId: string, categoryId: string, completed: boolean) => {
      const now = new Date().toISOString();
      let prevTodos: TodoDTO[] = [];
      updateCategory(categoryId, (c) => ({
        ...c,
        groups: c.groups.map((g) => {
          if (g.id !== groupId) return g;
          prevTodos = g.todos;
          return {
            ...g,
            todos: g.todos.map((t) =>
              t.id === todoId
                ? { ...t, completed, completedAt: completed ? now : null, updatedAt: now }
                : t
            ),
          };
        }),
      }));
      try {
        await toggleTodoAction(todoId, completed);
      } catch {
        updateGroupInState(categoryId, groupId, (g) => ({ ...g, todos: prevTodos }));
        toast.error("Couldn't update to-do. Try again.");
      }
    },
    [updateCategory, updateGroupInState]
  );

  const removeTodo = useCallback(
    async (todoId: string, groupId: string, categoryId: string) => {
      let prevTodos: TodoDTO[] = [];
      updateCategory(categoryId, (c) => ({
        ...c,
        groups: c.groups.map((g) => {
          if (g.id !== groupId) return g;
          prevTodos = g.todos;
          return { ...g, todos: g.todos.filter((t) => t.id !== todoId) };
        }),
      }));
      try {
        await deleteTodoAction(todoId);
      } catch {
        updateGroupInState(categoryId, groupId, (g) => ({ ...g, todos: prevTodos }));
        toast.error("Couldn't delete to-do. Try again.");
      }
    },
    [updateCategory, updateGroupInState]
  );

  return (
    <BoardContext.Provider
      value={{
        categories,
        aiConfigured,
        addCategory,
        removeCategory,
        reorderLanes,
        addGroup,
        editGroup,
        removeGroup,
        archiveGroup,
        reorderGroups,
        moveGroup,
        saveNote,
        addTodo,
        addTodos,
        editTodo,
        toggleTodoDone,
        removeTodo,
      }}
    >
      {children}
    </BoardContext.Provider>
  );
}

export function useBoard() {
  const ctx = useContext(BoardContext);
  if (!ctx) throw new Error("useBoard must be used within a BoardProvider");
  return ctx;
}
