import { create } from "zustand";
import { persist, createJSONStorage } from "zustand/middleware";
import { followUpIn } from "./format";
import { nid, nowIso } from "./ids";
import { SEED } from "./seed";
import type {
  Board,
  BoardKind,
  Note,
  Priority,
  Project,
  Reminder,
  ScratchNote,
  Status,
  WaitingItem,
  Task,
  WorkspaceData,
} from "./types";

type CreateTaskInput = {
  title: string;
  body?: string;
  projectId?: string | null;
  dueAt?: string | null;
  priority?: Priority;
  noteId?: string;
  source?: Task["source"];
};

type WorkspaceState = WorkspaceData & {
  hydrated: boolean;
  setHydrated: (v: boolean) => void;

  addProject: (name: string, description?: string) => string;
  updateProject: (id: string, patch: Partial<Pick<Project, "name" | "description" | "status">>) => void;
  removeProject: (id: string) => void;

  addTask: (input: CreateTaskInput) => string;
  updateTask: (id: string, patch: Partial<Omit<Task, "id" | "createdAt">>) => void;
  setTaskStatus: (id: string, status: Status) => void;
  removeTask: (id: string) => void;
  setFocus: (id: string | null) => void;
  linkTaskNote: (taskId: string, noteId: string) => void;

  addNote: (title?: string, body?: string, projectId?: string | null) => string;
  updateNote: (id: string, patch: Partial<Omit<Note, "id" | "createdAt">>) => void;
  removeNote: (id: string) => void;
  extractTaskFromNote: (noteId: string, title?: string) => string;

  addReminder: (input: {
    title: string;
    dueAt: string;
    taskId?: string | null;
    projectId?: string | null;
  }) => string;
  remindFromTask: (taskId: string, dueAt?: string) => string;
  toggleReminder: (id: string) => void;
  updateReminder: (id: string, patch: Partial<Pick<Reminder, "title" | "dueAt" | "done">>) => void;
  removeReminder: (id: string) => void;

  addWaiting: (input: {
    title: string;
    who?: string;
    notes?: string;
    projectId?: string | null;
    followUpAt?: string | null;
  }) => string;
  updateWaiting: (
    id: string,
    patch: Partial<Pick<WaitingItem, "title" | "who" | "notes" | "projectId" | "since" | "followUpAt">>,
  ) => void;
  /** Log that you chased it, and schedule the next follow-up. */
  nudgeWaiting: (id: string, nextInDays?: number) => void;
  toggleWaiting: (id: string) => void;
  removeWaiting: (id: string) => void;

  addScratch: (text: string, color?: ScratchNote["color"]) => string;
  updateScratch: (id: string, patch: Partial<Pick<ScratchNote, "text" | "color">>) => void;
  /** Returns what was removed, so the caller can offer an undo. */
  removeScratch: (id: string) => ScratchNote | null;
  clearScratch: () => ScratchNote[];
  restoreScratch: (items: ScratchNote[]) => void;

  addBoard: (kind: BoardKind, title?: string, projectId?: string | null) => string;
  updateBoard: (id: string, patch: Partial<Pick<Board, "title" | "projectId">>) => void;
  patchBoard: (
    id: string,
    patch: Partial<Pick<Board, "nodes" | "edges" | "strokes">>,
  ) => void;
  removeBoard: (id: string) => void;
  promoteNode: (boardId: string, nodeId: string, as: "task" | "note") => string | null;

  importData: (data: WorkspaceData) => void;
  resetSeed: () => void;
};

function stamp() {
  return nowIso();
}

function tomorrowIso(): string {
  const d = new Date();
  d.setDate(d.getDate() + 1);
  d.setHours(9, 0, 0, 0);
  return d.toISOString();
}

const memoryStorage = {
  getItem: () => null,
  setItem: () => {},
  removeItem: () => {},
};

