"use client";

import { useCallback, useEffect, useState } from "react";

/**
 * Persist a value in localStorage with SSR safety and JSON serialization.
 *
 * When `T` is a plain object, stored values are merged with `defaultValue`
 * so fields added to the schema later get their defaults filled in.
 * For primitive types (string, number, boolean) the stored value is used as-is.
 *
 * The optional `changeEventName` parameter causes every write to dispatch a
 * CustomEvent so other instances of the hook in the same tab stay in sync.
 */
export function useLocalStorage<T>(
  key: string,
  defaultValue: T,
  changeEventName?: string,
): [T, (updater: T | ((prev: T) => T)) => void] {
  const [value, setValue] = useState<T>(() => read(key, defaultValue));

  // Re-read after hydration (SSR → client) and subscribe to cross-instance events.
  useEffect(() => {
    setValue(read(key, defaultValue));

    if (!changeEventName) return;
    function handleChange(e: Event) {
      setValue((e as CustomEvent<T>).detail);
    }
    window.addEventListener(changeEventName, handleChange);
    return () => window.removeEventListener(changeEventName, handleChange);
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [key, changeEventName]);

  const set = useCallback(
    (updater: T | ((prev: T) => T)) => {
      setValue((prev) => {
        const next = typeof updater === "function" ? (updater as (p: T) => T)(prev) : updater;
        if (typeof window !== "undefined") {
          localStorage.setItem(key, JSON.stringify(next));
          if (changeEventName) {
            window.dispatchEvent(new CustomEvent<T>(changeEventName, { detail: next }));
          }
        }
        return next;
      });
    },
    [key, changeEventName],
  );

  return [value, set];
}

function read<T>(key: string, defaultValue: T): T {
  if (typeof window === "undefined") return defaultValue;
  try {
    const raw = localStorage.getItem(key);
    if (!raw) return defaultValue;
    const parsed = JSON.parse(raw) as T;
    // For plain objects, merge with defaults to handle schema evolution.
    if (isPlainObject(defaultValue) && isPlainObject(parsed)) {
      return { ...defaultValue, ...parsed } as T;
    }
    return parsed;
  } catch {
    return defaultValue;
  }
}

function isPlainObject(v: unknown): v is Record<string, unknown> {
  return typeof v === "object" && v !== null && !Array.isArray(v);
}
