import {
  createContext,
  createEffect,
  createMemo,
  createSignal,
  onCleanup,
  onMount,
  useContext,
  type ParentComponent
} from "solid-js";
import { createStore, produce } from "solid-js/store";
import { addMinutes, differenceInSeconds, formatISO } from "date-fns";

import {
  apiAcceptInvite,
  apiCreateBoardGroup,
  apiDeleteTaskAttachment,
  apiCreateEvent,
  apiCreateFocusSession,
  apiCreateInvite,
  apiCreateLabel,
  apiCreateNote,
  apiRevokeInvite,
  apiCreateTask,
  apiGetInviteByToken,
  apiUploadTaskAttachment,
  apiUpdateBoardGroup,
  apiUpdateEvent,
  apiUpdateFocusSession,
  apiUpdateMember,
  apiUpdateNote,
  apiUpdateTask,
  fetchWorkspaceBundle
} from "./api";
import { authClient } from "./auth-client";
import { createSeedState } from "./seed";
import { useUndoRedo, setupUndoRedoShortcuts } from "./undo-redo";
import type {
  ActivityEntry,
  AppState,
  Attachment,
  BoardGroup,
  CalendarEvent,
  FocusMode,
  FocusSession,
  Invite,
  Label,
  Member,
  Note,
  OfflineQueueItem,
  Task,
  ThemeMode,
  WorkspaceRole
} from "./types";

const STORAGE_KEY = "productier:state";
const AppContext = createContext<ReturnType<typeof createAppContext>>();

function createId(prefix: string) {
  return `${prefix}-${crypto.randomUUID()}`;
}

async function fileToDataUrl(file: File) {
  return new Promise<string>((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => resolve(String(reader.result));
    reader.onerror = reject;
    reader.readAsDataURL(file);
  });
}

async function dataUrlToFile(dataUrl: string, name: string, fallbackMimeType: string) {
  const response = await fetch(dataUrl);
  const blob = await response.blob();
  const mimeType = blob.type || fallbackMimeType || "application/octet-stream";
  return new File([blob], name, { type: mimeType });
}

function createLocalTask(input: Partial<Task> & { workspaceSlug: string; boardGroupId: string; title: string }): Task {
  const now = formatISO(new Date());
  return {
    id: createId("task"),
    workspaceSlug: input.workspaceSlug,
    boardGroupId: input.boardGroupId,
    title: input.title,
    description: input.description ?? "",
    status: input.status ?? "todo",
    color: input.color ?? "slate",
    dueAt: input.dueAt,
    scheduledStart: input.scheduledStart,
    scheduledEnd: input.scheduledEnd,
    assigneeId: input.assigneeId,
    labelIds: input.labelIds ?? [],
    attachments: input.attachments ?? [],
    comments: input.comments ?? [],
    createdAt: now,
    updatedAt: now
  };
}

function createLocalEvent(input: Omit<CalendarEvent, "id">): CalendarEvent {
  return {
    ...input,
    id: createId("event")
  };
}

function createLocalNote(workspaceSlug: string): Note {
  return {
    id: createId("note"),
    workspaceSlug,
    title: "Untitled note",
    content: "Start writing here...",
    updatedAt: formatISO(new Date())
  };
}

function createLocalFocusSession(
  workspaceSlug: string,
  mode: FocusMode,
  durationSeconds: number,
  taskId?: string,
): FocusSession {
  return {
    id: createId("focus"),
    workspaceSlug,
    taskId,
    mode,
    startedAt: formatISO(new Date()),
    pausedTotalSeconds: 0,
    durationSeconds
  };
}

type NoteSyncStatus = "idle" | "saving" | "synced" | "queued" | "error";

interface NoteSyncMeta {
  status: NoteSyncStatus;
  message: string;
  updatedAt: string;
}