/**
 * Writes stay disarmed until a read has demonstrably succeeded.
 *
 * The bug this closes: the store's initial value IS `SEED`. If the read of
 * localStorage failed or the stored JSON no longer parsed, rehydration left
 * the store on SEED — and the very next write persisted SEED straight over
 * the user's real book. Loading the page was enough to destroy it, and the
 * 600ms hydration fallback in components/shell.tsx guaranteed a write.
 *
 * So: never write until `onRehydrateStorage` has reported a clean read. A
 * brand-new browser reads `null` cleanly and arms normally; a browser whose
 * storage is unreadable or corrupt keeps whatever it has, which is what makes
 * recovery possible.
 */
let persistArmed = false;

/** Surfaced by the shell so a browser that cannot save says so. */
export let localCacheBroken = false;

const guardedLocalStorage: Storage = {
  get length() {
    return localStorage.length;
  },
  key: (i: number) => localStorage.key(i),
  clear: () => localStorage.clear(),
  getItem: (name: string) => localStorage.getItem(name),
  removeItem: (name: string) => localStorage.removeItem(name),
  setItem: (name: string, value: string) => {
    if (!persistArmed) return;
    try {
      localStorage.setItem(name, value);
    } catch (err) {
      // Quota or blocked storage. Swallow it: the server copy is the record
      // of truth now, and a throw here would surface as a broken UI action.
      localCacheBroken = true;
      console.warn("[store] local cache write failed:", err);
    }
  },
};

