import {
  ActionIcon,
  Box,
  Divider,
  Group,
  Indicator,
  Loader,
  type MantineColor,
  Paper,
  Stack,
  Tabs,
  Text,
  Title,
  Tooltip,
  UnstyledButton
} from '@mantine/core';
import {
  IconLayoutSidebarLeftCollapse,
  IconLayoutSidebarRightCollapse
} from '@tabler/icons-react';
import {
  type ReactNode,
  useCallback,
  useEffect,
  useMemo,
  useState
} from 'react';
import {
  Navigate,
  Route,
  Routes,
  useLocation,
  useNavigate,
  useParams
} from 'react-router-dom';

import { Boundary } from '@lib/components/Boundary';
import { StylishText } from '@lib/components/StylishText';
import type { ModelType, PluginPanelKey } from '@lib/enums/ModelType';
import { identifierString } from '@lib/functions/Conversion';
import {
  type InvenTreeHotkeyItem,
  cancelEvent,
  useInvenTreeHotkeys
} from '@lib/functions/Events';
import { eventModified, getBaseUrl } from '@lib/functions/Navigation';
import { navigateToLink } from '@lib/functions/Navigation';
import type {
  PanelGroupType,
  PanelIndicatorType,
  PanelType
} from '@lib/types/Panel';
import { t } from '@lingui/core/macro';
import { useDocumentVisibility, useWindowEvent } from '@mantine/hooks';
import { modals } from '@mantine/modals';
import { useQuery } from '@tanstack/react-query';
import { useShallow } from 'zustand/react/shallow';
import { generateUrl } from '../../functions/urls';
import { usePluginPanels } from '../../hooks/UsePluginPanels';
import { useLocalState } from '../../states/LocalState';
import { vars } from '../../theme';
import * as classes from './PanelGroup.css';

/**
 * Set of properties which define a panel group:
 *
 * @param pageKey - Unique key for this panel group
 * @param panels - List of panels to display
 * @param model - The target model for this panel group (e.g. 'part' / 'salesorder')
 * @param id - The target ID for this panel group (set to *null* for groups which do not target a specific model instance)
 * @param instance - The target model instance for this panel group
 * @param reloadInstance - Function to reload the model instance
 * @param selectedPanel - The currently selected panel
 * @param onPanelChange - Callback when the active panel changes
 * @param collapsible - If true, the panel group can be collapsed (defaults to true)
 * @param pluginPanelWithoutId - If true, the panel group will load plugin panels even with no id provided
 * @param pluginPanelKey - The plugin panel key to use when loading plugin panels for this group from the backend
 */
export type PanelProps = {
  pageKey: string;
  panels: PanelType[];
  groups?: PanelGroupType[];
  instance?: any;
  reloadInstance?: () => void;
  model?: ModelType;
  id?: number | null;
  selectedPanel?: string;
  defaultPanel?: string;
  onPanelChange?: (panel: string) => void;
  collapsible?: boolean;
  pluginPanelWithoutId?: boolean;
  pluginPanelKey?: PluginPanelKey;
};

/**
 * Render a single panel tab within the side menu
 */