function createAppContext() {
  const authSession = authClient.useSession();
  const [ready, setReady] = createSignal(false);
  const [online, setOnline] = createSignal(true);
  const [state, setState] = createStore<AppState>(createSeedState());
  const [noteSyncStates, setNoteSyncStates] = createStore<Record<string, NoteSyncMeta>>({});
  const noteSaveTimers = new Map<string, number>();

  const setNoteSyncStatus = (noteId: string, status: NoteSyncStatus, message: string) => {
    setNoteSyncStates(noteId, {
      status,
      message,
      updatedAt: formatISO(new Date())
    });
  };

  const clearNoteSaveTimer = (noteId: string) => {
    const activeTimer = noteSaveTimers.get(noteId);
    if (activeTimer !== undefined) {
      clearTimeout(activeTimer);
      noteSaveTimers.delete(noteId);
    }
  };

  onCleanup(() => {
    for (const timer of noteSaveTimers.values()) {
      clearTimeout(timer);
    }
    noteSaveTimers.clear();
  });

  const applyTheme = (theme: ThemeMode) => {
    document.documentElement.classList.toggle("dark", theme === "dark");
  };

  const queueMutation = (
    item: Omit<OfflineQueueItem, "id" | "createdAt" | "status"> & { status?: OfflineQueueItem["status"] },
  ) => {
    const nextItem: OfflineQueueItem = {
      id: createId("queue"),
      createdAt: formatISO(new Date()),
      status: item.status ?? "queued",
      ...item
    };
    setState(
      "offlineQueue",
      produce(queue => {
        const existing = queue.find(
          entry =>
            entry.action === nextItem.action &&
            entry.entityType === nextItem.entityType &&
            entry.entityId === nextItem.entityId,
        );
        if (existing) {
          existing.description = nextItem.description;
          existing.payload = nextItem.payload;
          existing.status = nextItem.status;
          return;
        }
        queue.unshift(nextItem);
      }),
    );
    return nextItem.id;
  };

  const markQueueItem = (queueId: string, status: OfflineQueueItem["status"], description?: string) => {
    setState(
      "offlineQueue",
      produce(queue => {
        const item = queue.find(entry => entry.id === queueId);
        if (!item) {
          return;
        }
        item.status = status;
        if (description) {
          item.description = description;
        }
      }),
    );
  };

  const replaceQueuedEntityId = (localId: string, remoteId: string) => {
    setState(
      "offlineQueue",
      produce(queue => {
        for (const item of queue) {
          if (item.entityId === localId) {
            item.entityId = remoteId;
          }
        }
      }),
    );
  };

  const logActivity = (title: string, detail: string) => {
    const activity: ActivityEntry = {
      id: createId("activity"),
      workspaceSlug: state.workspaces[0]?.slug ?? "personal",
      title,
      detail,
      createdAt: formatISO(new Date())
    };
    setState("activities", activities => [activity, ...activities].slice(0, 30));
  };

  const hydrateFromApi = async () => {
    const workspaceSlug = state.workspaces[0]?.slug ?? "personal";
    const bundle = await fetchWorkspaceBundle(workspaceSlug);
    setState(
      produce(draft => {
        if (bundle.workspaces.length) {
          draft.workspaces = bundle.workspaces;
        }
        draft.members = bundle.members as Member[];
        draft.invites = bundle.invites as Invite[];
        draft.boardGroups = bundle.boardGroups as BoardGroup[];
        draft.labels = bundle.labels as Label[];
        draft.tasks = bundle.tasks as Task[];
        draft.events = bundle.events as CalendarEvent[];
        draft.notes = bundle.notes as Note[];
        draft.focusSessions = bundle.focusSessions as FocusSession[];
        draft.activities = bundle.activities as ActivityEntry[];
      }),
    );
  };

  const replaceLocalEntity = (entityType: OfflineQueueItem["entityType"], localId: string, remoteEntity: unknown) => {
    switch (entityType) {
      case "task":
        setState(
          "tasks",
          tasks =>
            tasks.map(task =>
              task.id === localId
                ? {
                    ...task,
                    ...(remoteEntity as Task),
                    attachments: task.attachments,
                    comments: task.comments,
                    labelIds: task.labelIds
                  }
                : task,
            ),
        );
        break;
      case "event":
        setState(
          "events",
          events =>
            events.map(event =>
              event.id === localId
                ? {
                    ...event,
                    ...(remoteEntity as CalendarEvent),
                    attachments: event.attachments
                  }
                : event,
            ),
        );
        break;
      case "note":
        setState("notes", notes => notes.map(note => (note.id === localId ? ({ ...note, ...(remoteEntity as Note) }) : note)));
        break;
      case "focus_session":
        setState(
          "focusSessions",
          sessions => sessions.map(session => (session.id === localId ? ({ ...session, ...(remoteEntity as FocusSession) }) : session)),
        );
        break;
      case "board_group":
        setState("boardGroups", groups => groups.map(group => (group.id === localId ? ({ ...group, ...(remoteEntity as BoardGroup) }) : group)));
        break;
      case "label":
        setState("labels", labels => labels.map(label => (label.id === localId ? ({ ...label, ...(remoteEntity as Label) }) : label)));
        break;
    }
  };

  const replayQueuedMutation = async (item: OfflineQueueItem) => {
    const payload = item.payload ?? {};

    if (item.action === "create") {
      switch (item.entityType) {
        case "board_group": {
          const saved = await apiCreateBoardGroup(payload as Parameters<typeof apiCreateBoardGroup>[0]);
          if (saved && item.entityId) {
            replaceLocalEntity("board_group", item.entityId, saved);
            replaceQueuedEntityId(item.entityId, (saved as BoardGroup).id);
          }
          return;
        }
        case "label": {
          const saved = await apiCreateLabel(payload as Parameters<typeof apiCreateLabel>[0]);
          if (saved && item.entityId) {
            replaceLocalEntity("label", item.entityId, saved);
            replaceQueuedEntityId(item.entityId, (saved as Label).id);
          }
          return;
        }
        case "task": {
          const saved = await apiCreateTask(payload as Parameters<typeof apiCreateTask>[0]);
          if (saved && item.entityId) {
            replaceLocalEntity("task", item.entityId, saved);
            replaceQueuedEntityId(item.entityId, (saved as Task).id);
          }
          return;
        }
        case "event": {
          const saved = await apiCreateEvent(payload as Parameters<typeof apiCreateEvent>[0]);
          if (saved && item.entityId) {
            replaceLocalEntity("event", item.entityId, saved);
            replaceQueuedEntityId(item.entityId, (saved as CalendarEvent).id);
          }
          return;
        }
        case "note": {
          const saved = await apiCreateNote(payload as Parameters<typeof apiCreateNote>[0]);
          if (saved && item.entityId) {
            replaceLocalEntity("note", item.entityId, saved);
            replaceQueuedEntityId(item.entityId, (saved as Note).id);
          }
          return;
        }
        case "focus_session": {
          const saved = await apiCreateFocusSession(payload as Parameters<typeof apiCreateFocusSession>[0]);
          if (saved && item.entityId) {
            replaceLocalEntity("focus_session", item.entityId, saved);
            replaceQueuedEntityId(item.entityId, (saved as FocusSession).id);
          }
        }
      }
      return;
    }

    if (!item.entityId) {
      return;
    }

    switch (item.entityType) {
      case "board_group":
        await apiUpdateBoardGroup(item.entityId, payload as Parameters<typeof apiUpdateBoardGroup>[1]);
        return;
      case "task":
        await apiUpdateTask(item.entityId, payload as Parameters<typeof apiUpdateTask>[1]);
        return;
      case "event":
        await apiUpdateEvent(item.entityId, payload as Parameters<typeof apiUpdateEvent>[1]);
        return;
      case "note":
        await apiUpdateNote(item.entityId, payload as Parameters<typeof apiUpdateNote>[1]);
        return;
      case "focus_session":
        await apiUpdateFocusSession(item.entityId, payload as Parameters<typeof apiUpdateFocusSession>[1]);
    }
  };

  const syncQueuedMutations = async () => {
    const pending = [...state.offlineQueue]
      .filter(item => item.status !== "synced")
      .sort((left, right) => left.createdAt.localeCompare(right.createdAt));

    if (!pending.length) {
      await hydrateFromApi();
      return;
    }

    for (const item of pending) {
      try {
        markQueueItem(item.id, "queued", `Syncing ${item.description.toLowerCase()}`);
        await replayQueuedMutation(item);
        markQueueItem(item.id, "synced", `${item.description} synced`);
        if (item.entityType === "note" && item.entityId) {
          setNoteSyncStatus(item.entityId, "synced", "Synced");
        }
      } catch {
        markQueueItem(item.id, "conflict", `${item.description} needs review`);
        if (item.entityType === "note" && item.entityId) {
          setNoteSyncStatus(item.entityId, "error", "Sync failed, needs review");
        }
      }
    }

    if (!state.offlineQueue.some(item => item.status !== "synced")) {
      await hydrateFromApi();
    }
  };

  const syncLocalTaskAttachments = async () => {
    if (!online()) {
      return;
    }

    let syncedCount = 0;
    const candidates = state.tasks.filter(
      task => !task.id.startsWith("task-") && task.attachments.some(attachment => attachment.dataUrl?.startsWith("data:")),
    );

    for (const task of candidates) {
      const localAttachments = task.attachments.filter(attachment => attachment.dataUrl?.startsWith("data:"));
      for (const attachment of localAttachments) {
        if (!attachment.dataUrl) {
          continue;
        }

        try {
          const file = await dataUrlToFile(attachment.dataUrl, attachment.name, attachment.mimeType);
          const uploaded = await apiUploadTaskAttachment(task.id, file);
          if (!uploaded) {
            continue;
          }

          setState(
            "tasks",
            tasks =>
              tasks.map(entry =>
                entry.id === task.id
                  ? {
                      ...entry,
                      attachments: entry.attachments.map(current =>
                        current.id === attachment.id ? (uploaded as Attachment) : current,
                      ),
                      updatedAt: formatISO(new Date())
                    }
                  : entry,
              ),
          );
          syncedCount += 1;
        } catch {
          // Keep local attachment fallback and retry later on next reconnect.
        }
      }
    }

    if (syncedCount > 0) {
      logActivity("Attachments synced", `${syncedCount} offline attachment(s) moved to backend storage.`);
    }
  };

  const activeFocusSession = createMemo(() => state.focusSessions.find(session => !session.completedAt));
  const primaryWorkspace = createMemo(() => state.workspaces[0]);
  const workspaceForSlug = (slug: string) => state.workspaces.find(workspace => workspace.slug === slug);
  const memberForId = (id?: string) => state.members.find(member => member.id === id);

  onMount(() => {
    try {
      const raw = localStorage.getItem(STORAGE_KEY);
      if (raw) {
        setState(JSON.parse(raw) as AppState);
      }
    } catch {
      localStorage.removeItem(STORAGE_KEY);
    }

    const startOnline = navigator.onLine;
    setOnline(startOnline);
    applyTheme(state.theme);
    setReady(true);

    if (startOnline) {
      const hasPendingLocalState = state.offlineQueue.some(item => item.status !== "synced");
      if (hasPendingLocalState) {
        void syncQueuedMutations()
          .then(() => syncLocalTaskAttachments())
          .catch(() => undefined);
      } else {
        void hydrateFromApi()
          .then(() => syncLocalTaskAttachments())
          .catch(() => undefined);
      }
    }

    const handleOnline = () => {
      setOnline(true);
      logActivity("Reconnected", "Live workspace sync is back.");
      void syncQueuedMutations()
        .then(() => syncLocalTaskAttachments())
        .catch(() => undefined);
    };
    const handleOffline = () => {
      setOnline(false);
      logActivity("Offline mode", "Changes will be queued locally until the connection returns.");
    };

    window.addEventListener("online", handleOnline);
    window.addEventListener("offline", handleOffline);
    onCleanup(() => {
      window.removeEventListener("online", handleOnline);
      window.removeEventListener("offline", handleOffline);
    });
  });

  createEffect(() => {
    if (!ready()) {
      return;
    }
    localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
    applyTheme(state.theme);
  });

  createEffect(() => {
    const current = authSession();
    if (current.isPending) {
      return;
    }

    const user = current.data?.user;
    setState(
      "session",
      user
        ? {
            id: user.id,
            name: user.name,
            email: user.email
          }
        : null,
    );
  });

  const login = (name: string, email: string) => {
    setState("session", { id: createId("user"), name, email });
  };

  const logout = async () => {
    await authClient.signOut();
    setState("session", null);
  };
  const setTheme = (theme: ThemeMode) => setState("theme", theme);

  const addBoardGroup = async (workspaceSlug: string, name: string, color = "slate") => {
    const optimistic: BoardGroup = {
      id: createId("group"),
      workspaceSlug,
      name,
      color,
      order: state.boardGroups.filter(group => group.workspaceSlug === workspaceSlug).length
    };
    setState("boardGroups", groups => [...groups, optimistic]);
    logActivity("Board group added", `${name} is ready for new tasks.`);

    if (!online()) {
      queueMutation({
        entityType: "board_group",
        action: "create",
        entityId: optimistic.id,
        description: `Created board group "${name}"`,
        payload: { workspaceSlug, name, color }
      });
      return optimistic;
    }

    try {
      const saved = await apiCreateBoardGroup({ workspaceSlug, name, color });
      if (saved) {
        setState("boardGroups", groups => groups.map(group => (group.id === optimistic.id ? (saved as BoardGroup) : group)));
        return saved as BoardGroup;
      }
    } catch {
      queueMutation({
        entityType: "board_group",
        action: "create",
        entityId: optimistic.id,
        description: `Created board group "${name}"`,
        payload: { workspaceSlug, name, color },
        status: "conflict"
      });
    }

    return optimistic;
  };

  const syncBoardGroup = async (groupId: string) => {
    const group = state.boardGroups.find(entry => entry.id === groupId);
    if (!group) {
      return;
    }
    const payload = {
      name: group.name,
      color: group.color,
      order: group.order
    };

    if (!online()) {
      queueMutation({
        entityType: "board_group",
        action: "update",
        entityId: groupId,
        description: `Updated board group "${group.name}"`,
        payload
      });
      return;
    }

    try {
      const saved = await apiUpdateBoardGroup(groupId, payload);
      if (saved) {
        setState("boardGroups", groups => groups.map(entry => (entry.id === groupId ? ({ ...entry, ...(saved as BoardGroup) }) : entry)));
      }
    } catch {
      queueMutation({
        entityType: "board_group",
        action: "update",
        entityId: groupId,
        description: `Updated board group "${group.name}"`,
        payload,
        status: "conflict"
      });
    }
  };

  const updateBoardGroup = (groupId: string, input: { name?: string; color?: string; order?: number }) => {
    setState(
      "boardGroups",
      produce(groups => {
        const group = groups.find(entry => entry.id === groupId);
        if (!group) {
          return;
        }

        if (input.name !== undefined) {
          group.name = input.name;
        }
        if (input.color !== undefined) {
          group.color = input.color;
        }
        if (input.order !== undefined) {
          group.order = Math.max(0, Math.floor(input.order));

          const workspaceGroups = groups
            .filter(entry => entry.workspaceSlug === group.workspaceSlug)
            .sort((left, right) => {
              if (left.order === right.order) {
                return left.name.localeCompare(right.name);
              }
              return left.order - right.order;
            });

          workspaceGroups.forEach((entry, index) => {
            const target = groups.find(candidate => candidate.id === entry.id);
            if (target) {
              target.order = index;
            }
          });
        }
      }),
    );

    logActivity("Board group updated", "Board grouping settings changed.");
    void syncBoardGroup(groupId);
  };

  const reorderBoardGroup = (groupId: string, direction: -1 | 1) => {
    const group = state.boardGroups.find(entry => entry.id === groupId);
    if (!group) {
      return;
    }
    const workspaceGroups = [...state.boardGroups]
      .filter(entry => entry.workspaceSlug === group.workspaceSlug)
      .sort((left, right) => left.order - right.order);
    const currentIndex = workspaceGroups.findIndex(entry => entry.id === groupId);
    if (currentIndex < 0) {
      return;
    }
    const nextIndex = Math.max(0, Math.min(workspaceGroups.length - 1, currentIndex + direction));
    if (nextIndex === currentIndex) {
      return;
    }
    updateBoardGroup(groupId, { order: nextIndex });
  };

  const addLabel = async (workspaceSlug: string, name: string, color: string) => {
    const optimistic: Label = { id: createId("label"), workspaceSlug, name, color };
    setState("labels", labels => [...labels, optimistic]);

    if (!online()) {
      queueMutation({
        entityType: "label",
        action: "create",
        entityId: optimistic.id,
        description: `Created label "${name}"`,
        payload: { workspaceSlug, name, color }
      });
      return optimistic;
    }

    try {
      const saved = await apiCreateLabel({ workspaceSlug, name, color });
      if (saved) {
        setState("labels", labels => labels.map(label => (label.id === optimistic.id ? (saved as Label) : label)));
        return saved as Label;
      }
    } catch {
      queueMutation({
        entityType: "label",
        action: "create",
        entityId: optimistic.id,
        description: `Created label "${name}"`,
        payload: { workspaceSlug, name, color },
        status: "conflict"
      });
    }

    return optimistic;
  };

  const createTask = async (input: Partial<Task> & { workspaceSlug: string; boardGroupId: string; title: string }) => {
    const optimistic = createLocalTask(input);
    const payload = {
      workspaceSlug: input.workspaceSlug,
      boardGroupId: input.boardGroupId,
      title: input.title,
      description: input.description ?? "",
      dueAt: input.dueAt,
      color: input.color ?? "slate"
    };
    setState("tasks", tasks => [optimistic, ...tasks]);
    logActivity("Task created", optimistic.title);

    if (!online()) {
      queueMutation({
        entityType: "task",
        action: "create",
        entityId: optimistic.id,
        description: `Created task "${optimistic.title}"`,
        payload
      });
      return optimistic;
    }

    try {
      const saved = await apiCreateTask(payload);
      if (saved) {
        const hydrated = {
          ...(saved as Task),
          labelIds: (saved as Task).labelIds ?? [],
          attachments: input.attachments ?? [],
          comments: input.comments ?? []
        };
        setState("tasks", tasks => tasks.map(task => (task.id === optimistic.id ? hydrated : task)));
        return hydrated;
      }
    } catch {
      queueMutation({
        entityType: "task",
        action: "create",
        entityId: optimistic.id,
        description: `Created task "${optimistic.title}"`,
        payload,
        status: "conflict"
      });
    }

    return optimistic;
  };

  const syncTask = async (taskId: string) => {
    const task = state.tasks.find(entry => entry.id === taskId);
    if (!task) {
      return;
    }
    const payload = {
      title: task.title,
      description: task.description,
      status: task.status,
      boardGroupId: task.boardGroupId,
      color: task.color,
      dueAt: task.dueAt,
      scheduledStart: task.scheduledStart,
      scheduledEnd: task.scheduledEnd,
      assigneeId: task.assigneeId,
      labelIds: task.labelIds,
      attachments: task.attachments,
      comments: task.comments
    };
    if (!online()) {
      queueMutation({
        entityType: "task",
        action: "update",
        entityId: taskId,
        description: `Updated task "${task.title}"`,
        payload
      });
      return;
    }
    try {
      const saved = await apiUpdateTask(taskId, payload);
      if (saved) {
        setState("tasks", tasks => tasks.map(entry => (entry.id === taskId ? ({ ...entry, ...(saved as Task) }) : entry)));
      }
    } catch {
      queueMutation({
        entityType: "task",
        action: "update",
        entityId: taskId,
        description: `Updated task "${task.title}"`,
        payload,
        status: "conflict"
      });
    }
  };

  const updateTask = (taskId: string, updater: (task: Task) => void) => {
    setState(
      "tasks",
      produce(tasks => {
        const task = tasks.find(entry => entry.id === taskId);
        if (!task) {
          return;
        }
        updater(task);
        task.updatedAt = formatISO(new Date());
      }),
    );
    void syncTask(taskId);
  };

  const moveTask = (taskId: string, boardGroupId: string) => {
    updateTask(taskId, task => {
      task.boardGroupId = boardGroupId;
      task.status = boardGroupId.includes("done") ? "done" : boardGroupId.includes("doing") ? "in_progress" : "todo";
    });
    logActivity("Task moved", "Board status changed.");
  };

  const toggleTask = (taskId: string) => {
    updateTask(taskId, task => {
      task.status = task.status === "done" ? "todo" : "done";
      if (task.status === "done") {
        task.boardGroupId = "group-done";
      }
    });
  };

  const scheduleTaskOnDay = (taskId: string, date: Date) => {
    updateTask(taskId, task => {
      const startsAt = addMinutes(new Date(date.setHours(10, 0, 0, 0)), 0);
      const endsAt = addMinutes(startsAt, 60);
      task.scheduledStart = formatISO(startsAt);
      task.scheduledEnd = formatISO(endsAt);
    });
    logActivity("Task scheduled", "Calendar plan updated.");
  };

  const createEvent = async (input: Omit<CalendarEvent, "id">) => {
    const optimistic = createLocalEvent(input);
    setState("events", events => [optimistic, ...events]);
    logActivity("Event created", optimistic.title);

    if (!online()) {
      queueMutation({
        entityType: "event",
        action: "create",
        entityId: optimistic.id,
        description: `Created event "${optimistic.title}"`,
        payload: input
      });
      return optimistic;
    }

    try {
      const saved = await apiCreateEvent(input);
      if (saved) {
        setState("events", events => events.map(event => (event.id === optimistic.id ? ({ ...(saved as CalendarEvent), attachments: optimistic.attachments }) : event)));
        return saved as CalendarEvent;
      }
    } catch {
      queueMutation({
        entityType: "event",
        action: "create",
        entityId: optimistic.id,
        description: `Created event "${optimistic.title}"`,
        payload: input,
        status: "conflict"
      });
    }

    return optimistic;
  };

  const syncEvent = async (eventId: string) => {
    const event = state.events.find(entry => entry.id === eventId);
    if (!event) {
      return;
    }
    const payload = {
      title: event.title,
      description: event.description,
      startsAt: event.startsAt,
      endsAt: event.endsAt,
      linkedTaskId: event.linkedTaskId,
      color: event.color,
      attachments: event.attachments
    };
    if (!online()) {
      queueMutation({
        entityType: "event",
        action: "update",
        entityId: eventId,
        description: `Updated event "${event.title}"`,
        payload
      });
      return;
    }
    try {
      const saved = await apiUpdateEvent(eventId, payload);
      if (saved) {
        setState("events", events => events.map(entry => (entry.id === eventId ? ({ ...entry, ...(saved as CalendarEvent) }) : entry)));
      }
    } catch {
      queueMutation({
        entityType: "event",
        action: "update",
        entityId: eventId,
        description: `Updated event "${event.title}"`,
        payload,
        status: "conflict"
      });
    }
  };

  const updateEvent = (eventId: string, updater: (event: CalendarEvent) => void) => {
    setState(
      "events",
      produce(events => {
        const event = events.find(entry => entry.id === eventId);
        if (event) {
          updater(event);
        }
      }),
    );
    void syncEvent(eventId);
  };

  const moveEventToDay = (eventId: string, date: Date) => {
    updateEvent(eventId, event => {
      const previousStart = new Date(event.startsAt);
      const previousEnd = new Date(event.endsAt);
      const duration = differenceInSeconds(previousEnd, previousStart);
      const nextStart = new Date(date);
      nextStart.setHours(previousStart.getHours(), previousStart.getMinutes(), 0, 0);
      const nextEnd = addMinutes(nextStart, Math.round(duration / 60));
      event.startsAt = formatISO(nextStart);
      event.endsAt = formatISO(nextEnd);
    });
    logActivity("Event moved", "Calendar item was rescheduled.");
  };

  const createNote = async (workspaceSlug: string) => {
    const optimistic = createLocalNote(workspaceSlug);
    const payload = {
      workspaceSlug,
      title: optimistic.title,
      content: optimistic.content
    };
    setState("notes", notes => [optimistic, ...notes]);
    setNoteSyncStatus(
      optimistic.id,
      online() ? "saving" : "queued",
      online() ? "Saving note..." : "Queued for sync while offline.",
    );

    if (!online()) {
      queueMutation({
        entityType: "note",
        action: "create",
        entityId: optimistic.id,
        description: `Created note "${optimistic.title}"`,
        payload
      });
      return optimistic;
    }

    try {
      const saved = await apiCreateNote(payload);
      if (saved) {
        setState("notes", notes => notes.map(note => (note.id === optimistic.id ? (saved as Note) : note)));
        setNoteSyncStatus((saved as Note).id, "synced", "Synced");
        return saved as Note;
      }
    } catch {
      queueMutation({
        entityType: "note",
        action: "create",
        entityId: optimistic.id,
        description: `Created note "${optimistic.title}"`,
        payload,
        status: "conflict"
      });
      setNoteSyncStatus(optimistic.id, "error", "Save failed. Will retry when possible.");
    }

    return optimistic;
  };

  const updateNote = (noteId: string, title: string, content: string) => {
    setState(
      "notes",
      produce(notes => {
        const note = notes.find(entry => entry.id === noteId);
        if (note) {
          note.title = title;
          note.content = content;
          note.updatedAt = formatISO(new Date());
        }
      }),
    );

    const note = state.notes.find(entry => entry.id === noteId);
    if (!note) {
      return;
    }
    const payload = { title, content };

    clearNoteSaveTimer(noteId);

    if (!online()) {
      queueMutation({
        entityType: "note",
        action: "update",
        entityId: noteId,
        description: `Updated note "${note.title}"`,
        payload
      });
      setNoteSyncStatus(noteId, "queued", "Queued for sync while offline.");
      return;
    }

    setNoteSyncStatus(noteId, "saving", "Saving changes...");
    const timer = setTimeout(() => {
      void apiUpdateNote(noteId, payload)
        .then(() => {
          setNoteSyncStatus(noteId, "synced", "Synced");
        })
        .catch(() => {
          queueMutation({
            entityType: "note",
            action: "update",
            entityId: noteId,
            description: `Updated note "${title}"`,
            payload,
            status: "conflict"
          });
          setNoteSyncStatus(noteId, "error", "Sync failed. Will retry when possible.");
        })
        .finally(() => {
          noteSaveTimers.delete(noteId);
        });
    }, 450);
    noteSaveTimers.set(noteId, timer);
  };

  const createInvite = async (workspaceSlug: string, email: string, role: WorkspaceRole) => {
    if (!online()) {
      logActivity("Invite blocked", "Invites require a live connection.");
      return null;
    }

    let saved: Awaited<ReturnType<typeof apiCreateInvite>> | null = null;
    try {
      saved = await apiCreateInvite({ workspaceSlug, email, role });
    } catch {
      logActivity("Invite failed", `Could not create invite for ${email}.`);
      return null;
    }
    if (!saved) {
      return null;
    }
    setState("invites", invites => [saved as Invite, ...invites.filter(invite => invite.id !== (saved as Invite).id)]);
    logActivity("Invite created", `Shared workspace access with ${email}.`);
    return saved as Invite;
  };

  const revokeInvite = async (inviteId: string) => {
    if (!online()) {
      logActivity("Invite blocked", "Revoking invites requires a live connection.");
      return false;
    }
    const invite = state.invites.find(entry => entry.id === inviteId);
    if (!invite) {
      return false;
    }

    try {
      await apiRevokeInvite(inviteId);
      setState("invites", invites => invites.filter(entry => entry.id !== inviteId));
      logActivity("Invite revoked", `Invite for ${invite.email} was revoked.`);
      return true;
    } catch {
      logActivity("Invite failed", `Could not revoke invite for ${invite.email}.`);
      return false;
    }
  };

  const updateMember = async (memberId: string, input: { role?: WorkspaceRole; status?: "active" | "removed" }) => {
    if (!online()) {
      logActivity("Member update blocked", "Member changes require a live connection.");
      return null;
    }
    const target = state.members.find(member => member.id === memberId);
    if (!target) {
      return null;
    }

    try {
      const updated = await apiUpdateMember(memberId, input);
      if (!updated) {
        return null;
      }
      setState("members", members => members.map(member => (member.id === memberId ? (updated as Member) : member)));
      logActivity("Member updated", `${target.email} access was updated.`);
      return updated as Member;
    } catch {
      logActivity("Member update failed", `Could not update member ${target.email}.`);
      return null;
    }
  };

  const getInviteByToken = async (token: string) => {
    const localInvite = state.invites.find(entry => entry.token === token);
    if (localInvite) {
      return localInvite;
    }
    try {
      const invite = await apiGetInviteByToken(token);
      return (invite as Invite | undefined) ?? null;
    } catch {
      return null;
    }
  };

  const acceptInvite = async (token: string) => {
    const invite = state.invites.find(entry => entry.token === token) ?? (await getInviteByToken(token));
    if (!invite || !state.session) {
      return false;
    }

    if (!online()) {
      logActivity("Invite blocked", "Accepting invites requires a live connection.");
      return false;
    }

    try {
      const accepted = await apiAcceptInvite(token, {
        name: state.session.name,
        email: state.session.email
      });
      if (!accepted) {
        return false;
      }
      setState(
        produce(draft => {
          const target = draft.invites.find(entry => entry.token === token);
          if (target) {
            target.status = "accepted";
          } else {
            draft.invites.unshift(accepted as Invite);
          }
          const existingMember = draft.members.find(member => member.email === state.session?.email);
          if (!existingMember) {
            draft.members.unshift({
              id: createId("member"),
              workspaceSlug: invite.workspaceSlug,
              email: state.session?.email ?? invite.email,
              name: state.session?.name ?? invite.email.split("@")[0],
              role: invite.role,
              status: "active"
            });
          }
        }),
      );
    } catch {
      return false;
    }

    logActivity("Invite accepted", `${invite.email} joined the workspace.`);
    return true;
  };

  const attachFilesToTask = async (taskId: string, files: FileList | null) => {
    if (!files?.length) {
      return;
    }
    const uploadedAttachments: Attachment[] = [];
    const localFallbackAttachments: Attachment[] = [];

    for (const file of Array.from(files)) {
      if (online()) {
        try {
          const uploaded = await apiUploadTaskAttachment(taskId, file);
          if (uploaded) {
            uploadedAttachments.push(uploaded as Attachment);
            continue;
          }
        } catch {
          // Fall back to local attachment payload if upload fails.
        }
      }

      localFallbackAttachments.push({
        id: createId("attachment"),
        name: file.name,
        mimeType: file.type || "application/octet-stream",
        size: file.size,
        dataUrl: await fileToDataUrl(file)
      });
    }

    if (uploadedAttachments.length) {
      setState(
        "tasks",
        tasks =>
          tasks.map(task =>
            task.id === taskId
              ? {
                  ...task,
                  attachments: [...uploadedAttachments, ...task.attachments],
                  updatedAt: formatISO(new Date())
                }
              : task,
          ),
      );
    }

    if (localFallbackAttachments.length) {
      updateTask(taskId, task => {
        task.attachments = [...localFallbackAttachments, ...task.attachments];
      });
    }
  };

  const removeTaskAttachment = async (taskId: string, attachmentId: string) => {
    const task = state.tasks.find(entry => entry.id === taskId);
    if (!task) {
      return;
    }
    const nextAttachments = task.attachments.filter(attachment => attachment.id !== attachmentId);
    if (nextAttachments.length === task.attachments.length) {
      return;
    }

    if (!online()) {
      updateTask(taskId, current => {
        current.attachments = nextAttachments;
      });
      return;
    }

    const previousAttachments = task.attachments;
    setState(
      "tasks",
      tasks =>
        tasks.map(entry =>
          entry.id === taskId
            ? {
                ...entry,
                attachments: nextAttachments,
                updatedAt: formatISO(new Date())
              }
            : entry,
        ),
    );

    try {
      await apiDeleteTaskAttachment(taskId, attachmentId);
      logActivity("Attachment removed", "Task file removed.");
    } catch {
      setState(
        "tasks",
        tasks =>
          tasks.map(entry =>
            entry.id === taskId
              ? {
                  ...entry,
                  attachments: previousAttachments
                }
              : entry,
          ),
      );
      queueMutation({
        entityType: "task",
        action: "update",
        entityId: taskId,
        description: `Attachment removal failed for "${task.title}"`,
        payload: {
          attachments: previousAttachments
        },
        status: "conflict"
      });
    }
  };

  const createTaskComment = (taskId: string, content: string) => {
    if (!state.session || !content.trim()) {
      return;
    }
    updateTask(taskId, task => {
      task.comments.unshift({
        id: createId("comment"),
        taskId,
        author: state.session?.name ?? "Unknown",
        content,
        createdAt: formatISO(new Date())
      });
    });
  };

  const noteSyncFor = (noteId?: string) => {
    if (!noteId) {
      return null;
    }
    return noteSyncStates[noteId] ?? null;
  };

  const startFocusSession = async (workspaceSlug: string, mode: FocusMode, durationSeconds: number, taskId?: string) => {
    if (activeFocusSession()) {
      return activeFocusSession();
    }

    const optimistic = createLocalFocusSession(workspaceSlug, mode, durationSeconds, taskId);
    setState("focusSessions", sessions => [optimistic, ...sessions]);

    if (!online()) {
      queueMutation({
        entityType: "focus_session",
        action: "create",
        entityId: optimistic.id,
        description: `Started ${mode.replace("_", " ")} session`,
        payload: { workspaceSlug, taskId, mode, durationSeconds }
      });
      return optimistic;
    }

    try {
      const saved = await apiCreateFocusSession({ workspaceSlug, taskId, mode, durationSeconds });
      if (saved) {
        setState("focusSessions", sessions => sessions.map(session => (session.id === optimistic.id ? (saved as FocusSession) : session)));
        return saved as FocusSession;
      }
    } catch {
      queueMutation({
        entityType: "focus_session",
        action: "create",
        entityId: optimistic.id,
        description: `Started ${mode.replace("_", " ")} session`,
        payload: { workspaceSlug, taskId, mode, durationSeconds },
        status: "conflict"
      });
    }

    return optimistic;
  };

  const syncFocusSession = async (sessionId: string) => {
    const session = state.focusSessions.find(entry => entry.id === sessionId);
    if (!session) {
      return;
    }
    const payload = {
      completedAt: session.completedAt,
      pausedAt: session.pausedAt,
      pausedTotalSeconds: session.pausedTotalSeconds
    };
    if (!online()) {
      queueMutation({
        entityType: "focus_session",
        action: "update",
        entityId: sessionId,
        description: "Updated focus session",
        payload
      });
      return;
    }
    try {
      const saved = await apiUpdateFocusSession(sessionId, payload);
      if (saved) {
        setState("focusSessions", sessions => sessions.map(entry => (entry.id === sessionId ? ({ ...entry, ...(saved as FocusSession) }) : entry)));
      }
    } catch {
      queueMutation({
        entityType: "focus_session",
        action: "update",
        entityId: sessionId,
        description: "Updated focus session",
        payload,
        status: "conflict"
      });
    }
  };

  const pauseFocusSession = (sessionId: string) => {
    setState(
      "focusSessions",
      produce(sessions => {
        const session = sessions.find(entry => entry.id === sessionId && !entry.completedAt);
        if (session && !session.pausedAt) {
          session.pausedAt = formatISO(new Date());
        }
      }),
    );
    void syncFocusSession(sessionId);
  };

  const resumeFocusSession = (sessionId: string) => {
    setState(
      "focusSessions",
      produce(sessions => {
        const session = sessions.find(entry => entry.id === sessionId && !entry.completedAt);
        if (!session || !session.pausedAt) {
          return;
        }
        const pausedSeconds = differenceInSeconds(new Date(), new Date(session.pausedAt));
        session.pausedTotalSeconds += pausedSeconds;
        session.pausedAt = undefined;
      }),
    );
    void syncFocusSession(sessionId);
  };

  const completeFocusSession = (sessionId: string) => {
    setState(
      "focusSessions",
      produce(sessions => {
        const session = sessions.find(entry => entry.id === sessionId && !entry.completedAt);
        if (!session) {
          return;
        }
        if (session.pausedAt) {
          const pausedSeconds = differenceInSeconds(new Date(), new Date(session.pausedAt));
          session.pausedTotalSeconds += pausedSeconds;
          session.pausedAt = undefined;
        }
        session.completedAt = formatISO(new Date());
      }),
    );
    logActivity("Focus session completed", "A pomodoro session was logged.");
    void syncFocusSession(sessionId);
  };

  return {
    state,
    ready,
    online,
    primaryWorkspace,
    activeFocusSession,
    workspaceForSlug,
    memberForId,
    login,
    logout,
    setTheme,
    hydrateFromApi,
    addBoardGroup,
    updateBoardGroup,
    reorderBoardGroup,
    createTask,
    updateTask,
    moveTask,
    toggleTask,
    scheduleTaskOnDay,
    createEvent,
    updateEvent,
    moveEventToDay,
    createNote,
    updateNote,
    noteSyncFor,
    addLabel,
    createInvite,
    revokeInvite,
    updateMember,
    getInviteByToken,
    acceptInvite,
    attachFilesToTask,
    removeTaskAttachment,
    createTaskComment,
    startFocusSession,
    pauseFocusSession,
    resumeFocusSession,
    completeFocusSession,
    // CRM helpers - these use the API directly and update local state
    refreshContacts: async (workspaceSlug: string) => {
      const { apiListContacts } = await import("./api-crm");
      const contacts = await apiListContacts(workspaceSlug);
      setState("contacts", contacts);
    },
    refreshCompanies: async (workspaceSlug: string) => {
      const { apiListCompanies } = await import("./api-crm");
      const companies = await apiListCompanies(workspaceSlug);
      setState("companies", companies);
    },
    refreshInbox: async (workspaceSlug: string) => {
      const { apiListInboxItems } = await import("./api-crm");
      const items = await apiListInboxItems(workspaceSlug);
      setState("inboxItems", items);
    },
    refreshNotifications: async () => {
      const { apiListNotifications } = await import("./api-integrations");
      const notifications = await apiListNotifications();
      setState("notifications", notifications);
    },
    // Undo/Redo
    ...useUndoRedo()
  };
}

export const AppProvider: ParentComponent = props => {
  const value = createAppContext();
  return <AppContext.Provider value={value}>{props.children}</AppContext.Provider>;
};

export function useApp() {
  const context = useContext(AppContext);
  if (!context) {
    throw new Error("useApp must be used inside AppProvider");
  }
  return context;
}
