const _this = {};

export const init_module = async (e) => {
  _this.func = e.func;
  _this.glb = e.glb;
  _this.SESSION_OBJ = e.SESSION_OBJ;
  _this.APP_OBJ = e.APP_OBJ;
  _this.IS_DOCKER = e.IS_DOCKER;
  _this.IS_API_SERVER = e.IS_API_SERVER;
  _this.IS_PROCESS_SERVER = e.IS_PROCESS_SERVER;
  if (!_this.xuda_dbs_plugin && typeof _this.IS_DOCKER === 'undefined' && typeof _this.IS_PROCESS_SERVER === 'undefined' && typeof _this.IS_API_SERVER === 'undefined') {
    await init_xuda_dbs_plugin(e.SESSION_ID);
  }
  _this.db_replication_store;
  _this.db_changes_store;
  _this.db_replication_from_stat = 3;
  _this.db_replication_to_stat = 3;
};

export let _db = {};

_db.save_data = async function (SESSION_ID, dsSessionP, keyP) {
  // const app_id = _this.SESSION_OBJ[SESSION_ID].app_id;

  var db_driver = await this.get_db_driver(SESSION_ID, dsSessionP);
  if (db_driver === 'pouchdb' && _this.glb.IS_WORKER) {
    var callback_id = _this.func.utils.set_callback_queue(SESSION_ID, callbackP);
    var obj = _this.func.utils.clean_returned_datasource(SESSION_ID, dsSessionP);
    return _this.func.utils.post_back_to_client(SESSION_ID, 'execute_local_sava_data', _this.SESSION_OBJ[SESSION_ID].worker_id, {
      ds_obj: obj,
      dsSessionP: dsSessionP,
      keyP: keyP,
      callback_id: callback_id,
    });
  }

  _db.save_indicator(SESSION_ID, true);

  var _ds = _this.SESSION_OBJ[SESSION_ID].DS_GLB[dsSessionP];

  let _view_obj = await _this.func.utils.VIEWS_OBJ.get(SESSION_ID, _ds.prog_id);

  var dataSourceTableId = _view_obj?.progDataSource?.dataSourceTableId;

  if (!dataSourceTableId) {
    _db.save_indicator(SESSION_ID, false);
    _this.func.utils.alerts.invoke(SESSION_ID, 'system_msg', 'SYS_MSG_0105'); //Table entry missing
    return;
  }

  var db_driver = await _db.get_db_driver(SESSION_ID, dsSessionP);

  let table_ret = await _this.func.utils.FILES_OBJ.get(SESSION_ID, dataSourceTableId);

  if (!table_ret.tableIndexes || _.isEmpty(table_ret.tableIndexes)) {
    _db.save_indicator(SESSION_ID, false);
    _this.func.utils.alerts.invoke(SESSION_ID, 'system_msg', 'SYS_MSG_0104'); //index missing
    return;
  }

  if (!_ds.data_feed.rows) {
    return console.error('data_feed rows is empty');
  }

  const clean_virtual_fields = async function (table_id, data) {
    let table_obj = await func.utils.VIEWS_OBJ.get(SESSION_ID, table_id);
    var data_obj = {};
    for (const [key, val] of Object.entries(data)) {
      const _tableFieldsObj = func.common.find_item_by_key(table_obj.tableFields, 'field_id', key);
      if (_tableFieldsObj) {
        data_obj[key] = val;
      }
    }
    return data_obj;
  };
  let save_ret;
  // component
  if (_view_obj.properties.menuType === 'component') {
    if (_view_obj?.properties?.rwMode !== 'U') {
      return _this.func.utils.alerts.invoke(SESSION_ID, 'system_msg', 'SYS_MSG_0126'); //Program Read Only
    }

    // check if rows exist
    // ==============
    // add
    //===============
    let data = { prog_id: _ds.prog_id };
    if (_ds.data_feed.rows_added?.length) {
      if (!_view_obj?.properties?.allowCreate) {
        _this.func.utils.alerts.invoke(SESSION_ID, 'system_msg', 'SYS_MSG_0120'); //Create Mode Denied
      }

      _.forEach(_ds.data_feed.rows_added, function (val, key) {
        data[key] = val;
      });

      delete _ds.data_feed.rows_added['newRecord'];
      save_ret = await _this.func.common.db(SESSION_ID, 'dbs_create', data, {}, dsSessionP);
      refresh_document_changes_for_realtime_update(SESSION_ID, {
        row_id: save_ret.data.id,
        table_id: dataSourceTableId,
      });
      return save_ret;
    }
    // ==============
    // delete
    //===============
    if (_ds.data_feed.rows_deleted?.length) {
      data.table_id = _ds._dataSourceTableId;
      data.ids = [];
      for await (const [key, val] of Object.entries(_ds.data_feed.rows_deleted)) {
        data.ids.push(val);

        try {
          const row_idx = func.common.find_ROWID_idx(_ds, val);

          delete _ds.data_feed.rows[row_idx];
        } catch (error) {}
      }
      _ds.data_feed.rows_deleted = [];
      save_ret = await _this.func.common.db(SESSION_ID, 'dbs_delete', data, {}, dsSessionP);
      refresh_document_changes_for_realtime_update(SESSION_ID, {
        row_id: save_ret.data?.id?.[0],
        table_id: dataSourceTableId,
      });
      return save_ret;
    }
    // ==============
    // update
    //===============
    if (_ds.data_feed.rows_changed?.length) {
      for await (const [key, row_id] of Object.entries(_ds.data_feed.rows_changed)) {
        if (row_id === 'datasource_main') continue;

        let data = { prog_id: _ds.prog_id };

        try {
          const row_idx = func.common.find_ROWID_idx(_ds, row_id);

          data.table_data = await clean_virtual_fields(_ds._dataSourceTableId, _ds.data_feed.rows[row_idx]);
        } catch (err) {
          console.error(err);
        }

        data.table_id = _ds._dataSourceTableId;
        data.row_id = row_id;
        save_ret = await _this.func.common.db(SESSION_ID, 'dbs_update', data, {}, dsSessionP);
        refresh_document_changes_for_realtime_update(SESSION_ID, {
          row_id: save_ret.data?.id?.[0],
          table_id: dataSourceTableId,
        });
      }
      return save_ret;
    }
    return;
  }
  // alert Program Read Only
  if (_view_obj.properties.menuType !== 'set_data') {
    return _this.func.utils.alerts.invoke(SESSION_ID, 'system_msg', 'SYS_MSG_0126'); //Program Read Only
  }

  // set_data
  var mode = _ds.set_mode; //screenInfo.crudMode;

  if (!mode || mode === 'U') {
    let data = { prog_id: _ds.prog_id };
    data.table_id = _ds._dataSourceTableId;
    data.row_id = _ds.currentRecordId;
    data.table_data = {};

    try {
      const row_idx = func.common.find_ROWID_idx(_ds, _ds.currentRecordId);

      data.table_data = await clean_virtual_fields(_ds._dataSourceTableId, _ds.data_feed.rows[row_idx]);
    } catch (err) {
      console.error(err);
    }

    save_ret = await _this.func.common.db(SESSION_ID, 'dbs_update', data, {}, dsSessionP);
    refresh_document_changes_for_realtime_update(SESSION_ID, {
      row_id: save_ret.data?.id?.[0],
      table_id: dataSourceTableId,
    });
  }
  if (mode === 'D') {
    // _id = _ds.key._id;
    let data = { prog_id: _ds.prog_id };
    data.table_id = _ds._dataSourceTableId;
    data.ids = [_ds.currentRecordId];
    save_ret = await _this.func.common.db(SESSION_ID, 'dbs_delete', data, {}, dsSessionP);
    refresh_document_changes_for_realtime_update(SESSION_ID, {
      row_id: save_ret.data?.id?.[0],
      table_id: dataSourceTableId,
    });
    return save_ret;
  }
  if (mode === 'C') {
    if (_view_obj.properties.crudMode !== 'C' && _view_obj.properties.crudMode === 'U' && !_view_obj.properties.allowCreate) {
      return _this.func.utils.alerts.invoke(SESSION_ID, 'system_msg', 'SYS_MSG_0120'); //Create Mode Denied
    }

    let data = { prog_id: _ds.prog_id };
    data.table_id = _ds._dataSourceTableId;

    try {
      const row_idx = func.common.find_ROWID_idx(_ds, _ds.currentRecordId);

      data.table_data = _ds.data_feed.rows[row_idx];
      save_ret = await _this.func.common.db(SESSION_ID, 'dbs_create', data, {}, dsSessionP);
      refresh_document_changes_for_realtime_update(SESSION_ID, {
        row_id: save_ret.data.id,
        table_id: dataSourceTableId,
      });
      return save_ret;
    } catch (err) {
      console.error(err);
    }
  }
};

