import type { MMKV } from '../specs/MMKV.nitro'
import type { Configuration } from '../specs/MMKVFactory.nitro'
import { createTextDecoder } from '../web/createTextDecoder'
import { createTextEncoder } from '../web/createTextEncoder'
import {
  getLocalStorage,
  LOCAL_STORAGE_KEY_WILDCARD,
} from '../web/getLocalStorage'

export function createMMKV(
  config: Configuration = { id: 'mmkv.default' }
): MMKV {
  if (config.encryptionKey != null) {
    throw new Error("MMKV: 'encryptionKey' is not supported on Web!")
  }
  if (config.path != null) {
    throw new Error("MMKV: 'path' is not supported on Web!")
  }

  const textDecoder = createTextDecoder()
  const textEncoder = createTextEncoder()
  const listeners = new Set<(key: string) => void>()

  if (config.id.includes(LOCAL_STORAGE_KEY_WILDCARD)) {
    throw new Error('MMKV: `id` cannot contain the backslash character (`\\`)!')
  }

  const keyPrefix = `${config.id}${LOCAL_STORAGE_KEY_WILDCARD}` // mmkv.default\\
  const prefixedKey = (key: string) => {
    if (key.includes('\\')) {
      throw new Error(
        'MMKV: `key` cannot contain the backslash character (`\\`)!'
      )
    }
    return `${keyPrefix}${key}`
  }

  const callListeners = (key: string) => {
    listeners.forEach((l) => l(key))
  }

  return {
    id: config.id,
    get length(): number {
      return getLocalStorage().length
    },
    get size(): number {
      return this.byteSize
    },
    get byteSize(): number {
      // estimate - assumes UTF8
      return JSON.stringify(getLocalStorage()).length
    },
    isReadOnly: false,
    isEncrypted: false,
    clearAll: () => {
      const storage = getLocalStorage()
      const keys = Object.keys(storage)
      for (const key of keys) {
        if (key.startsWith(keyPrefix)) {
          storage.removeItem(key)
          callListeners(key)
        }
      }
    },
    remove: (key) => {
      const storage = getLocalStorage()
      storage.removeItem(prefixedKey(key))
      const wasRemoved = storage.getItem(prefixedKey(key)) === null
      if (wasRemoved) callListeners(key)
      return wasRemoved
    },
    set: (key, value) => {
      const storage = getLocalStorage()
      if (key === '') throw new Error('Cannot set a value for an empty key!')
      if (value instanceof ArrayBuffer) {
        storage.setItem(prefixedKey(key), textDecoder.decode(value))
      } else {
        storage.setItem(prefixedKey(key), String(value))
      }
      callListeners(key)
    },
    getString: (key) => {
      const storage = getLocalStorage()
      return storage.getItem(prefixedKey(key)) ?? undefined
    },
    getNumber: (key) => {
      const storage = getLocalStorage()
      const value = storage.getItem(prefixedKey(key))
      if (value == null) return undefined
      return Number(value)
    },
    getBoolean: (key) => {
      const storage = getLocalStorage()
      const value = storage.getItem(prefixedKey(key))
      if (value == null) return undefined
      return value === 'true'
    },
    getBuffer: (key) => {
      const storage = getLocalStorage()
      const value = storage.getItem(prefixedKey(key))
      if (value == null) return undefined
      return textEncoder.encode(value).buffer
    },
    getAllKeys: () => {
      const storage = getLocalStorage()
      const keys = Object.keys(storage)
      return keys
        .filter((key) => key.startsWith(keyPrefix))
        .map((key) => key.slice(keyPrefix.length))
    },
    contains: (key) => {
      const storage = getLocalStorage()
      return storage.getItem(prefixedKey(key)) != null
    },
    recrypt: () => {
      throw new Error('`recrypt(..)` is not supported on Web!')
    },
    encrypt: () => {
      throw new Error('`encrypt(..)` is not supported on Web!')
    },
    decrypt: () => {
      throw new Error('`decrypt(..)` is not supported on Web!')
    },
    trim: () => {
      // no-op
    },
    dispose: () => {},
    equals: () => false,
    name: 'MMKV',
    addOnValueChangedListener: (listener) => {
      listeners.add(listener)
      return {
        remove: () => {
          listeners.delete(listener)
        },
      }
    },
    importAllFrom: (other) => {
      const storage = getLocalStorage()
      const keys = other.getAllKeys()
      let imported = 0
      for (const key of keys) {
        const string = other.getString(key)
        if (string != null) {
          storage.set(key, string)
          imported++
        }
      }
      return imported
    },
  }
}
