/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2026, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import { Box } from '@mui/material';
import { styled } from '@mui/material/styles';
import React, { useState, useMemo, useCallback } from 'react';
import PropTypes from 'prop-types';
import HelpIcon from '@mui/icons-material/HelpRounded';
import SearchRoundedIcon from '@mui/icons-material/SearchRounded';
import pgAdmin from 'sources/pgadmin';
import gettext from 'sources/gettext';
import url_for from 'sources/url_for';
import Loader from 'sources/components/Loader';
import getApiInstance, { parseApiError } from '../../../../static/js/api_instance';
import { PrimaryButton, PgIconButton } from '../../../../static/js/components/Buttons';
import { FormFooterMessage, InputSelect, InputText, MESSAGE_TYPE } from '../../../../static/js/components/FormComponents';
import PgReactDataGrid from '../../../../static/js/components/PgReactDataGrid';

const StyledBox = styled(Box)(({theme}) => ({
  display:'flex',
  flexGrow: '1',
  flexDirection:'column',
  position:'relative',
  overflow:'hidden',
  height: '100%',
  '& .SearchObjects-toolbar': {
    padding: '4px',
    display: 'flex',
    ...theme.mixins.panelBorder?.bottom,
    '& .SearchObjects-inputSearch': {
      lineHeight: 1,
    },
    '& .SearchObjects-Btnmargin': {
      marginLeft: '0.25rem',
    },
  },
  '& .SearchObjects-footer1': {
    justifyContent: 'space-between',
    padding: '4px 8px',
    display: 'flex',
    alignItems: 'center',
    borderTop: `1px solid ${theme.otherVars.inputBorderColor}`,
  },
  '& .SearchObjects-footer': {
    borderTop: `1px solid ${theme.otherVars.inputBorderColor} !important`,
    padding: '0.5rem',
    display: 'flex',
    width: '100%',
    background: theme.otherVars.headerBg,
  },
  '& .SearchObjects-grid': {
    fontSize: '13px !important',
    '& .rdg-header-row': {
      '& .rdg-cell': {
        padding: '0px 4px !important',
      }
    },
    '& .rdg-cell': {
      padding: '0px 4px',
      '&[aria-colindex="1"]': {
        padding: '0px 4px  !important',
        '&.rdg-editor-container': {
          padding: '0px',
        },
      },
      '& .SearchObjects-textWrap': {
        textOverflow: 'ellipsis',
        overflow: 'hidden'
      },
      '& .SearchObjects-cellMuted': {
        color: theme.otherVars.textMuted + ' !important',
        cursor: 'default !important',
      },
      '& .SearchObjects-gridCell': {
        display: 'inline-block',
        height: '1.3rem',
        width: '1.3rem',
        '& .SearchObjects-funcArgs': {
          cursor: 'pointer',
        },

      },
    }
  },
}));

const pgBrowser = pgAdmin.Browser;

function ObjectNameFormatter({row}) {

  return (
    <div className='rdg-cell-value'>
      <Box className={row.show_node ? '' : 'SearchObjects-cellMuted'}>
        <span className={['SearchObjects-gridCell', row.icon].join(' ')}></span>
        {row.name}
        {row.other_info != null && row.other_info != '' && (
          <span tabIndex="-1" className='SearchObjects-funcArgs' onClick={()=>{row.showArgs = true;}} onKeyDown={()=>{/* no need */}}> {row?.showArgs ? `(${row.other_info})` : '(...)'}</span>
        )}
      </Box>
    </div>
  );
}
ObjectNameFormatter.propTypes = {
  row: PropTypes.object,
};

function TypePathFormatter({row, column}) {

  let val = '';

  if(column.key == 'type') {
    val = row.type_label;
  } else if(column.key == 'path') {
    val = row.path;
  }

  return (
    <Box className={'SearchObjects-textWrap ' + (row.show_node ? '' : 'SearchObjects-cellMuted')}>{val}</Box>
  );
}
TypePathFormatter.propTypes = {
  row: PropTypes.object,
  column: PropTypes.object,
};


const columns = [
  {
    key: 'name',
    name: gettext('Object name'),
    width: 250,
    renderCell: ObjectNameFormatter,
  },{
    key: 'type',
    name: gettext('Type'),
    width: 70,
    renderCell: TypePathFormatter,
  },{
    key: 'path',
    name: gettext('Object path'),
    enableSorting: false,
    renderCell: TypePathFormatter,
  }
];

/* This function is used to get the final data with the proper icon
 * based on the type and translated path.
 */
const finaliseData = (nodeData, datum)=> {
  datum.icon = 'icon-' + datum.type;
  /* finalise path */
  [datum.path, datum.id_path] = translateSearchObjectsPath(nodeData, datum.path, datum.catalog_level);
  /* id is required by dataview */
  datum.id = datum.id_path ? datum.id_path.join('.') : _.uniqueId(datum.name);

  datum.other_info = datum.other_info ? _.escape(datum.other_info) : datum.other_info;

  return datum;
};

