'use client';

import { ComponentType, ReactNode, useEffect, useMemo, useState } from 'react';
import Select, {
  OptionProps,
  GroupBase,
  InputActionMeta,
  MultiValue,
  SingleValue,
  components as ReactSelectComponents,
  ControlProps,
  MenuListProps,
} from 'react-select';
import CreatableSelect from 'react-select/creatable';
import makeAnimated from 'react-select/animated';
import { useDebounce } from 'use-debounce';
import { cn, getByPath } from '@/lib/helper';
import useSWRInfinite from 'swr/infinite';
import { httpClientFetcher } from '@/services/http/client';
import {
  BaseApiResponse,
  ErrorApiResponse,
  SuccessApiResponse,
} from '@/types/api/api-general';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';

export interface OptionType<T = string | number> {
  value: T;
  label: string;
  className?: string;
  labelClassName?: string;
}

export type OptionComponent<T = OptionType> = ComponentType<
  OptionProps<T, boolean, GroupBase<T>>
>;

interface SelectInputBaseProps<T = OptionType> {
  label?: ReactNode;
  bottomLabel?: ReactNode;
  options: T[];
  optionComponent?: OptionComponent<T>;
  components?: Partial<typeof ReactSelectComponents>;
  isDisabled?: boolean;
  readOnly?: boolean;
  isLoading?: boolean;
  isClearable?: boolean;
  isRtl?: boolean;
  isSearchable?: boolean;
  isMulti?: boolean;
  placeholder?: string;
  required?: boolean;
  className?: {
    wrapper?: string;
    label?: string;
    select?: string;
    inputPrefix?: string;
    inputSuffix?: string;
    inputPrefixSuffixWrapper?: string;
  };
  isError?: boolean;
  errorMessage?: string;
  isAnimated?: boolean;
  openMenu?: boolean;
  delay?: number;
  onInputChange?: (search: string) => void;
  startAdornment?: ReactNode;
  inputPrefix?: ReactNode;
  inputSuffix?: ReactNode;
  menuPortalTarget?: HTMLElement | null;
  closeMenuOnSelect?: boolean;
  hideSelectedOptions?: boolean;
  onMenuScrollToBottom?: ((event: WheelEvent | TouchEvent) => void) | undefined;
}

export interface SelectInputProps<T = OptionType>
  extends SelectInputBaseProps<T> {
  createables?: boolean;
  value?: T | T[] | null;
  onChange?: (val: T | T[] | null) => void;
}

const animatedComponents = makeAnimated();

const CustomControl = <
  Option,
  IsMulti extends boolean,
  Group extends GroupBase<Option>,
>(
  props: ControlProps<Option, IsMulti, Group>
) => {
  const { children, innerProps } = props;

  const customProps = props.selectProps as unknown as {
    shouldShowAdornment?: boolean;
    startAdornment?: ReactNode;
  };

  const shouldShowAdornment = customProps.shouldShowAdornment ?? false;
  const startAdornment = customProps.startAdornment;

  return (
    <ReactSelectComponents.Control {...props}>
      <div className='flex-1 pl-3 gap-1 flex items-center' {...innerProps}>
        {shouldShowAdornment && startAdornment}
        {children}
      </div>
    </ReactSelectComponents.Control>
  );
};

const CustomMenuList = <
  Option,
  IsMulti extends boolean,
  Group extends GroupBase<Option>,
>(
  props: MenuListProps<Option, IsMulti, Group>
) => {
  const { children, selectProps, options } = props;
  const { isLoading } = selectProps;

  return (
    <ReactSelectComponents.MenuList {...props}>
      {children}

      {options.length > 0 && isLoading && (
        <div className='px-3 py-2 rounded-md text-center text-gray-400'>
          <span className='loading loading-spinner loading-md' />
        </div>
      )}
    </ReactSelectComponents.MenuList>
  );
};

