/**
 * Модуль работы с IndexedDB для проекта BEAST.
 *
 * Назначение:
 * - Предоставляет простую обёртку над IndexedDB.
 * - Скрывает детали открытия базы, создания хранилищ и выполнения операций.
 * - Обеспечивает единообразный интерфейс для чтения и записи данных по ID.
 *
 * Основные функции:
 * - openDatabase(config)  — открыть/создать базу и объектные хранилища.
 * - put(params)           — сохранить или обновить запись.
 * - get(params)           — получить одну запись по ключу.
 * - getAll(params)        — получить все записи из хранилища.
 * - remove(params)        — удалить запись по ключу.
 * - clearStore(params)    — очистить хранилище.
 *
 * Пример использования:
 *
 *   // Открываем базу и создаём хранилище 'samples' с ключом 'id'
 *   IndexedDBModule.openDatabase({
 *     dbName: 'BEAST_DB',
 *     version: 1,
 *     stores: [
 *       { name: 'samples', options: { keyPath: 'id' } }
 *     ]
 *   }).then(function (db) {
 *     return IndexedDBModule.put({
 *       db: db,
 *       storeName: 'samples',
 *       value: { id: 1, text: 'Привет, IndexedDB!' }
 *     });
 *   }).then(function () {
 *     return IndexedDBModule.getAll({
 *       db: db,
 *       storeName: 'samples'
 *     });
 *   }).then(function (allItems) {
 *     console.log(allItems);
 *   }).catch(function (error) {
 *     console.error('Ошибка IndexedDB:', error);
 *   });
 *
 * Все комментарии даны на русском языке в соответствии с правилами проекта.
 */