const getCollNode = (node_type)=> {
  if('coll-'+node_type in pgBrowser.Nodes) {
    return pgBrowser.Nodes['coll-'+node_type];
  } else if(node_type in pgBrowser.Nodes &&
      typeof(pgBrowser.Nodes[node_type].collection_type) === 'string') {
    return pgBrowser.Nodes[pgBrowser.Nodes[node_type].collection_type];
  }

  return null;
};

/* This function will translate the path given by search objects API into two parts
 * 1. The display path on the UI
 * 2. The tree search path to locate the object on the tree.
 *
 * Sample path returned by search objects API
 * :schema.11:/pg_catalog/:table.2604:/pg_attrdef
 *
 * Sample path required by tree locator
 * Normal object  - server_group/1.server/3.coll-database/3.database/13258.coll-schema/13258.schema/2200.coll-table/2200.table/41773
 * pg_catalog schema - server_group/1.server/3.coll-database/3.database/13258.coll-catalog/13258.catalog/11.coll-table/11.table/2600
 * Information Schema, sys:
 *  server_group/1.server/3.coll-database/3.database/13258.coll-catalog/13258.catalog/12967.coll-catalog_object/12967.catalog_object/13204
 *  server_group/1.server/11.coll-database/11.database/13258.coll-catalog/13258.catalog/12967.coll-catalog_object/12967.catalog_object/12997.coll-catalog_object_column/12997.catalog_object_column/13
 *
 * Column catalog_level has values as
 * N - Not a catalog schema
 * D - Catalog schema with DB support - pg_catalog
 * O - Catalog schema with object support only - info schema, sys
 */
const translateSearchObjectsPath = (nodeData, path, catalog_level)=> {
  if (path === null) {
    return [null, null];
  }

  catalog_level = catalog_level || 'N';

  /* path required by tree locator */
  /* the path received from the backend is after the DB node, initial path setup */
  let id_path = [
    nodeData?.server_group?.id,
    nodeData?.server?.id,
    getCollNode('database').type + '_' + nodeData?.server?._id,
    nodeData?.database?.id,
  ];

  let prev_node_id = nodeData?.database?._id;

  /* add the slash to match regex, remove it from display path later */
  path = '/' + path;
  /* the below regex will match all /:schema.2200:/ */
  let new_path = path.replace(/\/:[a-zA-Z_]+\.\d+:\//g, (token)=>{
    let orig_token = token;
    /* remove the slash and colon */
    token = token.slice(2, -2);
    let [node_type, node_oid, others] = token.split('.');
    if(typeof(others) !== 'undefined') {
      return token;
    }

    /* schema type is "catalog" for catalog schemas */
    node_type = (['D', 'O'].indexOf(catalog_level) != -1 && node_type == 'schema') ? 'catalog' : node_type;

    /* catalog like info schema will only have views and tables AKA catalog_object except for pg_catalog */
    node_type = (catalog_level === 'O' && ['view', 'table'].indexOf(node_type) != -1) ? 'catalog_object' : node_type;

    /* catalog_object will have column node as catalog_object_column */
    node_type = (catalog_level === 'O' && node_type == 'column') ? 'catalog_object_column' : node_type;

    /* If collection node present then add it */
    let coll_node = getCollNode(node_type);
    if(coll_node) {
      /* Add coll node to the path */
      if(prev_node_id != null) id_path.push(`${coll_node.type}_${prev_node_id}`);

      /* Add the node to the path */
      id_path.push(`${node_type}_${node_oid}`);

      /* This will be needed for coll node */
      prev_node_id = node_oid;

      /* This will be displayed in the grid */
      return  `/${coll_node.label}/`;
    } else if(node_type in pgBrowser.Nodes) {
      /* Add the node to the path */
      id_path.push(`${node_type}_${node_oid}`);

      /* This will be need for coll node id path */
      prev_node_id = node_oid;

      /* Remove the token and replace with slash. This will be displayed in the grid */
      return '/';
    }
    prev_node_id = null;
    return orig_token;
  });

  /* Remove the slash we had added */
  new_path = new_path.substring(1);

  return [new_path, id_path];
};

