import { useEffect, useMemo, useState } from 'react';

import AddIcon from '@mui/icons-material/Add';
import SearchIcon from '@mui/icons-material/Search';
import { Box, Button, ListSubheader, MenuItem, MenuList, Popover, TextField, type Theme, Typography } from '@mui/material';
import { useDebounce } from '@northern.tech/utils/debouncehook';

import { fetchClasses } from '../api';
import { DEFAULT_INVENTORY_FILTER_CLASSES, SEARCH_DEBOUNCE_DELAY } from '../constants';
import { gray } from '../../../../themes/common';
import { groupAttributes } from '../utils/groupAttributes';
import type { CfeAttribute, InventoryMapping } from '../types';

// Bold top-level section header (Inventory / Classes).
const topGroupSx = (theme: Theme) => ({
  ...theme.typography.h6,
  backgroundColor: theme.palette.mode === 'dark' ? gray[700] : gray[100],
  color: theme.palette.text.primary,
  mx: 0,
  my: 1,
  pb: '6px',
  pl: 1,
  pr: 0,
  pt: '10px'
});

const categorySx = (theme: Theme) => ({
  ...theme.typography.labelSmall,
  backgroundColor: 'transparent',
  color: 'text.muted',
  lineHeight: '32px',
  textTransform: 'uppercase'
});

const matchesTerm = (label: string, term: string) => label.toLowerCase().includes(term);

const NO_CLASSES: string[] = [];

interface AttributePickerProps {
  disabled?: boolean;
  includeClasses?: boolean;
  mapping: InventoryMapping;
  // Omitted by callers that change an existing rule, where host search does not apply.
  onHostSearch?: (query: string) => void;
  onSelectAttribute: (attribute: CfeAttribute) => void;
  onSelectClass: (className: string) => void;
  // Custom trigger. Defaults to the "Add rule" button; a filter row passes its
  // attribute chip so the same menu can change an existing rule's attribute.
  renderTrigger?: (openMenu: (anchor: HTMLElement) => void, open: boolean) => React.ReactNode;
}

export const AttributePicker = ({
  mapping,
  includeClasses = false,
  disabled = false,
  onSelectAttribute,
  onSelectClass,
  onHostSearch,
  renderTrigger
}: AttributePickerProps) => {
  const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
  const [search, setSearch] = useState('');
  // Keyed by the query they were fetched for, so a stale response is simply
  // ignored instead of having to be cleared.
  const [remote, setRemote] = useState<{ classes: string[]; query: string }>({ classes: NO_CLASSES, query: '' });

  const groups = useMemo(() => groupAttributes(mapping), [mapping]);

  const open = Boolean(anchorEl);
  const query = search.trim();
  const term = query.toLowerCase();
  const debouncedQuery = useDebounce(query, SEARCH_DEBOUNCE_DELAY);
  const remoteClasses = remote.query === query ? remote.classes : NO_CLASSES;

  const close = () => {
    setAnchorEl(null);
    setSearch('');
  };

  // Debounced class autocomplete against the backend, merged with the defaults.
  useEffect(() => {
    if (!open || !includeClasses || !debouncedQuery) return;
    fetchClasses(debouncedQuery)
      .then(classes => setRemote({ query: debouncedQuery, classes }))
      .catch(() => setRemote({ query: debouncedQuery, classes: NO_CLASSES }));
  }, [debouncedQuery, open, includeClasses]);

  const filteredGroups = useMemo(
    () =>
      groups
        .map(group => ({ ...group, items: group.items.filter(item => matchesTerm(item.label, term)) }))
        .filter(group => group.items.length > 0),
    [groups, term]
  );

  const classOptions = useMemo(() => {
    const merged = Array.from(new Set([...DEFAULT_INVENTORY_FILTER_CLASSES, ...remoteClasses]));
    return term ? merged.filter(name => matchesTerm(name, term)) : merged;
  }, [remoteClasses, term]);

  const classNotFound = includeClasses && term.length > 0 && !classOptions.some(name => name.toLowerCase() === term);

  const handleAttribute = (attribute: CfeAttribute) => {
    onSelectAttribute(attribute);
    close();
  };
  const handleClass = (className: string) => {
    onSelectClass(className);
    close();
  };
  const handleHostSearch = () => {
    onHostSearch?.(query);
    close();
  };

  // Enter picks the first option the menu is showing, so a typed attribute or
  // class name can be added without reaching for the mouse.
  const handleSearchKeyDown = (event: React.KeyboardEvent) => {
    if (event.key !== 'Enter' || !query) return;
    event.preventDefault();
    const firstAttribute = filteredGroups[0]?.items[0];
    if (firstAttribute) {
      handleAttribute(firstAttribute);
    } else if (includeClasses) {
      handleClass(classOptions[0] ?? query);
    }
  };

  return (
    <>
      {renderTrigger ? (
        renderTrigger(anchor => setAnchorEl(anchor), open)
      ) : (
        <Button
          startIcon={<AddIcon />}
          variant="text"
          disabled={disabled}
          aria-haspopup="menu"
          aria-expanded={open}
          sx={{ color: 'link.primary', fontWeight: 500, px: 0, '&:hover': { backgroundColor: 'transparent' } }}
          onClick={event => setAnchorEl(event.currentTarget)}
        >
          Add rule
        </Button>
      )}
      <Popover
        open={open}
        anchorEl={anchorEl}
        onClose={close}
        anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
        slotProps={{ paper: { sx: { backgroundColor: 'background.menu', backgroundImage: 'none', maxHeight: 460, width: 320 } } }}
      >
        <Box sx={{ backgroundColor: 'background.menu', p: 1, position: 'sticky', top: 0, zIndex: 1 }}>
          <TextField
            autoFocus
            fullWidth
            placeholder="Search..."
            value={search}
            onChange={event => setSearch(event.target.value)}
            onKeyDown={handleSearchKeyDown}
          />
        </Box>
        <MenuList dense disablePadding>
          {includeClasses && (
            <ListSubheader key="inventory-top" disableSticky sx={topGroupSx}>
              Inventory
            </ListSubheader>
          )}
          {filteredGroups.map(group => [
            <ListSubheader key={`${group.category}-header`} disableSticky sx={categorySx}>
              {group.category}
            </ListSubheader>,
            ...group.items.map(item => (
              <MenuItem key={item.id || item.label} onClick={() => handleAttribute(item)}>
                {item.label}
              </MenuItem>
            ))
          ])}

          {filteredGroups.length === 0 && !includeClasses && (
            <MenuItem disabled>No matching attributes</MenuItem>
          )}

          {includeClasses && [
            <ListSubheader key="classes-header" disableSticky sx={topGroupSx}>
              Classes
            </ListSubheader>,
            ...classOptions.map(name => (
              <MenuItem key={`class-${name}`} onClick={() => handleClass(name)}>
                {name}
              </MenuItem>
            )),
            classNotFound && (
              <MenuItem key="class-add" onClick={() => handleClass(query)}>
                <Typography variant="body2" color="primary">
                  Add class &ldquo;{query}&rdquo;
                </Typography>
              </MenuItem>
            ),
            term && onHostSearch && (
              <MenuItem key="host-search" onClick={handleHostSearch}>
                <SearchIcon fontSize="small" sx={{ mr: 1 }} />
                <Typography variant="body2" color="primary">
                  Host search for &ldquo;{query}&rdquo;
                </Typography>
              </MenuItem>
            )
          ]}
        </MenuList>
      </Popover>
    </>
  );
};