function PanelTabComponent({
  expanded,
  panel,
  onClick
}: {
  expanded: boolean;
  panel: PanelType;
  onClick: (event: any) => void;
}) {
  const visibility = useDocumentVisibility();

  // Check if we should display an indicator dot for this panel
  const notificationDot = useQuery({
    enabled: panel.notification_dot !== undefined && visibility === 'visible',
    queryKey: ['panel-notification', panel.name],
    queryFn: async () => {
      if (panel.notification_dot === undefined) {
        return null;
      } else if (typeof panel.notification_dot === 'function') {
        return await panel.notification_dot();
      } else {
        return panel.notification_dot as PanelIndicatorType;
      }
    },
    staleTime: 5 * 60 * 1000, // cache for 5 minutes
    refetchOnMount: false,
    refetchOnWindowFocus: false
  });

  const indicatorColor: MantineColor | undefined = useMemo(() => {
    switch (notificationDot.data) {
      case 'info':
        return 'blue';
      case 'warning':
        return 'yellow';
      case 'danger':
        return 'red';
      default:
        return undefined;
    }
  }, [notificationDot.data]);

  return (
    <Tooltip
      label={panel.label ?? panel.name}
      key={panel.name}
      disabled={expanded}
      position='right'
    >
      <Tabs.Tab
        p='xs'
        key={`panel-label-${panel.name}`}
        w={'100%'}
        value={panel.name}
        leftSection={
          <Indicator
            position='top-end'
            disabled={!indicatorColor}
            color={indicatorColor}
            withBorder
            size={14}
          >
            {panel.icon}
          </Indicator>
        }
        hidden={panel.hidden}
        disabled={panel.disabled}
        style={{
          cursor: panel.disabled ? 'unset' : 'pointer'
        }}
        onClick={(event: any) => onClick(event)}
      >
        <Group justify='left' gap='xs' wrap='nowrap'>
          <UnstyledButton
            component={'a'}
            style={{
              textAlign: 'left'
            }}
            href={generateUrl(
              `/${getBaseUrl()}${location.pathname}/${panel.name}`
            )}
          >
            {expanded && panel.label}
          </UnstyledButton>
        </Group>
      </Tabs.Tab>
    </Tooltip>
  );
}

