import clsx from 'clsx';
import React, { PropsWithChildren, ReactNode, useState } from 'react';
import { assertNever } from '../../../helpers/utils';
import { Data } from '../../../types/data';
import { Button, ButtonContext } from '../../Buttons';
import { IconName } from '../../Icons';
import { formatDate, formatDateTime } from '../../Utils/Transformers/DateTime';
import { FilterType, FilterTypes, FilterValue } from '../Filters.model';
import { DateTimeFilter } from '../SelectionTypes/DateTimeFilter/DateTimeFilter';
import { FreeTextFilter } from '../SelectionTypes/FreeTextFilter/FreeTextFilter';
import { MultiOptionsFilter } from '../SelectionTypes/MultiOptionFilter/MultiOptionFilter';
import { NumericTextFilter } from '../SelectionTypes/NumericTextFilter/NumericTextFilter';
import { OptionsFilter } from '../SelectionTypes/OptionsFilter/OptionsFilter';
import { SearcheableOptionsFilter } from '../SelectionTypes/SearcheableOptionsFilter/SearcheableOptionsFilter';
import { renderSearcheableOptionsFilterValue } from '../SelectionTypes/SearcheableOptionsFilter/helpers';
import classes from './Filter.scss';

export interface FilterProps<T extends Data> {
  /** Focus on the element when the filter is opened. (default: true) */
  selectOnFocus?: boolean;
  /** Filter options */
  options: FilterType<T>;
  /** Current value of the filter */
  value?: FilterValue;
  /** Index of the filter in the list of filters */
  index?: number;
  /** Wether or not the filter is active. */
  isActive: boolean;
  /** Additional classNames to add to the filter container */
  className?: string;
  /** Custom renderer for the selected value */
  selectedValueRenderer?: (value: FilterValue) => string;
  /** Callback triggered when a new filter value is selected */
  onFilterChange: (prop: keyof T, value: FilterValue, index: number) => void;
  /** Callback triggered for custom validations of the filter */
  onValidate?: (currentValue: FilterValue) => string | null | undefined;
  /** Callback triggered when the filter is clicked */
  onFilterClicked: () => void;
}

