import type { SyncMapValues } from '@logux/actions'
import type { Action, Meta } from '@logux/core'
import type { MapCreator, MapStore, ReadableAtom, StoreValue } from 'nanostores'

import type { Client } from '../client/index.js'
import type { FilterValue, LoadedFilterValue } from '../create-filter/index.js'

interface SyncMapStoreExt {
  /**
   * Logux Client instance.
   */
  readonly client: Client

  /**
   * Meta from create action if the store was created locally.
   */
  createdAt?: Meta

  /**
   * Mark that store was deleted.
   */
  deleted?: true

  /**
   * While store is loading initial data from server or log.
   */
  readonly loading: Promise<void>

  /**
   * Does store keep data in the log after store is destroyed.
   */
  offline: boolean

  /**
   * Name of map class.
   */
  readonly plural: string

  /**
   * Does store use server to load and save data.
   */
  remote: boolean
}

export type LoadedSyncMapValue<Value extends SyncMapValues> = {
  id: string
  isLoading: false
} & Value

export type SyncMapValue<Value extends SyncMapValues> =
  | { id: string; isLoading: true }
  | LoadedSyncMapValue<Value>

export type SyncMapStore<Value extends SyncMapValues = any> = MapStore<
  SyncMapValue<Value>
> &
  SyncMapStoreExt

export interface SyncMapTemplate<
  Value extends SyncMapValues = any,
  StoreExt = object
> extends MapCreator {
  (
    id: string,
    client: Client,
    ...args: [] | [Action, Meta, boolean | undefined]
  ): StoreExt & SyncMapStore<Value>
  cache: {
    [id: string]: StoreExt & SyncMapStore<Value>
  }
  offline: boolean
  readonly plural: string
  remote: boolean
}

export interface SyncMapTemplateLike<
  Value extends object = any,
  Args extends any[] = []
> {
  (id: string, client: Client, ...args: Args): MapStore<Value>
}

/**
 * CRDT LWW Map. It can use server validation or be fully offline.
 *
 * The best option for classic case with server and many clients.
 * Store will resolve client’s edit conflicts with last write wins strategy.
 *
 * ```ts
 * import { syncMapTemplate } from '@logux/client'
 *
 * export const User = syncMapTemplate<{
 *   login: string,
 *   name?: string,
 *   isAdmin: boolean
 * }>('users')
 * ```
 *
 * @param plural Plural store name. It will be used in action type
 *               and channel name.
 * @param opts Options to disable server validation or keep actions in log
 *             for offline support.
 */
export function syncMapTemplate<Value extends SyncMapValues>(
  plural: string,
  opts?: {
    offline?: boolean
    remote?: boolean
  }
): SyncMapTemplate<Value>

/**
 * Send create action to the server or to the log.
 *
 * Server will create a row in database on this action. {@link FilterStore}
 * will update the list.
 *
 * ```js
 * import { createSyncMap } from '@logux/client'
 *
 * showLoader()
 * await createSyncMap(client, User, {
 *   id: nanoid(),
 *   login: 'test'
 * })
 * hideLoader()
 * ```
 *
 * @param client Logux Client instance.
 * @param Template Store template from {@link syncMapTemplate}.
 * @param value Initial value.
 * @return Promise until server validation for remote classes
 *         or saving action to the log of fully offline classes.
 */
export function createSyncMap<Value extends SyncMapValues>(
  client: Client,
  Template: SyncMapTemplate<Value>,
  value: { id: string } & Value
): Promise<void>

/**
 * Send create action and build store instance.
 *
 * ```js
 * import { buildNewSyncMap } from '@logux/client'
 *
 * let userStore = buildNewSyncMap(client, User, {
 *   id: nanoid(),
 *   login: 'test'
 * })
 * ```
 *
 * @param client Logux Client instance.
 * @param Template Store template from {@link syncMapTemplate}.
 * @param value Initial value.
 * @return Promise with store instance.
 */
export function buildNewSyncMap<Value extends SyncMapValues>(
  client: Client,
  Template: SyncMapTemplate<Value>,
  value: { id: string } & Value
): Promise<SyncMapStore<Value>>

