import { codedMessage, devBuild } from './error-message'
/**
 * IndexedDB Database Wrapper
 *
 * This module provides promise-based utilities for working with IndexedDB.
 * All functions return promises and wrap IndexedDB errors with descriptive messages.
 */

/**
 * Gets the IndexedDB factory, with cross-environment support.
 * @param idbFactory - Optional custom IDBFactory for testing
 * @returns The IDBFactory to use
 * @throws Error if IndexedDB is not available
 */
function getIDBFactory(idbFactory?: IDBFactory): IDBFactory {
  if (idbFactory) {
    return idbFactory
  }

  // Try window.indexedDB first (browser environment)
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime check needed
  if (typeof window !== 'undefined' && window.indexedDB) {
    return window.indexedDB
  }

  // Try globalThis.indexedDB (modern environments, including Node.js with polyfill)
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime check needed
  if (typeof globalThis !== 'undefined' && globalThis.indexedDB) {
    return globalThis.indexedDB
  }

  throw new Error(
    devBuild() && process.env.NODE_ENV !== `production`
      ? 'IndexedDB is not available in this environment. ' +
          'Ensure you are running in a browser or provide a custom IDBFactory for testing.'
      : codedMessage(179),
  )
}

function executeRequest<T>(
  createRequest: () => IDBRequest<T>,
  /** The whole message, given the native failure's text. */
  describeFailure: (cause: string) => string,
): Promise<T> {
  return new Promise((resolve, reject) => {
    let request: IDBRequest<T>
    try {
      request = createRequest()
    } catch (error) {
      reject(
        new Error(
          describeFailure(
            error instanceof Error ? error.message : String(error),
          ),
          { cause: error },
        ),
      )
      return
    }
    request.onsuccess = () => resolve(request.result)
    request.onerror = () => {
      const errorMessage = request.error?.message || 'Unknown error'
      reject(
        new Error(describeFailure(errorMessage), {
          cause: request.error,
        }),
      )
    }
  })
}

/**
 * Opens an IndexedDB database with the specified name and version.
 * A blocked request stays pending until native success or error. The caller
 * owns the returned connection and must close it when no longer needed.
 *
 * @param name - The name of the database to open
 * @param version - The version number of the database schema
 * @param onUpgrade - Optional callback that runs during the onupgradeneeded event.
 *                    Use this to create object stores and indexes.
 * @param idbFactory - Optional IDBFactory for testing/mocking (defaults to window.indexedDB or globalThis.indexedDB)
 * @param onBlocked - Optional diagnostic callback for native blocked events. The request stays pending.
 * @returns A promise that resolves to the IDBDatabase instance
 *
 * @example
 * ```typescript
 * const db = await openDatabase('myApp', 1, (db, oldVersion, newVersion, transaction) => {
 *   if (oldVersion < 1) {
 *     db.createObjectStore('todos', { keyPath: 'id' })
 *   }
 * })
 * ```
 */
export function openDatabase(
  name: string,
  version: number,
  onUpgrade?: (
    db: IDBDatabase,
    oldVersion: number,
    newVersion: number,
    transaction: IDBTransaction,
  ) => void,
  idbFactory?: IDBFactory,
  onBlocked?: (event: IDBVersionChangeEvent) => void,
): Promise<IDBDatabase> {
  return new Promise((resolve, reject) => {
    const factory = getIDBFactory(idbFactory)

    let request: IDBOpenDBRequest
    try {
      request = factory.open(name, version)
    } catch (error) {
      reject(
        new Error(
          devBuild() && process.env.NODE_ENV !== `production`
            ? `Failed to open IndexedDB database "${name}": ${error instanceof Error ? error.message : String(error)}`
            : codedMessage(182, { name, error }),
          { cause: error },
        ),
      )
      return
    }

    request.onupgradeneeded = (event) => {
      const db = request.result
      const transaction = request.transaction
      if (onUpgrade && transaction) {
        try {
          onUpgrade(
            db,
            event.oldVersion,
            event.newVersion ?? version,
            transaction,
          )
        } catch (error) {
          // If the upgrade callback throws, abort the transaction
          transaction.abort()
          reject(
            new Error(
              devBuild() && process.env.NODE_ENV !== `production`
                ? `Database upgrade failed for "${name}": ${error instanceof Error ? error.message : String(error)}`
                : codedMessage(183, { name, error }),
              { cause: error },
            ),
          )
        }
      }
    }

    if (onBlocked) request.addEventListener('blocked', onBlocked)

    request.onsuccess = () => {
      resolve(request.result)
    }

    request.onerror = () => {
      const errorMessage = request.error?.message || 'Unknown error'
      reject(
        new Error(
          devBuild() && process.env.NODE_ENV !== `production`
            ? `Failed to open IndexedDB database "${name}": ${errorMessage}`
            : codedMessage(184, { name, errorMessage }),
          { cause: request.error },
        ),
      )
    }
  })
}