// This function is used to sort the column.
function getComparator(sortColumn) {
  const key = sortColumn?.columnKey;
  const dir = sortColumn?.direction == 'ASC' ? 1 : -1;

  if (!key) return ()=>0;

  return (a, b) => {
    return dir*(a[key].localeCompare(b[key]));
  };
}
export default function SearchObjects({nodeData}) {
  const [type, setType] = React.useState('all');
  const [loaderText, setLoaderText] = useState('');
  const [search, setSearch] = useState('');
  const [footerText, setFooterText] = useState('0 matches found.');
  const [searchData, setSearchData] = useState([]);
  const [sortColumns, setSortColumns] = useState([]);
  const [errorMsg, setErrorMsg] = useState('');
  const api = getApiInstance();

  const onDialogHelp = ()=> {
    window.open(url_for('help.static', { 'filename': 'search_objects.html' }), 'pgadmin_help');
  };

  const sortedItems = useMemo(()=>(
    [...searchData].sort(getComparator(sortColumns[0]))
  ), [searchData, sortColumns]);

  const onItemEnter = useCallback((rowData)=>{
    let tree = pgBrowser.tree;
    setErrorMsg('');

    if(!rowData.show_node) {
      setErrorMsg(
        gettext('%s objects are disabled in the browser. You can enable them in the <a id="prefdlgid" class="pref-dialog-link">preferences</a>.', rowData.type_label));

      setTimeout(()=> {
        document.getElementById('prefdlgid').addEventListener('click', ()=>{
          if(pgAdmin.Preferences) {
            pgAdmin.Preferences.show();
          }
        });
      }, 100);

      return false;
    }
    tree.suppressEventsForPath = '/browser/' + rowData.id_path.join('/');
    setLoaderText(gettext('Locating...'));
    tree.findNodeWithToggle(rowData.id_path)
      .then((treeItem)=>{
        setTimeout(() => {
          tree.select(treeItem, true, 'center');
        }, 100);
        setLoaderText(null);
      })
      .catch(()=>{
        setLoaderText(null);
        setErrorMsg(gettext('Unable to locate this object in the browser.'));
      });
  }, []);

  const onSearch = ()=> {
    // If user press the Enter key and the search characters are
    // less than 3 characters then return from the function.
    if (search.length < 3)
      return;
    setLoaderText(gettext('Searching....'));
    setErrorMsg('');

    let searchType = type;
    if(type === 'constraints') {
      searchType = ['constraints', 'check_constraint', 'foreign_key', 'primary_key', 'unique_constraint', 'exclusion_constraint'];
    }

    api.get(url_for('search_objects.search',{
      sid: nodeData?.server?._id,
      did: nodeData?.database?._id,
    }), { params: {
      text: search,
      type: searchType,
    }})
      .then(res=>{
        setLoaderText(null);
        let finalData = [];
        // Get the finalise list of data.
        res?.data?.data.forEach((element) => {
          finalData.push(finaliseData(nodeData, element));
        });
        setSearchData(finalData);
        setFooterText(res?.data?.data?.length + ' matches found');
      })
      .catch((err)=>{
        setLoaderText(null);
        pgAdmin.Browser.notifier.errorText(parseApiError(err));
      });
  };

  const onEnterPress = (e) => {
    if (e.key === 'Enter') {
      e.preventDefault();
      onSearch();
    }
  };

  const typeOptions = ()=> {
    return new Promise((resolve, reject)=>{
      try {
        api.get(url_for('search_objects.types', {
          sid: nodeData?.server?._id,
          did: nodeData?.database?._id,
        }))
          .then(res=>{
            let typeOpt = [{label:gettext('All types'), value:'all'}];
            let typesRes = Object.entries(res.data.data).sort((a,b)=>a?.localeCompare?.(b));
            typesRes.forEach((element) => {
              typeOpt.push({label:gettext(element[1]), value:element[0]});
            });

            resolve(typeOpt);
          })
          .catch((err)=>{
            pgAdmin.Browser.notifier.errorText(parseApiError(err));
            reject(err instanceof Error ? err : Error(gettext('Something went wrong')));
          });
      } catch (error) {
        pgAdmin.Browser.notifier.errorText(parseApiError(error));
        reject(error instanceof Error ? error : Error(gettext('Something went wrong')));
      }
    });
  };

  return (
    <StyledBox>
      <Loader message={loaderText} />
      <Box className='SearchObjects-toolbar'>
        <InputText type="search" className='SearchObjects-inputSearch' data-label="search" placeholder={gettext('Type at least 3 characters')} value={search} onChange={setSearch} onKeyPress={onEnterPress} autoFocus/>
        <Box sx={{marginLeft: '4px', width: '50%'}}>
          <InputSelect value={type} controlProps={{allowClear: false}} options={typeOptions} onChange={(v)=>setType(v)}/>
        </Box>
        <PrimaryButton style={{width: '120px'}} data-test="search" className='SearchObjects-Btnmargin' startIcon={<SearchRoundedIcon />}
          onClick={onSearch} disabled={search.length < 3}>{gettext('Search')}</PrimaryButton>
      </Box>
      <Box flexGrow="1" display="flex" flexDirection="column" position="relative" overflow="hidden">
        <PgReactDataGrid
          id="searchobjects"
          className='SearchObjects-grid'
          hasSelectColumn={false}
          columns={columns}
          rows={sortedItems}
          defaultColumnOptions={{
            sortable: true,
            resizable: true
          }}
          headerRowHeight={35}
          rowHeight={28}
          mincolumnWidthBy={25}
          enableCellSelect={false}
          sortColumns={sortColumns}
          onSortColumnsChange={setSortColumns}
          onItemEnter={onItemEnter}
        />
      </Box>
      <Box className='SearchObjects-footer1'>
        <Box>{footerText}</Box>
      </Box>
      <FormFooterMessage type={MESSAGE_TYPE.ERROR} message={errorMsg} closable onClose={()=>setErrorMsg('')}  />
      <Box className='SearchObjects-footer'>
        <PgIconButton data-test="dialog-help" onClick={onDialogHelp} icon={<HelpIcon />} title={gettext('Help for this dialog.')} />
      </Box>
    </StyledBox>
  );
}

SearchObjects.propTypes = {
  nodeData: PropTypes.object,
};
