import * as React from 'react';
import * as GTM from '@assets/filters/common/utils/gtm';
import { updateHistoryState } from '@assets/filters/server-bindings/utils/updateCategory';
import { type BaseFilter, parseBaseFilter } from '@assets/filters/common/state-parsers/baseFilter';
import {
   type FilterValuesCache,
   parseValuesCache
} from '@assets/filters/common/state-parsers/filterValuesCache';
import { FilterParams, parseFilterParams } from '@assets/filters/common/state-parsers/filterParams';
import type { OrderedFilterEntry } from '@assets/filters/common/contexts/sharedStatics';
import { type Theme, ThemeProvider } from '@assets/filters/common/contexts/theme';
import { useOrdering } from '@assets/filters/server-bindings/hooks/useOrdering';
import { usePager } from '@assets/filters/server-bindings/hooks/usePager';
import { useLoadNextPage } from '@assets/filters/server-bindings/hooks/useLoadNextPage';
import { useListShow } from '@assets/filters/server-bindings/hooks/useListShow';
import { FILTER_SECTION } from './common/query';
import { dispatchResponseEvent } from '@assets/filters/common/events/events';
import { useDebounced } from '@assets/js-shop/hooks';
import { useQuery } from '@apollo/client';
import * as sectionPersistence from '@assets/filters/common/utils/persistentFilters';
import { initializeTooltips } from '@assets/components/tooltips';
import { Provider as SharedStaticsProvider } from '@assets/filters/common/contexts/sharedStatics';
import { Provider as ConfigProvider, type FilterConfig } from '@assets/filters/common/contexts/config';
import { Provider as FilterDataProvider, type FilterData } from '@assets/filters/common/contexts/filterData';
import { OrProduceWith } from '@assets/js-shop/helpers';
import { mergeConfigs } from '@assets/filters/common/state-parsers/mergeConfigs';
import { useGetQueryStrlen } from '@assets/filters/common/hooks/useGetQueryStrlen';
import { useCatalogLoading } from '@assets/filters/common/hooks/useCatalogLoading';

/** @deprecated */
export type FiltersState = {
   config: FilterConfig;
   filterData: FilterData;
};

/** @deprecated */
export { useConfig } from '@assets/filters/common/contexts/config';

/** @deprecated */
export { useFilterData } from '@assets/filters/common/contexts/filterData';

// === Context providers - logic (request / response handling, external DOM bindings) resides here
type FiltersStateProviderProps = {
   children: React.ReactNode;

   // filters available in section with product counts
   // (later gets updated from graphql api)
   initialConfig: FilterValuesCache;

   // dynamic filter (when navigation to the page)
   preselectedFilters: FilterParams;

   // category id / producer id / ...
   baseFilterParams: FilterParams;
};

type StateProviderProps<T extends object> = [T, (state: OrProduceWith<T>) => void];

type CustomProviderProps = {
   filterData: StateProviderProps<FilterData>;
   filterConfig: StateProviderProps<FilterConfig>;

   children?: React.ReactNode;
   baseFilter?: BaseFilter;
   theme?: Partial<Theme>;
   filterOrder?: OrderedFilterEntry[];
};

// custom context -> use when you want filters outside section view
const emptyBaseFilter = {} as const;
export function CustomContextProvider(props: CustomProviderProps) {
   const { children, baseFilter, theme, filterOrder, filterData, filterConfig } = props;

   const sharedStatics = React.useMemo(() => {
      return {
         baseFilter: (baseFilter ?? emptyBaseFilter),
         filterOrder,
      };
   }, [baseFilter, filterOrder]);

   return (
      <ConfigProvider value={filterConfig}>
         <FilterDataProvider value={filterData}>
            <SharedStaticsProvider value={sharedStatics}>
               {theme ? (
                  <ThemeProvider theme={theme}>
                     {children}
                  </ThemeProvider>
               ) : (
                  <>
                     {children}
                  </>
               )}
            </SharedStaticsProvider>
         </FilterDataProvider>
      </ConfigProvider>
   );
}

const DEBOUNCE_TIMEOUT = 500;