_db.get_db_driver = async function (SESSION_ID, dsSessionP) {
  var db_driver;

  if (dsSessionP) {
    var _ds = _this.SESSION_OBJ[SESSION_ID].DS_GLB[dsSessionP];
    let _view_obj = await _this.func.utils.VIEWS_OBJ.get(SESSION_ID, _ds.prog_id);
    var dataSourceTableId = _view_obj?.progDataSource?.dataSourceTableId;

    if (!dataSourceTableId) {
      _this.func.utils.alerts.invoke(SESSION_ID, 'system_msg', 'SYS_MSG_0105'); //Table repository missing
      return;
    }
    let file_ret = await _this.func.utils.FILES_OBJ.get(SESSION_ID, dataSourceTableId);
    db_driver = file_ret.properties.db_driver || 'xuda';
  }

  if (_this.SESSION_OBJ[SESSION_ID].engine_mode === 'live_preview' && !db_driver && _this.SESSION_OBJ[SESSION_ID].app_id === 'temp') {
    db_driver = 'pouchdb';
  }

  return db_driver;
};
_db.get_query = async function (
  SESSION_ID,
  table_id,
  queryP,
  dsSessionP,
  viewSourceDescP,
  sourceP,
  reduceP,
  skipP,
  limitP,
  countP,
  idsP,
  sortModel,
  sort_dir,
  filterModel = {
    filterModelNative,
    filterModelMongo,
    filterModelSql,
    filterModelUserMongo,
    filterModelUserSql,
  },
  dataSourceFilterModelType = 'index',
  total_fields_info,
) {
  const get_fields = async function (_ds) {
    var fields = [];
    const _view_obj = await _this.func.utils.VIEWS_OBJ.get(SESSION_ID, _ds.prog_id);
    if (_view_obj.progFields) {
      for (let val of _view_obj.progFields) {
        if (val.data.type === 'table') {
          fields.push(val.data.field_id);
        }
      }
    }
    return fields;
  };
  const run_local = async function () {
    if (!_this.SESSION_OBJ[SESSION_ID]) return;

    if (!_this.SESSION_OBJ[SESSION_ID].DS_GLB[dsSessionP]) return;
    var ds = _this.SESSION_OBJ[SESSION_ID].DS_GLB[dsSessionP];
    var v = ds.v;

    var query_string;
    if (queryP.indexId) {
      if (countP && queryP.desc) {
        query_string = `startkey=${JSON.stringify(queryP.endkey)}&endkey=${JSON.stringify(queryP.startkey)}`;
      } else {
        query_string = `startkey=${JSON.stringify(queryP.startkey)}&endkey=${JSON.stringify(queryP.endkey)}&desc=${queryP.desc}`;
      }
    } else {
      query_string = 'key=' + queryP.table_id;
    }
    var db_driver = await _db.get_db_driver(SESSION_ID, dsSessionP);
    var fields_arr = await get_fields(ds);

    const response = {
      success: async function (json) {
        if (dsSessionP && !_this.SESSION_OBJ[SESSION_ID]?.DS_GLB?.[dsSessionP]) {
          return;
        }

        if (json.code < 0) {
          response.error(json.data);
          return;
        }
        var data = json.data;
        // if (!countP)
        //   //!reduceP &&
        //   data = {
        //     rows: data.rows.map((item) => ({
        //       ...item.value,
        //       _ROWID: item.id,
        //     })),
        //   };

        // const get_locate = function (data) {
        //   var ret = false;
        //   _this.$_each2(data, function (key, val) {
        //     if (
        //       val >= v.locateQuery.startkey[2][key] &&
        //       val <= v.locateQuery.endkey[2][key]
        //     ) {
        //       ret = true;
        //     } else {
        //       ret = false;
        //       return;
        //     }
        //   });
        //   return ret;
        // };

        if (dsSessionP)
          _this.func.utils.debug.log(SESSION_ID, ds._dataSourceTableId, {
            module: ds.viewModule,
            action: 'read',
            prop: sourceP,
            details: query_string,
            result: data.rows ? data.rows.length : 0,
            error: null,
            source: viewSourceDescP,
            json: data,
            fields: _.assignIn(ds.locate_from, ds.locate_to),
            type: 'file',
            dsSession: dsSessionP,
          });

        return data;
      },
      error: async function (err) {
        if (dsSessionP && !ds) return;
        if (dsSessionP) {
          var err_msg = err && err.data ? err.data : err;
          _this.func.utils.debug.log(SESSION_ID, _this.SESSION_OBJ[SESSION_ID].DS_GLB[dsSessionP]._dataSourceTableId, {
            module: ds.viewModule,
            action: 'read',
            prop: sourceP,
            details: query_string,
            result: null,
            error: err_msg,
            source: viewSourceDescP,
            json: null,
            fields: _.assignIn(ds.locate_from, ds.locate_to),
            type: 'file',
            dsSession: dsSessionP,
          });
        }
        _this.func.utils.debug_report(SESSION_ID, 'datasource couchDB query ' + sourceP, err_msg, 'E');

        return {
          rows: [],
          error: true,
        };
      },
    };

    var data = {
      desc: queryP.desc,
      indexId: queryP.indexId,
      key: queryP.table_id,
      reduce: reduceP,
      viewSourceDesc: viewSourceDescP,
      limit: limitP,
      skip: skipP,
      count: countP,
      ids: idsP,
      table_id: queryP.table_id,
      prog_id: ds.prog_id,
    };

    data.filter_from = JSON.stringify(queryP.filter_from);
    data.filter_to = JSON.stringify(queryP.filter_to);
    data.sortOrder = queryP.sortOrder;
    data.table_id = queryP.table_id;

    data.startkey = JSON.stringify(queryP.startkey);
    data.endkey = JSON.stringify(queryP.endkey);

    if (countP && queryP.desc) {
      data.sortOrder = 'asc';
      data.startkey = JSON.stringify(queryP.endkey);
      data.endkey = JSON.stringify(queryP.startkey);
    }

    data.dataSourceFilterModelType = dataSourceFilterModelType;
    // if (dataSourceFilterModelType === "query") {
    if (sortModel) {
      data.sortModel = JSON.stringify(sortModel);
      data.sort_dir = sort_dir;
    }

    if (!_.isEmpty(filterModel?.filterModelNative)) {
      data.filterModelNative = JSON.stringify(filterModel.filterModelNative);
    }
    if (!_.isEmpty(filterModel?.filterModelMongo)) {
      data.filterModelMongo = JSON.stringify(filterModel.filterModelMongo);
    }

    if (filterModel?.filterModelSql) {
      data.filterModelSql = JSON.stringify(filterModel.filterModelSql);
    }
    if (!_.isEmpty(filterModel?.filterModelUserMongo)) {
      data.filterModelUserMongo = JSON.stringify(filterModel.filterModelUserMongo);
    }
    if (filterModel?.filterModelUserSql) {
      data.filterModelUserSql = JSON.stringify(filterModel.filterModelUserSql);
    }
    // }

    if (total_fields_info) {
      data.total_fields_info = JSON.stringify(total_fields_info);
    }

    if (!reduceP && !countP) {
      data.fields = fields_arr; //[];
    }

    try {
      const json = await _this.func.common.db(SESSION_ID, 'dbs_read', data, { node: true }, dsSessionP);
      return await response.success(json);
    } catch (e) {
      return await response.error(e?.message || e?.data || e);
    }
  };

  var db_driver = await this.get_db_driver(SESSION_ID, dsSessionP);
  if (db_driver === 'pouchdb' && _this.glb.IS_WORKER && !_this.SESSION_OBJ[SESSION_ID].engine_mode === 'live_preview') {
    var callback_id = _this.func.utils.set_callback_queue(SESSION_ID, callbackP);
    var obj = _this.func.utils.clean_returned_datasource(SESSION_ID, dsSessionP);
    return _this.func.utils.post_back_to_client(SESSION_ID, 'execute_local_db_query', _this.SESSION_OBJ[SESSION_ID].worker_id, {
      ds_obj: obj,
      dsSessionP,
      table_id,
      queryP,
      dsSessionP,
      viewSourceDescP,
      sourceP,
      reduceP,
      skipP,
      limitP,
      countP,
      idsP,
      callback_id,
    });
  }
  return await run_local();
};
_db.utils = {};