/**
 * Creates an object store during a database upgrade.
 *
 * This function must be called within an onupgradeneeded callback
 * (i.e., within a versionchange transaction). Calling it outside of
 * an upgrade context will throw an error.
 *
 * @param db - The IDBDatabase instance
 * @param storeName - The name of the object store to create
 * @param options - Optional configuration for the object store (keyPath, autoIncrement)
 * @returns The created IDBObjectStore
 * @throws Error if not called during a version change transaction
 *
 * @example
 * ```typescript
 * const db = await openDatabase('myApp', 1, (db) => {
 *   createObjectStore(db, 'todos', { keyPath: 'id' })
 *   createObjectStore(db, 'users', { keyPath: 'id', autoIncrement: true })
 * })
 * ```
 */
export function createObjectStore(
  db: IDBDatabase,
  storeName: string,
  options?: IDBObjectStoreParameters,
): IDBObjectStore {
  try {
    return db.createObjectStore(storeName, options)
  } catch (error) {
    // Check if this is being called outside of a version change transaction
    if (error instanceof DOMException && error.name === 'InvalidStateError') {
      throw new Error(
        devBuild() && process.env.NODE_ENV !== `production`
          ? `Cannot create object store "${storeName}": This operation is only allowed during a database upgrade. ` +
              'Ensure you are calling createObjectStore within the onUpgrade callback of openDatabase.'
          : codedMessage(185, { storeName }),
        { cause: error },
      )
    }

    // Check if the object store already exists
    if (error instanceof DOMException && error.name === 'ConstraintError') {
      throw new Error(
        devBuild() && process.env.NODE_ENV !== `production`
          ? `Object store "${storeName}" already exists in the database. ` +
              'Check the database version and only create stores when needed.'
          : codedMessage(186, { storeName }),
        { cause: error },
      )
    }

    throw new Error(
      devBuild() && process.env.NODE_ENV !== `production`
        ? `Failed to create object store "${storeName}": ${error instanceof Error ? error.message : String(error)}`
        : codedMessage(187, { storeName, error }),
      { cause: error },
    )
  }
}

/**
 * Executes a callback within an IndexedDB transaction.
 *
 * This function handles transaction lifecycle automatically:
 * - Creates the transaction with the specified mode
 * - Provides the transaction and object stores to the callback
 * - Waits for both the callback and the transaction to complete (or abort)
 * - Returns the callback's result or rejects with an error
 *
 * IndexedDB can commit while an async callback is still pending. If that callback
 * later rejects, this function rejects but cannot undo the committed writes.
 *
 * @template T - The return type of the callback
 * @param db - The IDBDatabase instance
 * @param storeNames - A single store name or array of store names to include in the transaction
 * @param mode - The transaction mode ('readonly', 'readwrite', or 'readwriteflush')
 * @param callback - A function that performs operations within the transaction.
 *                   Receives the transaction and a record of object stores keyed by name.
 *                   Can be sync or async.
 * @returns A promise that resolves to the callback's return value when the transaction completes
 *
 * @example
 * ```typescript
 * // Single store
 * const result = await executeTransaction(db, 'todos', 'readwrite', (tx, stores) => {
 *   stores.todos.put({ id: 1, text: 'Buy milk' })
 *   return 'done'
 * })
 *
 * // Multiple stores
 * await executeTransaction(db, ['todos', 'users'], 'readwrite', (tx, stores) => {
 *   stores.todos.put({ id: 1, text: 'Task' })
 *   stores.users.put({ id: 1, name: 'Alice' })
 * })
 * ```
 */
