import * as R from "ramda";

import { StorageLevel, StorageType } from "./consts";
import {
  getNamespaceAndKey,
  keyIsValid,
  buildNamespaceKey,
  savingResultIsSuccess,
} from "./utils";
import { utilsLogger } from "../../logger";
import {
  sessionStorage as SessionStorage,
  localStorage as LocalStorage,
  secureStorage as SecureStorage,
} from "./storage";

export { StorageLevel };

type KeyNameObj = {
  namespace: string;
  key: string;
};

type KeyName = string | KeyNameObj;

type ParsedKeyAndNamespace = {
  namespace?: string;
  key?: string;
  isValid: boolean;
};

export const NOTHING = null;

type Value = string | number | boolean | Array<any> | Object;

type ValueOrNothing = Value | typeof NOTHING;

type KeyProp = {
  key: KeyName;
  value: Value;
  storageLevel?: StorageLevel;
};

const UtilsLogger = utilsLogger.addSubsystem("ContextKeysManager");

type Logger = {
  warn: Function;
};

type Storage = {
  getItem: (key: string, namespace: string) => Promise<Value>;
  setItem: (key: string, value: Value, namespace: string) => Promise<boolean>;
  removeItem: (key: string, namespace: string) => Promise<boolean>;
};

type Setup = {
  logger?: Logger;
  sessionStorage?: Storage;
  localStorage?: Storage;
  secureStorage?: Storage;
  referenceStorage?: Storage;
};

export const REFERENCE_NAMESPACE = "applicaster.v2.references";

export class ContextKeysManager {
  private static _instance: ContextKeysManager;
  private _logger: Logger;
  private _sessionStorage: Storage;
  private _localStorage: Storage;
  private _secureStorage: Storage;
  private _referenceStorage: Storage;

  public static get instance() {
    if (!this._instance) {
      const instance = new ContextKeysManager({
        logger: UtilsLogger,
        sessionStorage: SessionStorage,
        localStorage: LocalStorage,
        secureStorage: SecureStorage,
        referenceStorage: SessionStorage,
      });

      this._instance = instance;
    }

    return this._instance;
  }

  // in case you need to tune dependency - use constructor
  constructor({
    logger = UtilsLogger,
    sessionStorage = SessionStorage,
    localStorage = LocalStorage,
    secureStorage = SecureStorage,
    referenceStorage = SessionStorage,
  }: Setup) {
    this._logger = logger;
    this._sessionStorage = sessionStorage;
    this._localStorage = localStorage;
    this._secureStorage = secureStorage;
    this._referenceStorage = referenceStorage;
  }

  private parseKey(key: KeyName): ParsedKeyAndNamespace {
    const parsedKey = getNamespaceAndKey(key);

    if (!keyIsValid(parsedKey)) {
      this._logger.warn({
        message:
          "Provided key is not valid - expecing an object with namespace and key properties",
        data: { key },
      });

      return { isValid: false };
    }

    return { isValid: true, ...parsedKey };
  }

  // GET
  public async getKeyByReference(
    key: string,
    namespace: string
  ): Promise<ValueOrNothing> {
    // 1) look for storage
    // 2) if storage found - look value in it
    // 3) if storage not found return NOTHING

    return this._referenceStorage
      .getItem(buildNamespaceKey(key, namespace), REFERENCE_NAMESPACE)
      .then((storage: StorageType | unknown) => {
        switch (storage) {
          case StorageType.session:
            return this._sessionStorage.getItem(key, namespace);
          case StorageType.local:
            return this._localStorage.getItem(key, namespace);
          case StorageType.secure:
            return this._secureStorage.getItem(key, namespace);
          default:
            // unknown storage
            return NOTHING;
        }
      })
      .catch((error) => {
        // something went wrong
        this._logger.warn({
          message: error.message,
          data: { key, namespace },
        });

        return NOTHING;
      });
  }

  public async getKeys(
    keys: Array<KeyName>
  ): Promise<Map<KeyName, ValueOrNothing>> {
    const values = await Promise.all(keys.map((key) => this.getKey(key)));

    return new Map(R.zip(keys, values));
  }