(function (global) {
  'use strict';

  /**
   * Открыть (или создать) базу данных IndexedDB.
   *
   * @param {Object} config - конфигурация базы данных.
   * @param {string} config.dbName - имя базы данных.
   * @param {number} [config.version] - версия базы данных.
   * @param {Array<Object>} [config.stores] - описание создаваемых хранилищ.
   *        Каждый элемент: { name: string, options?: IDBObjectStoreParameters, indexes?: Array<{name, keyPath, options}> }
   * @returns {Promise<IDBDatabase>} - промис с открытой базой данных.
   */
  function openDatabase(config) {
    if (!config || !config.dbName) {
      return Promise.reject(new Error('Не указано имя базы данных (config.dbName).'));
    }

    var dbName = config.dbName;
    var version = config.version || 1;
    var stores = Array.isArray(config.stores) ? config.stores : null;

    return new Promise(function (resolve, reject) {
      var request;
      try {
        request = global.indexedDB.open(dbName, version);
      } catch (e) {
        reject(e);
        return;
      }

      // Вызывается один раз при создании новой БД или повышении версии.
      request.onupgradeneeded = function (event) {
        var db = event.target.result;

        if (!stores) {
          return;
        }

        stores.forEach(function (storeConfig) {
          if (!storeConfig || !storeConfig.name) {
            return;
          }

          // Если хранилище уже есть — пропускаем.
          if (db.objectStoreNames.contains(storeConfig.name)) {
            return;
          }

          var options = storeConfig.options || { keyPath: 'id' };
          var objectStore = db.createObjectStore(storeConfig.name, options);

          // Создаём индексы, если описаны.
          if (Array.isArray(storeConfig.indexes)) {
            storeConfig.indexes.forEach(function (idx) {
              if (!idx || !idx.name || !idx.keyPath) {
                return;
              }
              objectStore.createIndex(idx.name, idx.keyPath, idx.options || {});
            });
          }
        });
      };

      request.onsuccess = function (event) {
        var db = event.target.result;

        // Если версия БД меньше требуемой — закрываем и пробуем ещё раз.
        if (db.version !== version && version > db.version) {
          db.close();
          // Рекурсивный вызов для повышения версии.
          openDatabase({
            dbName: dbName,
            version: version,
            stores: stores
          }).then(resolve).catch(reject);
          return;
        }

        resolve(db);
      };

      request.onerror = function (event) {
        reject(event.target.error || new Error('Неизвестная ошибка при открытии IndexedDB.'));
      };

      request.onblocked = function () {
        reject(new Error('Операция обновления IndexedDB заблокирована. Закройте другие вкладки с этим приложением.'));
      };
    });
  }

  /**
   * Совместимость со старым интерфейсом модулей BEAST.
   *
   * Старые модули вызывали:
   *   IndexedDBModule.openDB({ dbName, version, storeName, keyPath })
   *
   * Новый openDatabase ожидает:
   *   { dbName, version, stores: [{ name, options: { keyPath } }] }
   *
   * Эта обёртка принимает оба формата.
   */
  function openDB(config) {
    config = config || {};
    // Если уже передан новый формат — просто проксируем.
    if (Array.isArray(config.stores)) {
      return openDatabase(config);
    }
    // Старый формат: storeName/keyPath.
    var storeName = config.storeName;
    var keyPath = config.keyPath || 'id';
    // Важно: если БД уже была создана ранее без нужного store (из-за старого API),
    // то onupgradeneeded не сработает при той же версии. В этом случае повышаем версию и переоткрываем.
    var desired = {
      dbName: config.dbName,
      version: config.version,
      stores: storeName ? [{ name: storeName, options: { keyPath: keyPath } }] : undefined
    };
    if (!storeName) {
      return openDatabase(desired);
    }
    return openDatabase(desired).then(function (db) {
      try {
        if (db && db.objectStoreNames && db.objectStoreNames.contains(storeName)) {
          return db;
        }
      } catch (e) {}
      // Store не найден — повышаем версию и пробуем создать.
      var nextVersion = Math.max(parseInt(desired.version, 10) || 1, db && db.version ? db.version : 1) + 1;
      try { if (db) db.close(); } catch (e2) {}
      return openDatabase({
        dbName: desired.dbName,
        version: nextVersion,
        stores: desired.stores
      });
    });
  }

  /**
   * Вспомогательная функция для выполнения транзакции.
   *
   * @param {IDBDatabase} db - открытая база данных.
   * @param {string} storeName - имя хранилища объектов.
   * @param {string} mode - режим транзакции ('readonly' или 'readwrite').
   * @param {function(IDBObjectStore): IDBRequest} operation - операция над хранилищем.
   * @returns {Promise<any>} - промис с результатом операции.
   */
  function runTransaction(db, storeName, mode, operation) {
    if (!db) {
      return Promise.reject(new Error('База данных не открыта.'));
    }
    if (!storeName) {
      return Promise.reject(new Error('Не указано имя хранилища (storeName).'));
    }

    return new Promise(function (resolve, reject) {
      var tx;
      var store;
      try {
        tx = db.transaction(storeName, mode);
        store = tx.objectStore(storeName);
      } catch (e) {
        reject(e);
        return;
      }

      var request;
      try {
        request = operation(store);
      } catch (e) {
        reject(e);
        return;
      }

      request.onsuccess = function (event) {
        resolve(event.target.result);
      };

      request.onerror = function (event) {
        reject(event.target.error || new Error('Ошибка операции с IndexedDB.'));
      };
    });
  }

  /**
   * Сохранить или обновить запись в хранилище.
   *
   * @param {Object} params
   * @param {IDBDatabase} params.db - база данных.
   * @param {string} params.storeName - имя хранилища.
   * @param {any} params.value - сохраняемое значение.
   * @returns {Promise<IDBValidKey>} - промис с ключом записи.
   */
  function put(params) {
    if (typeof global.showSaveIndicator === 'function') global.showSaveIndicator();
    var p = runTransaction(params.db, params.storeName, 'readwrite', function (store) {
      return store.put(params.value);
    });
    return p.then(function (r) {
      if (typeof global.hideSaveIndicator === 'function') global.hideSaveIndicator();
      return r;
    }, function (err) {
      if (typeof global.hideSaveIndicator === 'function') global.hideSaveIndicator();
      throw err;
    });
  }

  /**
   * Получить одну запись по ключу.
   *
   * @param {Object} params
   * @param {IDBDatabase} params.db - база данных.
   * @param {string} params.storeName - имя хранилища.
   * @param {IDBValidKey | IDBKeyRange} params.key - ключ поиска.
   * @returns {Promise<any>} - промис с найденной записью или undefined.
   */
  function get(params) {
    return runTransaction(params.db, params.storeName, 'readonly', function (store) {
      return store.get(params.key);
    });
  }

  /**
   * Получить все записи из хранилища.
   *
   * @param {Object} params
   * @param {IDBDatabase} params.db - база данных.
   * @param {string} params.storeName - имя хранилища.
   * @returns {Promise<Array<any>>} - промис с массивом записей.
   */
  function getAll(params) {
    return runTransaction(params.db, params.storeName, 'readonly', function (store) {
      if (typeof store.getAll === 'function') {
        return store.getAll();
      }

      // Для старых браузеров без getAll — вручную обходим курсор.
      var dummyRequest = {};
      setTimeout(function () {
        var tx = params.db.transaction(params.storeName, 'readonly');
        var st = tx.objectStore(params.storeName);
        var cursorRequest = st.openCursor();
        var items = [];

        cursorRequest.onsuccess = function (event) {
          var cursor = event.target.result;
          if (cursor) {
            items.push(cursor.value);
            cursor.continue();
          } else {
            // Эмулируем интерфейс request и вызываем onsuccess у оригинального промиса.
            if (typeof dummyRequest.onsuccess === 'function') {
              dummyRequest.onsuccess({ target: { result: items } });
            }
          }
        };

        cursorRequest.onerror = function (event) {
          if (typeof dummyRequest.onerror === 'function') {
            dummyRequest.onerror(event);
          }
        };
      }, 0);

      return dummyRequest;
    });
  }

  /**
   * Удалить запись по ключу.
   *
   * @param {Object} params
   * @param {IDBDatabase} params.db - база данных.
   * @param {string} params.storeName - имя хранилища.
   * @param {IDBValidKey | IDBKeyRange} params.key - ключ.
   * @returns {Promise<void>} - промис без значения.
   */
  function remove(params) {
    if (typeof global.showSaveIndicator === 'function') global.showSaveIndicator();
    var p = runTransaction(params.db, params.storeName, 'readwrite', function (store) {
      return store.delete(params.key);
    });
    return p.then(function (r) {
      if (typeof global.hideSaveIndicator === 'function') global.hideSaveIndicator();
      return r;
    }, function (err) {
      if (typeof global.hideSaveIndicator === 'function') global.hideSaveIndicator();
      throw err;
    });
  }

  /**
   * Очистить всё хранилище.
   *
   * @param {Object} params
   * @param {IDBDatabase} params.db - база данных.
   * @param {string} params.storeName - имя хранилища.
   * @returns {Promise<void>} - промис без значения.
   */
  function clearStore(params) {
    if (typeof global.showSaveIndicator === 'function') global.showSaveIndicator();
    var p = runTransaction(params.db, params.storeName, 'readwrite', function (store) {
      return store.clear();
    });
    return p.then(function (r) {
      if (typeof global.hideSaveIndicator === 'function') global.hideSaveIndicator();
      return r;
    }, function (err) {
      if (typeof global.hideSaveIndicator === 'function') global.hideSaveIndicator();
      throw err;
    });
  }

  /**
   * Регистрация отложенного сохранения по пульсу.
   * Модули регистрируют функцию fn(); она вызывается раз в пульс (из puls.js).
   *
   * Сохранение по мере изменения данных (не полный дамп каждый пульс):
   * - Модуль ведёт флаг dirty (например, epDirty, treeDirty): устанавливать true только при реальном
   *   изменении данных (добавление/удаление/редактирование кадров, узлов, записей).
   * - В зарегистрированной функции: если !dirty — сразу return (сериализация и put не вызываются).
   * - Если dirty: сбросить dirty, сериализовать состояние, сравнить подпись (sig) с lastSavedSig;
   *   если подпись не изменилась — return без put; иначе обновить lastSavedSig и выполнить put(state).
   * Так в IndexedDB пишется только при изменении данного вида данных (ЭП — при новом/удалённом кадре,
   * дерево — при добавлении узла, семантика — при новой/изменённой записи, УР/ОД — при изменении массивов).
   *
   * Модули, которые должны вызывать registerIndexedDBSaver (чеклист при добавлении хранилища):
   * - semantic_memory.js  (семантическая память)
   * - perception_tree.js  (дерево восприятия; dirty только в addNode, не в updateActiveBranchView)
   * - synonyms_reflexes.js (условные рефлексы)
   * - episodic_memory.js  (эпизодическая память)
   * - actions_image.js    (образы действия)
   * - automatizms.js      (автоматизмы)
   * - goals.js            (образы цели)
   * - situatioms_tree.js  (образы тем и ситуаций)
   * - mental_automatism.js (абстракции и ментальные автоматизмы — заготовка)
   * - abstract_images.js  (абстрактные образы восприятия/действия — заготовка)
   * - gestalt.js            (гештальты и буфер резонанса)
   */
  var indexedDBSavers = [];
  global.registerIndexedDBSaver = function (fn) {
    if (typeof fn === 'function') indexedDBSavers.push(fn);
  };
  global.runIndexedDBSavesForPulse = function () {
    for (var i = 0; i < indexedDBSavers.length; i++) {
      try {
        indexedDBSavers[i]();
      } catch (e) {
        if (typeof global.console !== 'undefined' && global.console.error) {
          global.console.error('IndexedDB saver error:', e);
        }
      }
    }
  };

  // Экспортируем модуль в глобальное пространство имён.
  global.IndexedDBModule = {
    openDatabase: openDatabase,
    openDB: openDB,
    put: put,
    get: get,
    getAll: getAll,
    remove: remove,
    clearStore: clearStore
  };
})(window);