_db.save_indicator = function (SESSION_ID, stateP) {
  if (!_this.glb.IS_WORKER) {
    _this.func.UI.utils.save(SESSION_ID, stateP);
  }
};

// _db.save_image = function (SESSION_ID, filename, base64) {
//   var response = {
//     success: function (jsonP, ajaxP) {
//       // console.log(jsonP);
//       _this.func.utils.post_back_to_client(
//         SESSION_ID,
//         "change_loaded_image",
//         _this.SESSION_OBJ[SESSION_ID].worker_id,
//         jsonP
//       );
//     },
//     error: function (status) {
//       console.log("error save_image:" + status);
//     },
//     fail: function (status) {
//       console.log("error save_image:" + status);
//     },
//   };

//   // const app_id = _this.SESSION_OBJ[SESSION_ID].app_id;

//   let app_id = _this.SESSION_OBJ[SESSION_ID].app_id;
//   let app_id_reference = APP_OBJ[app_id].app_id_reference;

//   _this.$_ajax({
//     url: _this.func.common.get_url(SESSION_ID, "rpi", "runtime_save_image"),
//     dataType: "json",
//     type: "POST",
//     data: {
//       base64: base64,
//       filename: filename,
//       app_id_reference: app_id_reference,
//       app_id: app_id,
//     },

