import { useSyncExternalStore } from 'react';

import { isBrowser } from '../utils/js-utils';

type Store<T> = {
  getValue: (defaultValue: T) => T;
  setValue: (next: T) => void;
  subscribe: (callback: () => void) => Unsubscribe;
};

type Unsubscribe = () => void;

type CreateStoreProps<T> = {
  storageKey: string;
  storageType?: 'localStorage' | 'sessionStorage';
  serializer?: {
    parse: (value: string) => T;
    serialize: (value: T) => string;
  };
};

export function createStore<T>({
  storageKey,
  storageType = 'localStorage',
  serializer = {
    parse: (value: string) => JSON.parse(value) as T,
    serialize: (value: T) => JSON.stringify(value),
  },
}: CreateStoreProps<T>): Store<T> {
  const subscribers = new Set<() => void>();
  let cachedValue: T;

  const shouldSerialize = (value: unknown) => typeof value !== 'string';

  const getValue = (defaultValue: T): T => {
    if (!isBrowser()) return defaultValue;
    if (cachedValue !== undefined) return cachedValue;

    const value = window[storageType].getItem(storageKey);
    if (!value) {
      cachedValue = defaultValue;
      return cachedValue;
    }

    if (!shouldSerialize(defaultValue)) {
      cachedValue = value as T;
      return cachedValue;
    }

    try {
      cachedValue = JSON.parse(value) as T;
    } catch {
      cachedValue = defaultValue;
    }

    return cachedValue;
  };

  const setValue = (next: T): void => {
    if (!isBrowser()) return;
    try {
      window[storageType].setItem(
        storageKey,
        shouldSerialize(next) ? serializer.serialize(next) : (next as string),
      );
      cachedValue = next;
      subscribers.forEach((callback) => callback());
    } catch {
      return;
    }
  };

  const subscribe = (callback: () => void): Unsubscribe => {
    subscribers.add(callback);
    return () => {
      subscribers.delete(callback);
    };
  };

  return {
    getValue,
    setValue,
    subscribe,
  };
}

export function useStore<T>(store: Store<T>, defaultValue: T) {
  const value = useSyncExternalStore(
    store.subscribe,
    () => store.getValue(defaultValue),
    () => defaultValue,
  );

  return [value, store.setValue] as const;
}
