import clsx from 'clsx';
import React, { PropsWithChildren, useEffect, useRef, useState } from 'react';
import { Data } from '../../types/data';
import { Filter } from './Filter/Filter';
import { FilterType, FilterValue, FilterValues } from './Filters.model';
import classes from './Filters.scss';

export interface FiltersProps<T extends Data> {
  options?: FilterType<T>[];
  defaultValues?: FilterValues<T>;
  onFiltersChange?: (filters: FilterValues<T>) => void;
  className?: string;
}

type CompositeFilters<T extends Data> = {
  [K in keyof T]?: (FilterType<T> & { index: number })[];
};

/**
 * Renders filter components depending on the options specified in params
 * FreeTextFilter and OptionsFilter are inbuilt filter types and its possible to pass custom
 * filters with filter type of CustomFilter
 * @param array - filters: Array of FilterControl elements to define all the filters
 * @Param object - filters: Object of currently active filters
 * @param function - onFiltersChange: callBack function to invoke when the selected value for filter is changed
 * @example
 *   <Filter filters={[freeTextFilter, optionsFilter, customFilter]} onFiltersChange={callBackFn} />
 */
export const Filters = <T extends Data>({
  defaultValues: defaultFilters = {},
  options,
  onFiltersChange,
  className = '',
}: PropsWithChildren<FiltersProps<T>>): JSX.Element => {
  const activeFilters = useRef<FilterValues<T>>(defaultFilters);
  const [compositeFilters, setCompositeFilters] = useState<CompositeFilters<T>>(
    {},
  );
  const [activeFilterIndex, setActiveFilterIndex] = useState<number>(-1);

  useEffect(() => {
    const compositeFiltersUpdate: CompositeFilters<T> = {};

    options?.forEach((filter, index) => {
      const { property } = filter;
      if (
        options?.slice(index + 1).findIndex((v) => v.property === property) > -1
      ) {
        compositeFiltersUpdate[property] = [];

        options?.forEach((filter, index) => {
          if (filter.property === property) {
            compositeFiltersUpdate[property]?.push({ ...filter, index });
          }
        });
      }
    });

    setCompositeFilters(compositeFiltersUpdate);
  }, [options]);

  const filtersChangedHandler = (
    prop: keyof T,
    value: FilterValue,
    index: number,
  ): void => {
    let updateValue = value;
    if (Object.keys(compositeFilters).indexOf(String(prop)) > -1) {
      const existing = activeFilters.current[prop];

      if (!existing) {
        updateValue = compositeFilters[prop]?.map((filter) => {
          if (filter.index === index) {
            return value;
          }
          return undefined;
        });
      } else {
        updateValue = [...(existing as FilterValue[])];
        const updateIndex = compositeFilters[prop]?.findIndex(
          (filter) => filter.index === index,
        );

        if (updateIndex !== undefined) {
          (updateValue as FilterValue[])[updateIndex] = value;
        }
      }

      if ((updateValue as FilterValue[]).every((v) => v === undefined)) {
        updateValue = undefined;
      }
    }

    // Add the filter and remove any filters whose value is undefined
    activeFilters.current = Object.entries({
      ...activeFilters.current,
      [prop]: updateValue,
    })
      .filter(([, v]) => v !== undefined)
      .reduce<FilterValues<T>>((obj, [key, value]) => {
        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
        // @ts-ignore
        obj[key] = value;
        return obj;
      }, {});

    // Emit new set of filters
    if (onFiltersChange) {
      onFiltersChange(activeFilters.current);
    }
  };

  const onFilterClickedHandler = (idx: number): void => {
    setActiveFilterIndex(idx);
  };

  const renderFilter = (filter: FilterType<T>, index: number): JSX.Element => {
    let value = activeFilters.current[filter.property];
    if (Object.keys(compositeFilters).indexOf(String(filter.property)) > -1) {
      const valueIndex = compositeFilters[filter.property]?.findIndex(
        (f) => f.index === index,
      );

      if (valueIndex !== undefined && activeFilters.current[filter.property]) {
        value = (activeFilters.current[filter.property] as [])[valueIndex];
      }
    }

    return (
      <Filter<T>
        options={filter}
        value={value}
        key={filter.label}
        index={index}
        isActive={index === activeFilterIndex}
        onFilterChange={filtersChangedHandler}
        selectedValueRenderer={filter.selectedValueRenderer}
        onValidate={(currentValue) =>
          filter.onValidate &&
          filter.onValidate(currentValue, activeFilters.current)
        }
        onFilterClicked={() => onFilterClickedHandler(index)}
      />
    );
  };

  return (
    <div
      className={clsx(
        { [classes.container]: options },
        'filter-container',
        className,
      )}
      data-test-id="filters"
    >
      {options?.map((filter: FilterType<T>, index: number) =>
        renderFilter(filter, index),
      )}
    </div>
  );
};