//     success: function (json) {
//       response.success(json);
//     },
//     fail: function (jqXHR, textStatus) {
//       response.error(jqXHR, textStatus, errorThrown);
//     },
//     error: function (jqXHR, textStatus, errorThrown) {
//       response.error(jqXHR, textStatus, errorThrown);
//     },
//     timeout: 60000, //60 seconds timeout
//   });
// };

const init_xuda_dbs_plugin = async function (SESSION_ID) {
  var _session = _this.SESSION_OBJ[SESSION_ID];

  const plugin_name = '@xuda.io/xuda-dbs-plugin-xuda';

  const get_path = function (plugin_name, resource) {
    var path = `https://${_session.domain}/plugins/${plugin_name}${resource ? '/' + resource : ''}?app_id=${_session.app_id}`;

    return path;
  };

  try {
    _this.xuda_dbs_plugin = await import(get_path(plugin_name, 'studio.mjs'));

    const db = await func.utils.connect_pouchdb(SESSION_ID);
    await _db.call_pouch_xuda_dbs_plugin(SESSION_ID, 'create_design', { db });
  } catch (err) {
    return console.error(err.message);
  }
};

_db.call_pouch_xuda_dbs_plugin = async function (SESSION_ID, method, params) {
  return new Promise(async (resolve, reject) => {
    try {
      // var table_obj = {};
      if (params.e) {
        params.table_obj = await func.utils.DOCS_OBJ.get(SESSION_ID, params.e.table_id);
      }

      await _this.xuda_dbs_plugin[method](params, resolve, reject);
    } catch (err) {
      reject();
    }
  });
};
_db.pouch = {};
_db.pouch.dbs_read = async function (SESSION_ID, e) {
  const db = await func.utils.connect_pouchdb(SESSION_ID);
  return await _db.call_pouch_xuda_dbs_plugin(SESSION_ID, 'read', { db, e });
};
_db.pouch.dbs_create = async function (SESSION_ID, e) {
  const db = await func.utils.connect_pouchdb(SESSION_ID);
  return await _db.call_pouch_xuda_dbs_plugin(SESSION_ID, 'create', { db, e });
};
_db.pouch.dbs_delete = async function (SESSION_ID, e) {
  const db = await func.utils.connect_pouchdb(SESSION_ID);
  return await _db.call_pouch_xuda_dbs_plugin(SESSION_ID, 'del', { db, e });
};
_db.pouch.dbs_update = async function (SESSION_ID, e) {
  const db = await func.utils.connect_pouchdb(SESSION_ID);
  return await _db.call_pouch_xuda_dbs_plugin(SESSION_ID, 'update', { db, e });
};