  public async getKey(key: KeyName): Promise<ValueOrNothing> {
    const {
      isValid: isValidKey,
      key: parsedKey,
      namespace: parsedNamespace,
    } = this.parseKey(key);

    if (!isValidKey) {
      // for invalid key - return nothing
      return NOTHING;
    }

    // at first - try to find by reference
    // if not found, look in session > local > secure

    const resultByReference = await this.getKeyByReference(
      parsedKey,
      parsedNamespace
    );

    if (!R.isNil(resultByReference)) {
      return resultByReference;
    }

    // try to full-search
    try {
      const resultFromSessionStorage = await this._sessionStorage.getItem(
        parsedKey,
        parsedNamespace
      );

      if (!R.isNil(resultFromSessionStorage)) {
        return resultFromSessionStorage;
      }

      const resultFromLocalStorage = await this._localStorage.getItem(
        parsedKey,
        parsedNamespace
      );

      if (!R.isNil(resultFromLocalStorage)) {
        return resultFromLocalStorage;
      }

      const resultFromSecureStorage = await this._secureStorage.getItem(
        parsedKey,
        parsedNamespace
      );

      if (!R.isNil(resultFromSecureStorage)) {
        return resultFromSecureStorage;
      }

      // nothing found
      return NOTHING;
    } catch (error) {
      // something went wrong
      this._logger.warn({
        message: error.message,
        data: { key },
      });

      return NOTHING;
    }
  }
  // GET

  // SET
  public async setReferenceForKey(
    key: string,
    namespace: string,
    storageLevel: StorageLevel
  ): Promise<boolean> {
    return this._referenceStorage
      .setItem(
        buildNamespaceKey(key, namespace),
        storageLevel,
        REFERENCE_NAMESPACE
      )
      .then((result) => {
        if (savingResultIsSuccess(result)) {
          return true;
        }

        this._logger.warn({
          message:
            "While saving reference - reference storage return non expected value: true",
          data: { key, namespace, storageLevel },
        });

        return false;
      })
      .catch((error) => {
        // something went wrong
        this._logger.warn({
          message: error.message,
          data: { key, namespace, storageLevel },
        });

        return false;
      });
  }

  // for all keys where saving were succeed return true
  // for others return false
  public async setKeys(
    keysProp: Array<KeyProp>
  ): Promise<Map<KeyName, boolean>> {
    const keys = keysProp.map((keyProp) => keyProp.key);

    const values = await Promise.all(
      keysProp.map((keyProp) => this.setKey(keyProp))
    );

    return new Map(R.zip(keys, values));
  }

  // when succeed saving - return true
  // otherwise return false
  public async setKey({
    key,
    value,
    storageLevel = StorageLevel.default,
  }: KeyProp): Promise<boolean> {
    const {
      isValid: isValidKey,
      key: parsedKey,
      namespace: parsedNamespace,
    } = this.parseKey(key);

    if (!isValidKey) {
      // for invalid key - return false
      return false;
    }

    try {
      switch (storageLevel) {
        case StorageLevel.persisted:
          return await Promise.all([
            this._localStorage.setItem(parsedKey, value, parsedNamespace),
            this.setReferenceForKey(parsedKey, parsedNamespace, storageLevel),
          ]).then(([setItemResult]) => {
            return savingResultIsSuccess(setItemResult);
          });
        case StorageLevel.secure:
          return await Promise.all([
            this._secureStorage.setItem(parsedKey, value, parsedNamespace),
            this.setReferenceForKey(parsedKey, parsedNamespace, storageLevel),
          ]).then(([setItemResult]) => {
            return savingResultIsSuccess(setItemResult);
          });
        default:
          return await Promise.all([
            this._sessionStorage.setItem(parsedKey, value, parsedNamespace),
            this.setReferenceForKey(parsedKey, parsedNamespace, storageLevel),
          ]).then(([setItemResult]) => {
            return savingResultIsSuccess(setItemResult);
          });
      }
    } catch (error) {
      // something went wrong
      this._logger.warn({
        message: error.message,
        data: { key, value, storageLevel },
      });

      return false;
    }
  }
  // SET

  // REMOVE
  public async removeReferenceForKey(
    key: string,
    namespace: string
  ): Promise<boolean> {
    return this._referenceStorage
      .removeItem(buildNamespaceKey(key, namespace), REFERENCE_NAMESPACE)
      .then(() => true)
      .catch((error) => {
        // something went wrong
        this._logger.warn({
          message: error.message,
          data: { key, namespace },
        });

        return false;
      });
  }

  public async removeKey(key: KeyName): Promise<boolean> {
    const {
      isValid: isValidKey,
      key: parsedKey,
      namespace: parsedNamespace,
    } = this.parseKey(key);

    if (!isValidKey) {
      // for invalid key - return false
      return false;
    }

    return Promise.all([
      this._sessionStorage.removeItem(parsedKey, parsedNamespace),
      this._localStorage.removeItem(parsedKey, parsedNamespace),
      this._secureStorage.removeItem(parsedKey, parsedNamespace),
      this.removeReferenceForKey(parsedKey, parsedNamespace),
    ])
      .then(() => true)
      .catch((error) => {
        // something went wrong
        this._logger.warn({
          message: error.message,
          data: { key },
        });

        return false;
      });
  }

  public async removeKeys(keys: KeyName[]): Promise<Map<KeyName, boolean>> {
    const values = await Promise.all(keys.map((key) => this.removeKey(key)));

    return new Map(R.zip(keys, values));
  }
  // REMOVE
}
