/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2026, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////

import MainMenuFactory from './MainMenuFactory';
import _ from 'lodash';
import { checkMasterPassword, showQuickSearch } from '../../../static/js/Dialogs/index';
import { send_heartbeat, stop_heartbeat } from './heartbeat';
import getApiInstance from '../../../static/js/api_instance';
import usePreferences, { setupPreferenceBroadcast } from '../../../preferences/static/js/store';
import checkNodeVisibility from '../../../static/js/check_node_visibility';
import {appAutoUpdateNotifier} from '../../../static/js/helpers/appAutoUpdateNotifier';

define('pgadmin.browser', [
  'sources/gettext', 'sources/url_for', 'sources/pgadmin',
  'sources/csrf', 'pgadmin.authenticate.kerberos',
  'pgadmin.browser.utils', 'pgadmin.browser.messages',
  'pgadmin.browser.node', 'pgadmin.browser.collection', 'pgadmin.browser.activity',
  'pgadmin.browser.keyboard', 'sources/tree/pgadmin_tree_save_state',
], function(
  gettext, url_for, pgAdmin, csrfToken, Kerberos,
) {
  let pgBrowser = pgAdmin.Browser = pgAdmin.Browser || {};
  let select_object_msg = gettext('Please select an object in the tree view.');

  csrfToken.setPGCSRFToken(pgAdmin.csrf_token_header, pgAdmin.csrf_token);

  Kerberos.validate_kerberos_ticket();

  // Extend the browser class attributes
  _.extend(pgAdmin.Browser, {
    // The base url for browser
    URL: url_for('browser.index'),
    docker:null,
    // Reversed Engineer query for the selected database node object goes
    // here
    editor:null,
    // Left hand browser tree
    tree:null,
    // list of script to be loaded, when a certain type of node is loaded
    // It will be used to register extensions, tools, child node scripts,
    // etc.
    scripts: {},
    // Standard Widths and Height for dialogs in px
    stdW: {
      sm: 500,
      md: 700,
      lg: 900,
      default: 500,
      // If you change above values then make sure to update
      // calc method logic
      calc: (passed_width) => {
        let iw = window.innerWidth;
        if(iw > passed_width)
          return passed_width;
        else if (iw > pgAdmin.Browser.stdW.lg)
          return pgAdmin.Browser.stdW.lg;
        else if (iw > pgAdmin.Browser.stdW.md)
          return pgAdmin.Browser.stdW.md;
        else if (iw > pgAdmin.Browser.stdW.sm)
          return pgAdmin.Browser.stdW.sm;
        else
          // if avilable screen resolution is still
          // less then return the width value as it
          return iw;
      },
    },
    stdH: {
      sm: 200,
      md: 400,
      lg: 550,
      default: 550,
      // If you change above values then make sure to update
      // calc method logic
      calc: (passed_height) => {
        // We are excluding sm as it is too small for dialog
        let ih = window.innerHeight;
        if (ih > passed_height)
          return passed_height;
        else if (ih > pgAdmin.Browser.stdH.lg)
          return pgAdmin.Browser.stdH.lg;
        else if (ih > pgAdmin.Browser.stdH.md)
          return pgAdmin.Browser.stdH.md;
        else
          // if avilable screen resolution is still
          // less then return the height value as it
          return ih;
      },
    },
    // Default panels
    panels: {},
    // We also support showing dashboards, HTML file, external URL
    frames: {},
    /* Menus */
    // add_menus(...) will register all the menus
    // in this container
    all_menus_cache: {
      // All context menu goes here under certain menu types.
      // i.e. context: {'server': [...], 'server-group': [...]}
      context: {},
      // File menus
      file: {},
      // Edit menus
      edit: {},
      // Object menus
      object: {},
      // Management menus
      management: {},
      // Tools menus
      tools: {},
      // Help menus
      help: {},
    },
    MainMenus: [],
    BrowserContextMenu: [],

    menu_categories: {
      /* name, label (pair) */
      'register': {
        name: 'register',
        label: gettext('Register'),
        priority: 1,
        /* separator above this menu */
        above: false,
        below: true,
        /* icon: 'fa fa-magic', */
        single: true,
      },
      'create': {
        name: 'create',
        label: gettext('Create'),
        priority: 2,
        /* separator above this menu */
        above: false,
        below: true,
        /* icon: 'fa fa-magic', */
        single: true,
      },
    },
    // A callback to load/fetch a script when a certain node is loaded
    register_script: function(n, m, p) {
      let scripts = this.scripts;
      scripts[n] = _.isArray(scripts[n]) ? scripts[n] : [];
      scripts[n].push({'name': m, 'path': p, loaded: false});
    },
    masterpass_callback_queue: [],
    // Enable/disable menu options
    enable_disable_menus: function(item) {
      MainMenuFactory.enableDisableMenus(item);
    },
    init: function() {
      let obj=this;
      if (obj.initialized) {
        return;
      }
      obj.initialized = true;

      // Cache preferences
      usePreferences.getState().cache();
      setupPreferenceBroadcast();

      setTimeout(function() {
        obj?.editor?.setValue('-- ' + select_object_msg);
        obj?.editor?.refresh();
      }, 10);

      // Register scripts and add menus
      pgBrowser.utils.registerScripts(this);

      // Ping the server every 5 minutes
      setInterval(function() {
        getApiInstance().post(
          url_for('misc.cleanup')
        ).then(()=> {
          /*This is intentional (SonarQube)*/
        }).catch(function() {
          /*This is intentional (SonarQube)*/
        });
      }, 300000);

      obj.Events.on(
        'pgadmin:server:connected', send_heartbeat.bind(obj)
      );

      obj.Events.on(
        'pgadmin:server:disconnect', stop_heartbeat.bind(obj)
      );

      obj.check_corrupted_db_file();
      obj.Events.on('pgadmin:browser:tree:add', obj.onAddTreeNode.bind(obj));
      obj.Events.on('pgadmin:browser:tree:update', obj.onUpdateTreeNode.bind(obj));
      obj.Events.on('pgadmin:browser:tree:refresh', obj.onRefreshTreeNode.bind(obj));
      obj.Events.on('pgadmin-browser:tree:loadfail', obj.onLoadFailNode.bind(obj));
      obj.bind_beforeunload();

      /* User UI activity */
      obj.log_activity(); /* The starting point */
      obj.register_to_activity_listener(document);
      obj.start_inactivity_timeout_daemon();
    },
    uiloaded: function() {
      this.set_master_password('');
      this.check_version_update();
      // Assign and Update shortcuts from preferences.
      MainMenuFactory.subscribeShortcutChanges();
    },
    check_corrupted_db_file: function() {
      getApiInstance().get(
        url_for('browser.check_corrupted_db_file')
      ).then(({data: res})=> {
        if(res.data.length > 0) {

          pgAdmin.Browser.notifier.alert(
            'Warning',
            'pgAdmin detected unrecoverable corruption in it\'s SQLite configuration database. ' +
            'The database has been backed up and recreated with default settings. '+
            'It may be possible to recover data such as query history manually from '+
            'the original/corrupt file using a tool such as DB Browser for SQLite if desired.'+
            '<br><br>Original file: ' + res.data + '<br>Replacement file: ' +
            res.data.substring(0, res.data.length - 14),
          );
        }
      }).catch(function(error) {
        pgAdmin.Browser.notifier.alert(error);
      });
    },
    check_master_password: function(on_resp_callback) {
      getApiInstance().get(
        url_for('browser.check_master_password')
      ).then(({data: res})=> {
        if(on_resp_callback) {
          if(res.data) {
            on_resp_callback(true);
          } else {
            on_resp_callback(false);
          }
        }
      }).catch(function(error) {
        pgAdmin.Browser.notifier.pgRespErrorNotify(error);
      });
    },

    reset_master_password: function() {
      let self = this;
      getApiInstance().delete(
        url_for('browser.set_master_password')
      ).then(({data: res})=> {
        if(!res.data) {
          self.set_master_password('');
        }
      }).catch(function(error) {
        pgAdmin.Browser.notifier.pgRespErrorNotify(error);
      });
    },

    set_master_password: function(password='',
      set_callback=()=>{/*This is intentional (SonarQube)*/},
      cancel_callback=()=>{/*This is intentional (SonarQube)*/}) {
      let data=null, self = this;

      data = {
        'password': password,
      };

      self.masterpass_callback_queue.push(set_callback);
      // Check master passowrd.
      checkMasterPassword(data, self.masterpass_callback_queue, cancel_callback);
    },

    check_version_update: async function(trigger_update_check=false) {
      getApiInstance().get(
        url_for('misc.upgrade_check') + '?trigger_update_check=' + trigger_update_check
      ).then((res)=> {
        const data = res.data.data;
        window.electronUI?.sendDataForAppUpdate({
          'check_for_updates': data.check_for_auto_updates,
        });
        const isDesktopWithAutoUpdate = pgAdmin.server_mode == 'False' && data.check_for_auto_updates && data.auto_update_url !== '';
        const isUpdateAvailable = data.outdated && data.upgrade_version_int > data.current_version_int;
        const noUpdateMessage = 'No update available.';
        // This is for desktop installers whose auto_update_url is mentioned in https://www.pgadmin.org/versions.json
        if (isDesktopWithAutoUpdate) {
          if (isUpdateAvailable) {
            const message = `${gettext('You are currently running version %s of %s, however the current version is %s.', data.current_version, data.product_name, data.upgrade_version)}`;
            appAutoUpdateNotifier(
              message,
              'warning',
              () => {
                window.electronUI?.sendDataForAppUpdate(data);
              },
              null,
              'Update available',
              'download_update'
            );
          }
        } else if(data.outdated) {
          //This is for server mode or auto-update not supported desktop installer or not mentioned auto_update_url
          // Render as plain text so the server-supplied download URL
          // cannot inject markup into the notification body.
          pgAdmin.Browser.notifier.warningText(
            gettext('You are currently running version %s of %s, however the current version is %s.', data.current_version, data.product_name, data.upgrade_version)
            + '\n\n'
            + gettext('For more information, please visit: %s', data.download_url),
            null
          );
        }
        // If the user manually triggered a check for updates (trigger_update_check is true)
        // and no update is available (data.outdated is false), show an info notification.
        if (!data.outdated && trigger_update_check){
          appAutoUpdateNotifier(noUpdateMessage, 'info', null, 10000);
        }
      }).catch((error)=>{
        console.error('Error during version check', error);
        pgAdmin.Browser.notifier.errorText(gettext(`${error.response?.data?.errormsg || error?.message}`));
      });
    },

    bind_beforeunload: function() {
      window.addEventListener('beforeunload', function(e) {
        /* Can open you in new tab */
        const prefStore = usePreferences.getState();
        let tree_save_interval = prefStore.getPreferences('browser', 'browser_tree_state_save_interval'),
          confirm_on_refresh_close = prefStore.getPreferences('browser', 'confirm_on_refresh_close');

        if (!_.isUndefined(tree_save_interval) && tree_save_interval.value !== -1)
          pgAdmin.Browser.browserTreeState.save_state();

        if(!_.isUndefined(confirm_on_refresh_close) && confirm_on_refresh_close.value) {
          /* This message will not be displayed in Chrome, Firefox, Safari as they have disabled it*/
          let msg = gettext('Are you sure you want to close the %s browser?', pgBrowser.utils.app_name);
          if(e.originalEvent?.returnValue) {
            e.originalEvent.returnValue = msg;
          }
          return msg;
        }
      });
    },

    add_menu_category: function({name, ...options}) {
      this.menu_categories[name] = {
        name: name,
        label: '(No Label)',
        priority: 10,
        icon: '',
        above: false,
        below: false,
        parent: null,
        isCategory: true,
        ...options,
      };
    },

    // Add menus of module/extension at appropriate menu
    add_menus: function(menus) {
      const self = this;
      let allMenus = this.all_menus_cache;

      _.each(menus, function(m) {
        _.each(m.applies, function(a) {
          /* We do support menu type only from this list */
          if(['context', 'file', 'edit', 'object','management', 'tools', 'help'].indexOf(a) > -1){
            // If current node is not visible in browser tree
            // then return from here
            if(!checkNodeVisibility(m.node)) {
              return;
            } else if(_.has(m, 'module') && !_.isUndefined(m.module)) {
              // If module to which this menu applies is not visible in
              // browser tree then also we do not display menu
              if(!checkNodeVisibility(m.module.type)) {
                return;
              }
            }
            const getFullPath = (currPath, currMenu)=>{
              if(currMenu.node) {
                return currPath.concat([currMenu.node]);
              } else if((currMenu.category??'common') != 'common') {
                const currCat = self.menu_categories[currMenu.category];
                if(currCat?.category) {
                  return getFullPath(currPath.concat([currMenu.category]), currCat);
                }
                return [currMenu.category].concat(currPath);
              } else {
                return currPath;
              }
            };

            let get_menuitem_obj = function(_m) {
              let enable = _m.enable;
              if(_m.enable == '') {
                enable = true;
              } else if(_.isString(_m.enable) && _m.enable.toLowerCase() == 'false') {
                enable = false;
              }

              // This is to handel quick search callback
              if(_m.name == 'mnu_quick_search_help') {
                _m.callback = () => {
                  showQuickSearch();
                };
                _m.shortcut_preference =['browser', 'open_quick_search'];
              }

              return {
                name: _m.name,
                label: _m.label,
                module: _m.module,
                category: _m.category,
                callback: typeof _m.module == 'object' && _m.module[_m.callback] && _m.callback in _m.module[_m.callback] ? _m.module[_m.callback] : _m.callback,
                priority: _m.priority,
                data: _m.data,
                url: _m.url || '#',
                target: _m.target,
                icon: _m.icon,
                enable: enable || true,
                node: _m.node,
                checked: _m.checked,
                below: _m.below,
                applies: _m.applies,
                permission: _m.permission,
                shortcut_preference: _m.shortcut_preference,
                shortcut:_m.shortcut
              };
            };

            const menuPath = [a].concat(getFullPath([], m)).concat([m.name]);
            const _menus = _.set(allMenus, menuPath, get_menuitem_obj(m));

            if(m.menu_items) {
              let sub_menu_items = [];

              for(let mnu_val of m.menu_items) {
                sub_menu_items.push(get_menuitem_obj(mnu_val));
              }
              _menus[m.name]['menu_items'] = sub_menu_items;
            }
          } else {
            console.warn(
              'Developer warning: Category \'' +
                  a +
                  '\' is not supported!\nSupported categories are: context, file, edit, object, tools, management, help'
            );
          }
        });
      });
    },
    _findTreeChildNode: function(_i, _d, _o) {
      let loaded = _o.t.wasLoad(_i),
        onLoad = function() {
          let items = _o.t.children(_i),
            i, d, n, idx = 0, size = items.length;
          for (; idx < size; idx++) {
            i = items[idx];
            d = _o.t.itemData(i);
            if (d._type === _d._type) {
              if (!_o.hasId || d._id == _d._id) {
                _o.i = i;
                _o.d = _d;
                _o.pathOfTreeItems.push({coll: false, item: i, d: _d});

                _o.success();
                return;
              }
            } else {
              n = _o.b.Nodes[d._type];
              // Are we looking at the collection node for the given node?
              if (
                n?.collection_node && d.nodes &&
                  _.indexOf(d.nodes, _d._type) != -1
              ) {
                _o.i = i;
                _o.d = null;
                _o.pathOfTreeItems.push({coll: true, item: i, d: d});

                // Set load to false when the current collection node's inode is false
                if (!_o.t.isInode(i)) {
                  _o.load = false;
                }
                _o.b._findTreeChildNode(i, _d, _o);
                return;
              }
            }
          }
          _o.notFound && typeof(_o.notFound) == 'function' &&
            _o.notFound(_d);
        };

      if (!loaded && _o.load) {
        _o.t.open(_i).then(
          () => {
            onLoad();
          },
          () => {
            let fail = _o?.o?.fail;
            if (fail && typeof(fail) == 'function') {
              fail.apply(_o.t, []);
            }
          }
        );
      } else if (loaded) {
        onLoad();
      } else {
        _o.notFound && typeof(_o.notFound) == 'function' &&
          _o.notFound(_d);
      }
    },

    onAddTreeNode: function(_data, _hierarchy, _opts) {
      let ctx = {
          b: this, // Browser
          d: null, // current parent
          hasId: true,
          i: null, // current item
          p: _.toArray(_hierarchy || {}).sort(
            function(a, b) {
              if (a.priority === b.priority) {
                return 0;
              }
              return (a.priority < b.priority ? -1 : 1);
            }
          ), // path of the parent
          pathOfTreeItems: [], // path Item
          t: this.tree, // Tree Api
          o: _opts,
        },
        traversePath = function() {
          let _ctx = this, data;

          _ctx.success = traversePath;
          if (_ctx.p.length) {
            data = _ctx.p.shift();
            // This is the parent node.
            // Replace the parent-id of the data, which could be different
            // from the given hierarchy.
            if (!_ctx.p.length) {
              data._id = _data._pid;
              _ctx.success = addItemNode;
            }
            _ctx.b._findTreeChildNode(
              _ctx.i, data, _ctx
            );
            // if parent node is null
            if (!_data._pid) {
              addItemNode.apply(_ctx, arguments);
            }
          }
          return true;
        }.bind(ctx),
        addItemNode = function() {
          // Append the new data in the tree under the current item.
          // We may need to pass it to proper collection node.
          let _ctx = this,
            first = (_ctx.i || this.t.wasLoad(_ctx.i)) &&
            this.t.first(_ctx.i),
            findChildNode = function(success, notFound) {
              let __ctx = this;
              __ctx.success = success;
              __ctx.notFound = notFound;

              __ctx.b._findTreeChildNode(__ctx.i, _data, __ctx);
            }.bind(_ctx),
            selectNode = function() {
              this.t.openPath(this.i);
              this.t.select(this.i);
              if (typeof(_ctx?.o?.success) == 'function') {
                _ctx.o.success.apply(_ctx.t, [_ctx.i, _data]);
              }
            }.bind(_ctx),
            addNode = function() {
              let __ctx = this,
                items = __ctx.t.children(__ctx.i),
                s = 0, e = items.length - 1, i,
                linearSearch = function() {
                  while (e >= s) {
                    i = items[s];
                    let d = __ctx.t.itemData(i);
                    if (d._type === 'column') {
                      if (pgAdmin.numeric_comparator(d._id, _data._id) == 1)
                        return true;
                    } else if (pgAdmin.natural_sort(d._label, _data._label) == 1)
                      return true;
                    s++;
                  }
                  //when the current element is greater than the end element
                  if (e != items.length - 1) {
                    i = items[e+1];
                    return true;
                  }
                  i = null;
                  return false;
                },
                binarySearch = function() {
                  let d, m;
                  // Binary search only outperforms Linear search for n > 44.
                  // Reference:
                  // https://en.wikipedia.org/wiki/Binary_search_algorithm#cite_note-30
                  //
                  // We will try until it's half.
                  while (e - s > 22) {
                    i = items[s];
                    d = __ctx.t.itemData(i);
                    if (d._type === 'column') {
                      if (pgAdmin.numeric_comparator(d._id, _data._id) != -1)
                        return true;
                    } else if (pgAdmin.natural_sort(d._label, _data._label) != -1)
                      return true;
                    i = items[e];
                    d = __ctx.t.itemData(i);
                    let result;
                    if (d._type === 'column') {
                      result = pgAdmin.numeric_comparator(d._id, _data._id);
                    } else {
                      result = pgAdmin.natural_sort(d._label, _data._label);
                    }
                    if (result !=1) {
                      if (e != items.length - 1) {
                        i = items[e+1];
                        return true;
                      }
                      i = null;
                      return false;
                    }
                    m = s + Math.round((e - s) / 2);
                    i = items[m];
                    d = __ctx.t.itemData(i);
                    if(d._type === 'column'){
                      result = pgAdmin.numeric_comparator(d._id, _data._id);
                    } else {
                      result = pgAdmin.natural_sort(d._label, _data._label);
                    }
                    //result will never become 0 because of remove operation
                    //which happens with updateTreeNode
                    if (result == 0)
                      return true;

                    if (result == -1) {
                      s = m + 1;
                      e--;
                    } else {
                      s++;
                      e = m - 1;
                    }
                  }
                  return linearSearch();
                };

              if (binarySearch()) {
                __ctx.t.before(i, _data).then((_item) => {
                  if (typeof(__ctx?.o?.success) == 'function') {
                    __ctx.o.success.apply(__ctx.t, [i, _data]);
                  } else {
                    __ctx.t.select(_item);
                  }
                }, () => {
                  console.warn('Failed to add before...', arguments);
                  if (typeof(__ctx.o?.fail) == 'function') {
                    __ctx.o.fail.apply(__ctx.t, [i, _data]);
                  }
                });
              } else {
                let _append = function() {
                  let ___ctx = this,
                    _parent_data = ___ctx.t.itemData(___ctx.i);

                  ___ctx.t.append(___ctx.i, _data).then(
                    (_i) => {
                      // Open the item path only if its parent is loaded
                      // or parent type is same as nodes
                      if(
                        ___ctx.t.wasLoad(___ctx.i) &&
                          _parent_data &&  _parent_data._type.search(
                          _data._type
                        ) > -1
                      ) {
                        ___ctx.t.open(___ctx.i);
                        ___ctx.t.select(_i);
                      } else if (_parent_data) {
                        // Unload the parent node so that we'll get
                        // latest data when we try to expand it
                        ___ctx.t.unload(___ctx.i).then(
                          () => {
                            ___ctx.t.open(___ctx.i);
                          }
                        );
                      }
                      if (typeof(___ctx?.o?.success) == 'function') {
                        ___ctx.o.success.apply(___ctx.t, [_i, _data]);
                      }
                    },
                    () => {
                      console.warn('Failed to append...', arguments);
                      if (typeof(___ctx?.o?.fail) == 'function') {
                        ___ctx.o.fail.apply(___ctx.t, [___ctx.i, _data]);
                      }
                    }
                  );
                }.bind(__ctx);

                if (__ctx.i && !__ctx.t.isInode(__ctx.i)) {
                  __ctx.t.setInode(__ctx.i).then(
                    () => {
                      _append();
                    }
                  );
                } else {
                  // Handle case for node without parent i.e. server-group
                  // or if parent node's inode is true.
                  _append();
                }
              }
            }.bind(_ctx);

          // Parent node do not have any children, let me unload it.
          if (!first && _ctx.t.wasLoad(_ctx.i)) {
            _ctx.t.unload(_ctx.i).then(() => {
              findChildNode(
                selectNode,
                function() {
                  let o = this && this.o;
                  if (typeof(o?.fail) == 'function') {
                    o.fail.apply(this.t, [this.i, _data]);
                  }
                }.bind(this)
              );
            },
            () => {
              let o = this && this.o;
              if (typeof(o?.fail) == 'function') {
                o.fail.apply(this.t, [this.i, _data]);
              }
            });
            return;
          }

          // We can find the collection node using _findTreeChildNode
          // indirectly.
          findChildNode(selectNode, addNode);
        }.bind(ctx);

      if (!ctx.t.wasInit() || !_data) {
        return;
      }

      traversePath();
    },

    onUpdateTreeNode: function(_old, _new, _hierarchy, _opts) {
      let ctx = {
          b: this, // Browser
          d: null, // current parent
          i: null, // current item
          hasId: true,
          p: _.toArray(_hierarchy || {}).sort(
            function(a, b) {
              if (a.priority === b.priority) {
                return 0;
              }
              return (a.priority < b.priority ? -1 : 1);
            }
          ), // path of the old object
          pathOfTreeItems: [], // path items
          t: this.tree, // Tree Api
          o: _opts,
          load: true,
          old: _old,
          new: _new,
          op: null,
        },
        errorOut = function() {
          let fail = this.o?.fail;
          if (fail && typeof(fail) == 'function') {
            fail.apply(this.t, [this.i, _new, _old]);
          }
        }.bind(ctx),
        deleteNode = function() {
          let self = this,
            pathOfTreeItems = this.pathOfTreeItems,
            findParent = function() {
              if (pathOfTreeItems.length) {
                pathOfTreeItems.pop();
                let length = pathOfTreeItems.length;
                this.i = (length && pathOfTreeItems[length - 1].item) || null;
                this.d = (length && pathOfTreeItems[length - 1].d) || null;

                // It is a collection item, let's find the node item
                if (length && pathOfTreeItems[length - 1].coll) {
                  pathOfTreeItems.pop();
                  length = pathOfTreeItems.length;
                  this.i = (length && pathOfTreeItems[length - 1].item) || null;
                  this.d = (length && pathOfTreeItems[length - 1].d) || null;
                }
              } else {
                this.i = null;
                this.d = null;
              }
            }.bind(this);

          let _item_parent = (this.i
            && this.t.hasParent(this.i)
              && this.t.parent(this.i)) || null,
            _item_grand_parent = _item_parent ?
              (this.t.hasParent(_item_parent)
              && this.t.parent(_item_parent) &&
              _item_parent.root != this.t.parent(_item_parent))
              : null;

          // Remove the current node first.
          if (
            this.i && this.d && this.old._id == this.d._id &&
              this.old._type == this.d._type
          ) {
            let _parent = this.t.parent(this.i) || null;

            // If there is no parent then just update the node
            if(this.t.isRootNode(_parent) ||
             (_parent && _parent.length == 0 && ctx.op == 'UPDATE')) {
              //Update node if browser has single child node.
              if(this.t.children().length === 1) {
                updateNode();
              } else {
                let that = this;
                this.t.remove(this.i).then(() => {
                  that.t.before(that.i, that.new).then((new_item) => {
                    that.t.select(new_item);
                  }, () => {
                    console.warn('Failed to add before..', arguments);
                  });
                });
              }
            } else {
              let postRemove = function() {
                // If item has parent but no grand parent
                if (_item_parent.path !== '/browser' && !_item_grand_parent) {
                  let parent = null;
                  // We need to search in all parent siblings (eg: server groups)
                  let parents = this.t.siblings(this.i) || [];
                  parents.push(this.i);
                  _.each(parents, function (p) {
                    let d = self.t.itemData(p);
                    // If new server group found then assign it parent
                    if(d._id == self.new._pid) {
                      parent = p;
                      self.pathOfTreeItems.push({coll: true, item: parent, d: d});
                    }
                  });

                  if (parent) {
                    this.load = true;

                    this.success = function() {
                      addItemNode();
                    };
                    // We can refresh the collection node, but - let's not bother about
                    // it right now.
                    this.notFound = errorOut;

                    let loaded = this.t.wasLoad(parent),
                      onLoad = function() {
                        self.i = parent;
                        self.pathOfTreeItems.push({coll: false, item: parent, d: self.d});
                        self.success();
                      };

                    if (!loaded && self.load) {
                      self.t.open(parent).then(
                        () => {
                          onLoad();
                        },
                        () => {
                          let fail = self?.o?.fail;
                          if (
                            fail && typeof(fail) == 'function'
                          ) {
                            fail.apply(self.t, []);
                          }
                        }
                      );
                    } else {
                      onLoad();
                    }
                  }
                } else {
                  // This is for rest of the nodes
                  let _parentData = this.d;
                  // Find the grand-parent, or the collection node of parent.
                  findParent();

                  if (this.i) {
                    this.load = true;

                    this.success = function() {
                      addItemNode();
                    };
                    // We can refresh the collection node, but - let's not bother about
                    // it right now.
                    this.notFound = errorOut;

                    // Find the new parent
                    this.b._findTreeChildNode(
                      this.i, {_id: this.new._pid, _type: _parentData._type}, this
                    );
                  } else {
                    addItemNode();
                  }
                }
              }.bind(this);

              this.t.remove(this.i).then(() => {
                findParent();
                if (_item_parent && !_item_grand_parent && _parent
                    && self.t.children(_parent).length == 0) {
                  self.t.unload(_parent).then( () => { setTimeout(postRemove); });
                }
                else {
                  setTimeout(postRemove);
                }
                return true;
              });
            }

          }
          errorOut();

        }.bind(ctx),
        findNewParent = function(_d) {
          let findParent = function() {
            let pathOfTreeItems = this.pathOfTreeItems;

            if (pathOfTreeItems.length) {
              pathOfTreeItems.pop();
              let length = pathOfTreeItems.length;
              this.i = (length && pathOfTreeItems[length - 1].item) || null;
              this.d = (length && pathOfTreeItems[length - 1].d) || null;

              // It is a collection item, let's find the node item
              if (length && pathOfTreeItems[length - 1].coll) {
                pathOfTreeItems.pop();
                length = pathOfTreeItems.length;
                this.i = (length && pathOfTreeItems[length - 1].item) || null;
                this.d = (length && pathOfTreeItems[length - 1].d) || null;
              }
            } else {
              this.i = null;
              this.d = null;
            }
          }.bind(this);

          // old parent was not found, can we find the new parent?
          if (this.i) {
            this.load = true;
            this.success = function() {
              addItemNode();
            };

            if (_d._type == this.old._type) {
              // We were already searching the old object under the parent.
              findParent();
              _d = this.d;
              // Find the grand parent
              findParent();
            }
            _d = this.new._pid;

            // We can refresh the collection node, but - let's not bother about
            // it right now.
            this.notFound = errorOut;

            // Find the new parent
            this.b._findTreeChildNode(this.i, _d, this);
          } else {
            addItemNode();
          }
        }.bind(ctx),
        updateNode = function() {
          if (
            this.i && this.d && this.new._type == this.d._type
          ) {
            let self = this,
              _id = this.d._id;
            if (this.new._id != this.d._id) {
              // Found the new oid, update its node_id
              let node_data = this.t.itemData(ctx.i);
              node_data._id = _id = this.new._id;
            }
            if (this.new._id == _id) {
              // Found the current
              _.extend(this.d, this.new);
              this.t.update(ctx.i, this.d);
              this.t.setLabel(ctx.i, {label: this.new.label});
              this.t.addIcon(ctx.i, {icon: this.new.icon});
              this.t.setId(ctx.i, {id: this.new.id});
              this.t.openPath(this.i);
              this.t.deselect(this.i);

              // select tree item
              self.t.select(self.i);
            }
          }
          let success = this.o?.success;
          if (success && typeof(success) == 'function') {
            success.apply(this.t, [this.i, _old, _new]);
          }
        }.bind(ctx),
        traversePath = function() {
          let _ctx = this, data;

          _ctx.success = traversePath;
          if (_ctx.p.length) {
            data = _ctx.p.shift();
            // This is the node, we can now do the required operations.
            // We should first delete the existing node, if the parent-id is
            // different.
            if (!_ctx.p.length) {
              if (_ctx.op == 'RECREATE') {
                _ctx.load = false;
                _ctx.success = deleteNode;
                _ctx.notFound = findNewParent;
              } else {
                _ctx.success = updateNode;
                _ctx.notFound = errorOut;
              }
            }
            _ctx.b._findTreeChildNode(
              _ctx.i, data, _ctx
            );
          } else if (_ctx.p.length == 1) {
            _ctx.notFound = findNewParent;
          }
          return true;
        }.bind(ctx),
        addItemNode = function() {
          let _ctx = this,
            first = (_ctx.i || this.t.wasLoad(_ctx.i)) &&
            this.t.first(_ctx.i),
            findChildNode = function(success, notFound) {
              let __ctx = this;
              __ctx.success = success;
              __ctx.notFound = notFound;

              __ctx.b._findTreeChildNode(__ctx.i, _new, __ctx);
            }.bind(_ctx),
            selectNode = function() {
              this.t.openPath(this.i);
              this.t.select(this.i);
              if (
                _ctx.o?.success && typeof(_ctx.o?.success) == 'function'
              ) {
                _ctx.o.success.apply(_ctx.t, [_ctx.i, _new]);
              }
            }.bind(_ctx),
            addNode = function() {
              let __ctx = this,
                items = __ctx.t.children(__ctx.i),
                s = 0, e = items.length - 1, i,
                linearSearch = function() {
                  while (e >= s) {
                    i = items[s];
                    let d = __ctx.t.itemData(i);
                    if (d._type === 'column') {
                      if (pgAdmin.numeric_comparator(d._id, _new._id) == 1)
                        return true;
                    } else if (pgAdmin.natural_sort(d._label, _new._label) == 1)
                      return true;
                    s++;
                  }
                  if (e != items.length - 1) {
                    i = items[e+1];
                    return true;
                  }
                  i = null;
                  return false;
                },
                binarySearch = function() {
                  while (e - s > 22) {
                    i = items[s];
                    let d = __ctx.t.itemData(i);
                    if (d._type === 'column') {
                      if (pgAdmin.numeric_comparator(d._id, _new._id) != -1)
                        return true;
                    } else if (pgAdmin.natural_sort(d._label, _new._label) != -1)
                      return true;
                    i = items[e];
                    d = __ctx.t.itemData(i);
                    let result;
                    if (d._type === 'column') {
                      result = pgAdmin.numeric_comparator(d._id, _new._id);
                    } else {
                      result = pgAdmin.natural_sort(d._label, _new._label);
                    }
                    if (result !=1) {
                      if (e != items.length - 1) {
                        i = items[e+1];
                        return true;
                      }
                      i = null;
                      return false;
                    }
                    let m = s + Math.round((e - s) / 2);
                    i = items[m];
                    d = __ctx.t.itemData(i);
                    if(d._type === 'column'){
                      result = pgAdmin.numeric_comparator(d._id, _new._id);
                    } else {
                      result = pgAdmin.natural_sort(d._label, _new._label);
                    }
                    //result will never become 0 because of remove operation
                    //which happens with updateTreeNode
                    if (result == 0)
                      return true;

                    if (result == -1) {
                      s = m + 1;
                      e--;
                    } else {
                      s++;
                      e = m - 1;
                    }
                  }
                  return linearSearch();
                };

              if (binarySearch()) {
                __ctx.t.before(i, _new).then((new_item) => {
                  __ctx.t.openPath(new_item);
                  __ctx.t.select(new_item);
                  if (typeof(__ctx?.o?.success) == 'function') {
                    __ctx.o.success.apply(__ctx.t, [i, _old, _new]);
                  }
                }, () => {
                  console.warn('Failed to add before..', arguments);
                  if (typeof(__ctx?.o?.fail) == 'function') {
                    __ctx.o.fail.apply(__ctx.t, [i, _old, _new]);
                  }
                });
              } else {
                let _appendNode = function() {
                  __ctx.t.append(__ctx.i, _new).then(
                    (new_item) => {
                      __ctx.t.openPath(new_item);
                      __ctx.t.select(new_item);
                      if (typeof(__ctx?.o?.success) == 'function') {
                        __ctx.o.success.apply(__ctx.t, [__ctx.i, _old, _new]);
                      }
                    },
                    () => {
                      console.warn('Failed to append...', arguments);
                      if (typeof(__ctx?.o?.fail) == 'function') {
                        __ctx.o.fail.apply(__ctx.t, [__ctx.i, _old, _new]);
                      }
                    },
                  );
                };

                // If the current node's inode is false
                if (__ctx.i && !__ctx.t.isInode(__ctx.i)) {
                  __ctx.t.setInode(__ctx.i, {success: _appendNode});
                  // Open the collection node.
                  pgBrowser.tree.open(__ctx.i);
                } else {
                  // Handle case for node without parent i.e. server-group
                  // or if parent node's inode is true.
                  _appendNode();
                }

              }
            }.bind(_ctx);

          // Parent node do not have any children, let me unload it.
          if (!first && _ctx.t.wasLoad(_ctx.i)) {
            _ctx.t.unload(_ctx.i).then( () => {
              findChildNode(
                selectNode,
                function() {
                  let o = this && this.o;
                  if (typeof(o?.fail) == 'function') {
                    o.fail.apply(this.t, [this.i, _old, _new]);
                  }
                }
              );
            },
            () => {
              let o = this && this.o;
              if (typeof(o?.fail) == 'function') {
                o.fail.apply(this.t, [this.i, _old, _new]);
              }
            }
            );
            return;
          }

          // We can find the collection node using _findTreeChildNode
          // indirectly.
          findChildNode(selectNode, addNode);
        }.bind(ctx);

      if (!ctx.t.wasInit() || !_new || !_old) {
        return;
      }
      ctx.pathOfTreeItems.push(_old);
      _new._label = _new.label;
      _new.label = _.escape(_new.label);

      // We need to check if this is collection node and have
      // children count, if yes then add count with label too
      if(ctx.b.Nodes[_new._type].is_collection) {
        if ('collection_count' in _old && _old['collection_count'] > 0) {
          _new.label = _.escape(_new._label) +
            ' <span>(' + _old['collection_count'] + ')</span>';
        }
      }

      // If server icon/background changes then also we need to re-create it
      if ((
        _old._type == 'server' && _new._type == 'server' && (
          _old._pid != _new._pid || _old.icon != _new.icon
        )) || _old._pid != _new._pid || _old._id != _new._id
      ) {
        ctx.op = 'RECREATE';
        traversePath();
      } else {
        ctx.op = 'UPDATE';
        traversePath();
      }
    },

    onRefreshTreeNode: async function(nodeItem, opts) {
      this.tree.toggleItemLoader(nodeItem, true);

      const itemNodeData = nodeItem && this.tree.itemData(nodeItem);
      let nodeObj = this.Nodes[itemNodeData?._type];

      // If the node is a collection node, we can directly refresh it.
      if(!nodeObj?.collection_node) {
        // If the node is not a collection node, we need to fetch its data
        // from the server and update the tree node.
        try {
          const url = nodeObj.generate_url(nodeItem, 'nodes', itemNodeData, true);
          const api = getApiInstance();
          const resp = await api.get(url);
          // server response data comes in result
          const respData = resp.data.data || resp.data.result;

          if(respData) {
            this.tree.update(nodeItem, {
              ...itemNodeData, ...respData
            });
            this.tree.setLabel(nodeItem, {label: respData.label});
            this.tree.addIcon(nodeItem, {icon: respData.icon});
          }
        } catch (error) {
          console.error('Failed to refresh tree node:', error);
          return;
        }
      }

      await this.tree.refresh(nodeItem);
      this.tree.toggleItemLoader(nodeItem, false);
      opts?.success?.();
    },

    onLoadFailNode: function(_nodeData) {
      let self = this,
        isSelected = self.tree.isSelected(_nodeData);

      /** Check if master password set **/
      self.check_master_password((is_set)=>{
        if(!is_set) {
          self.set_master_password('', ()=>{
            if(isSelected) { self.tree.select(_nodeData); }
            self.tree.open(_nodeData);
          });
        }
      });
    },

    removeChildTreeNodesById: function(_parentNode, _collType, _childIds) {
      let tree_local = pgBrowser.tree, childNode, childNodeData;
      if(_parentNode && _collType) {
        let children = tree_local.children(_parentNode),
          idx = 0, size = children.length;

        _parentNode = null;

        for (; idx < size; idx++) {
          childNode = children[idx];
          childNodeData = tree_local.itemData(childNode);

          if (childNodeData._type == _collType) {
            _parentNode = childNode;
            break;
          }
        }
      }

      if (_parentNode) {
        let children = tree_local.children(_parentNode),
          idx = 0, size = children.length;

        for (; idx < size; idx++) {
          childNode = children[idx];
          childNodeData = tree_local.itemData(childNode);

          if (_childIds.indexOf(childNodeData._id) != -1) {
            pgBrowser.removeTreeNode(childNode, false, _parentNode);
          }
        }
        return true;
      }
      return false;
    },

    removeTreeNode: function(_node, _selectNext, _parentNode) {
      let tree_local = pgBrowser.tree,
        nodeToSelect = null;

      if (!_node)
        return false;

      if (_selectNext) {
        nodeToSelect = tree_local.next(_node);
        if (!nodeToSelect) {
          nodeToSelect = tree_local.prev(_node);

          if (!nodeToSelect) {
            if (!_parentNode) {
              nodeToSelect = tree_local.parent(_node);
            } else {
              nodeToSelect = _parentNode;
            }
          }
        }
        if (nodeToSelect)
          tree_local.select(nodeToSelect);
      }

      tree_local.remove(_node);
      return true;
    },

    findSiblingTreeNode: function(_node, _id) {
      let tree_local = pgBrowser.tree,
        parentNode = tree_local.parent(_node),
        siblings = tree_local.children(parentNode),
        idx = 0, nodeData, node;

      for(; idx < siblings.length; idx++) {
        node = siblings[idx];
        nodeData = tree_local.itemData(node);

        if (nodeData && nodeData._id == _id)
          return node;
      }
      return null;
    },

    findParentTreeNodeByType: function(_node, _parentType) {
      let tree_local = pgBrowser.tree,
        nodeData,
        node = _node;

      do {
        nodeData = tree_local.itemData(node);
        if (nodeData && nodeData._type == _parentType)
          return node;
        node = tree_local.hasParent(node) ? tree_local.parent(node) : null;
      } while (node);

      return null;
    },

    findChildCollectionTreeNode: function(_node, _collType) {
      let tree_local = pgBrowser.tree,
        nodeData, idx = 0,
        node,
        children = _node && tree_local.children(_node);

      if (!children?.length)
        return null;

      for(; idx < children.length; idx++) {
        node = children[idx];
        nodeData = tree_local.itemData(node);

        if (nodeData && nodeData._type == _collType)
          return node;
      }
      return null;
    },

    addChildTreeNodes: function(_treeHierarchy, _node, _type, _arrayIds, _callback) {
      let api = getApiInstance();
      let module = _type in pgBrowser.Nodes && pgBrowser.Nodes[_type],
        childTreeInfo = _arrayIds.length && _.extend(
          {}, _.mapValues(_treeHierarchy, function(_val) {
            _val.priority -= 1; return _val;
          })),
        arrayChildNodeData = [],
        fetchNodeInfo = function(__callback) {
          if (!_arrayIds.length) {
            if (__callback) {
              __callback();
            }
            return;
          }

          let childDummyInfo = {
              '_id': _arrayIds.pop(), '_type': _type, 'priority': 0,
            },
            childNodeUrl;

          childTreeInfo[_type] = childDummyInfo;
          childNodeUrl = module.generate_url(
            null, 'nodes', childDummyInfo, true, childTreeInfo
          );

          _node = _node || arguments[1];

          api.get(
            childNodeUrl
          ).then(({data: res})=> {
            if (res.success) {
              arrayChildNodeData.push(res.data);
            }
            fetchNodeInfo(_callback);
          }).catch(function(error) {
            pgAdmin.Browser.notifier.pgRespErrorNotify(error);
            fetchNodeInfo(_callback);
          });
        };


      if (!module) {
        console.warn(
          'Developer: Couldn\'t find the module for the given child: ',
          _.clone(arguments)
        );
        return;
      }

      if (pgBrowser.tree.wasLoad(_node) || pgBrowser.tree.isLeaf(_node)) {
        fetchNodeInfo(function() {
          _.each(arrayChildNodeData, function(_nodData) {
            pgBrowser.Events.trigger(
              'pgadmin:browser:tree:add', _nodData, _treeHierarchy
            );
          });

          _callback?.();
        });
      } else {
        _callback?.();
      }
    },

    _refreshNode: function(_ctx, _d) {
      let traverseNodes = function(__d) {
        let __ctx = this, idx = 0, ctx, d,
          size = __d?.branch?.length || 0,
          findNode = function(i_findNode, d_findNode, ctx_findNode) {
            setTimeout(
              function() {
                ctx_findNode.b._findTreeChildNode(i_findNode, d_findNode, ctx_findNode);
              }, 0
            );
          };

        for (; idx < size; idx++) {
          d = __d.branch[idx];
          let n = __ctx.b.Nodes[d._type];
          ctx = {
            b: __ctx.b,
            t: __ctx.t,
            pathOfTreeItems: [],
            i: __ctx.i,
            d: d,
            select: __ctx.select,
            hasId: n && !n.collection_node,
            o: __ctx.o,
            load: true,
          };
          ctx.success = function() {
            this.b._refreshNode(this, this.d);
          }.bind(ctx);
          findNode(__ctx.i, d, ctx);
        }
      }.bind(_ctx, _d);

      if (!_d?.open)
        return;

      if (!_ctx.t.isOpen(_ctx.i)) {
        _ctx.t.open(_ctx.i, {
          unanimated: true,
          success: traverseNodes,
          fail: function() { /* Do nothing */ },
        });
      } else {
        traverseNodes();
      }

    },
  });

  return pgAdmin.Browser;
});