_db.pouch.init_db_replication = async function (SESSION_ID) {
  const db = await func.utils.connect_pouchdb(SESSION_ID);
  var _session = _this.SESSION_OBJ[SESSION_ID];

  if (_session.url_params.avoid_local || _session.engine_mode === 'miniapp' || (_session.engine_mode !== 'live_preview' && !_session.opt.enable_offline)) {
    return;
  }

  // handle realtime refresh programs
  const changes = db
    .changes({
      // since: "now",
      live: true,
      include_docs: true,
    })
    .on('change', function (change) {
      // handle change

      if (!change.doc.udfData) return;
      // console.log("handle change", change);
      func.UI.screen.refresh_document_changes_for_realtime_update(SESSION_ID, {
        row_id: change.id,
        table_id: change.doc.udfData.udffileid,
      });
    })
    .on('complete', function (info) {
      // changes() was canceled
      console.log('complete', info);
    })
    .on('error', function (err) {
      console.error(err);
    });

  set_replication_stat(SESSION_ID, null, 3);

  // replicate to server
  const db_name = APP_OBJ[_session.app_id].app_db_name;
  const rep = db.replicate
    .to(`https://${_session.domain}/db/${db_name}`, {
      selector: { docType: 'database' },
      live: true,
      retry: true,
      fetch: function (url, opts) {
        if (_session.gtp_token) {
          opts.headers.set('xu-gtp-token', _session.gtp_token);
        }
        if (_session.app_token) opts.headers.set('xu-app-token', _session.app_token);
        {
          opts.headers.set('xu-db', db_name);
        }
        return PouchDB.fetch(url, opts);
      },
    })
    .on('change', function (info) {
      // handle change
    })
    .on('paused', function (err) {
      if (err) {
        return set_replication_stat(SESSION_ID, null, 2);
      }
      set_replication_stat(SESSION_ID, null, 3);
      // replication paused (e.g. replication up to date, user went offline)
    })
    .on('active', function () {
      // replicate resumed (e.g. new changes replicating, user went back online)
    })
    .on('denied', function (err) {
      set_replication_stat(SESSION_ID, null, 2);
      // a document failed to replicate (e.g. due to permissions)
    })
    .on('complete', function (info) {
      // handle complete
    })
    .on('error', function (err) {
      set_replication_stat(SESSION_ID, null, 2);
      IS_ONLINE = false;
      // handle error
    });

  _db.pouch.set_db_replication_from_server(SESSION_ID);
  $('body').on('set_db_replication_from_server', () => {
    _db.pouch.set_db_replication_from_server(SESSION_ID);
  });
};
_db.pouch.set_db_replication_from_server = async function (SESSION_ID) {
  if (!IS_ONLINE) return;

  const db = await func.utils.connect_pouchdb(SESSION_ID);
  const rep_docs = await _db.pouch.get_replications(SESSION_ID);
  const _session = _this.SESSION_OBJ[SESSION_ID];
  const db_name = APP_OBJ[_session.app_id].app_db_name;
  const remote_db_url = `https://${_session.domain}/db/${db_name}`;
  const remote_db = new PouchDB(remote_db_url, {
    fetch: function (url, opts) {
      if (_session.gtp_token) {
        opts.headers.set('xu-gtp-token', _session.gtp_token);
      }
      if (_session.app_token) opts.headers.set('xu-app-token', _session.app_token);
      {
        opts.headers.set('xu-db', db_name);
      }
      return PouchDB.fetch(url, opts);
    },
  });

  if (_this.db_changes_store) {
    _this.db_changes_store.cancel();
  }

  const write_stat = async function (rep_id, stat, reason) {
    try {
      let doc = await db.get(rep_id);
      // console.log("> replication ", stat, reason, doc);
      doc.stat = stat;
      doc.stat_reason = reason;
      try {
        db.put(doc);
      } catch (err) {}
    } catch (err) {}
  };
  const download_documents = async function (rep_doc) {
    await write_stat(rep_doc._id, 2);

    const remote_db = new PouchDB(remote_db_url, {
      fetch: function (url, opts) {
        opts.headers.set('xu-prog_id', rep_doc.prog_id);

        if (_session.gtp_token) {
          opts.headers.set('xu-gtp-token', _session.gtp_token);
        }
        if (_session.app_token) opts.headers.set('xu-app-token', _session.app_token);
        {
          opts.headers.set('xu-db', db_name);
        }
        return PouchDB.fetch(url, opts);
      },
    });

    const ret = await remote_db.find({
      selector: rep_doc.selector,
      fields: ['_id'],
      limit: 9999999999,
      // prog_id: rep_doc.prog_id,
    });

    if (ret?.code < 0) {
      return func.utils.debug_report(SESSION_ID, 'download_documents', ret.data, 'E');
    }

    try {
      var doc_ids = [];
      for await (let doc of ret.docs) {
        doc_ids.push(doc._id);
      }
      db.replicate
        .from(remote_db_url, {
          doc_ids,
          retry: true,
          style: 'main_only',
          fetch: function (url, opts) {
            if (_session.gtp_token) {
              opts.headers.set('xu-gtp-token', _session.gtp_token);
            }
            if (_session.app_token) opts.headers.set('xu-app-token', _session.app_token);
            {
              opts.headers.set('xu-db', db_name);
            }
            return PouchDB.fetch(url, opts);
          },
        })
        .on('paused', async function (err) {
          // replication paused (e.g. replication up to date, user went offline)

          if (err) {
            // set_replication_stat(SESSION_ID, 2);
            return;
          }
          const read_ret = await _db.pouch.dbs_read(SESSION_ID, rep_doc.e); // trigger reindexing
          await write_stat(rep_doc._id, 3);
        });
    } catch (err) {
      await write_stat(rep_doc._id, 2, err.message);
      console.error('replication from error', err);
    }
    // console.log(ret);
  };

  let filter_tables = [];
  for (let doc of rep_docs) {
    if (!filter_tables.includes(doc.table_id)) filter_tables.push(doc.table_id);
    if (doc.stat < 3) await download_documents(doc);
  }

  try {
    _this.db_changes_store = remote_db
      .changes({
        since: 'now',
        live: true,
        timeout: 300000, // 5 min
        style: 'main_only',
        include_docs: false,
        // heartbeat: 10000
      })
      .on('change', async function (change) {
        if (change.id.substr(0, 4) !== 'dbs-') return;

        if (_this.db_replication_from_stat !== 3) {
          set_replication_stat(SESSION_ID, 3);
        }

        try {
          const existing_doc = await db.get(change.id);

          db.replicate.from(remote_db_url, {
            doc_ids: [change.id],
            retry: true,
            style: 'main_only',
            fetch: function (url, opts) {
              if (_session.gtp_token) {
                opts.headers.set('xu-gtp-token', _session.gtp_token);
              }
              if (_session.app_token) opts.headers.set('xu-app-token', _session.app_token);
              {
                opts.headers.set('xu-db', db_name);
              }
              return PouchDB.fetch(url, opts);
            },
          });
        } catch (err) {
          // new doc change
          try {
            const table_id = change.id.split('_')[0].substr(4);

            for (let doc of rep_docs) {
              if (doc.table_id === table_id) {
                download_documents(doc);
              }
            }
          } catch (err) {
            console.warn(err);
          }
        }
      })
      .on('complete', function (info) {
        // changes() was canceled
      })
      .on('error', function (err) {
        console.error(err);
        set_replication_stat(SESSION_ID, 2);
        _this.db_changes_store.cancel();
      });
  } catch (err) {
    console.error(err);
  }

  return;
};
_db.pouch.remove_db_replication_from_server = async function (SESSION_ID, rep_id) {
  const db = await func.utils.connect_pouchdb(SESSION_ID);

  try {
    const doc = await db.get(rep_id);

    // _this.db_replication_store[rep_id].cancel();
    db.remove(doc);
    // delete _this.db_replication_store[rep_id];
  } catch (error) {}
};

