import { useState, useEffect, useMemo, useRef } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';

import type {
  CodeWalkthroughFile,
  CodeWalkthroughFilter,
  InputsMarkdocAttr,
  TogglesMarkdocAttr,
  CodeWalkthroughControls,
  CodeWalkthroughFilterItem,
  CodeWalkthroughConditionsObject,
  CodeWalkthroughControlsState,
} from '@redocly/config';

import { downloadCodeWalkthrough } from '../../utils/download-code-walkthrough';
import { matchCodeWalkthroughConditions } from '../../utils/match-code-walkthrough-conditions';
import { getCodeWalkthroughFileText } from '../../utils/get-code-walkthrough-file-text';
import { replaceInputsWithValue } from '../../utils/replace-inputs-with-value';

export type ActiveFilter = {
  id: string;
  label?: string;
  items: CodeWalkthroughFilterItem[];
};

export type WalkthroughControlsState = {
  activeFilters: ActiveFilter[];
  getControlState: (id: string) => { value: string | boolean; render: boolean } | null;
  changeControlState: (id: string, value: string | boolean) => void;
  /* Utility */
  areConditionsMet: (conditions: CodeWalkthroughConditionsObject) => boolean;
  handleDownloadCode: (files: CodeWalkthroughFile[]) => Promise<void>;
  getFileText: (file: CodeWalkthroughFile) => string;
  populateInputsWithValue: (node: string) => string;
};

const defaultControlsValues: CodeWalkthroughControls = {
  input: '',
  toggle: false,
  filter: '',
};

