'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 {
  value: string | number;
  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;
  isLoading?: boolean;
  isClearable?: boolean;
  isRtl?: boolean;
  isSearchable?: boolean;
  isMulti?: boolean;
  placeholder?: string;
  required?: boolean;
  className?: {
    wrapper?: string;
    label?: string;
    select?: string;
  };
  isError?: boolean;
  errorMessage?: string;
  isAnimated?: boolean;
  openMenu?: boolean;
  delay?: number;
  onInputChange?: (search: string) => void;
  startAdornment?: 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 } = 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 px-4! py-1.5 gap-1 flex items-center'>
        {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='mt-1 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,
    menuPortalTarget,
    closeMenuOnSelect,
    hideSelectedOptions,
    onMenuScrollToBottom,
  } = 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 gap-2 text-start',
        className?.wrapper
      )}
    >
      {label && (
        <span
          className={cn(
            'w-full text-sm font-normal leading-5',
            { 'text-error': isError },
            className?.label
          )}
        >
          {label}
          {required && (
            <>
              {' '}
              <span className='tooltip tooltip-error' data-tip='required'>
                <span className='text-error'>*</span>
              </span>
            </>
          )}
        </span>
      )}

      <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}
        isLoading={isLoading}
        isClearable={isClearable}
        isRtl={isRtl}
        isSearchable={isSearchable}
        placeholder={placeholder}
        closeMenuOnSelect={closeMenuOnSelect}
        hideSelectedOptions={hideSelectedOptions}
        className={cn('w-full', className?.select)}
        classNames={{
          ...(!startAdornment && {
            control: ({ isFocused, isDisabled }) =>
              cn(
                'w-full min-h-12! rounded border bg-white transition-shadow cursor-pointer!',
                {
                  'border-red-500! ring-2 ring-red-200': isError,
                  'border-indigo-500 ring-2 ring-indigo-200': isFocused,
                  'border-gray-300': !isError && !isFocused,
                  'bg-gray-100 text-gray-400 cursor-not-allowed': isDisabled,
                }
              ),
            valueContainer: () => cn('flex-1 px-4! py-2! gap-1'),
          }),
          placeholder: () =>
            cn({ 'text-gray-400': !isError, 'text-red-300!': isError }),
          singleValue: () =>
            cn({ 'text-gray-900': !isError, 'text-error!': isError }),
          input: () => cn('text-gray-900'),
          indicatorsContainer: () => cn('flex items-center gap-1 pr-2'),
          dropdownIndicator: ({ isFocused }) =>
            cn('p-1 rounded hover:bg-gray-100', {
              'text-gray-900': isFocused,
              'text-gray-500': !isFocused,
              'text-error!': isError,
            }),
          menu: () =>
            cn('border border-gray-200 rounded! bg-base-100 shadow-lg!'),
          menuList: () => cn('p-2! max-h-60 overflow-auto'),
          option: ({ isFocused, isSelected }) =>
            cn('mt-1 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-indigo-50 rounded py-0.5 pl-2 pr-1 flex items-center gap-1!',
              selectedValues[index]?.className
            );
          },
          multiValueLabel: ({ getValue, index }) => {
            const selectedValues = getValue() as T[];
            return cn('text-indigo-700', 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 text-sm text-error'>{errorMessage}</p>}
      {!isError && bottomLabel && (
        <p className='w-full text-sm 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]: 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);
  };

  let formattedSuccessRawData: SuccessApiResponse<T[]> | undefined = undefined;
  let formattedErrorRawData: ErrorApiResponse | undefined = undefined;

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

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

  if (isResponseError(pages?.[latestPagesIndex])) {
    formattedErrorRawData = 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;