function BasePanelGroup({
  pageKey,
  panels,
  groups,
  onPanelChange,
  selectedPanel,
  reloadInstance,
  instance,
  model,
  id,
  collapsible = true,
  pluginPanelWithoutId = false,
  pluginPanelKey
}: Readonly<PanelProps>): ReactNode {
  const localState = useLocalState();
  const location = useLocation();
  const navigate = useNavigate();

  const { panel } = useParams();

  const [expanded, setExpanded] = useState<boolean>(true);

  // Hook to load plugins for this panel
  const _pluginId = useMemo(() => {
    if (id === undefined && pluginPanelWithoutId) return null;
    return id;
  }, [id, pluginPanelWithoutId]);
  const _pluginKey = useMemo(() => {
    if (model === undefined && pluginPanelWithoutId) return pluginPanelKey;
    return model;
  }, [model, pluginPanelWithoutId, pluginPanelKey]);
  const pluginPanelSet = usePluginPanels({
    id: _pluginId,
    model: _pluginKey,
    instance: instance,
    reloadFunc: reloadInstance
  });

  // Rebuild the list of panels
  const [allPanels, groupedPanels] = useMemo(() => {
    const _grouped_panels: PanelGroupType[] = [];
    const _panels = [...panels];
    const _allpanels: PanelType[] = [...panels];

    groups?.forEach((group) => {
      const newVal: any = { ...group, panels: [] };
      // Add panel to group and remove from main list
      group.panelIDs?.forEach((panelID) => {
        const index = _panels.findIndex((p) => p.name === panelID);
        if (index !== -1) {
          newVal.panels.push(_panels[index]);
          _panels.splice(index, 1);
        }
      });
      _grouped_panels.push(newVal);
    });

    // Add remaining panels to group
    if (_panels.length > 0) {
      _grouped_panels.push({
        id: 'ungrouped',
        label: '',
        panels: _panels
      });
    }

    // Add plugin panels
    const pluginPanels: any = [];
    pluginPanelSet.panels?.forEach((panel) => {
      let panelKey = panel.name;

      // Check if panel with this name already exists
      const existingPanel = panels.find((p) => p.name === panelKey);

      if (existingPanel) {
        // Create a unique key for the panel which includes the plugin slug
        panelKey = identifierString(`${panel.pluginName}-${panel.name}`);
      }

      pluginPanels.push({
        ...panel,
        name: panelKey
      });
      _allpanels.push({
        ...panel,
        name: panelKey
      });
    });

    if (pluginPanels.length > 0) {
      _grouped_panels.push({
        id: 'plugins',
        label: t`Plugin Provided`,
        panels: pluginPanels
      });
    }

    return [_allpanels, _grouped_panels];
  }, [groups, panels, pluginPanelSet]);

  const activePanels = useMemo(
    () => allPanels.filter((panel) => !panel.hidden && !panel.disabled),
    [allPanels]
  );

  const [isDirty, setIsDirty] = useState(false);
  useWindowEvent('beforeunload', (event) => {
    if (isDirty) {
      event.preventDefault();
    }
  });

  const performPanelChange = useCallback(
    (targetPanel: string, event?: any) => {
      if (event && eventModified(event)) {
        const url = `${location.pathname}/../${targetPanel}`;
        navigateToLink(url, navigate, event);
      } else {
        navigate(`../${targetPanel}`);
      }

      localState.setLastUsedPanel(pageKey)(targetPanel);

      if (targetPanel && onPanelChange) {
        onPanelChange(targetPanel);
      }

      setIsDirty(false);
    },
    [navigate, location, pageKey, onPanelChange]
  );

  // Callback when the active panel changes
  const handlePanelChange = useCallback(
    (targetPanel: string, event?: any) => {
      cancelEvent(event);

      if (isDirty) {
        modals.openConfirmModal({
          title: <Title order={4}>{t`Unsaved Changes`}</Title>,
          children: (
            <>
              <Divider />
              <Text>{t`You have unsaved changes. Are you sure you want to leave this panel?`}</Text>
            </>
          ),
          labels: { confirm: t`Leave`, cancel: t`Stay` },
          confirmProps: { color: 'red' },
          onConfirm: () => performPanelChange(targetPanel, event)
        });
        return;
      }

      performPanelChange(targetPanel, event);
    },
    [isDirty, performPanelChange]
  );

  // if the selected panel state changes update the current panel
  useEffect(() => {
    if (selectedPanel && selectedPanel !== panel) {
      handlePanelChange(selectedPanel);
    }
  }, [selectedPanel, panel]);

  // Determine the current panels selection (must be a valid panel)
  const currentPanel: string = useMemo(() => {
    if (activePanels.findIndex((p) => p.name === panel) === -1) {
      return activePanels[0]?.name ?? '';
    } else {
      return panel ?? '';
    }
  }, [activePanels, panel]);

  // hotkeys
  const hotkeys = useMemo(() => {
    const keys: InvenTreeHotkeyItem[] = [];
    activePanels.forEach((panel) => {
      if (panel.hotkey) {
        keys.push([
          panel.hotkey,
          t`Navigate to panel ${panel.name}`,
          () => handlePanelChange(panel.name)
        ]);
      }
    });
    return keys;
  }, [activePanels]);
  useInvenTreeHotkeys(hotkeys);

  return (
    <Boundary label={`PanelGroup-${pageKey}`}>
      <Paper p='sm' radius='xs' shadow='xs' aria-label={`${pageKey}`}>
        <Tabs
          value={currentPanel}
          orientation='vertical'
          keepMounted={false}
          aria-label={`panel-group-${pageKey}`}
          classNames={{ tab: classes.selectedPanelTab }}
        >
          <Tabs.List justify='left' aria-label={`panel-tabs-${pageKey}`}>
            {groupedPanels.map((group) => (
              <Box key={`group-${group.id}`} w={'100%'}>
                <Text
                  hidden={!group.label || !expanded}
                  c={vars.colors.primaryColors[7]}
                  key={`group-label-${group.id}`}
                  style={{
                    paddingLeft: '10px'
                  }}
                >
                  {group.label}
                </Text>
                {group.label && <Divider c={vars.colors.primaryColors[7]} />}
                {group.panels?.map(
                  (panel) =>
                    !panel.hidden && (
                      <PanelTabComponent
                        key={`panel-tab-${group.id}-${panel.name}`}
                        expanded={expanded}
                        panel={panel}
                        onClick={(event: any) =>
                          handlePanelChange(panel.name, event)
                        }
                      />
                    )
                )}
              </Box>
            ))}
            {collapsible && <Divider />}
            {collapsible && (
              <Group wrap='nowrap' gap='xs'>
                <Tooltip
                  position='right'
                  label={expanded ? t`Collapse panels` : t`Expand panels`}
                >
                  <ActionIcon
                    style={{
                      paddingLeft: '10px'
                    }}
                    onClick={() => setExpanded(!expanded)}
                    variant='transparent'
                    size='lg'
                  >
                    {expanded ? (
                      <IconLayoutSidebarLeftCollapse opacity={0.75} />
                    ) : (
                      <IconLayoutSidebarRightCollapse opacity={0.75} />
                    )}
                  </ActionIcon>
                </Tooltip>
                {pluginPanelSet.isLoading && <Loader size='xs' />}
              </Group>
            )}
          </Tabs.List>
          {allPanels.map(
            (panel) =>
              !panel.hidden && (
                <Tabs.Panel
                  key={`panel-${panel.name}`}
                  value={panel.name}
                  aria-label={`nav-panel-${identifierString(
                    `${pageKey}-${panel.name}`
                  )}`}
                  p='sm'
                  style={{
                    overflowX: 'scroll',
                    width: '100%'
                  }}
                >
                  <Stack gap='md'>
                    {panel.showHeadline !== false && (
                      <>
                        <Group justify='space-between'>
                          <StylishText size='xl'>{panel.label}</StylishText>
                          {panel.controls && (
                            <Group justify='right' wrap='nowrap'>
                              {panel.controls}
                            </Group>
                          )}
                        </Group>
                        <Divider />
                      </>
                    )}
                    <Boundary label={`PanelContent-${panel.name}`}>
                      {getPanelContent(panel.content, panel, setIsDirty)}
                    </Boundary>
                  </Stack>
                </Tabs.Panel>
              )
          )}
        </Tabs>
      </Paper>
    </Boundary>
  );
}

