import { parseJsonIfNeeded } from "@applicaster/zapp-react-native-utils/functionUtils";
import {
  throwBridgeNotFoundError,
  throwNativeStorageError,
  throwUndefinedKeyError,
  throwUndefinedValueError,
  getItemError,
  setItemError,
  removeItemError,
} from "./errors";
import { DEFAULT_NAMESPACE } from "./consts";
import { logger } from "./logger";

const UNDEFINED_KEY_MESSAGE = "Requested key is null or undefined";

const UNDEFINED_VALUE_MESSAGE =
  "Value to set in storage cannot be null or undefined";

const ALL_KEYS = "ALL_KEYS";

export function stringifyIfNeeded(value) {
  if (typeof value === "string") return value;

  return JSON.stringify(value);
}

export abstract class Storage {
  private nativeStorageModule: NativeStorageModule;
  private storageName: string;
  private listeners: Record<string, Array<StorageListener>>;

  constructor(storage: string, nativeStorageModule: NativeStorageModule) {
    if (!nativeStorageModule) throwBridgeNotFoundError(storage);
    this.nativeStorageModule = nativeStorageModule;
    this.storageName = storage;
    this.listeners = {};
  }

  private getListenerKey = (key = ALL_KEYS, namespace = DEFAULT_NAMESPACE) => {
    return `${key}:${namespace}`;
  };

  // Storage Listeners

  addListener = (
    { key = ALL_KEYS, namespace = DEFAULT_NAMESPACE },
    listener: StorageListener
  ) => {
    const listenerKey = this.getListenerKey(key, namespace);

    if (!this.listeners[listenerKey]) this.listeners[listenerKey] = [];
    this.listeners[listenerKey].push(listener);

    return () => this.removeListener(key, namespace, listener);
  };

  removeListener = (
    key = ALL_KEYS,
    namespace = DEFAULT_NAMESPACE,
    listener
  ) => {
    const listenerKey = this.getListenerKey(key, namespace);
    const listenersForKey = this.listeners[listenerKey];

    this.listeners[listenerKey] = listenersForKey.filter(
      (_listener) => _listener !== listener
    );
  };

  private invokeListenerFunction = ({
    listener,
    key,
    namespace = DEFAULT_NAMESPACE,
    value,
  }) => {
    if (listener) {
      try {
        listener({ key, namespace, value });
      } catch (error) {
        logger.error({
          message: `invokeListenerFunction: throw error: ${
            error.message || "no error message"
          } inStorage: ${
            this.storageName
          } for key: ${key}, namespace: ${namespace} with value: ${value}`,
        });
      }
    }
  };

  invokeListener = (key, namespace = DEFAULT_NAMESPACE, value) => {
    const listenersKey = this.getListenerKey(key, namespace);
    const listenersAllKeys = this.getListenerKey(ALL_KEYS, namespace);

    const specificListeners = this.listeners[listenersKey] || [];
    const allListeners = this.listeners[listenersAllKeys] || [];

    if (allListeners.length === 0 && specificListeners.length === 0) {
      return;
    }

    const listeners = specificListeners.concat(allListeners);

    listeners.forEach((listener) => {
      if (listener) {
        this.invokeListenerFunction({ listener, key, namespace, value });
      }
    });
  };

  // Storage methods

  getItem = async (
    key,
    namespace = DEFAULT_NAMESPACE,
    skipJsonParse = false
  ) => {
    if (!key) throwUndefinedKeyError(this.storageName, UNDEFINED_KEY_MESSAGE);

    try {
      const result = await this.nativeStorageModule.getItem(key, namespace);

      if (skipJsonParse) {
        return result;
      }

      return parseJsonIfNeeded(result);
    } catch (error) {
      throwNativeStorageError(
        this.storageName,
        getItemError(error),
        key,
        namespace
      );
    }
  };

  setItem = async (key: string, value: any, namespace = DEFAULT_NAMESPACE) => {
    if (!key) throwUndefinedKeyError(this.storageName, UNDEFINED_KEY_MESSAGE);

    if (typeof value === "undefined" || value === null) {
      throwUndefinedValueError(this.storageName, UNDEFINED_VALUE_MESSAGE, key);
    }

    try {
      const result = await this.nativeStorageModule.setItem(
        key,
        stringifyIfNeeded(value),
        namespace
      );

      if (result) {
        this.invokeListener(key, namespace, value);
      }

      return !!result;
    } catch (error) {
      throwNativeStorageError(
        this.storageName,
        setItemError(error),
        key,
        namespace,
        value
      );
    }
  };

  removeItem = async (key, namespace = DEFAULT_NAMESPACE) => {
    if (!key) throwUndefinedKeyError(this.storageName, UNDEFINED_KEY_MESSAGE);

    try {
      const result = await this.nativeStorageModule.removeItem(key, namespace);

      if (result) {
        this.invokeListener(key, namespace, null);
      }

      return !!result;
    } catch (error) {
      throwNativeStorageError(
        this.storageName,
        removeItemError(error),
        key,
        namespace
      );
    }
  };

  getAllItems = async (namespace = DEFAULT_NAMESPACE) => {
    try {
      const result = await this.nativeStorageModule.getAllItems(namespace);

      if (!result) return {};

      if (typeof result === "string") return parseJsonIfNeeded(result);

      return Object.entries(result).reduce((obj, [key, value]) => {
        if (key === ".namespace") return obj;
        obj[key] = parseJsonIfNeeded(value);

        return obj;
      }, {});
    } catch (error) {
      throwNativeStorageError(
        this.storageName,
        getItemError(error),
        "GET_ALL",
        namespace
      );
    }
  };
}