const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
  const {
    label,
    bottomLabel,
    value,
    onChange,
    options,
    optionComponent,
    components: customComponents,
    isDisabled,
    isLoading,
    isClearable,
    isRtl,
    isSearchable = true,
    isMulti,
    placeholder,
    required,
    className,
    isError,
    errorMessage,
    isAnimated = true,
    openMenu,
    delay = 300,
    createables = false,
    onInputChange,
    startAdornment,
    inputPrefix,
    inputSuffix,
    menuPortalTarget,
    closeMenuOnSelect,
    hideSelectedOptions,
    onMenuScrollToBottom,
    readOnly,
  } = props;

  const [internalInputValue, setInternalInputValue] = useState('');
  const [debouncedInputValue] = useDebounce(internalInputValue, delay);

  const shouldShowAdornment = startAdornment && !internalInputValue;

  const components = useMemo(() => {
    const base = isAnimated ? animatedComponents : {};
    const mergedComponents = { ...base, IndicatorSeparator: () => null };

    if (startAdornment) {
      mergedComponents.Control = CustomControl;
    }

    if (customComponents) {
      Object.assign(mergedComponents, customComponents);
    }

    return mergedComponents;
  }, [isAnimated, startAdornment, customComponents]);

  const internalInputChangeHandler = (val: string, meta: InputActionMeta) => {
    if (meta.action === 'input-change') setInternalInputValue(val);
    if (meta.action === 'menu-close') setInternalInputValue('');
  };

  useEffect(() => {
    onInputChange?.(debouncedInputValue);
  }, [onInputChange, debouncedInputValue]);

  const SelectComponent = createables ? CreatableSelect : Select;

  /** 🎯 handleChange tanpa any */
  const handleChange = (val: MultiValue<T> | SingleValue<T>): void => {
    if (!val) {
      onChange?.(null);
      return;
    }

    if (isMulti) {
      onChange?.(val as T[]);
    } else {
      onChange?.(val as T);
    }
  };

  return (
    <div className={cn('w-full flex flex-col text-start', className?.wrapper)}>
      {label && (
        <span
          className={cn(
            'w-full py-2 text-xs font-semibold leading-5',
            { 'text-error': isError },
            className?.label
          )}
        >
          {label}
          {required && (
            <>
              {' '}
              <span className='tooltip tooltip-error' data-tip='required'>
                <span className='text-error'>*</span>
              </span>
            </>
          )}
        </span>
      )}

      {inputPrefix || inputSuffix ? (
        <div
          className={cn(
            'relative flex text-sm',
            className?.inputPrefixSuffixWrapper
          )}
        >
          {inputPrefix && (
            <div
              className={cn(
                'inline-flex items-center px-3 border border-r-0 border-base-content/10 rounded-l-lg transition-all duration-200',
                {
                  'bg-base-100 border-base-content/10': !isDisabled,
                  'bg-base-200 border-base-content/10': isDisabled,
                  'border-error': isError,
                },
                className?.inputPrefix
              )}
            >
              {inputPrefix}
            </div>
          )}

          <SelectComponent<T, boolean, GroupBase<T>>
            instanceId='select'
            value={value ?? (isMulti ? [] : null)}
            onChange={onChange ? handleChange : undefined}
            options={options}
            menuIsOpen={openMenu}
            inputValue={internalInputValue}
            onInputChange={internalInputChangeHandler}
            onMenuClose={() => setInternalInputValue('')}
            isMulti={isMulti}
            isDisabled={isDisabled || readOnly}
            isLoading={isLoading}
            isClearable={isClearable}
            isRtl={isRtl}
            isSearchable={isSearchable}
            placeholder={placeholder}
            closeMenuOnSelect={closeMenuOnSelect}
            hideSelectedOptions={hideSelectedOptions}
            className={cn('w-full flex-1', className?.select)}
            classNames={{
              control: ({ isFocused, isDisabled }) =>
                cn('w-full border transition-shadow', 'rounded-lg!', {
                  'bg-base-100!': !isDisabled && !readOnly,
                  'bg-base-200! text-gray-400 cursor-not-allowed':
                    isDisabled && !readOnly,
                  'bg-transparent! cursor-not-allowed!': readOnly,
                  'cursor-pointer!': !readOnly && !isDisabled,
                  'border-error!': isError,
                  'ring-2 ring-error/20': isError,
                  'border-indigo-500 ring-2 ring-indigo-200':
                    isFocused && !startAdornment && !isError,
                  'border-base-content/10!': !isError && !isFocused,
                  'rounded-l-none!': inputPrefix && !startAdornment,
                  'rounded-r-none!': inputSuffix && !startAdornment,
                }),
              valueContainer: () => cn('flex-1 px-3! pr-2! py-2.5! gap-1'),
              placeholder: () =>
                cn('text-gray-400 text-sm leading-tight', {
                  'text-error!': isError,
                }),
              singleValue: () =>
                cn('m-0! text-gray-900 text-sm leading-tight', {
                  'text-error!': isError && !readOnly,
                  'text-gray-900!': readOnly,
                }),
              input: () => cn('text-gray-900 m-0! p-0! text-sm leading-tight'),
              indicatorsContainer: () =>
                cn('flex items-center gap-1 pr-3 py-2'),
              dropdownIndicator: ({ isFocused }) =>
                cn('p-0! rounded hover:bg-gray-100', {
                  'text-gray-900': isFocused,
                  'text-gray-500': !isFocused,
                  'text-error!': isError,
                }),
              clearIndicator: () => cn('p-0! rounded hover:bg-gray-100'),
              menu: () =>
                cn(
                  'border border-base-content/5 rounded-xl! bg-base-100 shadow-lg! my-1.5!'
                ),
              menuList: () => cn('p-0! max-h-60 overflow-auto'),
              option: ({ isFocused, isSelected }) =>
                cn('px-3 py-2 rounded-md cursor-pointer!', {
                  'bg-indigo-600 text-white': isFocused,
                  'bg-blue-500!': isSelected,
                  'text-gray-700': !isFocused && !isSelected,
                }),
              multiValue: ({ getValue, index }) => {
                const selectedValues = getValue() as T[];
                return cn(
                  'bg-base-200! rounded-lg! py-[3px] px-2.5 m-0! flex items-center gap-1! w-fit gap-2!',
                  selectedValues[index]?.className
                );
              },
              multiValueRemove: () => cn('p-0! w-3 h-3'),
              multiValueLabel: ({ getValue, index }) => {
                const selectedValues = getValue() as T[];
                return cn(
                  'p-0! text-base-content! text-xs!',
                  selectedValues[index]?.labelClassName
                );
              },
            }}
            components={{
              ...components,
              ...(optionComponent ? { Option: optionComponent } : {}),
              MenuList: CustomMenuList,
            }}
            {...(startAdornment && {
              shouldShowAdornment,
              startAdornment,
            })}
            menuPortalTarget={
              typeof document !== 'undefined'
                ? (menuPortalTarget ?? document.body)
                : undefined
            }
            styles={{
              menuPortal: (base) => ({ ...base, zIndex: 9999 }),
              multiValue(base) {
                return {
                  ...base,
                  borderRadius: '8px',
                };
              },
            }}
            onMenuScrollToBottom={onMenuScrollToBottom}
          />

          {inputSuffix && (
            <div
              className={cn(
                'inline-flex items-center px-3 border border-l-0 border-base-content/10 rounded-r-lg transition-all duration-200',
                {
                  'bg-base-100 border-base-content/10': !isDisabled,
                  'bg-base-200 border-base-content/10': isDisabled,
                  'border-error': isError,
                },
                className?.inputSuffix
              )}
            >
              {inputSuffix}
            </div>
          )}
        </div>
      ) : (
        <SelectComponent<T, boolean, GroupBase<T>>
          instanceId='select'
          value={value ?? (isMulti ? [] : null)}
          onChange={onChange ? handleChange : undefined}
          options={options}
          menuIsOpen={openMenu}
          inputValue={internalInputValue}
          onInputChange={internalInputChangeHandler}
          onMenuClose={() => setInternalInputValue('')}
          isMulti={isMulti}
          isDisabled={isDisabled || readOnly}
          isLoading={isLoading}
          isClearable={isClearable}
          isRtl={isRtl}
          isSearchable={isSearchable}
          placeholder={placeholder}
          closeMenuOnSelect={closeMenuOnSelect}
          hideSelectedOptions={hideSelectedOptions}
          className={cn('w-full', className?.select)}
          classNames={{
            control: ({ isFocused, isDisabled }) =>
              cn('w-full border transition-shadow rounded-lg!', {
                'bg-base-100!': !isDisabled && !readOnly,
                'bg-base-200! text-gray-400 cursor-not-allowed':
                  isDisabled && !readOnly,
                'bg-transparent! cursor-not-allowed!': readOnly,
                'cursor-pointer!': !readOnly && !isDisabled,
                'border-error!': isError,
                'ring-2 ring-error/20': isError,
                'border-indigo-500 ring-2 ring-indigo-200':
                  isFocused && !startAdornment && !isError,
                'border-base-content/10!': !isError && !isFocused,
              }),
            valueContainer: () => cn('flex-1 px-3! pr-2! py-2.5! gap-1'),
            placeholder: () =>
              cn('text-gray-400 text-sm leading-tight', {
                'text-error!': isError,
              }),
            singleValue: () =>
              cn('m-0! text-gray-900 text-sm leading-tight', {
                'text-error!': isError && !readOnly,
                'text-gray-900!': readOnly,
              }),
            input: () => cn('text-gray-900 m-0! p-0! text-sm leading-tight'),
            indicatorsContainer: () => cn('flex items-center gap-1 pr-3 py-2'),
            dropdownIndicator: ({ isFocused }) =>
              cn('p-0! rounded hover:bg-gray-100', {
                'text-gray-900': isFocused,
                'text-gray-500': !isFocused,
                'text-error!': isError,
              }),
            clearIndicator: () => cn('p-0! rounded hover:bg-gray-100'),
            menu: () =>
              cn(
                'border border-base-content/5 rounded-xl! bg-base-100 shadow-lg! my-1.5!'
              ),
            menuList: () => cn('p-0! max-h-60 overflow-auto'),
            option: ({ isFocused, isSelected }) =>
              cn('px-3 py-2 rounded-md cursor-pointer!', {
                'bg-indigo-600 text-white': isFocused,
                'bg-blue-500!': isSelected,
                'text-gray-700': !isFocused && !isSelected,
              }),
            multiValue: ({ getValue, index }) => {
              const selectedValues = getValue() as T[];
              return cn(
                'bg-base-200! rounded-lg! py-[3px] px-2.5 m-0! flex items-center gap-1! w-fit gap-2!',
                selectedValues[index]?.className
              );
            },
            multiValueRemove: () => cn('p-0! w-3 h-3'),
            multiValueLabel: ({ getValue, index }) => {
              const selectedValues = getValue() as T[];
              return cn(
                'p-0! text-base-content! text-xs!',
                selectedValues[index]?.labelClassName
              );
            },
          }}
          components={{
            ...components,
            ...(optionComponent ? { Option: optionComponent } : {}),
            MenuList: CustomMenuList,
          }}
          {...(startAdornment && {
            shouldShowAdornment,
            startAdornment,
          })}
          menuPortalTarget={
            typeof document !== 'undefined'
              ? (menuPortalTarget ?? document.body)
              : undefined
          }
          styles={{
            menuPortal: (base) => ({ ...base, zIndex: 9999 }),
            multiValue(base) {
              return {
                ...base,
                borderRadius: '8px',
              };
            },
          }}
          onMenuScrollToBottom={onMenuScrollToBottom}
        />
      )}

      {isError && (
        <p className='w-full mt-1.5 text-xs text-error'>{errorMessage}</p>
      )}
      {!isError && bottomLabel && (
        <p className='w-full mt-1.5 text-xs opacity-60'>{bottomLabel}</p>
      )}
    </div>
  );
};