export const useWorkspace = create<WorkspaceState>()(
  persist(
    (set, get) => ({
      ...SEED,
      hydrated: false,
      setHydrated: (v) => set({ hydrated: v }),

      addProject: (name, description = "") => {
        const id = nid("proj");
        const t = stamp();
        const project: Project = {
          id,
          name: name.trim() || "Untitled project",
          description,
          status: "active",
          createdAt: t,
          updatedAt: t,
        };
        set({ projects: [project, ...get().projects] });
        return id;
      },

      updateProject: (id, patch) => {
        set({
          projects: get().projects.map((p) =>
            p.id === id ? { ...p, ...patch, updatedAt: stamp() } : p,
          ),
        });
      },

      removeProject: (id) => {
        set({
          projects: get().projects.filter((p) => p.id !== id),
          tasks: get().tasks.map((t) =>
            t.projectId === id ? { ...t, projectId: null } : t,
          ),
          notes: get().notes.map((n) =>
            n.projectId === id ? { ...n, projectId: null } : n,
          ),
          reminders: get().reminders.map((r) =>
            r.projectId === id ? { ...r, projectId: null } : r,
          ),
          boards: get().boards.map((b) =>
            b.projectId === id ? { ...b, projectId: null } : b,
          ),
          waiting: get().waiting.map((w) =>
            w.projectId === id ? { ...w, projectId: null } : w,
          ),
        });
      },

      addTask: (input) => {
        const id = nid("task");
        const t = stamp();
        const task: Task = {
          id,
          title: input.title.trim() || "Untitled task",
          body: input.body ?? "",
          status: "todo",
          priority: input.priority ?? "med",
          projectId: input.projectId ?? null,
          dueAt: input.dueAt ?? null,
          reminderId: null,
          noteIds: input.noteId ? [input.noteId] : [],
          source: input.source,
          createdAt: t,
          updatedAt: t,
        };
        let notes = get().notes;
        if (input.noteId) {
          notes = notes.map((n) =>
            n.id === input.noteId
              ? { ...n, taskIds: Array.from(new Set([...n.taskIds, id])), updatedAt: t }
              : n,
          );
        }
        set({ tasks: [task, ...get().tasks], notes });
        return id;
      },

      updateTask: (id, patch) => {
        set({
          tasks: get().tasks.map((t) =>
            t.id === id ? { ...t, ...patch, updatedAt: stamp() } : t,
          ),
        });
      },

      setTaskStatus: (id, status) => {
        const t = stamp();
        const task = get().tasks.find((x) => x.id === id);
        set({
          tasks: get().tasks.map((x) => (x.id === id ? { ...x, status, updatedAt: t } : x)),
          reminders: get().reminders.map((r) =>
            task && r.taskId === id ? { ...r, done: status === "done" } : r,
          ),
        });
      },

      removeTask: (id) => {
        set({
          tasks: get().tasks.filter((t) => t.id !== id),
          notes: get().notes.map((n) => ({
            ...n,
            taskIds: n.taskIds.filter((tid) => tid !== id),
          })),
          reminders: get().reminders.map((r) =>
            r.taskId === id ? { ...r, taskId: null } : r,
          ),
          boards: get().boards.map((b) => ({
            ...b,
            nodes: b.nodes.map((n) =>
              n.linkedTaskId === id ? { ...n, linkedTaskId: undefined } : n,
            ),
          })),
          focusTaskId: get().focusTaskId === id ? null : get().focusTaskId,
        });
      },

      setFocus: (id) => set({ focusTaskId: id }),

      linkTaskNote: (taskId, noteId) => {
        const t = stamp();
        set({
          tasks: get().tasks.map((task) =>
            task.id === taskId
              ? { ...task, noteIds: Array.from(new Set([...task.noteIds, noteId])), updatedAt: t }
              : task,
          ),
          notes: get().notes.map((n) =>
            n.id === noteId
              ? { ...n, taskIds: Array.from(new Set([...n.taskIds, taskId])), updatedAt: t }
              : n,
          ),
        });
      },

      addNote: (title = "Untitled note", body = "", projectId = null) => {
        const id = nid("note");
        const t = stamp();
        const note: Note = {
          id,
          title: title.trim() || "Untitled note",
          body,
          tags: [],
          projectId,
          taskIds: [],
          createdAt: t,
          updatedAt: t,
        };
        set({ notes: [note, ...get().notes] });
        return id;
      },

      updateNote: (id, patch) => {
        set({
          notes: get().notes.map((n) =>
            n.id === id ? { ...n, ...patch, updatedAt: stamp() } : n,
          ),
        });
      },

      removeNote: (id) => {
        set({
          notes: get().notes.filter((n) => n.id !== id),
          tasks: get().tasks.map((t) => ({
            ...t,
            noteIds: t.noteIds.filter((nid_) => nid_ !== id),
          })),
          boards: get().boards.map((b) => ({
            ...b,
            nodes: b.nodes.map((n) =>
              n.linkedNoteId === id ? { ...n, linkedNoteId: undefined } : n,
            ),
          })),
        });
      },

      extractTaskFromNote: (noteId, title) => {
        const note = get().notes.find((n) => n.id === noteId);
        if (!note) return "";
        const line =
          title?.trim() ||
          note.body
            .split("\n")
            .map((l) => l.trim())
            .find(Boolean) ||
          note.title;
        return get().addTask({
          title: line.slice(0, 140),
          body: `Extracted from note “${note.title}”.`,
          projectId: note.projectId,
          noteId,
          source: { kind: "note", id: noteId },
        });
      },

      addReminder: (input) => {
        const id = nid("rem");
        const rem: Reminder = {
          id,
          title: input.title.trim() || "Reminder",
          dueAt: input.dueAt,
          done: false,
          taskId: input.taskId ?? null,
          projectId: input.projectId ?? null,
          createdAt: stamp(),
        };
        let tasks = get().tasks;
        if (input.taskId) {
          tasks = tasks.map((t) =>
            t.id === input.taskId ? { ...t, reminderId: id, updatedAt: stamp() } : t,
          );
        }
        set({ reminders: [rem, ...get().reminders], tasks });
        return id;
      },

      remindFromTask: (taskId, dueAt) => {
        const task = get().tasks.find((t) => t.id === taskId);
        if (!task) return "";
        if (task.reminderId) {
          const existing = get().reminders.find((r) => r.id === task.reminderId);
          if (existing) {
            if (dueAt) get().updateReminder(existing.id, { dueAt, done: false });
            return existing.id;
          }
        }
        return get().addReminder({
          title: task.title,
          dueAt: dueAt ?? task.dueAt ?? tomorrowIso(),
          taskId,
          projectId: task.projectId,
        });
      },

      toggleReminder: (id) => {
        const rem = get().reminders.find((r) => r.id === id);
        if (!rem) return;
        const done = !rem.done;
        set({
          reminders: get().reminders.map((r) => (r.id === id ? { ...r, done } : r)),
          tasks: rem.taskId
            ? get().tasks.map((t) =>
                t.id === rem.taskId
                  ? { ...t, status: done ? "done" : t.status === "done" ? "todo" : t.status, updatedAt: stamp() }
                  : t,
              )
            : get().tasks,
        });
      },

      updateReminder: (id, patch) => {
        set({
          reminders: get().reminders.map((r) => (r.id === id ? { ...r, ...patch } : r)),
        });
      },

      removeReminder: (id) => {
        set({
          reminders: get().reminders.filter((r) => r.id !== id),
          tasks: get().tasks.map((t) =>
            t.reminderId === id ? { ...t, reminderId: null } : t,
          ),
        });
      },

      addWaiting: (input) => {
        const id = nid("wait");
        const t = stamp();
        const item: WaitingItem = {
          id,
          title: input.title.trim() || "Waiting on something",
          who: input.who?.trim() ?? "",
          notes: input.notes ?? "",
          projectId: input.projectId ?? null,
          since: t,
          // Default to a follow-up rather than none: an item with no date is
          // exactly the kind that gets forgotten. Pass null to opt out.
          followUpAt: input.followUpAt === undefined ? followUpIn(3) : input.followUpAt,
          nudges: 0,
          lastNudgedAt: null,
          done: false,
          doneAt: null,
          createdAt: t,
          updatedAt: t,
        };
        set({ waiting: [item, ...get().waiting] });
        return id;
      },

      updateWaiting: (id, patch) => {
        set({
          waiting: get().waiting.map((w) =>
            w.id === id ? { ...w, ...patch, updatedAt: stamp() } : w,
          ),
        });
      },

      nudgeWaiting: (id, nextInDays = 3) => {
        const t = stamp();
        set({
          waiting: get().waiting.map((w) =>
            w.id === id
              ? {
                  ...w,
                  nudges: w.nudges + 1,
                  lastNudgedAt: t,
                  followUpAt: followUpIn(nextInDays),
                  updatedAt: t,
                }
              : w,
          ),
        });
      },

      toggleWaiting: (id) => {
        const t = stamp();
        set({
          waiting: get().waiting.map((w) =>
            w.id === id
              ? { ...w, done: !w.done, doneAt: w.done ? null : t, updatedAt: t }
              : w,
          ),
        });
      },

      removeWaiting: (id) => {
        set({ waiting: get().waiting.filter((w) => w.id !== id) });
      },

      addScratch: (text, color = 1) => {
        const id = nid("jot");
        const t = stamp();
        set({ scratch: [{ id, text, color, createdAt: t, updatedAt: t }, ...get().scratch] });
        return id;
      },

      updateScratch: (id, patch) => {
        set({
          scratch: get().scratch.map((n) =>
            n.id === id ? { ...n, ...patch, updatedAt: stamp() } : n,
          ),
        });
      },

      removeScratch: (id) => {
        const item = get().scratch.find((n) => n.id === id) ?? null;
        if (item) set({ scratch: get().scratch.filter((n) => n.id !== id) });
        return item;
      },

      clearScratch: () => {
        const items = get().scratch;
        set({ scratch: [] });
        return items;
      },

      restoreScratch: (items) => {
        // Keep anything jotted since the removal; only put back what is missing.
        const have = new Set(get().scratch.map((n) => n.id));
        set({ scratch: [...items.filter((n) => !have.has(n.id)), ...get().scratch] });
      },

      addBoard: (kind, title, projectId = null) => {
        const id = nid(kind === "flow" ? "flow" : "board");
        const t = stamp();
        const board: Board = {
          id,
          title:
            title?.trim() ||
            (kind === "flow" ? "Untitled flow" : "Untitled board"),
          kind,
          projectId,
          nodes: [],
          edges: [],
          strokes: [],
          createdAt: t,
          updatedAt: t,
        };
        set({ boards: [board, ...get().boards] });
        return id;
      },

      updateBoard: (id, patch) => {
        set({
          boards: get().boards.map((b) =>
            b.id === id ? { ...b, ...patch, updatedAt: stamp() } : b,
          ),
        });
      },

      patchBoard: (id, patch) => {
        set({
          boards: get().boards.map((b) =>
            b.id === id ? { ...b, ...patch, updatedAt: stamp() } : b,
          ),
        });
      },

      removeBoard: (id) => {
        set({ boards: get().boards.filter((b) => b.id !== id) });
      },

      promoteNode: (boardId, nodeId, as) => {
        const board = get().boards.find((b) => b.id === boardId);
        const node = board?.nodes.find((n) => n.id === nodeId);
        if (!board || !node) return null;
        const text = node.text.replace(/\s+/g, " ").trim() || "Untitled";
        if (as === "task") {
          if (node.linkedTaskId) return node.linkedTaskId;
          const taskId = get().addTask({
            title: text.slice(0, 140),
            body: `Promoted from ${board.kind === "flow" ? "flowchart" : "board"} “${board.title}”.`,
            projectId: board.projectId,
            source: { kind: board.kind, id: boardId, nodeId },
          });
          const latest = get().boards.find((b) => b.id === boardId);
          if (latest) {
            get().patchBoard(boardId, {
              nodes: latest.nodes.map((n) =>
                n.id === nodeId ? { ...n, linkedTaskId: taskId } : n,
              ),
            });
          }
          return taskId;
        }
        if (node.linkedNoteId) return node.linkedNoteId;
        const noteId = get().addNote(text.slice(0, 80), node.text, board.projectId);
        const latest = get().boards.find((b) => b.id === boardId);
        if (latest) {
          get().patchBoard(boardId, {
            nodes: latest.nodes.map((n) =>
              n.id === nodeId ? { ...n, linkedNoteId: noteId } : n,
            ),
          });
        }
        return noteId;
      },

      importData: (data) => {
        set({
          projects: data.projects ?? [],
          tasks: data.tasks ?? [],
          notes: data.notes ?? [],
          reminders: data.reminders ?? [],
          boards: data.boards ?? [],
          waiting: data.waiting ?? [],
          scratch: data.scratch ?? [],
          focusTaskId: data.focusTaskId ?? null,
        });
      },

      resetSeed: () => set({ ...SEED }),
    }),
    {
      name: "fieldbook-ledger-v1",
      storage: createJSONStorage(() =>
        typeof window === "undefined" ? memoryStorage : guardedLocalStorage,
      ),
      skipHydration: true,
      partialize: (s) => ({
        projects: s.projects,
        tasks: s.tasks,
        notes: s.notes,
        reminders: s.reminders,
        boards: s.boards,
        waiting: s.waiting,
        scratch: s.scratch,
        focusTaskId: s.focusTaskId,
      }),
      // A cache written before `waiting`/`scratch` existed has no such key, and the
      // default shallow merge would then keep SEED's sample items instead.
      // zustand calls this even when nothing is stored, so a brand-new
      // browser must fall through to the initial state untouched.
      merge: (persisted, current) => {
        if (!persisted) return current;
        const p = persisted as Partial<WorkspaceState>;
        return { ...current, ...p, waiting: p.waiting ?? [], scratch: p.scratch ?? [] };
      },
      onRehydrateStorage: () => (state, error) => {
        // Only a clean read arms writing — see `persistArmed` above.
        if (error) {
          localCacheBroken = true;
          console.warn("[store] local cache unreadable; not overwriting it:", error);
        } else {
          persistArmed = true;
        }
        state?.setHydrated(true);
      },
    },
  ),
);

export function useHydrated(): boolean {
  return useWorkspace((s) => s.hydrated);
}
