import {openDB, deleteDB} from "idb"
import type {IDBPDatabase} from "idb"
import {writable} from "svelte/store"
import type {Unsubscriber, Writable} from "svelte/store"
import {indexBy, throttle, fromPairs} from "@welshman/lib"
import type {TrustedEvent, Repository} from "@welshman/util"
import type {Tracker} from "@welshman/net"
import {withGetter, adapter, throttled, custom} from "@welshman/store"

export type IndexedDbAdapter = {
  keyPath: string
  store: Writable<any[]>
}

export let db: IDBPDatabase

export const dead = withGetter(writable(false))

export const subs: Unsubscriber[] = []

export const getAll = async (name: string) => {
  const tx = db.transaction(name, "readwrite")
  const store = tx.objectStore(name)
  const result = await store.getAll()

  await tx.done

  return result
}

export const bulkPut = async (name: string, data: any[]) => {
  const tx = db.transaction(name, "readwrite")
  const store = tx.objectStore(name)

  await Promise.all(data.map(item => store.put(item)))
  await tx.done
}

export const bulkDelete = async (name: string, ids: string[]) => {
  const tx = db.transaction(name, "readwrite")
  const store = tx.objectStore(name)

  await Promise.all(ids.map(id => store.delete(id)))
  await tx.done
}

export const initIndexedDbAdapter = async (name: string, adapter: IndexedDbAdapter) => {
  let prevRecords = await getAll(name)

  adapter.store.set(prevRecords)

  adapter.store.subscribe(
    async (currentRecords: any[]) => {
      if (dead.get()) {
        return
      }

      const currentIds = new Set(currentRecords.map(item => item[adapter.keyPath]))
      const removedRecords = prevRecords.filter(r => !currentIds.has(r[adapter.keyPath]))

      const prevRecordsById = indexBy(item => item[adapter.keyPath], prevRecords)
      const updatedRecords = currentRecords.filter(r => r !== prevRecordsById.get(r[adapter.keyPath]))

      prevRecords = currentRecords

      if (updatedRecords.length > 0) {
        await bulkPut(name, updatedRecords)
      }

      if (removedRecords.length > 0) {
        await bulkDelete(
          name,
          removedRecords.map(item => item[adapter.keyPath]),
        )
      }
    },
  )
}

export const initStorage = async (name: string, version: number, adapters: Record<string, IndexedDbAdapter>) => {
  if (!window.indexedDB) return

  window.addEventListener("beforeunload", () => closeStorage())

  if (db) {
    throw new Error("Db initialized multiple times")
  }

  db = await openDB(name, version, {
    upgrade(db: IDBPDatabase) {
      const names = Object.keys(adapters)

      for (const name of db.objectStoreNames) {
        if (!names.includes(name)) {
          db.deleteObjectStore(name)
        }
      }

      for (const [name, {keyPath}] of Object.entries(adapters)) {
        try {
          db.createObjectStore(name, {keyPath})
        } catch (e) {
          console.warn(e)
        }
      }
    },
  })

  await Promise.all(
    Object.entries(adapters).map(([name, config]) => initIndexedDbAdapter(name, config)),
  )
}

export const closeStorage = async () => {
  dead.set(true)
  subs.forEach(unsub => unsub())
  await db?.close()
}

export const clearStorage = async () => {
  await closeStorage()
  await deleteDB(db.name)
}

export type StorageAdapterOptions = {
  throttle?: number
  migrate?: (items: any[]) => any[]
}

const migrate = (data: any[], options: StorageAdapterOptions) =>
  options.migrate ? options.migrate(data) : data

export const storageAdapters = {
  fromObjectStore: <T>(store: Writable<Record<string, T>>, options: StorageAdapterOptions = {}) => ({
    options,
    keyPath: "key",
    store: adapter({
      store: throttled(options.throttle || 0, store),
      forward: (data: Record<string, T>) =>
        migrate(Object.entries(data).map(([key, value]) => ({key, value})), options),
      backward: (data: {key: string, value: T}[]) =>
        fromPairs(data.map(({key, value}) => [key, value])),
    }),
  }),
  fromMapStore: <T>(store: Writable<Map<string, T>>, options: StorageAdapterOptions = {}) => ({
    options,
    keyPath: "key",
    store: adapter({
      store: throttled(options.throttle || 0, store),
      forward: (data: Map<string, T>) =>
        migrate(Array.from(data.entries()).map(([key, value]) => ({key, value})), options),
      backward: (data: {key: string, value: T}[]) =>
        new Map(data.map(({key, value}) => [key, value])),
    }),
  }),
  fromTracker: (tracker: Tracker, options: StorageAdapterOptions = {}) => ({
    options,
    keyPath: 'key',
    store: custom(setter => {
      let onUpdate = () =>
        setter(
          migrate(
            Array.from(tracker.relaysById.entries())
              .map(([key, urls]) => ({key, value: Array.from(urls)})),
            options
          )
        )

      if (options.throttle) {
        onUpdate = throttle(options.throttle, onUpdate)
      }

      onUpdate()
      tracker.on('update', onUpdate)

      return () => tracker.off('update', onUpdate)
    }, {
      set: (data: {key: string, value: string[]}[]) =>
        tracker.load(new Map(data.map(({key, value}) => [key, new Set(value)]))),
    }),
  }),
  fromRepository: (repository: Repository, options: StorageAdapterOptions = {}) => ({
    options,
    keyPath: 'id',
    store: custom(setter => {
      let onUpdate = () => setter(migrate(repository.dump(), options))

      if (options.throttle) {
        onUpdate = throttle(options.throttle, onUpdate)
      }

      onUpdate()
      repository.on('update', onUpdate)

      return () => repository.off('update', onUpdate)
    }, {
      set: (events: TrustedEvent[]) => repository.load(events),
    }),
  }),
  fromRepositoryAndTracker: (
    repository: Repository,
    tracker: Tracker,
    options: StorageAdapterOptions = {}
  ) => ({
    options,
    keyPath: 'id',
    store: custom(setter => {
      let onUpdate = () => {
        const events = migrate(repository.dump(), options)

        setter(
          events.map(event => {
            const relays = Array.from(tracker.getRelays(event.id))

            return {id: event.id, event, relays}
          })
        )
      }

      if (options.throttle) {
        onUpdate = throttle(options.throttle, onUpdate)
      }

      onUpdate()
      tracker.on('update', onUpdate)
      repository.on('update', onUpdate)

      return () => {
        tracker.off('update', onUpdate)
      }
    }, {
      set: (items: {event: TrustedEvent, relays: string[]}[]) => {
        const events: TrustedEvent[] = []
        const relaysById = new Map<string, Set<string>>()

        for (const {event, relays} of items) {
          events.push(event)
          relaysById.set(event.id, new Set(relays))
        }

        repository.load(events)
        tracker.load(relaysById)
      },
    }),
  })
}

