import { assert, assertString, once, isString } from './util/mod.ts';
import type { Session, SessionMetadata } from './config.ts';


/** Helps retrieve and update session metadata and persist it in LocalStorage. */
export interface LocalStorageSession extends Session {
  /**
   * Method for retrieving session information synchronously
   * from `LocalStorage`. Returns a `SessionMetadata` object that
   * contains `customerKey` and `sessionKey`.
   *
   * @example
   * ```ts
   * declare const session: LocalStorageSession;
   *
   * const { customerKey, sessionKey } = session();
   * ```
   */
  (): SessionMetadata;
  /**
   * Generates new customer- & sessionKeys via `crypto.randomUUID()` and persists them.
   * Suitable to use e.g. when a visitor logs out.
   */
  reset(): void;
  /**
   * Updates the `customerKey` to the provided value and persists it.
   * Suitable to use e.g. when a visitor is identified by signing in to their account.
   */
  updateCustomerKey(key: string): void;
}

const __storage: Storage = /* @__PURE__ */ (() => globalThis.localStorage)();

/** @internal exported for testing */
export const __sessionMetadataCache = new Map<string, SessionMetadata>();

/**
 * Create a `Session` compatible object for retrieving and mutating session metadata.
 * This method is only intended to be used in Browsers.
 *
 * Storage is backed by LocalStorage, where reads/writes will happen. Reads are cached
 * in memory, and invalidated by `storage` events on Window. This method can be called
 * multiple times, but shares Window listener between them.
 *
 * If session information does not exist, customer- & sessionKeys will be generated
 * automatically via `crypto.randomUUID()`.
 *
 * @param storageKey - The key that will be used to store session metadata. Defaults to `'voyado.session'`.
 *
 * @example
 * ```ts
 * import { elevate, localStorageBackedSession } from '@apptus/esales-api';
 *
 * const api = elevate({
 *   // ...
 *   session: localStorageBackedSession()
 * });
 * ```
 * @example
 * ```ts
 * const session = localStorageBackedSession();
 *
 * // Set a specific customerKey (e.g. on visitor sign-in)
 * session.updateCustomerKey(user.id);
 *
 * // Generate new customer/sessionKeys (e.g. on visitor signout)
 * session.reset();
 * ```
 */
export function localStorageBackedSession(storageKey = 'voyado.session'): LocalStorageSession {
  enableCachePruning();

  const fetcher = () => read(storageKey);
  fetcher.updateCustomerKey = (customerKey: string) => update(storageKey, { ...fetcher(), customerKey });
  fetcher.reset = () => update(storageKey, generateSession());

  return fetcher;
}

const enableCachePruning = /* @__PURE__ */ once(() => {
  globalThis.addEventListener('storage', ({ key, storageArea }: StorageEvent) => {
    if (key && storageArea === __storage) __sessionMetadataCache.delete(key);
  });
});

function generateSession(): SessionMetadata {
  return { customerKey: crypto.randomUUID(), sessionKey: crypto.randomUUID() };
}

function read(key: string): SessionMetadata {
  // Update cached value by reading from LocalStorage if needed
  if (!__sessionMetadataCache.has(key)) {
    const strData = __storage.getItem(key);
    const session = generateSession();

    try {
      assertString(strData);

      const d: unknown = JSON.parse(strData);
      assert(d && typeof d === 'object');

      if ('customerKey' in d && isString(d.customerKey)) session.customerKey = d.customerKey;
      if ('sessionKey' in d && isString(d.sessionKey)) session.sessionKey = d.sessionKey;

      update(key, session, strData);
    } catch {
      update(key, session);
    }
  }

  return __sessionMetadataCache.get(key)!;
}

function update(key: string, data: SessionMetadata, prevData = '') {
  updateCache(key, data);
  const currData = JSON.stringify(data);
  if (prevData !== currData) __storage.setItem(key, currData);
}

function updateCache(key: string, data: SessionMetadata) {
  __sessionMetadataCache.set(key, data);
}