// More appropriate name would be SectionFiltersContextProvider.
// specific implementation for section filters
export function FiltersContextProvider(props: FiltersStateProviderProps) {
   const {
      children,
      preselectedFilters,
      initialConfig,
      baseFilterParams,
   } = props;

   // E.g. when the user is on page 3 and applies some filter,
   // we want to "redirect" them to the 1st page
   //    - since the products change with the filter.
   // This sets the override for the query variable "page" to 1 and is
   // cleared after the query returns a response.
   const [pageOverride, setPageOverride] = React.useState<1 | undefined>(undefined);

   // Do not fetch the GraphQL query on the initial load of the page.
   // Everything should be available in the HTML from the server.
   const [hasChanged, setHasChanged] = React.useState(false);

   // Parsed data from JSON in templates
   const [initialFilterData, setInitialFilterData] = React.useState<object>({});
   const [baseFilter, setBaseFilter] = React.useState<BaseFilter>({});

   // When `listShow` changes - matrix / grid / ... view of catalog:
   //    -> This gets set into a cookie in JS, but the `cache-key` cookie
   //       doesn't get changed, because the responses are cached on CDN
   //       and the request never gets to the server - the cache-key gets set there.
   //
   //    -> POST requests aren't cached, therefore we force the request to get to the server
   //       and the cookie to get set.
   const httpMethod = React.useRef<'GET' | 'POST'>('GET');

   // Filter data = current active filters
   // analogical to class.FilterParams (@see \Filter::setFilterData)
   const filterDataState = React.useState<FilterData>({});
   const [filterData, setFilterData] = filterDataState;

   // Filter config = names, product counts, ...
   // analogical to class.Filter
   const filterConfigState = React.useState<FilterConfig>({ common: {} });
   const [, setFilterConfig] = filterConfigState;

   const debouncedFilterState = useDebounced(filterData, DEBOUNCE_TIMEOUT);

   // region server-rendered-bindings
   const order = useOrdering(React.useCallback((payload) => {
      setHasChanged(true);

      const { order, form } = payload;
      const activeInput = form.querySelector(`[value="${order}"]`) as HTMLInputElement | null;
      form.querySelector('[data-toggle]')?.classList.remove('active');
      form.querySelector('[data-popup]')?.classList.remove('active');

      GTM.dataCollector.applyFilterSort(activeInput?.innerText ?? '', order);
   }, []));

   const pagerPage = usePager(
      React.useCallback(() => {
         setHasChanged(true);
         setPageOverride(undefined);
      }, []),
      // Handle pre-fetching the next / previous pages on pager link hover
      React.useCallback((prefetchPage) => {
         if (!variables) {
            return;
         }

         void client.query({
            query: FILTER_SECTION,
            variables: {
               baseFilter,
               filterData: JSON.stringify(debouncedFilterState),
               order,
               page: prefetchPage,
            },
            context: { getMethod: true },
         });
      }, [baseFilter, debouncedFilterState, order]),
   );

   useListShow(React.useCallback((listShow) => {
      // if it did not change - do not fetch the data
      if (listShow === sectionPersistence.get().listAppearance) {
         return;
      }

      // Force the request to get to server.
      //  -> Needs to reset the cache-key cookie.
      httpMethod.current = 'POST';

      // This sets a cookie which will then change the cache key
      // for full-page cache and potentially the rendered product list layout.
      sectionPersistence.save({ listAppearance: listShow });
      setHasChanged(true);

      // Before re-fetching with the new category layout we need to clear the cache.
      // Otherwise, the old list layout could be retrieved from the Apollo InMemoryCache.
      client.cache.evict({ id: 'ROOT_QUERY', fieldName: 'categoryFilter' });
      client.cache.evict({ id: 'ROOT_QUERY', fieldName: 'categoryLoadMore' });

      void refetch();
   }, []));

   useLoadNextPage(filterData, baseFilter, order);
   // endregion

   // "constructor" -> parses all the values passed from the HTML template,
   // initializes browser history state.
   React.useEffect(() => {
      const baseFilter = parseBaseFilter(baseFilterParams);
      setBaseFilter(baseFilter);

      const initialConfigParsed = parseValuesCache(baseFilter, initialConfig);
      const initialFilterDataParsed = parseFilterParams(initialConfigParsed, preselectedFilters, baseFilter);

      setInitialFilterData({ ...initialFilterDataParsed });
      setFilterData({ ...initialFilterDataParsed });
      setFilterConfig(initialConfigParsed);

      // initialize history state
      const search = new URLSearchParams(window.location.search);

      window.history.replaceState({
         page: +(search.get('page') ?? 1),
         order: search.get('order') ?? '',
         filterData: initialFilterDataParsed,
      }, '');
   }, []);

   // handle browser navigation for filterData
   React.useEffect(() => {
      function popStateHandler(ev: PopStateEvent) {
         if (!('filterData' in ev.state) || typeof ev.state.filterData !== 'object') {
            return;
         }

         const newFilterData = ev.state.filterData as FilterData;

         setFilterData(oldFilterData => {
            if (JSON.stringify(newFilterData) === JSON.stringify(oldFilterData)) {
               return oldFilterData;
            }

            return newFilterData;
         });
      }

      window.addEventListener('popstate', popStateHandler);

      return () => void window.removeEventListener('popstate', popStateHandler);
   }, []);

   React.useEffect(() => {
      if (hasChanged) {
         setPageOverride(1);

         return;
      }

      if (JSON.stringify(initialFilterData) === JSON.stringify(debouncedFilterState)) {
         return;
      }

      setHasChanged(true);
   }, [debouncedFilterState]);

   const page = pageOverride ?? pagerPage;
   const variables = React.useMemo(() => ({
      baseFilter,
      order,
      page,
      filterData: JSON.stringify(debouncedFilterState),
   }), [baseFilter, order, page, debouncedFilterState]);

   const queryUrlStrlen = useGetQueryStrlen(FILTER_SECTION, variables);

   const { data, refetch, loading, client, variables: lastUsedVariables } = useQuery(FILTER_SECTION, {
      variables,
      skip: !hasChanged,
      notifyOnNetworkStatusChange: true,
      context: {
         // Request using GET for URLs shorter than 1600 characters.
         getMethod: () => httpMethod.current === 'GET' && queryUrlStrlen < 1600,
      },
   });

   // Handle the GraphQL response.
   // Unfortunately has to be a useEffect...
   // https://github.com/apollographql/apollo-client/issues/12352
   React.useEffect(() => {
      if (!data || loading) {
         return;
      }

      // revert to making cacheable requests via HTTP GET
      httpMethod.current = 'GET';

      const response = data.categoryFilter;

      // Contains the updated product counts from \Filter::getValuesCache().
      // We only care for the product counts and not the updated values
      //    -> mergeConfigs() will only update the product counts and keep the old filter values.
      const filterValuesCache = JSON.parse(response.filterValues) as FilterValuesCache;
      const configFromApi = parseValuesCache(baseFilter, filterValuesCache);

      const filterData = JSON.parse(lastUsedVariables.filterData) as FilterData;
      setFilterConfig(previousConfig => {
         return mergeConfigs(previousConfig, configFromApi, filterData);
      });

      // Dispatches the response payload as a CustomEvent to use in Twig components (stimulus controllers).
      // The product list gets replaced this way. The response contains the HTML with the re-rendered product list.
      dispatchResponseEvent({ response });

      if (response.pager.maxPage !== 1) {
         setPageOverride(undefined);
      }

      initializeTooltips();

      const { indexedUrl, metaTitle } = response;
      updateHistoryState(indexedUrl, metaTitle, {
         page: lastUsedVariables.page,
         order: lastUsedVariables.order ?? '',
         filterData,
      });
   }, [data, loading, baseFilter, lastUsedVariables]);

   useCatalogLoading(loading, page);

   return (
      <CustomContextProvider
         baseFilter={baseFilter}
         filterData={filterDataState}
         filterConfig={filterConfigState}
         filterOrder={initialConfig.filterOrder}
      >
         {children}
      </CustomContextProvider>
   );
}
