/*!
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
import { Button, HStack } from "@chakra-ui/react";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { MdAdd, MdClear } from "react-icons/md";
import { useDebouncedCallback } from "use-debounce";

import { Menu } from "src/components/ui";

import { getDefaultFilterIcon } from "./defaultIcons";
import { DateFilter } from "./filters/DateFilter";
import { DateRangeFilter } from "./filters/DateRangeFilter";
import { NumberFilter } from "./filters/NumberFilter";
import { SelectFilter } from "./filters/SelectFilter";
import { TextSearchFilter } from "./filters/TextSearchFilter";
import type { FilterBarProps, FilterConfig, FilterState, FilterValue } from "./types";
import { getDefaultFilterValue, isValidFilterValue } from "./utils";

const defaultInitialValues: Record<string, FilterValue> = {};

const getFilterIcon = (config: FilterConfig): React.ReactNode =>
  config.icon ?? getDefaultFilterIcon(config.type);

export const FilterBar = ({
  configs,
  initialValues = defaultInitialValues,
  maxVisibleFilters = 10,
  onFiltersChange,
}: FilterBarProps) => {
  const { t: translate } = useTranslation(["admin", "common"]);
  const [filters, setFilters] = useState<Array<FilterState>>(() =>
    Object.entries(initialValues)
      .filter(([key, value]) => {
        const config = configs.find((filterConfig) => filterConfig.key === key);

        return config && isValidFilterValue(config.type, value);
      })
      .map(([key, value]) => {
        const config = configs.find((con) => con.key === key);

        if (!config) {
          throw new Error(`Filter config not found for key: ${key}`);
        }

        return {
          config,
          id: `${key}-${Date.now()}`,
          value,
        };
      }),
  );

  // Sync external URL changes (e.g. ceiling logic setting RUN_AFTER_LTE) into the filters state.
  // We use a JSON key to detect when initialValues actually changes rather than re-rendering.
  const initialValuesKey = JSON.stringify(initialValues);

  useEffect(() => {
    // Pre-compute which pills are valid to add based on current configs and initialValues.
    const pillsToAdd: Array<FilterState> = [];

    for (const [key, value] of Object.entries(initialValues)) {
      const config = configs.find((cfg) => cfg.key === key);

      if (config && isValidFilterValue(config.type, value)) {
        pillsToAdd.push({ config, id: `${key}-${Date.now()}`, value });
      }
    }

    setFilters((prevFilters) => {
      // Use a Set to avoid O(n²) lookup when checking for existing pills.
      const existingKeys = new Set(prevFilters.map((filter) => filter.config.key));
      const toAdd = pillsToAdd.filter((pill) => !existingKeys.has(pill.config.key));

      // Remove pills that had a committed value but whose URL param was cleared externally.
      const afterRemove = prevFilters.filter((filter) => {
        const pillHadValue = isValidFilterValue(filter.config.type, filter.value);
        const urlValue = initialValues[filter.config.key];

        return !pillHadValue || isValidFilterValue(filter.config.type, urlValue);
      });

      if (toAdd.length === 0 && afterRemove.length === prevFilters.length) {
        return prevFilters;
      }

      return [...afterRemove, ...toAdd];
    });
    // configs is intentionally omitted — it is structurally stable across renders and including
    // it would risk infinite re-render loops. initialValuesKey captures all relevant URL changes.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [initialValuesKey]);

  const debouncedOnFiltersChange = useDebouncedCallback((filtersRecord: Record<string, FilterValue>) => {
    onFiltersChange(filtersRecord);
  }, 100);

  const updateFiltersRecord = (updatedFilters: Array<FilterState>) => {
    const filtersRecord = updatedFilters.reduce<Record<string, FilterValue>>((accumulator, filter) => {
      if (isValidFilterValue(filter.config.type, filter.value)) {
        accumulator[filter.config.key] = filter.value;
      }

      return accumulator;
    }, {});

    debouncedOnFiltersChange(filtersRecord);
  };

  const addFilter = (config: FilterConfig) => {
    const newFilter: FilterState = {
      config,
      id: `${config.key}-${Date.now()}`,
      value: getDefaultFilterValue(config),
    };

    const updatedFilters = [...filters, newFilter];

    setFilters(updatedFilters);
    updateFiltersRecord(updatedFilters);
  };

  const updateFilter = (id: string, value: FilterValue) => {
    const updatedFilters = filters.map((filter) => (filter.id === id ? { ...filter, value } : filter));

    setFilters(updatedFilters);
    updateFiltersRecord(updatedFilters);
  };

  const removeFilter = (id: string) => {
    const updatedFilters = filters.filter((filter) => filter.id !== id);

    setFilters(updatedFilters);
    updateFiltersRecord(updatedFilters);
  };

  const resetFilters = () => {
    setFilters([]);
    onFiltersChange({});
  };

  const availableConfigs = configs.filter(
    (config) => !filters.some((filter) => filter.config.key === config.key),
  );

  const renderFilter = (filter: FilterState) => {
    const props = {
      filter,
      onChange: (value: FilterValue) => updateFilter(filter.id, value),
      onRemove: () => removeFilter(filter.id),
    };

    switch (filter.config.type) {
      case "date":
        return <DateFilter key={filter.id} {...props} />;
      case "daterange":
        return <DateRangeFilter key={filter.id} {...props} />;
      case "number":
        return <NumberFilter key={filter.id} {...props} />;
      case "select":
        return <SelectFilter key={filter.id} {...props} />;
      case "text":
        return <TextSearchFilter key={filter.id} {...props} />;
      default:
        return undefined;
    }
  };

  return (
    <HStack gap={2} wrap="wrap">
      {filters.slice(0, maxVisibleFilters).map(renderFilter)}
      {availableConfigs.length > 0 && (
        <Menu.Root>
          <Menu.Trigger asChild>
            <Button
              _hover={{ bg: "colorPalette.subtle" }}
              bg="gray.muted"
              borderRadius="full"
              data-testid="add-filter-button"
              variant="outline"
            >
              <MdAdd />
              {translate("common:filter")}
            </Button>
          </Menu.Trigger>
          <Menu.Content>
            {availableConfigs.map((config) => (
              <Menu.Item key={config.key} onClick={() => addFilter(config)} value={config.key}>
                <HStack gap={2}>
                  {getFilterIcon(config)}
                  {config.label}
                </HStack>
              </Menu.Item>
            ))}
          </Menu.Content>
        </Menu.Root>
      )}
      {filters.length > 0 && (
        <Button borderRadius="full" colorPalette="gray" onClick={resetFilters} variant="outline">
          <MdClear />
          {translate("common:reset")}
        </Button>
      )}
    </HStack>
  );
};
