import { useMemo, useCallback } from 'react';

import {
  type SelectableValue,
  getTimeZoneInfo,
  type TimeZoneInfo,
  getTimeZoneGroups,
  type GroupedTimeZones,
  type TimeZone,
  InternalTimeZones,
} from '@grafana/data';
import { t } from '@grafana/i18n';

import { Select } from '../Select/Select';

import { TimeZoneGroup } from './TimeZonePicker/TimeZoneGroup';
import { formatUtcOffset } from './TimeZonePicker/TimeZoneOffset';
import { CompactTimeZoneOption, WideTimeZoneOption, type SelectableZone } from './TimeZonePicker/TimeZoneOption';

export interface Props {
  onChange: (timeZone?: TimeZone) => void;
  value?: TimeZone;
  width?: number;
  autoFocus?: boolean;
  onBlur?: () => void;
  includeInternal?: boolean | InternalTimeZones[];
  disabled?: boolean;
  inputId?: string;
  menuShouldPortal?: boolean;
  openMenuOnFocus?: boolean;
}

/**
 * https://developers.grafana.com/ui/latest/index.html?path=/docs/date-time-pickers-timezonepicker--docs
 */
export const TimeZonePicker = (props: Props) => {
  const {
    onChange,
    width,
    autoFocus = false,
    onBlur,
    value,
    includeInternal = false,
    disabled = false,
    inputId,
    menuShouldPortal = true,
    openMenuOnFocus = false,
  } = props;
  const groupedTimeZones = useTimeZones(includeInternal);
  const selected = useSelectedTimeZone(groupedTimeZones, value);
  const filterBySearchIndex = useFilterBySearchIndex();
  const TimeZoneOption = width && width <= 45 ? CompactTimeZoneOption : WideTimeZoneOption;

  const onChangeTz = useCallback(
    (selectable: SelectableValue<string>) => {
      if (!selectable || typeof selectable.value !== 'string') {
        return onChange(value);
      }
      onChange(selectable.value);
    },
    [onChange, value]
  );

  return (
    <Select
      inputId={inputId}
      value={selected}
      placeholder={t('time-picker.zone.select-search-input', 'Type to search (country, city, abbreviation)')}
      autoFocus={autoFocus}
      menuShouldPortal={menuShouldPortal}
      openMenuOnFocus={openMenuOnFocus}
      width={width}
      filterOption={filterBySearchIndex}
      options={groupedTimeZones}
      onChange={onChangeTz}
      onBlur={onBlur}
      components={{ Option: TimeZoneOption, Group: TimeZoneGroup }}
      disabled={disabled}
      aria-label={t('time-picker.zone.select-aria-label', 'Time zone picker')}
    />
  );
};

interface SelectableZoneGroup extends SelectableValue<string> {
  options: SelectableZone[];
}

const useTimeZones = (includeInternal: boolean | InternalTimeZones[]): SelectableZoneGroup[] => {
  const now = Date.now();

  const timeZoneGroups = useMemo(() => {
    return getTimeZoneGroups(includeInternal).map((group: GroupedTimeZones) => {
      const options = group.zones.reduce((options: SelectableZone[], zone) => {
        const info = getTimeZoneInfo(zone, now);

        if (!info) {
          return options;
        }

        const name = info.name.replace(/_/g, ' ');

        options.push({
          label: name,
          value: info.zone,
          searchIndex: getSearchIndex(name, info, now),
        });

        return options;
      }, []);

      return {
        label: group.name,
        options,
      };
    });
  }, [includeInternal, now]);

  return timeZoneGroups;
};

const useSelectedTimeZone = (
  groups: SelectableZoneGroup[],
  timeZone: TimeZone | undefined
): SelectableZone | undefined => {
  return useMemo(() => {
    if (timeZone === undefined) {
      return undefined;
    }

    const tz = timeZone?.toLowerCase() ?? '';

    const group = groups.find((group) => {
      if (!group.label) {
        return isInternal(tz);
      }
      return tz.startsWith(group.label.toLowerCase());
    });

    return group?.options.find((option) => {
      if (tz === '') {
        return option.value === InternalTimeZones.default;
      }
      return option.value?.toLowerCase() === tz;
    });
  }, [groups, timeZone]);
};

const isInternal = (timeZone: TimeZone): boolean => {
  switch (timeZone) {
    case InternalTimeZones.default:
    case InternalTimeZones.localBrowserTime:
    case InternalTimeZones.utc:
      return true;

    default:
      return false;
  }
};

const useFilterBySearchIndex = () => {
  return useCallback((option: SelectableValue, searchQuery: string) => {
    if (!searchQuery || !option.data || !option.data.searchIndex) {
      return true;
    }
    return option.data.searchIndex.indexOf(searchQuery.toLowerCase()) > -1;
  }, []);
};

const getSearchIndex = (label: string, info: TimeZoneInfo, timestamp: number): string => {
  const parts: string[] = [
    info.zone.toLowerCase(),
    info.abbreviation.toLowerCase(),
    formatUtcOffset(timestamp, info.zone).toLowerCase(),
  ];

  if (label !== info.zone) {
    parts.push(label.toLowerCase());
  }

  for (const country of info.countries) {
    parts.push(country.name.toLowerCase());
    parts.push(country.code.toLowerCase());
  }

  return parts.join('|');
};