/*
 * Helper function to inject the setIsDirty callback into panel content if supported
 * This allows panels to mark themselves as dirty when changes are made, which will trigger a confirmation prompt when navigating away from the panel
 */
function getPanelContent(
  content: ReactNode,
  panel: PanelType,
  setIsDirty?: (dirty: boolean) => void
): ReactNode {
  if (content === null) {
    return null;
  }

  // pass setIsDirty callback to content if supported
  if (
    panel.supportsDirty &&
    typeof content === 'object' &&
    'props' in content &&
    setIsDirty
  ) {
    return {
      ...content,
      props: {
        ...(content.props || {}),
        setDirtyCallback: setIsDirty
      }
    };
  }

  // normal content, just return as is
  return content;
}

function IndexPanelComponent({
  pageKey,
  selectedPanel,
  defaultPanel,
  panels
}: Readonly<PanelProps>) {
  const lastUsedPanel = useLocalState(
    useShallow((state) => {
      const panelName =
        selectedPanel ||
        defaultPanel ||
        state.lastUsedPanels[pageKey] ||
        panels[0]?.name;

      const panel = panels.findIndex(
        (p) => p.name === panelName && !p.disabled && !p.hidden
      );
      if (panel === -1) {
        return panels.find((p) => !p.disabled && !p.hidden)?.name || '';
      }

      return panelName;
    })
  );

  return <Navigate to={lastUsedPanel} replace />;
}

/**
 * Render a panel group. The current panel will be appended to the current url.
 * The last opened panel will be stored in local storage and opened if no panel is provided via url param
 * @param panels - The list of panels to display
 * @param onPanelChange - Callback when the active panel changes
 * @param collapsible - If true, the panel group can be collapsed (defaults to true)
 */
export function PanelGroup(props: Readonly<PanelProps>) {
  return (
    <Routes>
      <Route index element={<IndexPanelComponent {...props} />} />
      <Route path='/:panel/*' element={<BasePanelGroup {...props} />} />
    </Routes>
  );
}