_db.pouch.get_replications = async function (SESSION_ID = Object.keys(SESSION_OBJ)[0]) {
  const db = await func.utils.connect_pouchdb(SESSION_ID);
  // source: "runtime"
  let ret = await db.find({
    selector: { docType: 'rep_request' },
  });

  return ret.docs;
};

_db.pouch.get_replication_stat = async function (SESSION_ID) {
  const _session = _this.SESSION_OBJ[SESSION_ID];
  return _session?.DS_GLB?.[0]?.data_system?.SYS_GLOBAL_BOL_REPLICATION_STAT;
};

const set_replication_stat = async function (SESSION_ID, from_stat, to_stat) {
  var _session = _this.SESSION_OBJ[SESSION_ID];
  if (!_session?.DS_GLB?.[0]) {
    return setTimeout(function () {
      set_replication_stat(SESSION_ID, from_stat, to_stat);
    }, 1000);
  }

  if (from_stat) _this.db_replication_from_stat = from_stat;
  if (to_stat) _this.db_replication_to_stat = to_stat;

  var data = {};
  if (_this.db_replication_from_stat === 3 && _this.db_replication_to_stat === 3) {
    data.SYS_GLOBAL_BOL_REPLICATION_STAT = 1;
    console.info('Replication On');
  } else {
    data.SYS_GLOBAL_BOL_REPLICATION_STAT = 0;
    console.info('Replication Off');
  }

  var datasource_changes = {
    [0]: {
      ['data_system']: data,
    },
  };
  await func.datasource.update(SESSION_ID, datasource_changes);
};

const refresh_document_changes_for_realtime_update = function (SESSION_ID, doc_change) {
  var _session = _this.SESSION_OBJ[SESSION_ID];

  if (glb.IS_WORKER) {
    return func.utils.post_back_to_client(SESSION_ID, 'refresh_document_changes_for_realtime_update', _session.worker_id, { doc_change });
  }

  func.UI.screen.refresh_document_changes_for_realtime_update(SESSION_ID, doc_change);
};