/**
 * Change store without store instance just by store ID.
 *
 * ```js
 * import { changeSyncMapById } from '@logux/client'
 *
 * let userStore = changeSyncMapById(client, User, 'user:4hs2jd83mf', {
 *   name: 'New name'
 * })
 * ```
 *
 * @param client Logux Client instance.
 * @param Template Store template from {@link syncMapTemplate}.
 * @param id Store’s ID.
 * @param diff Store’s changes.
 * @return Promise until server validation for remote classes
 *         or saving action to the log of fully offline classes.
 */
export function changeSyncMapById<Value extends SyncMapValues>(
  client: Client,
  Template: SyncMapTemplate<Value>,
  id: string,
  diff: Partial<Value>
): Promise<void>
export function changeSyncMapById<
  Value extends SyncMapValues,
  ValueKey extends keyof Value
>(
  client: Client,
  Template: SyncMapTemplate<Value>,
  id: string,
  key: ValueKey,
  value: Value[ValueKey]
): Promise<void>

/**
 * Change keys in the store’s value.
 *
 * ```js
 * import { changeSyncMap } from '@logux/client'
 *
 * showLoader()
 * await changeSyncMap(userStore, { name: 'New name' })
 * hideLoader()
 * ```
 *
 * @param store Store’s instance.
 * @param diff Store’s changes.
 * @return Promise until server validation for remote classes
 *         or saving action to the log of fully offline classes.
 */
export function changeSyncMap<Value extends SyncMapValues>(
  store: SyncMapStore<Value>,
  diff: Partial<Omit<Value, 'id'>>
): Promise<void>
export function changeSyncMap<
  Value extends SyncMapValues,
  ValueKey extends Exclude<keyof Value, 'id'>
>(
  store: SyncMapStore<Value>,
  key: ValueKey,
  value: Value[ValueKey]
): Promise<void>

/**
 * Delete store without store instance just by store ID.
 *
 * ```js
 * import { deleteSyncMapById } from '@logux/client'
 *
 * showLoader()
 * await deleteSyncMapById(client, User, 'user:4hs2jd83mf')
 * ```
 *
 * @param client Logux Client instance.
 * @param Template Store template from {@link syncMapTemplate}.
 * @param id Store’s ID.
 * @return Promise until server validation for remote classes
 *         or saving action to the log of fully offline classes.
 */
export function deleteSyncMapById(
  client: Client,
  Template: SyncMapTemplate,
  id: string
): Promise<void>

/**
 * Delete store.
 *
 * ```js
 * import { deleteSyncMap } from '@logux/client'
 *
 * showLoader()
 * await deleteSyncMap(User)
 * ```
 *
 * @param store Store’s instance.
 * @return Promise until server validation for remote classes
 *         or saving action to the log of fully offline classes.
 */
export function deleteSyncMap(store: SyncMapStore): Promise<void>

/**
 * Change store’s value type to value with `isLoaded: false`.
 *
 * If store is still loading, this function will trow an error.
 *
 * Use it for tests written on TypeScript.
 *
 * ```js
 * import { ensureLoaded } from '@logux/client'
 *
 * expect(ensureLoaded($currentUser)).toEqual({ id: 1, name: 'User' })
 * ```
 *
 * @param value Store’s value.
 */
export function ensureLoaded<Value extends SyncMapValues>(
  value: SyncMapValue<Value>
): LoadedSyncMapValue<Value>
export function ensureLoaded<Value extends SyncMapValues>(
  value: FilterValue<Value>
): LoadedFilterValue<Value>

export type LoadedValue<Type extends { isLoading: boolean }> = {
  isLoading: false
} & Type

export type LoadableStore = {
  readonly loading: Promise<unknown>
} & ReadableAtom<{ isLoading: boolean }>

/**
 * Return store’s value if store is loaded or wait until store will be loaded
 * and return its value.
 *
 * Returns `undefined` on 404.
 *
 * ```js
 * import { loadValue } from '@logux/client'
 *
 * let user = loadValue($currentUser)
 * ```
 *
 * @param store Store to load.
 */
export function loadValue<Store extends SyncMapStore>(
  store: Store
): Promise<LoadedValue<StoreValue<Store>> | undefined>
export function loadValue<Store extends LoadableStore>(
  store: Store
): Promise<LoadedValue<StoreValue<Store>>>

export type LoadedSyncMap<Store extends SyncMapStore> = MapStore<
  LoadedSyncMapValue<StoreValue<Store>>
> &
  SyncMapStoreExt

export function ensureLoadedStore<Store extends SyncMapStore>(
  store: Store
): LoadedSyncMap<Store>