export function useCodeWalkthroughControls(
  filters: Record<string, CodeWalkthroughFilter>,
  inputs: InputsMarkdocAttr,
  toggles: TogglesMarkdocAttr,
  enableDeepLink: boolean,
): WalkthroughControlsState {
  const location = useLocation();
  const navigate = useNavigate();
  const searchParams = useMemo(() => new URLSearchParams(location.search), [location.search]);

  const filtersRef = useRef(filters);
  const inputsRef = useRef(inputs);
  const togglesRef = useRef(toggles);

  const getInitialState = () => {
    const state: CodeWalkthroughControlsState = {};

    for (const [id, toggle] of Object.entries(toggles)) {
      state[id] = {
        ...toggle,
        render: true,
        type: 'toggle',
        value: enableDeepLink ? searchParams.get(id) === 'true' : false,
      };
    }

    for (const [id, input] of Object.entries(inputs)) {
      state[id] = {
        ...input,
        render: true,
        type: 'input',
        value: enableDeepLink ? (searchParams.get(id) ?? input.value) : input.value,
      };
    }

    for (const [id, filter] of Object.entries(filters)) {
      const defaultValue = filter?.items?.[0]?.value || '';
      state[id] = {
        ...filter,
        render: true,
        type: 'filter',
        value: enableDeepLink ? (searchParams.get(id) ?? defaultValue) : defaultValue,
      };
    }

    return state;
  };

  const [controlsState, setControlsState] = useState(getInitialState);

  useEffect(() => {
    const sameProps = [
      JSON.stringify(filters) === JSON.stringify(filtersRef.current),
      JSON.stringify(inputs) === JSON.stringify(inputsRef.current),
      JSON.stringify(toggles) === JSON.stringify(togglesRef.current),
    ];

    if (sameProps.every(Boolean)) {
      return;
    }

    filtersRef.current = filters;
    inputsRef.current = inputs;
    togglesRef.current = toggles;

    const newState = getInitialState();

    // Preserve existing values where control type hasn't changed
    Object.entries(newState).forEach(([id, control]) => {
      const existingControl = controlsState[id];
      if (existingControl && existingControl.type === control.type) {
        // @ts-ignore
        newState[id] = {
          ...control,
          value: existingControl.value,
        };
      }
    });

    setControlsState(newState);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [filters, inputs, toggles, enableDeepLink]);

  const changeControlState = (id: string, value: string | boolean) => {
    setControlsState((prev) => {
      const control = prev[id];
      if (!control) {
        console.error(`Control with id "${id}" not found.`);
        return prev;
      }

      switch (control.type) {
        case 'input':
          if (typeof value !== 'string') {
            console.error(
              `Invalid value type for input "${id}". Input control type requires a string value.`,
            );
            return prev;
          }
          break;

        case 'toggle':
          if (typeof value !== 'boolean') {
            console.error(
              `Invalid value type for toggle "${id}". Toggle control type requires a boolean value.`,
            );
            return prev;
          }
          break;

        case 'filter':
          if (typeof value !== 'string') {
            console.error(
              `Invalid value type for filter "${id}". Filter control type requires a string value.`,
            );
            return prev;
          }
          break;

        default:
          console.error(
            `Invalid control type "${(control as { type: string })?.type}" for control "${id}". Allowed types are "toggle", "input", or "filter".`,
          );
          return prev;
      }

      return {
        ...prev,
        [id]: {
          ...control,
          value,
        },
      } as CodeWalkthroughControlsState;
    });
  };

  const getControlState = (id: string) => {
    const controlState = controlsState[id];

    if (controlState) {
      return {
        render: controlState.render,
        value: controlState.value,
      };
    } else {
      return null;
    }
  };

  const walkthroughContext = useMemo(() => {
    const areConditionsMet = (conditions: CodeWalkthroughConditionsObject) =>
      matchCodeWalkthroughConditions(conditions, controlsState);
    // reset controls
    for (const control of Object.values(controlsState)) {
      control.render = true;
      control.value = control.value || defaultControlsValues[control.type];
    }

    for (const [id, control] of Object.entries(controlsState)) {
      if (control && !areConditionsMet(control)) {
        controlsState[id].render = false;
        controlsState[id].value = defaultControlsValues[control.type];
      }
    }

    const activeFilters = [];
    for (const [id, filter] of Object.entries(filters)) {
      if (!controlsState[id].render) {
        continue;
      }

      // eslint-disable-next-line no-warning-comments
      // code-walk-todo: need to check if we have a default fallback
      const items = Array.isArray(filter?.items) ? filter.items : [];

      const activeItems = items.filter((item) => areConditionsMet(item));

      if (activeItems.length === 0) {
        controlsState[id].render = false;
        controlsState[id].value = defaultControlsValues['filter'];
        continue;
      }

      const currentValue = controlsState[id].value;
      if (currentValue) {
        const isValueInActiveItems =
          activeItems.findIndex(({ value }) => value === currentValue) !== -1;
        controlsState[id].value = isValueInActiveItems ? currentValue : activeItems[0].value;
      } else {
        controlsState[id].value = activeItems[0].value;
      }

      activeFilters.push({
        id,
        label: filter.label,
        items: activeItems,
      });
    }

    const inputsState = Object.fromEntries(
      Object.entries(controlsState).filter(([_, controlState]) => controlState.type === 'input'),
    ) as Record<string, { value: string }>;

    const handleDownloadCode = (files: CodeWalkthroughFile[]) =>
      downloadCodeWalkthrough(files, controlsState, inputsState);

    const getFileText = (file: CodeWalkthroughFile) =>
      getCodeWalkthroughFileText(file, controlsState, inputsState);

    const populateInputsWithValue = (node: string) => replaceInputsWithValue(node, inputsState);

    return {
      activeFilters,
      areConditionsMet,
      handleDownloadCode,
      getFileText,
      populateInputsWithValue,
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [controlsState]);

  /**
   * Update the URL search params with the current state of the filters and inputs
   */
  useEffect(() => {
    if (!enableDeepLink) {
      return;
    }

    const newSearchParams = new URLSearchParams(Array.from(searchParams.entries()));

    for (const [id, { value }] of Object.entries(controlsState)) {
      if (value) {
        newSearchParams.set(id, value.toString());
      } else {
        newSearchParams.delete(id);
      }
    }

    const newSearch = newSearchParams.toString();
    if (newSearch === location.search.substring(1)) return;
    navigate({ search: newSearch }, { replace: true });
    // Ignore searchParams in dependency array to avoid infinite re-renders
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [controlsState]);

  return {
    changeControlState,
    getControlState,
    ...walkthroughContext,
  };
}
