/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2026, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React, {useCallback, useRef, useMemo, useState, useEffect} from 'react';
import _ from 'lodash';
import Layout, { LayoutDocker, LAYOUT_EVENTS } from '../../../../../static/js/helpers/Layout';
import EventBus from '../../../../../static/js/helpers/EventBus';
import Query from './sections/Query';
import { ConnectionBar } from './sections/ConnectionBar';
import { ResultSet } from './sections/ResultSet';
import { StatusBar } from './sections/StatusBar';
import { MainToolBar } from './sections/MainToolBar';
import { Messages } from './sections/Messages';
import getApiInstance, {callFetch, parseApiError} from '../../../../../static/js/api_instance';
import url_for from 'sources/url_for';
import { PANELS, QUERY_TOOL_EVENTS, CONNECTION_STATUS, MAX_QUERY_LENGTH, OS_EOL } from './QueryToolConstants';
import { useBeforeUnload, useInterval } from '../../../../../static/js/custom_hooks';
import { Box } from '@mui/material';
import { getDatabaseLabel, getTitle, setQueryToolDockerTitle } from '../sqleditor_title';
import gettext from 'sources/gettext';
import NewConnectionDialog from './dialogs/NewConnectionDialog';
import { evalFunc } from '../../../../../static/js/utils';
import { Notifications } from './sections/Notifications';
import MacrosDialog from './dialogs/MacrosDialog';
import FilterDialog from './dialogs/FilterDialog';
import { QueryHistory } from './sections/QueryHistory';
import { NLQChatPanel } from './sections/NLQChatPanel';
import * as showQueryTool from '../show_query_tool';
import * as commonUtils from 'sources/utils';
import * as Kerberos from 'pgadmin.authenticate.kerberos';
import PropTypes from 'prop-types';
import { retrieveNodeName } from '../show_view_data';
import { useModal } from '../../../../../static/js/helpers/ModalProvider';
import usePreferences from '../../../../../preferences/static/js/store';
import { useApplicationState } from '../../../../../settings/static/ApplicationStateProvider';
import { connectServer, connectServerModal } from './connectServer';
import { FileManagerUtils  } from '../../../../../misc/file_manager/static/js/components/FileManager';

export const QueryToolContext = React.createContext();
export const QueryToolConnectionContext = React.createContext();
export const QueryToolEventsContext = React.createContext();

function fetchConnectionStatus(api, transId) {
  return api.get(url_for('sqleditor.connection_status', {trans_id: transId}));
}

function initConnection(api, params, passdata) {
  return api.post(url_for('NODE-server.connect_id', params), passdata);
}

export function getRandomName(existingNames) {
  const maxNumber = existingNames.reduce((max, name) => {
    const match = name.match(/\d+$/); // Extract the number from the name
    if (match) {
      const number = parseInt(match[0], 10);
      return number > max ? number : max;
    }
    return max;
  }, 0);

  // Generate the new name
  const newName = `Macro ${maxNumber + 1}`;
  return newName;
}

function setPanelTitle(docker, panelId, title, qtState, dirty=false) {
  if(qtState.current_file) {
    title = qtState.current_file.split('\\').pop().split('/').pop();
  } else if (!qtState.is_new_tab && !title) {
    const internal = docker.getInternalAttrs(panelId);
    title = internal.title;
    if(internal.isDirty) {
      // remove asterisk
      title = title.slice(0, -1);
    }
  } else {
    title = title ?? qtState.params.title;
  }

  title = title + (dirty ? '*': '');
  if (qtState.is_new_tab) {
    window.document.title = title;
  } else {
    docker.setInternalAttrs(panelId, {
      isDirty: dirty,
      fileName: qtState.current_file
    });
    setQueryToolDockerTitle(docker, panelId, true, title, qtState.current_file);
  }
}

const FIXED_PREF = {
  indent: {
    'control': false,
    'shift': false,
    'alt': false,
    'key': {
      'key_code': 9,
      'char': 'Tab',
    },
  },
  unindent: {
    'control': false,
    'shift': true,
    'alt': false,
    'key': {
      'key_code': 9,
      'char': 'Tab',
    },
  },
};