export function executeTransaction<T>(
  db: IDBDatabase,
  storeNames: string | Array<string>,
  mode: IDBTransactionMode,
  callback: (
    transaction: IDBTransaction,
    stores: Record<string, IDBObjectStore>,
  ) => T | Promise<T>,
): Promise<T> {
  return new Promise((resolve, reject) => {
    const storeNamesArray = Array.isArray(storeNames)
      ? storeNames
      : [storeNames]
    let transaction: IDBTransaction

    try {
      transaction = db.transaction(storeNamesArray, mode)
    } catch (error) {
      reject(
        new Error(
          devBuild() && process.env.NODE_ENV !== `production`
            ? `Failed to create transaction for stores [${storeNamesArray.join(', ')}]: ${error instanceof Error ? error.message : String(error)}`
            : codedMessage(188, { storeNames: storeNamesArray, error }),
          { cause: error },
        ),
      )
      return
    }

    // Build the stores record
    const stores: Record<string, IDBObjectStore> = {}
    for (const storeName of storeNamesArray) {
      try {
        stores[storeName] = transaction.objectStore(storeName)
      } catch (error) {
        reject(
          new Error(
            devBuild() && process.env.NODE_ENV !== `production`
              ? `Object store "${storeName}" not found in the database. ` +
                  'Ensure the store was created during the database upgrade.'
              : codedMessage(189, { storeName }),
            { cause: error },
          ),
        )
        return
      }
    }

    // Success requires both obligations: callback result AND transaction
    // completion. Request success alone is not a durability receipt.
    const completed = new Promise<void>((complete, abort) => {
      // The callback may also set native event-handler properties.
      transaction.addEventListener('complete', () => complete())
      transaction.addEventListener('abort', () =>
        abort(
          transaction.error ??
            new Error(
              devBuild() && process.env.NODE_ENV !== `production`
                ? 'Transaction was aborted'
                : codedMessage(190),
            ),
        ),
      )
      // Request errors normally bubble before abort. Let abort report the
      // transaction outcome; callback rejection retains its original cause.
    })
    let result: T | Promise<T>
    try {
      result = callback(transaction, stores)
    } catch (error) {
      result = Promise.reject(error)
    }
    const callbackResult = Promise.resolve(result).catch((error) => {
      try {
        transaction.abort()
      } catch {
        // An async callback may settle after the native transaction has ended.
      }
      throw error
    })
    Promise.all([callbackResult, completed]).then(
      ([value]) => resolve(value),
      reject,
    )
  })
}

/**
 * Retrieves all items from an object store.
 *
 * Uses the native `getAll()` method for efficient bulk retrieval.
 *
 * @template T - The type of items in the object store
 * @param objectStore - The IDBObjectStore to read from
 * @returns A promise that resolves to an array of all items in the store
 *
 * @example
 * ```typescript
 * await executeTransaction(db, 'todos', 'readonly', async (tx, stores) => {
 *   const allTodos = await getAll<Todo>(stores.todos)
 *   console.log('All todos:', allTodos)
 * })
 * ```
 */
export function getAll<T>(objectStore: IDBObjectStore): Promise<Array<T>> {
  return executeRequest<Array<T>>(
    () => objectStore.getAll(),
    (cause) =>
      devBuild() && process.env.NODE_ENV !== `production`
        ? `Failed to get all items from object store "${objectStore.name}": ${cause}`
        : codedMessage(204, { name: objectStore.name, cause }),
  )
}

/**
 * Retrieves all keys from an object store.
 *
 * Uses the native `getAllKeys()` method for efficient bulk key retrieval.
 *
 * @param objectStore - The IDBObjectStore to read keys from
 * @returns A promise that resolves to an array of all keys in the store
 *
 * @example
 * ```typescript
 * await executeTransaction(db, 'todos', 'readonly', async (tx, stores) => {
 *   const allKeys = await getAllKeys(stores.todos)
 *   console.log('All keys:', allKeys)
 * })
 * ```
 */
export function getAllKeys(
  objectStore: IDBObjectStore,
): Promise<Array<IDBValidKey>> {
  return executeRequest(
    () => objectStore.getAllKeys(),
    (cause) =>
      devBuild() && process.env.NODE_ENV !== `production`
        ? `Failed to get all keys from object store "${objectStore.name}": ${cause}`
        : codedMessage(200, { name: objectStore.name, cause }),
  )
}

/**
 * Retrieves a single item by its key from an object store.
 *
 * @template T - The type of the item
 * @param objectStore - The IDBObjectStore to read from
 * @param key - The key of the item to retrieve
 * @returns A promise that resolves to the item, or undefined if not found
 *
 * @example
 * ```typescript
 * await executeTransaction(db, 'todos', 'readonly', async (tx, stores) => {
 *   const todo = await getByKey<Todo>(stores.todos, 1)
 *   if (todo) {
 *     console.log('Found todo:', todo)
 *   }
 * })
 * ```
 */
export function getByKey<T>(
  objectStore: IDBObjectStore,
  key: IDBValidKey,
): Promise<T | undefined> {
  return executeRequest<T | undefined>(
    () => objectStore.get(key),
    (cause) =>
      devBuild() && process.env.NODE_ENV !== `production`
        ? `Failed to get item with key "${String(key)}" from object store "${objectStore.name}": ${cause}`
        : codedMessage(205, {
            key: String(key),
            name: objectStore.name,
            cause,
          }),
  )
}