const useSelect = <T,>(
  basePath: string | null,
  valueKey: keyof T | string,
  labelKey: keyof T | string,
  searchKey: string = 'search',
  params?: { [key: string]: string }
) => {
  const [inputValue, setInputValue] = useState('');

  const pageKey = 'page';
  const limitKey = 'limit';
  const limit = params?.['limit'] ?? 10;

  const getKey = (
    pageIndex: number,
    previousPageData?: BaseApiResponse<T[]>
  ) => {
    // stop when backend says no more pages
    if (previousPageData && isResponseSuccess(previousPageData)) {
      const meta = previousPageData.meta;
      if (meta && meta.page >= meta.total_pages) return null;
    }

    const qs = new URLSearchParams({
      ...(params ?? {}),
      [searchKey ? searchKey : 'search']: inputValue ?? '',
      [pageKey]: String(pageIndex + 1),
      [limitKey]: String(limit),
    }).toString();

    return basePath ? `${basePath}?${qs}` : null;
  };

  const {
    data: pages,
    isLoading,
    isValidating,
    size,
    setSize,
  } = useSWRInfinite<BaseApiResponse<T[]>>(getKey, (url) =>
    httpClientFetcher<BaseApiResponse<T[]>>(url)
  );

  const options = useMemo(() => {
    if (!pages) return [];

    return pages.flatMap((page) =>
      isResponseSuccess(page)
        ? page.data.map((item) => ({
            value: getByPath<T, number>(item, valueKey as string),
            label: getByPath<T, string>(item, labelKey as string),
          }))
        : []
    );
  }, [pages, valueKey, labelKey]);

  const lastPage = pages?.[pages.length - 1];
  const hasMore =
    !!lastPage &&
    isResponseSuccess(lastPage) &&
    !!lastPage.meta &&
    lastPage.meta.page < lastPage.meta.total_pages;

  const loadMore = () => {
    if (!hasMore) return;
    setSize(size + 1);
  };

  const latestPagesIndex = pages?.length ? pages.length - 1 : 0;

  const { formattedSuccessRawData, formattedErrorRawData } = useMemo(() => {
    let successData: SuccessApiResponse<T[]> | undefined = undefined;
    let errorData: ErrorApiResponse | undefined = undefined;

    if (isResponseSuccess(pages?.[latestPagesIndex])) {
      successData = {
        ...pages![latestPagesIndex],
        data:
          pages?.flatMap((page) =>
            isResponseSuccess(page) ? page.data : []
          ) ?? [],
      };
    }

    if (isResponseError(pages?.[latestPagesIndex])) {
      errorData = pages![latestPagesIndex];
    }

    return {
      formattedSuccessRawData: successData,
      formattedErrorRawData: errorData,
    };
  }, [pages, latestPagesIndex]);

  return {
    inputValue,
    setInputValue,

    options,
    rawData: isResponseSuccess(pages?.[latestPagesIndex])
      ? formattedSuccessRawData
      : formattedErrorRawData,

    isLoadingOptions: isLoading || isValidating,
    isLoadingMore: isValidating && size > 1,
    hasMore,
    loadMore,
  };
};

export { useSelect };
export default SelectInput;