export default function QueryToolComponent({params, pgWindow, pgAdmin, selectedNodeInfo, qtPanelDocker, qtPanelId, eventBusObj}) {
  const containerRef = React.useRef(null);
  const preferencesStore = usePreferences();
  const [qtState, setQtState] = useState({
    preferences: {
      browser: preferencesStore.getPreferencesForModule('browser'),
      sqleditor: {...preferencesStore.getPreferencesForModule('sqleditor'), ...FIXED_PREF},
      graphs: preferencesStore.getPreferencesForModule('graphs'),
      misc: preferencesStore.getPreferencesForModule('misc'),
      editor: preferencesStore.getPreferencesForModule('editor'),
    },
    is_new_tab: window.location == window.parent?.location,
    is_visible: true,
    current_file: null,
    obtaining_conn: true,
    connected: false,
    connected_once: false,
    connection_status: null,
    connection_status_msg: '',
    server_cursor: preferencesStore.getPreferencesForModule('sqleditor').server_cursor === true,
    params: {
      ...params,
      title: _.unescape(params.title),
      is_query_tool: params.is_query_tool == 'true',
      node_name: retrieveNodeName(selectedNodeInfo),
      dbname: _.unescape(params.database_name) || getDatabaseLabel(selectedNodeInfo),
      server_cursor: preferencesStore.getPreferencesForModule('sqleditor').server_cursor === true,
    },
    connection_list: [{
      sgid: params.sgid,
      sid: params.sid,
      did: params.did,
      user: _.unescape(params.user),
      role: _.unescape(params.role),
      title: _.unescape(params.title),
      fgcolor: params.fgcolor,
      bgcolor: params.bgcolor,
      conn_title: getTitle(
        pgAdmin, null, selectedNodeInfo, true, _.unescape(params.server_name), _.unescape(params.database_name) || getDatabaseLabel(selectedNodeInfo),
        _.unescape(params.role) || _.unescape(params.user), params.is_query_tool == 'true'),
      server_name: _.unescape(params.server_name),
      database_name: _.unescape(params.database_name) || getDatabaseLabel(selectedNodeInfo),
      is_selected: true,
    }],
    editor_disabled:true,
    eol:OS_EOL
  });
  const [selectedText, setSelectedText] = useState('');

  const setQtStatePartial = (state)=>{
    setQtState((prev)=>({...prev,...evalFunc(null, state, prev)}));
  };
  const isDirtyRef = useRef(false); // usefull when conn change.
  const qtStateRef = useRef(qtState);
  const eventBus = useRef(eventBusObj || (new EventBus()));
  const docker = useRef(null);
  const api = useMemo(()=>getApiInstance(), []);
  const modal = useModal();
  const {isSaveToolDataEnabled, getToolContent} = useApplicationState();
  const fmUtilsObj = useMemo(()=>new FileManagerUtils(api, {modal}), []);

  /* Connection status poller */
  let pollTime = qtState.preferences.sqleditor.connection_status_fetch_time > 0
    && !qtState.obtaining_conn && qtState.connected_once && qtState.preferences?.sqleditor?.connection_status ?
    qtState.preferences.sqleditor.connection_status_fetch_time*1000 : -1;
  /* No need to poll when the query is executing. Query poller will get the txn status */
  if(qtState.connection_status === CONNECTION_STATUS.TRANSACTION_STATUS_ACTIVE && qtState.connected
      || !qtState.is_visible) {
    pollTime = -1;
  }

  const handleEndOfLineChange = useCallback((e)=>{
    const val = e.value || e;
    const lineSep = val === 'crlf' ? '\r\n' : '\n';
    setQtStatePartial({ eol: val });
    eventBus.current.fireEvent(QUERY_TOOL_EVENTS.CHANGE_EOL, lineSep);
  }, []);

  const refreshConnectionStatus = useCallback(async (transId) => {
    try {
      let {data: respData} = await fetchConnectionStatus(api, transId);
      if(respData.data) {
        setQtStatePartial({
          connected: true,
          connection_status: respData.data.status,
        });
        if(respData.data.notifies) {
          eventBus.current.fireEvent(QUERY_TOOL_EVENTS.PUSH_NOTICE, respData.data.notifies);
        }
      } else {
        setQtStatePartial({
          connected: false,
          connection_status: null,
          connection_status_msg: gettext('An unexpected error occurred - ensure you are logged into the application.')
        });
      }
    } catch (error) {
      console.error(error);
      setQtStatePartial({
        connected: false,
        connection_status: null,
        connection_status_msg: parseApiError(error),
      });
    }
  }, [api]);

  useInterval(()=>{
    refreshConnectionStatus(qtState.params.trans_id);
  }, pollTime);


  let defaultLayout = {
    dockbox: {
      mode: 'vertical',
      children: [
        {
          mode: 'horizontal',
          children: [
            {
              maximizable: true,
              tabs: [
                LayoutDocker.getPanel({id: PANELS.QUERY, title: gettext('Query'), content: <Query  onTextSelect={(text) => setSelectedText(text)} setQtStatePartial={setQtStatePartial}/>}),
                ...(pgAdmin.llm_enabled ? [LayoutDocker.getPanel({id: PANELS.AI_ASSISTANT, title: gettext('AI Assistant'), content: <NLQChatPanel />})] : []),
                LayoutDocker.getPanel({id: PANELS.HISTORY, title: gettext('Query History'), content: <QueryHistory />}),
              ],
            },
            {
              size: 75,
              maximizable: true,
              tabs: [
                LayoutDocker.getPanel({
                  id: PANELS.SCRATCH, title: gettext('Scratch Pad'),
                  closable: true,
                  content: <textarea style={{
                    border: 0,
                    height: '100%',
                    width: '100%',
                    resize: 'none'
                  }} title={gettext('Scratch Pad')}/>
                }),
              ]
            }
          ]
        },
        {
          mode: 'horizontal',
          children: [
            {
              maximizable: true,
              tabs: [
                LayoutDocker.getPanel({
                  id: PANELS.DATA_OUTPUT, title: gettext('Data Output'), content: <ResultSet />,
                }),
                LayoutDocker.getPanel({
                  id: PANELS.MESSAGES, title: gettext('Messages'), content: <Messages />,
                }),
                LayoutDocker.getPanel({
                  id: PANELS.NOTIFICATIONS, title: gettext('Notifications'), content: <Notifications />,
                }),
              ],
            }
          ]
        },
      ]
    },
  };

  const getSQLScript = () => {
    // Fetch the SQL for Scripts (eg: CREATE/UPDATE/DELETE/SELECT)
    // Call AJAX only if script type URL is present
    if (qtState.params.is_query_tool && qtState.params.query_url) {
      api.get(qtState.params.query_url)
        .then((res) => {
          eventBus.current.fireEvent(QUERY_TOOL_EVENTS.EDITOR_SET_SQL, res.data);
          setQtStatePartial({ editor_disabled: false });
        })
        .catch((err) => {
          eventBus.current.fireEvent(QUERY_TOOL_EVENTS.HANDLE_API_ERROR, err);
          setQtStatePartial({ editor_disabled: true });
        });
    } else if (qtState.params.sql_id && qtState.params.restore != 'true') {
      let sqlValue = localStorage.getItem(qtState.params.sql_id);
      localStorage.removeItem(qtState.params.sql_id);
      if (sqlValue) {
        eventBus.current.fireEvent(QUERY_TOOL_EVENTS.EDITOR_SET_SQL, sqlValue);
      }
      setQtStatePartial({ editor_disabled: false });
    } else if (qtState.params.restore == 'true') {
      restoreToolContent();
    } else {
      setQtStatePartial({ editor_disabled: false });
    }
  };

  const restoreToolContent = async () =>{
    let toolContent = await getToolContent(qtState.params.trans_id);
    if(toolContent){
      const connList = toolContent.connectionInfo?.connection_list;
      if (Array.isArray(connList) && connList.length > 0) {
        // ensure exactly one is_selected
        if (!connList.some(c => c.is_selected)) {
          connList[0].is_selected = true;
        }
        // pick selected connection to update params (so UI shows correct selected conn)
        const sel = connList.find(c => c.is_selected) || connList[0];

        setQtStatePartial(prev => ({
          ...prev,
          connection_list: connList,
          params: {
            ...prev.params,
            sid: sel.sid,
            did: sel.did,
            user: sel.user,
            role: sel.role,
            server_name: sel.server_name,
            database_name: sel.database_name,
            title: sel.title,
          },
        }));
      }
      if (toolContent?.modifiedExternally) {
        toolContent = await fmUtilsObj.warnFileReload(toolContent?.fileName, toolContent.data, '');
      }

      if(toolContent?.loadFile){
        eventBus.current.fireEvent(QUERY_TOOL_EVENTS.LOAD_FILE, toolContent.fileName, params?.storage);
      }else{
        eventBus.current.fireEvent(QUERY_TOOL_EVENTS.EDITOR_SET_SQL, toolContent.data);
        if(toolContent?.fileName)eventBus.current.fireEvent(QUERY_TOOL_EVENTS.LOAD_FILE_DONE, toolContent.fileName, true);
      }
    }
    setQtStatePartial({ editor_disabled: false });
  };

  const initializeQueryTool = (password, explainObject=null, macroSQL='', executeCursor=false, executeServerCursor=false, reexecute=false)=>{
    let selectedConn = _.find(qtState.connection_list, (c)=>c.is_selected);
    let baseUrl = '';
    if(qtState.params.is_query_tool) {
      let endpoint = 'sqleditor.initialize_sqleditor';

      if(qtState.params.did) {
        endpoint = 'sqleditor.initialize_sqleditor_with_did';
      }
      baseUrl = url_for(endpoint, {
        ...selectedConn,
        trans_id: qtState.params.trans_id,
      });
    } else {
      baseUrl = url_for('sqleditor.initialize_viewdata', {
        ...qtState.params,
      });
    }
    eventBus.current.fireEvent(QUERY_TOOL_EVENTS.SERVER_CURSOR, executeServerCursor);
    let requestParams = {
      user: selectedConn.user,
      role: selectedConn.role,
      password: password,
      dbname: selectedConn.database_name
    };
    api.post(baseUrl, qtState.params.is_query_tool ?
      {...requestParams} :
      {sql_filter: qtState.params.sql_filter, server_cursor: qtState.params.server_cursor, ...requestParams})
      .then(()=>{
        setQtStatePartial({
          connected: true,
          connected_once: true,
          obtaining_conn: false,
        });
        //this condition works if user is in View/Edit Data or user does not saved server or tunnel password and disconnected the server and executing the query
        if(!qtState.params.is_query_tool || reexecute) {
          eventBus.current.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_EXECUTION, explainObject, macroSQL, executeCursor, executeServerCursor);
          let msg = `${selectedConn['server_name']}/${selectedConn['database_name']} - Database connected`;
          pgAdmin.Browser.notifier.success(_.escape(msg));
        }
        // Open the file if filename passed on the parameters.
        if(qtState.params.fileName){
          eventBus.current.fireEvent(QUERY_TOOL_EVENTS.LOAD_FILE, params.fileName, params.storage);
        }
      }).catch((error)=>{
        if(error.response?.request?.responseText?.search('Ticket expired') !== -1) {
          Kerberos.fetch_ticket()
            .then(()=>{
              initializeQueryTool();
            })
            .catch((kberr)=>{
              setQtStatePartial({
                connected: false,
                obtaining_conn: false,
              });
              eventBus.current.fireEvent(QUERY_TOOL_EVENTS.HANDLE_API_ERROR, kberr);
            });
        } else if(error?.response?.status == 428) {
          connectServerModal(modal, error.response?.data?.result, async (passwordData)=>{
            await connectServer(api, modal, selectedConn.sid, selectedConn.user, passwordData, async ()=>{
              initializeQueryTool();
            });
          }, ()=>{
            setQtStatePartial({
              connected: false,
              obtaining_conn: false,
              connection_status_msg: gettext('Not Connected'),
            });
          });
        } else {
          setQtStatePartial({
            connected: false,
            obtaining_conn: false,
          });
          eventBus.current.fireEvent(QUERY_TOOL_EVENTS.HANDLE_API_ERROR, error, ()=>{});
        }
      });
  };

  const {forceClose} = useBeforeUnload({
    enabled: qtState.preferences.browser.confirm_on_refresh_close,
    isNewTab: qtState.is_new_tab,
    beforeClose: ()=>{
      eventBus.current.fireEvent(QUERY_TOOL_EVENTS.WARN_SAVE_DATA_CLOSE);
    },
    closePanel: ()=>{
      qtPanelDocker.close(qtPanelId, true);
    }
  });

  useEffect(()=>{
    getSQLScript();
    initializeQueryTool();

    eventBus.current.registerListener(QUERY_TOOL_EVENTS.REINIT_QT_CONNECTION, initializeQueryTool);

    eventBus.current.registerListener(QUERY_TOOL_EVENTS.FOCUS_PANEL, (qtPanelId)=>{
      docker.current?.focus(qtPanelId);
    });

    eventBus.current.registerListener(QUERY_TOOL_EVENTS.SET_CONNECTION_STATUS, (status)=>{
      setQtStatePartial({connection_status: status});
    });

    eventBus.current.registerListener(QUERY_TOOL_EVENTS.FORCE_CLOSE_PANEL, ()=>{
      forceClose();
    });

    const onLayoutClosing = (id)=>{
      if(qtPanelId == id) {
        eventBus.current.fireEvent(QUERY_TOOL_EVENTS.WARN_SAVE_DATA_CLOSE);
      }
    };
    qtPanelDocker.eventBus.registerListener(LAYOUT_EVENTS.CLOSING, onLayoutClosing);

    const onLayoutActive = _.debounce((currentTabId)=>{
      /* Focus the appropriate panel on visible */
      if(qtPanelId == currentTabId) {
        setQtStatePartial({is_visible: true});

        if(docker.current?.isTabVisible(PANELS.QUERY)) {
          docker.current.focus(PANELS.QUERY);
        } else if(docker.current?.isTabVisible(PANELS.HISTORY)) {
          docker.current.focus(PANELS.HISTORY);
        }

        eventBus.current.fireEvent(QUERY_TOOL_EVENTS.GOTO_LAST_SCROLL);
      } else {
        setQtStatePartial({is_visible: false});
      }
    }, 100);
    qtPanelDocker.eventBus.registerListener(LAYOUT_EVENTS.ACTIVE, onLayoutActive);

    /* If the tab or window is not visible, applicable for open in new tab */
    // Track whether this panel was active before the window was hidden,
    // so only the active instance refreshes on return.
    let wasActiveBeforeHide = false;
    const onVisibilityChange = function() {
      if(document.hidden) {
        wasActiveBeforeHide = qtStateRef.current.is_visible;
        setQtStatePartial({is_visible: false});
      } else {
        if(!wasActiveBeforeHide) return;
        setQtStatePartial({is_visible: true});
        // When the tab becomes visible again after being hidden (e.g. user
        // switched away on Linux Desktop), immediately check the connection
        // status.  This ensures a dead connection is detected right away
        // instead of waiting for the next poll interval, which was disabled
        // while the tab was hidden.
        const {params, connected_once} = qtStateRef.current;
        if(params?.trans_id && connected_once) {
          refreshConnectionStatus(params.trans_id);
        }
      }
    };
    document.addEventListener('visibilitychange', onVisibilityChange);
    return ()=>{
      document.removeEventListener('visibilitychange', onVisibilityChange);
      onLayoutActive.cancel();
      if(qtPanelDocker?.eventBus) {
        qtPanelDocker.eventBus.deregisterListener(LAYOUT_EVENTS.CLOSING, onLayoutClosing);
        qtPanelDocker.eventBus.deregisterListener(LAYOUT_EVENTS.ACTIVE, onLayoutActive);
      }
    };
  }, []);

  useEffect(() => { qtStateRef.current = qtState; }, [qtState]);

  useEffect(() => usePreferences.subscribe(
    state => {
      setQtStatePartial({preferences: {
        browser: state.getPreferencesForModule('browser'),
        sqleditor: {...state.getPreferencesForModule('sqleditor'), ...FIXED_PREF},
        graphs: state.getPreferencesForModule('graphs'),
        misc: state.getPreferencesForModule('misc'),
        editor: state.getPreferencesForModule('editor'),
      }});
    }
  ), []);

  useEffect(()=>{
    const closeConn = ()=>{
      /* Using fetch with keepalive as the browser may
      cancel the axios request on tab close. keepalive will
      make sure the request is completed */
      callFetch(
        url_for('sqleditor.close', {
          'trans_id': qtState.params.trans_id,
        }), {
          keepalive: true,
          method: 'DELETE'
        }
      )
        .then(()=>{/* Success */})
        .catch((err)=>console.error(err));
    };
    window.addEventListener('unload', closeConn);

    const pushHistory = (h)=>{
      // Do not store query text if max lenght exceeds.
      if(h?.query?.length > MAX_QUERY_LENGTH) {
        h = {
          ...h,
          query: gettext(`-- Query text not stored as it exceeds maximum length of ${MAX_QUERY_LENGTH}`)
        };
      }
      api.post(
        url_for('sqleditor.add_query_history', {
          'trans_id': qtState.params.trans_id,
        }),
        JSON.stringify(h),
      ).catch((error)=>{console.error(error);});
    };
    eventBus.current.registerListener(QUERY_TOOL_EVENTS.PUSH_HISTORY, pushHistory);
    return ()=>{
      eventBus.current.deregisterListener(QUERY_TOOL_EVENTS.PUSH_HISTORY, pushHistory);
      window.removeEventListener('unload', closeConn);
    };
  }, [qtState.params.trans_id]);


  const handleApiError = (error, handleParams)=>{
    let selectedConn = _.find(qtState.connection_list, (c)=>c.is_selected);
    if(error.response?.status == 503 && error.response.data?.info == 'CONNECTION_LOST') {
      // We will display re-connect dialog, no need to display error message again
      modal.confirm(
        gettext('Connection Warning'),
        <p>
          <span>{gettext('The application has lost the database connection:')}</span>
          <br/><span>{gettext('⁃ If the connection was idle it may have been forcibly disconnected.')}</span>
          <br/><span>{gettext('⁃ The application server or database server may have been restarted.')}</span>
          <br/><span>{gettext('⁃ The user session may have timed out.')}</span>
          <br />
          <span>{gettext('Do you want to continue and establish a new session')}</span>
        </p>,
        () => handleParams?.connectionLostCallback?.(),
        () => handleParams?.cancelCallback?.(),
        gettext('Continue'),
        gettext('Cancel')
      );
    } else if(handleParams?.checkTransaction && error.response?.data.info == 'DATAGRID_TRANSACTION_REQUIRED') {
      initConnection(api, {
        'gid': selectedConn.sgid,
        'sid': selectedConn.sid,
        'did': selectedConn.did,
        'role': selectedConn.role,
      }).then(()=>{
        initializeQueryTool();
      }).catch((err)=>{
        eventBus.current.fireEvent(QUERY_TOOL_EVENTS.HANDLE_API_ERROR, err);
      });
    } else if(error.response?.status == 403  && error.response?.data.info == 'ACCESS_DENIED') {
      pgAdmin.Browser.notifier.errorText(error.response.data.errormsg);

    }else if(error?.response?.status == 428) {
      connectServerModal(modal, error.response?.data?.result, async (passwordData)=>{

        await connectServer(api, modal, selectedConn.sid, selectedConn.user, passwordData, async ()=>{
          initializeQueryTool();
        });

      }, ()=>{
        /*This is intentional (SonarQube)*/
      });
    }else {
      let msg = parseApiError(error);
      eventBus.current.fireEvent(QUERY_TOOL_EVENTS.SET_MESSAGE, msg, true);
      eventBus.current.fireEvent(QUERY_TOOL_EVENTS.FOCUS_PANEL, PANELS.MESSAGES);
    }
  };

  useEffect(()=>{
    const fileDone = (fileName, success=true)=>{
      if(success) {
        setQtStatePartial({
          current_file: fileName
        });
        isDirtyRef.current = false;
        setPanelTitle(qtPanelDocker, qtPanelId, fileName, {...qtState, current_file: fileName}, isDirtyRef.current);

        if(isSaveToolDataEnabled('sqleditor'))eventBus.current.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_SAVE_QUERY_TOOL_DATA);
      }
      eventBus.current.fireEvent(QUERY_TOOL_EVENTS.EDITOR_LAST_FOCUS);
    };
    const events = [
      [QUERY_TOOL_EVENTS.TRIGGER_LOAD_FILE, (openInNewTab=false)=>{
        let fileParams = {
          'supported_types': ['sql', '*'], // file types allowed
          'dialog_type': 'open_file', // open select file dialog
        };
        if(openInNewTab){
          pgAdmin.Tools.FileManager.show(fileParams, (fileName, storage)=>{
            onNewQueryToolClick(null, fileName, storage);
          }, null, modal, openInNewTab);
        }else{
          pgAdmin.Tools.FileManager.show(fileParams,(fileName, storage)=>{
            eventBus.current.fireEvent(QUERY_TOOL_EVENTS.LOAD_FILE, fileName, storage);
          }, null, modal);
        }
      }],
      [QUERY_TOOL_EVENTS.TRIGGER_SAVE_FILE, (isSaveAs=false)=>{
        if(!isSaveAs && qtState.current_file) {
          eventBus.current.fireEvent(QUERY_TOOL_EVENTS.SAVE_FILE, qtState.current_file);
        } else {
          let fileParams = {
            'supported_types': ['sql', '*'],
            'dialog_type': 'create_file',
            'dialog_title': 'Save File',
            'btn_primary': 'Save',
          };
          pgAdmin.Tools.FileManager.show(fileParams, (fileName)=>{
            eventBus.current.fireEvent(QUERY_TOOL_EVENTS.SAVE_FILE, fileName);
          }, null, modal);
        }
      }],
      [QUERY_TOOL_EVENTS.LOAD_FILE_DONE, fileDone],
      [QUERY_TOOL_EVENTS.SAVE_FILE_DONE, fileDone],
      [QUERY_TOOL_EVENTS.QUERY_CHANGED, (isDirty)=>{
        if(isDirtyRef.current === isDirty) return;

        isDirtyRef.current = isDirty;
        if(qtState.params.is_query_tool) {
          setPanelTitle(qtPanelDocker, qtPanelId, null, qtState, isDirty);
        }
      }],
      [QUERY_TOOL_EVENTS.HANDLE_API_ERROR, handleApiError],
    ];

    events.forEach((e)=>{
      eventBus.current.registerListener(e[0], e[1]);
    });

    return ()=>{
      events.forEach((e)=>{
        eventBus.current.deregisterListener(e[0], e[1]);
      });
    };
  }, [qtState.params, qtState.current_file]);

  useEffect(()=>{
    /* Fire query change so that title changes to latest */
    eventBus.current.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_QUERY_CHANGE);
  }, [qtState.params.title]);


  const updateQueryToolConnection = (connectionData, isNew=false)=>{
    let currSelectedConn = _.find(qtState.connection_list, (c)=>c.is_selected);
    let currConnected = qtState.connected;

    const selectConn = (newConnData, connected=false, obtainingConn=true)=>{
      setQtStatePartial((prevQtState)=>{
        let newConnList = [...prevQtState.connection_list];
        /* If new, add to the list */
        if(isNew) {
          newConnList.push(newConnData);
        }
        for (const connItem of newConnList) {
          if(newConnData.sid == connItem.sid
            && newConnData.did == connItem.did
            && newConnData.user == connItem.user
            && newConnData.role == connItem.role) {
            connItem.is_selected = true;
          } else {
            connItem.is_selected = false;
          }
        }
        return {
          connection_list: newConnList,
          obtaining_conn: obtainingConn,
          connected: connected,
        };
      });
    };
    /* If not new, select it initially to show loading */
    if(!isNew) {
      selectConn(connectionData);
    }

    return new Promise((resolve, reject)=>{
      api.post(url_for('sqleditor.update_sqleditor_connection', {
        trans_id: qtState.params.trans_id,
        sgid: connectionData.sgid,
        sid: connectionData.sid,
        did: connectionData.did
      }), connectionData)
        .then(({data: respData})=>{
          if(isNew) {
            selectConn(connectionData);
          }
          setQtStatePartial((prev)=>{
            return {
              params: {
                ...prev.params,
                trans_id: respData.data.trans_id,
                server_name: connectionData.server_name,
                database_name: connectionData.database_name,
                dbname: connectionData.database_name,
                user: connectionData.user,
                sid: connectionData.sid,
                did: connectionData.did,
                title: connectionData.title,
                fgcolor: connectionData.fgcolor,
                bgcolor: connectionData.bgcolor,
              },
              connected: Boolean(respData.data.trans_id),
              obtaining_conn: false,
            };
          });
          setPanelTitle(qtPanelDocker, qtPanelId, connectionData.title, qtState, isDirtyRef.current);
          let msg = `${connectionData['server_name']}/${connectionData['database_name']} - Database connected`;
          pgAdmin.Browser.notifier.success(_.escape(msg));
          resolve();
        })
        .catch((error)=>{
          if(error?.response?.status == 428) {
            connectServerModal(modal, error.response?.data?.result, (passwordData)=>{
              resolve(
                updateQueryToolConnection({
                  ...connectionData,
                  ...passwordData,
                }, isNew)
              );
            }, ()=>{
              /*This is intentional (SonarQube)*/
            });
          } else {
            selectConn(currSelectedConn, currConnected, false);
            reject(error instanceof Error ? error : Error(gettext('Something went wrong')));
          }
        });
    });
  };

  const onNewConnClick = useCallback(()=>{
    const onClose = ()=>docker.current.close('new-conn');
    docker.current.openDialog({
      id: 'new-conn',
      title: gettext('Add New Connection'),
      content: <NewConnectionDialog onSave={(_isNew, data)=>{
        return new Promise((resolve, reject)=>{
          let connectionData = {
            sgid: 0,
            sid: data.sid,
            did: data.did,
            user: data.user,
            role: data.role,
            password: data.password,
            title: getTitle(pgAdmin, qtState.preferences.browser, null, false, data.server_name, data.database_name, data.role || data.user, true),
            conn_title: getTitle(pgAdmin, null, null, true, data.server_name, data.database_name, data.role || data.user, true),
            server_name: data.server_name,
            database_name: data.database_name,
            bgcolor: data.bgcolor,
            fgcolor: data.fgcolor,
            is_selected: true,
          };

          let existIdx = _.findIndex(qtState.connection_list, (conn)=>{
            conn.role= conn.role == ''? null :conn.role;
            return(
              conn.sid == connectionData.sid  && conn.database_name == connectionData.database_name
              && conn.user == connectionData.user && conn.role == connectionData.role
            );
          });
          if(existIdx > -1) {
            reject(new Error(gettext('Connection with this configuration already present.')));
            return;
          }
          updateQueryToolConnection(connectionData, true)
            .catch((err)=>{
              reject(err instanceof Error ? err : Error(gettext('Something went wrong')));
            }).then(()=>{
              resolve();
              onClose();
            });
        });
      }}
      onClose={onClose}/>
    });
  }, [qtState.preferences.browser, qtState.connection_list, qtState.params]);

  const onNewQueryToolClick = (event, fileName, storage)=>{
    const transId = commonUtils.getRandomInt(1, 9999999);
    let selectedConn = _.find(qtState.connection_list, (c)=>c.is_selected);
    let parentData = {
      server_group: {
        _id: selectedConn.sgid || 0,
      },
      server: {
        _id: selectedConn.sid,
        server_type: qtState.params.server_type,
      },
      database: {
        _id: selectedConn.did,
        label: selectedConn.database_name,
        _label: selectedConn.database_name,
      },
    };

    const gridUrl = showQueryTool.generateUrl(transId, parentData, null);
    const title = getTitle(pgAdmin, qtState.preferences.browser, null, false, selectedConn.server_name, selectedConn.database_name, selectedConn.role || selectedConn.user);
    showQueryTool.launchQueryTool(pgWindow.pgAdmin.Tools.SQLEditor, transId, gridUrl, title, {
      user: selectedConn.user,
      role: selectedConn.role,
      fileName: fileName,
      storage: storage
    });
  };

  const onManageMacros = useCallback(()=>{
    const onClose = ()=>docker.current.close('manage-macros');
    docker.current.openDialog({
      id: 'manage-macros',
      title: gettext('Manage Macros'),
      content: <MacrosDialog onSave={(newMacros)=>{
        setQtStatePartial((prev)=>{
          return {
            params: {
              ...prev.params,
              macros: newMacros,
            },
          };
        });
      }}
      onClose={onClose}/>
    }, 850, 500);
  }, [qtState.preferences.browser]);

  const onAddToMacros = () => {
    if (selectedText){
      let currMacros = qtState.params.macros;
      const existingNames = currMacros.map(macro => macro.name);
      const newName = getRandomName(existingNames);
      let changed = [{ 'name': newName, 'sql': selectedText }];

      api.put(
        url_for('sqleditor.set_macros', {
          'trans_id': qtState.params.trans_id,
        }),
        { changed: changed }
      )
        .then(({ data: respData }) => {
          const filteredData = respData.filter(m => Boolean(m.name));
          setQtStatePartial(prev => ({
            ...prev,
            params: {
              ...prev.params,
              macros: filteredData,
            },
          }));
        })
        .catch(error => {
          console.error(error);
        });
    }
    setSelectedText('');
  };


  const onFilterClick = useCallback(()=>{
    const onClose = ()=>docker.current.close('filter-dialog');
    docker.current.openDialog({
      id: 'filter-dialog',
      title: gettext('Sort/Filter options'),
      content: <FilterDialog onSave={()=>{
        onClose();
        eventBus.current.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_EXECUTION);
      }}
      onClose={onClose}/>
    }, 700, 400);
  }, [qtState.preferences.browser]);

  const onResetLayout = useCallback(()=>{
    docker.current?.resetLayout();
    eventBus.current.fireEvent(QUERY_TOOL_EVENTS.FOCUS_PANEL, PANELS.QUERY);
  }, []);

  const queryToolContextValue = React.useMemo(()=>({
    docker: docker.current,
    api: api,
    modal: modal,
    params: qtState.params,
    server_cursor: qtState.server_cursor,
    preferences: qtState.preferences,
    mainContainerRef: containerRef,
    editor_disabled: qtState.editor_disabled,
    eol: qtState.eol,
    connection_list: qtState.connection_list,
    current_file: qtState.current_file,
    toggleQueryTool: () => setQtStatePartial((prev)=>{
      return {
        ...prev,
        params: {
          ...prev.params,
          is_query_tool: true
        }
      };
    }),
    updateTitle: (title) => {
      setPanelTitle(qtPanelDocker, qtPanelId, title, qtState, isDirtyRef.current);
      setQtStatePartial((prev) => {
        // Update connection Title
        let newConnList = [...prev.connection_list];
        newConnList.forEach((conn) => {
          if (conn.sgid == params.sgid && conn.sid == params.sid && conn.did == params.did) {
            conn.title = title;
            conn.conn_title = title;
          }
        });
        return {
          ...prev,
          params: {
            ...prev.params,
            title: title
          },
          connection_list: newConnList,
        };
      });
    },
    updateServerCursor: (state) => {
      setQtStatePartial(state);
    },
  }), [qtState.params, qtState.preferences, containerRef.current, qtState.editor_disabled, qtState.eol,  qtState.current_file, qtState.server_cursor, docker.current]);


  const queryToolConnContextValue = React.useMemo(()=>({
    connected: qtState.connected,
    obtainingConn: qtState.obtaining_conn,
    connectionStatus: qtState.connection_status,
  }), [qtState]);

  /* Push only those things in context which do not change frequently */
  return (
    <QueryToolContext.Provider value={queryToolContextValue}>
      <QueryToolConnectionContext.Provider value={queryToolConnContextValue}>
        <QueryToolEventsContext.Provider value={eventBus.current}>
          <Box width="100%" height="100%" display="flex" flexDirection="column" flexGrow="1" tabIndex="0" ref={containerRef}>
            <ConnectionBar
              connected={qtState.connected}
              connecting={qtState.obtaining_conn}
              connectionStatus={qtState.connection_status}
              connectionStatusMsg={qtState.connection_status_msg}
              connectionList={qtState.connection_list}
              onConnectionChange={(connectionData)=>updateQueryToolConnection(connectionData)}
              onNewConnClick={onNewConnClick}
              onNewQueryToolClick={onNewQueryToolClick}
              onResetLayout={onResetLayout}
              docker={docker.current}
              containerRef={containerRef}
            />
            {React.useMemo(()=>(
              <MainToolBar
                containerRef={containerRef}
                onManageMacros={onManageMacros}
                onAddToMacros={onAddToMacros}
                onFilterClick={onFilterClick}
              />), [containerRef.current, onManageMacros, onFilterClick, onAddToMacros])}
            <Layout
              getLayoutInstance={(obj)=>docker.current=obj}
              defaultLayout={defaultLayout}
              layoutId="SQLEditor/Layout"
              savedLayout={params.layout}
              resetToTabPanel={PANELS.MESSAGES}
            />
            <StatusBar eol={qtState.eol} handleEndOfLineChange={handleEndOfLineChange}/>
          </Box>
        </QueryToolEventsContext.Provider>
      </QueryToolConnectionContext.Provider>
    </QueryToolContext.Provider>
  );
}

QueryToolComponent.propTypes = {
  params:PropTypes.shape({
    trans_id: PropTypes.number.isRequired,
    sgid: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
    sid: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
    did: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
    server_type: PropTypes.string,
    title: PropTypes.string.isRequired,
    bgcolor: PropTypes.string,
    fgcolor: PropTypes.string,
    is_query_tool: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]).isRequired,
    server_cursor: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]),
    user: PropTypes.string,
    role: PropTypes.string,
    server_name: PropTypes.string,
    database_name: PropTypes.string,
    layout: PropTypes.string,
    fileName: PropTypes.string,
    storage: PropTypes.string,
  }),
  pgWindow: PropTypes.object.isRequired,
  pgAdmin: PropTypes.object.isRequired,
  selectedNodeInfo: PropTypes.object,
  qtPanelDocker: PropTypes.object,
  qtPanelId: PropTypes.string,
  eventBusObj: PropTypes.objectOf(EventBus),
};