export const Filter = <T extends Data>({
  options,
  value,
  index = -1,
  isActive,
  className = '',
  selectedValueRenderer,
  onFilterChange,
  onFilterClicked,
  onValidate,
  selectOnFocus = true,
}: PropsWithChildren<FilterProps<T>>): JSX.Element => {
  const [isExpanded, setIsExpanded] = useState<boolean>(false);
  const [hasError, setHasError] = useState<boolean>(false);
  const [stringValue, setStringValue] = useState<string | undefined>();

  const inputId = `${String(options.property)}-filter-input`;
  const labelId = `${String(options.property)}-filter-label`;
  const inputBasedFilter = [
    FilterTypes.FreeText,
    FilterTypes.Numeric,
    FilterTypes.Date,
    FilterTypes.DateTime,
  ];

  const onFilterValueChange = (
    prop: keyof T,
    value: FilterValue,
    stringValue?: string,
  ): void => {
    onFilterChange(prop, value, index);
    setIsExpanded(false);
    setHasError(false);
    setStringValue(stringValue);
  };

  const onError = (): void => {
    setHasError(true);
  };

  const renderValue = (value: unknown): ReactNode => {
    if (selectedValueRenderer !== undefined) {
      return selectedValueRenderer(value);
    }

    switch (options.type) {
      case FilterTypes.Date:
        return formatDate(String(value));
      case FilterTypes.DateTime:
        return formatDateTime(String(value));
      case FilterTypes.Options:
        return options.options.find((option) => option.value === value)?.label;
      case FilterTypes.MultipleOptions: {
        const selectedVal: string[] = [];
        String(value)
          .split(',')
          .map((item: string) =>
            selectedVal.push(
              options?.options.find(
                (option) => option.value.toString() === item,
              )?.label ?? '',
            ),
          );
        return selectedVal.join(', ');
      }
      case FilterTypes.SearcheableOptions:
        return renderSearcheableOptionsFilterValue(
          value,
          stringValue,
          setStringValue,
          options,
        );
      default:
        return stringValue ?? String(value);
    }
  };

  const renderFilterContent = (): JSX.Element => {
    switch (options.type) {
      case FilterTypes.FreeText:
        return (
          <FreeTextFilter
            value={value}
            onSelect={(value: FilterValue) =>
              onFilterValueChange(options.property, value)
            }
            onError={onError}
            onValidate={onValidate}
            selectOnFocus={selectOnFocus}
            inputId={inputId}
          />
        );

      case FilterTypes.Numeric:
        return (
          <NumericTextFilter
            value={value as number}
            onSelect={(value: FilterValue) =>
              onFilterValueChange(options.property, value)
            }
            onError={onError}
            onValidate={onValidate}
            inputId={inputId}
          />
        );

      case FilterTypes.Options:
        return (
          <OptionsFilter
            value={value as string}
            options={options.options}
            onSelect={(value: FilterValue) =>
              onFilterValueChange(options.property, value)
            }
            labelId={labelId}
          />
        );

      case FilterTypes.MultipleOptions:
        return (
          <MultiOptionsFilter
            value={value as string}
            options={options.options}
            onSelect={(value: FilterValue) =>
              onFilterValueChange(options.property, value?.toString())
            }
            labelId={labelId}
          />
        );
      case FilterTypes.SearcheableOptions:
        return (
          <SearcheableOptionsFilter
            value={value as string}
            optionsProvider={options.optionsProvider}
            searchInputPlaceholder={options.searchInputPlaceholder}
            onSelect={(value: FilterValue, stringValue?: string) =>
              onFilterValueChange(options.property, value, stringValue)
            }
            maxItems={options.maxItems}
            inputId={inputId}
            labelId={labelId}
          />
        );

      case FilterTypes.Date:
        return (
          <DateTimeFilter
            value={value as string}
            onSelect={(value: FilterValue) =>
              onFilterValueChange(options.property, value)
            }
            modifyTime={false}
            onError={onError}
            onValidate={onValidate}
            inputId={inputId}
          />
        );

      case FilterTypes.DateTime:
        return (
          <DateTimeFilter
            value={value as string}
            onSelect={(value: FilterValue) =>
              onFilterValueChange(options.property, value)
            }
            modifyTime={true}
            onError={onError}
            onValidate={onValidate}
            inputId={inputId}
          />
        );

      case FilterTypes.Custom:
        return (
          <options.component
            value={value}
            active={isActive}
            onSelect={(value: FilterValue, stringValue?: string) => {
              onFilterValueChange(options.property, value, stringValue);
            }}
            onError={onError}
            onValidate={onValidate}
            labelId={labelId}
          />
        );

      default:
        //Exhaustiveness check
        throw assertNever(options);
    }
  };

  const hasValue = value !== undefined && value !== '';

  return (
    <div
      className={clsx(
        classes.container,
        { [classes.hasValue]: hasValue },
        'filter-container',
        className,
      )}
      data-test-id={`filter:${options.property as string}`}
      data-test-type={FilterTypes[options.type]}
      onClick={() => onFilterClicked()}
    >
      <div
        onClick={() => {
          setIsExpanded(!isExpanded);
          setHasError(false);
        }}
        className={clsx(classes.title, {
          [classes.hasValue]: hasValue,
          [classes.active]: isActive && isExpanded,
          [classes.hasError]: hasError,
        })}
        data-test-id="filter-button-toggle"
      >
        <label
          id={labelId}
          {...(inputBasedFilter.includes(options.type)
            ? {}
            : { htmlFor: inputId })}
          data-test-id="filter-label"
        >
          {options.label}
        </label>
        <Button
          icon={IconName.ChevronRight}
          className={clsx(classes.button, {
            [classes.rotated]: isExpanded || hasValue,
          })}
          buttonContext={ButtonContext.None}
        />
      </div>
      <div
        className={clsx(classes.contentContainer, {
          [classes.expanded]: isExpanded,
        })}
      >
        {hasValue && !isExpanded && (
          <div
            className={clsx(classes.selectedValue)}
            onClick={() => {
              setIsExpanded(!isExpanded);
            }}
          >
            <span data-test-id="filter-value">{renderValue(value)}</span>
            <Button
              icon={IconName.RemoveFilter}
              className={clsx(classes.button)}
              dataTestId="filter-button-clear"
              buttonContext={ButtonContext.None}
              onButtonClicked={(e) => {
                e.stopPropagation();
                onFilterValueChange(options.property, undefined);
              }}
            />
          </div>
        )}
        {isExpanded && (
          <div
            data-test-id="filter-content"
            className={clsx({ [classes.active]: isActive }, classes.content)}
          >
            {renderFilterContent()}
          </div>
        )}
      </div>
    </div>
  );
};