/**
 * Writes an item to an object store using upsert semantics.
 *
 * If an item with the same key exists, it will be replaced.
 * If no item with the key exists, a new one will be created.
 *
 * @template T - The type of the item
 * @param objectStore - The IDBObjectStore to write to
 * @param value - The item to write
 * @param key - Optional key for the item. Required if the object store doesn't have a keyPath.
 * @returns A promise that resolves to the key of the written item
 *
 * @example
 * ```typescript
 * // With keyPath (key extracted from value)
 * await executeTransaction(db, 'todos', 'readwrite', async (tx, stores) => {
 *   const key = await put(stores.todos, { id: 1, text: 'Buy milk' })
 *   console.log('Wrote item with key:', key)
 * })
 *
 * // Without keyPath (explicit key)
 * await executeTransaction(db, 'items', 'readwrite', async (tx, stores) => {
 *   const key = await put(stores.items, { text: 'Some data' }, 'myKey')
 *   console.log('Wrote item with key:', key)
 * })
 * ```
 */
export function put<T>(
  objectStore: IDBObjectStore,
  value: T,
  key?: IDBValidKey,
): Promise<IDBValidKey> {
  return executeRequest(
    () =>
      key !== undefined ? objectStore.put(value, key) : objectStore.put(value),
    (cause) =>
      devBuild() && process.env.NODE_ENV !== `production`
        ? `Failed to write item to object store "${objectStore.name}": ${cause}`
        : codedMessage(201, { name: objectStore.name, cause }),
  )
}

/**
 * Deletes an item by its key from an object store.
 *
 * @param objectStore - The IDBObjectStore to delete from
 * @param key - The key of the item to delete
 * @returns A promise that resolves when the item is deleted
 *
 * @example
 * ```typescript
 * await executeTransaction(db, 'todos', 'readwrite', async (tx, stores) => {
 *   await deleteByKey(stores.todos, 1)
 *   console.log('Todo deleted')
 * })
 * ```
 */
export function deleteByKey(
  objectStore: IDBObjectStore,
  key: IDBValidKey,
): Promise<void> {
  return executeRequest(
    () => objectStore.delete(key),
    (cause) =>
      devBuild() && process.env.NODE_ENV !== `production`
        ? `Failed to delete item with key "${String(key)}" from object store "${objectStore.name}": ${cause}`
        : codedMessage(202, {
            key: String(key),
            name: objectStore.name,
            cause,
          }),
  )
}

/**
 * Removes all items from an object store.
 *
 * @param objectStore - The IDBObjectStore to clear
 * @returns A promise that resolves when all items are removed
 *
 * @example
 * ```typescript
 * await executeTransaction(db, 'todos', 'readwrite', async (tx, stores) => {
 *   await clear(stores.todos)
 *   console.log('All todos cleared')
 * })
 * ```
 */
export function clear(objectStore: IDBObjectStore): Promise<void> {
  return executeRequest(
    () => objectStore.clear(),
    (cause) =>
      devBuild() && process.env.NODE_ENV !== `production`
        ? `Failed to clear object store "${objectStore.name}": ${cause}`
        : codedMessage(203, { name: objectStore.name, cause }),
  )
}

/**
 * Deletes an entire IndexedDB database.
 * A blocked request stays pending until native success or error.
 *
 * Use with caution - this removes the database and all of its object stores and data.
 *
 * @param name - The name of the database to delete
 * @param idbFactory - Optional IDBFactory for testing/mocking
 * @param onBlocked - Optional diagnostic callback for native blocked events. The request stays pending.
 * @returns A promise that resolves when the database is deleted
 *
 * @example
 * ```typescript
 * await deleteDatabase('myApp')
 * console.log('Database deleted')
 * ```
 */
export function deleteDatabase(
  name: string,
  idbFactory?: IDBFactory,
  onBlocked?: (event: IDBVersionChangeEvent) => void,
): Promise<void> {
  return new Promise((resolve, reject) => {
    const factory = getIDBFactory(idbFactory)

    let request: IDBOpenDBRequest
    try {
      request = factory.deleteDatabase(name)
    } catch (error) {
      reject(
        new Error(
          devBuild() && process.env.NODE_ENV !== `production`
            ? `Failed to delete IndexedDB database "${name}": ${error instanceof Error ? error.message : String(error)}`
            : codedMessage(191, { name, error }),
          { cause: error },
        ),
      )
      return
    }

    if (onBlocked) request.addEventListener('blocked', onBlocked)

    request.onsuccess = () => {
      resolve()
    }

    request.onerror = () => {
      const errorMessage = request.error?.message || 'Unknown error'
      reject(
        new Error(
          devBuild() && process.env.NODE_ENV !== `production`
            ? `Failed to delete IndexedDB database "${name}": ${errorMessage}`
            : codedMessage(192, { name, errorMessage }),
          { cause: request.error },
        ),
      )
    }
  })
}
