import { computed, inject, Injectable, signal } from '@angular/core';
import { InputType } from '../types';
import { getState, patchState, signalState } from '@ngrx/signals';
import { filterMapping, mapFilterInputToRecord } from './mappings';
import { isEqual } from 'lodash';
import {
  CheckboxFilterInputOption,
  DateRangeFilterInput,
  FilterInput,
  OrderByDirection,
  OrderByDirectionSchema,
  Query,
  QuerySchema,
} from './schemas';
import { FILTER_ON_COMMIT, FILTER_ON_INIT, QUERY_SETTINGS } from './tokens';
import { logger } from '@isa/core/logging';
import { parseISO } from 'date-fns';
import { checkboxOptionKeysHelper } from '../helpers/checkbox-option-keys.helper';

@Injectable()
export class FilterService {
  #logger = logger(() => ({
    library: 'shared/filter',
    class: 'FilterService',
  }));

  #onInit = inject(FILTER_ON_INIT, { optional: true })?.map((fn) => fn(this));
  #onCommit = inject(FILTER_ON_COMMIT, { optional: true })?.map((fn) =>
    fn(this),
  );

  readonly settings = inject(QUERY_SETTINGS);

  private readonly defaultState = filterMapping(this.settings);

  // Use a more lightweight approach for creating the initial state
  #commitdState = signal({ ...this.defaultState });

  #state = signalState(this.#commitdState());

  groups = this.#state.groups;

  inputs = this.#state.inputs;

  orderBy = this.#state.orderBy;

  constructor() {
    this.#onInit?.forEach((initFn) => {
      initFn();
    });
  }

  /**
   * Sets the ordering field and direction for the filter.
   *
   * @param by - The field to order by
   * @param dir - The direction to order by (asc or desc)
   * @param options - Optional parameters
   * @param options.commit - If true, commits the changes immediately
   */
  setOrderBy(
    by: string,
    dir: OrderByDirection | undefined,
    options?: { commit: boolean },
  ): void {
    const orderByList = this.#state.orderBy().map((o) => {
      if (o.by === by && o.dir === dir) {
        return { ...o, selected: true };
      }
      return { ...o, selected: false };
    });

    patchState(this.#state, { orderBy: orderByList });

    if (options?.commit) {
      this.commit();
    }
  }

  /**
   * Sets the text value for an input with the specified key.
   *
   * @param key - The key of the input to update
   * @param value - The new text value to set
   * @param options - Optional parameters
   * @param options.commit - If true, commits the changes immediately
   */
  setInputTextValue(
    key: string,
    value: string | undefined,
    options?: { commit: boolean },
  ): void {
    const inputs = this.#state.inputs().map((input) => {
      if (input.key !== key) {
        return input;
      }

      if (input.type === InputType.Text) {
        return { ...input, value };
      }

      this.logUnsupportedInputType(input, 'setInputTextValue');
      return input;
    });

    patchState(this.#state, { inputs });

    if (options?.commit) {
      this.commit();
    }
  }

  /**
   * Sets the selected values for a checkbox input with the specified key.
   *
   * This method directly sets the selected values for a checkbox input. Unlike
   * `setInputCheckboxOptionSelected`, this method does not handle hierarchical
   * parent-child relationships or enforce maximum options limits. It directly
   * replaces the entire selection array with the provided values.
   *
   * @param key - The key of the checkbox input to update
   * @param selected - Array of selected values to set (replaces current selection entirely)
   * @param options - Optional parameters
   * @param options.commit - If true, commits the changes immediately
   *
   * @example
   * ```typescript
   * // Set specific selected values
   * filterService.setInputCheckboxValue('categories', ['electronics', 'books'], { commit: true });
   *
   * // Clear all selections
   * filterService.setInputCheckboxValue('categories', []);
   * ```
   *
   * @remarks
   * This method bypasses the maximum options enforcement and hierarchical logic.
   * Use `setInputCheckboxOptionSelected` for individual option management with
   * proper limit enforcement and parent-child relationship handling.
   */
  setInputCheckboxValue(
    key: string,
    selected: string[],
    options?: { commit: boolean },
  ): void {
    const inputs = this.#state.inputs().map((input) => {
      if (input.key !== key) {
        return input;
      }

      if (input.type === InputType.Checkbox) {
        return { ...input, selected };
      }

      this.logUnsupportedInputType(input, 'setInputCheckboxValue');
      return input;
    });

    patchState(this.#state, { inputs });

    if (options?.commit) {
      this.commit();
    }
  }

  /**
   * Sets the selection state of a specific checkbox option within a checkbox input.
   *
   * This method allows you to select or deselect a specific checkbox option by its
   * group, groupKey, and option keys. It handles the selection logic, including
   * enforcing maximum options limits and managing parent-child relationships.
   *
   * @param checkboxOption - The checkbox option to set the selection for
   * @param selected - If true, selects the option; if false, deselects it
   * @param options - Optional parameters
   * @param options.commit - If true, commits the changes immediately after setting the selection
   */
  setInputCheckboxOptionSelected(
    checkboxOption: CheckboxFilterInputOption,
    selected: boolean,
    options?: { commit: boolean },
  ): void {
    const [group, groupKey, ...optionKeys] = checkboxOption.path;

    const inputs = this.#state.inputs().map((input) => {
      const target = input.group === group && input.key === groupKey;

      if (!target) {
        return input;
      }

      if (input.type !== InputType.Checkbox) {
        this.logUnsupportedInputType(input, 'setInputCheckboxValue');
        return input;
      }

      const hasChildren =
        Array.isArray(checkboxOption.values) &&
        checkboxOption.values.length > 0;
      // Collect the keys we want to add/remove. When the option has children we only include the
      // children's keys; otherwise we include the option's own key. Avoids non-null assertion.
      let keys: string[];
      if (hasChildren) {
        const children = checkboxOption.values as CheckboxFilterInputOption[]; // safe due to hasChildren
        keys = children.map((child) =>
          checkboxOptionKeysHelper(child).join('.'),
        );
      } else {
        keys = [optionKeys.join('.')];
      }

      if (selected) {
        const maxOptions = input.maxOptions;

        let newSelected = [...input.selected, ...keys];

        if (maxOptions && newSelected.length > maxOptions) {
          const excessCount = newSelected.length - maxOptions;
          newSelected = newSelected.slice(excessCount);
        }

        return {
          ...input,
          selected: newSelected,
        };
      } else {
        const filteredSelected = input.selected.filter((s) => {
          // Remove the key itself and any child keys
          return keys.includes(s) === false;
        });

        return {
          ...input,
          selected: filteredSelected,
        };
      }
    });

    patchState(this.#state, { inputs });

    if (options?.commit) {
      this.commit();
    }
  }

  /**
   * Toggles the selection state of all checkbox options within a specific group and key.
   *
   * This method allows you to select or deselect all options in a checkbox input by
   * providing the group and groupKey. It updates the selected state of the input
   * accordingly, either selecting all options or clearing the selection.
   *
   * @param group - The group name of the checkbox input
   * @param groupKey - The key of the checkbox input within the group
   * @param checkboxOptions - The list of checkbox options to toggle
   * @param selected - If true, selects all options; if false, clears all selections
   * @param options - Optional parameters
   * @param options.commit - If true, commits the changes immediately after toggling
   */
  toggleAllCheckboxOptions({
    group,
    groupKey,
    checkboxOptions,
    selected,
    options,
  }: {
    group: string;
    groupKey: string;
    checkboxOptions: CheckboxFilterInputOption[];
    selected: boolean;
    options?: { commit: boolean };
  }): void {
    // Get the group and groupKey from the first option
    if (checkboxOptions.length === 0) {
      this.#logger.warn('No checkbox options provided');
      return;
    }

    const inputs = this.#state.inputs().map((input) => {
      const target = input.group === group && input.key === groupKey;

      if (!target) {
        return input;
      }

      if (input.type !== InputType.Checkbox) {
        this.logUnsupportedInputType(
          input,
          'setAllInputCheckboxOptionsSelected',
        );
        return input;
      }

      if (selected) {
        // Get all option keys recursively from all provided options
        const getAllOptionKeys = (
          options: CheckboxFilterInputOption[],
        ): string[] =>
          options.flatMap((option) => {
            const isParent =
              Array.isArray(option.values) && option.values.length > 0;

            if (isParent) {
              // If the option has children, only include child keys
              return getAllOptionKeys(option.values!);
            } else {
              // If no children, include the option itself
              return [checkboxOptionKeysHelper(option).join('.')];
            }
          });

        return {
          ...input,
          selected: getAllOptionKeys(checkboxOptions),
        };
      } else {
        // Unselect all options
        return { ...input, selected: [] };
      }
    });

    patchState(this.#state, { inputs });

    if (options?.commit) {
      this.commit();
    }
  }

  /**
   * Sets the date range values for an input with the specified key.
   *
   * @param key - The key of the date range input to update
   * @param start - The start date as a string
   * @param stop - The end date as a string
   * @param options - Optional parameters
   * @param options.commit - If true, commits the changes immediately
   */
  setInputDateRangeValue(
    key: string,
    start?: string,
    stop?: string,
    options?: { commit: boolean },
  ): void {
    const inputs = this.#state.inputs().map((input) => {
      if (input.key !== key) {
        return input;
      }

      if (input.type === InputType.DateRange) {
        return { ...input, start, stop };
      }

      this.logUnsupportedInputType(input, 'setInputDateRangeValue');
      return input;
    });

    patchState(this.#state, { inputs });

    if (options?.commit) {
      this.commit();
    }
  }

  /**
   * Sets the number range values for an input with the specified key.
   *
   * This updates the current minimum (min) and maximum (max) values for a
   * NumberRange filter input. Passing `undefined` for either side will clear that
   * boundary while leaving the other intact.
   *
   * @param key - The key of the number range input to update
   * @param min - The minimum numeric value (inclusive) or undefined to clear
   * @param max - The maximum numeric value (inclusive) or undefined to clear
   * @param options - Optional parameters
   * @param options.commit - If true, commits the changes immediately
   */
  setInputNumberRangeValue(
    key: string,
    min?: number,
    max?: number,
    options?: { commit: boolean },
  ): void {
    const inputs = this.#state.inputs().map((input) => {
      if (input.key !== key) {
        return input;
      }

      if (input.type === InputType.NumberRange) {
        return { ...input, min, max };
      }

      this.logUnsupportedInputType(input, 'setInputNumberRangeValue');
      return input;
    });

    patchState(this.#state, { inputs });

    if (options?.commit) {
      this.commit();
    }
  }

  /**
   * Helper method to consistently log unsupported input type warnings
   * @private
   * @param input - The input that has an unsupported type
   * @param method - The method name where the warning occurred
   */
  private logUnsupportedInputType(input: FilterInput, method: string): void {
    this.#logger.warn(`Input type not supported`, () => ({
      inputType: input.type,
      inputKey: input.key,
      method,
    }));
  }

  /**
   * Indicates whether the current state is the default state.
   * This computed property checks if the current state is equal to the default state.
   */
  isDefaultFilter = computed(() => {
    const currentState = getState(this.#state);
    return isEqual(currentState.inputs, this.defaultState.inputs);
  });

  /**
   * Checks if a specific filter input is in its default state.
   *
   * @param filterInput - The filter input to check
   * @returns True if the input is in its default state, false otherwise
   */
  isDefaultFilterInput(filterInput: FilterInput): boolean {
    const currentInputState = this.#state
      .inputs()
      .find((i) => i.key === filterInput.key);
    const defaultInputState = this.defaultState.inputs.find(
      (i) => i.key === filterInput.key,
    );

    // For DateRange inputs, compare dates by value, not string precision
    if (filterInput.type === InputType.DateRange) {
      return this._areDateRangeInputsEqual(
        currentInputState as DateRangeFilterInput,
        defaultInputState as DateRangeFilterInput,
      );
    }

    return isEqual(currentInputState, defaultInputState);
  }

  /**
   * Compares two date range filter inputs for equality, accounting for possible differences
   * in date string precision (e.g., "2023-06-05T22:00:00Z" vs. "2023-06-05T22:00:00.000Z").
   * Converts ISO date strings to Date objects before comparison to ensure consistent results.
   *
   * @param currentInput - The current DateRangeFilterInput state
   * @param defaultInput - The default DateRangeFilterInput state
   * @returns True if both start and stop dates are equal (ignoring string precision), false otherwise
   */
  private _areDateRangeInputsEqual = (
    currentInput: DateRangeFilterInput | undefined,
    defaultInput: DateRangeFilterInput | undefined,
  ): boolean => {
    const currentStart = currentInput?.start;
    const defaultStart = defaultInput?.start;
    const currentStop = currentInput?.stop;
    const defaultStop = defaultInput?.stop;

    return (
      isEqual(
        currentStart ? parseISO(currentStart) : currentStart,
        defaultStart ? parseISO(defaultStart) : defaultStart,
      ) &&
      isEqual(
        currentStop ? parseISO(currentStop) : currentStop,
        defaultStop ? parseISO(defaultStop) : defaultStop,
      )
    );
  };

  /**
   * Computes the number of filter inputs that are currently selected (i.e., not in their default state).
   *
   * - For all input types, increments the count if the input differs from its default state.
   * - Uses isDefaultFilterInput for accurate comparison including default values.
   *
   * @remarks
   * This property is a computed signal and updates automatically when the filter state changes.
   *
   * @returns The number of filter inputs that are currently selected and differ from their default state.
   */
  selectedFilterCount = computed<number>(() => {
    const currentState = getState(this.#state);
    return currentState.inputs.reduce((count, input) => {
      // Simply check if input differs from default state
      // This handles all cases including NumberRange with default values
      // Excluding Text (Searchbox) inputs from the count
      if (!this.isDefaultFilterInput(input) && input.type !== InputType.Text) {
        return count + 1;
      }
      return count;
    }, 0);
  });

  /**
   * Indicates whether the current state is empty.
   */
  isEmpty = computed(() => {
    const currentState = getState(this.#state);
    return currentState.inputs.every((input) => {
      if (input.type === InputType.Text) {
        return !input.value;
      }

      if (input.type === InputType.Checkbox) {
        return !input.selected?.length;
      }

      if (input.type === InputType.DateRange) {
        return !input.start && !input.stop;
      }

      if (input.type === InputType.NumberRange) {
        return input.min == null && input.max == null;
      }

      this.#logger.warn(`Input type not supported`, () => ({
        input,
        method: 'isEmptyFilter',
      }));

      return true;
    });
  });

  /**
   * Checks if a specific filter input has an empty value.
   * For text inputs, checks if the value is falsy.
   * For checkbox inputs, checks if the selected array is empty.
   * For date range inputs, checks if both start and stop are falsy.
   * For number range inputs, checks if both min and max are null or undefined.
   *
   * @param filterInput - The filter input to check
   * @returns True if the filter input is empty, false otherwise
   */
  isEmptyFilterInput(filterInput: FilterInput): boolean {
    const currentInputState = this.#state
      .inputs()
      .find((i) => i.key === filterInput.key);

    if (!currentInputState) {
      this.#logger.warn(`Input not found`, () => ({
        inputKey: filterInput.key,
        method: 'isEmptyFilterInput',
      }));
      return true;
    }

    if (currentInputState.type === InputType.Text) {
      return !currentInputState.value;
    }

    if (currentInputState.type === InputType.Checkbox) {
      return !currentInputState.selected?.length;
    }

    if (currentInputState.type === InputType.DateRange) {
      return !currentInputState.start && !currentInputState.stop;
    }

    if (currentInputState.type === InputType.NumberRange) {
      return currentInputState.min == null && currentInputState.max == null;
    }

    this.logUnsupportedInputType(currentInputState, 'isEmptyFilterInput');
    return true;
  }

  /**
   * Reverts the current state to the last committed state.
   * This method restores the state by applying the previously saved committed state.
   */
  rollback(): void {
    const currentState = getState(this.#state);
    const committedState = this.#commitdState();
    if (isEqual(currentState, committedState)) {
      this.#logger.debug('No changes to rollback', () => ({
        changes: false,
      }));
      return;
    }
    this.#logger.debug('Rolling back filter state', () => ({
      changes: true,
      currentState,
      committedState,
    }));
    patchState(this.#state, this.#commitdState);
  }

  /**
   * Rolls back the input state for specific keys to their last committed state.
   * If the input with the given key exists in the committed state, it replaces
   * the current input with the committed one. Otherwise, the input remains unchanged.
   *
   * @param keys - The keys of the inputs to roll back
   */
  rollbackInput(keys: string[]): void {
    // Find committed inputs for the specified keys
    const committedInputs = this.#commitdState().inputs;

    // First check if there's anything to rollback
    const hasChangesToRollback = keys.some((key) => {
      const currentInput = this.#state.inputs().find((i) => i.key === key);
      const committedInput = committedInputs.find((i) => i.key === key);
      return committedInput && !isEqual(currentInput, committedInput);
    });

    // Only proceed if there are changes to rollback
    if (!hasChangesToRollback) {
      this.#logger.debug('No changes to rollback for specified inputs', () => ({
        inputKeys: keys,
      }));
      return;
    }

    // Apply rollback for changed inputs
    const inputs = this.#state.inputs().map((input) => {
      if (!keys.includes(input.key)) {
        return input;
      }

      // Get the committed version of this input
      const committedInput = committedInputs.find((i) => i.key === input.key);
      return committedInput || input;
    });

    this.#logger.debug('Rolling back specified inputs', () => ({
      inputKeys: keys,
    }));

    patchState(this.#state, { inputs });
  }

  /**
   * Commits the current state by capturing a snapshot of the internal state.
   * This method updates the private `#commitdState` property with the current state
   * and triggers any registered commit callbacks.
   *
   * @remarks
   * Uses structuredClone to create a deep copy of the state, preventing reference sharing
   * between the current state and the committed state.
   */
  commit(): void {
    const currentState = getState(this.#state);
    const committedState = this.#commitdState();

    if (!isEqual(currentState, committedState)) {
      // Deep clone to prevent reference sharing
      this.#commitdState.set(structuredClone(currentState));
      this.#logger.debug('Filter state committed', () => ({
        changes: true,
      }));
    } else {
      this.#logger.debug('No changes to commit', () => ({
        changes: false,
      }));
    }

    this.#onCommit?.forEach((commitFn) => {
      commitFn();
    });
  }

  /**
   * Clears all filter values without resetting to default values.
   * This sets text inputs to undefined, checkbox selections to empty arrays,
   * date ranges to undefined for both start and stop, and number ranges to undefined for both min and max.
   *
   * @param options - Optional parameters
   * @param options.commit - If true, commits the changes immediately after clearing
   */
  clear(options?: { commit: boolean }): void {
    // First check if there's anything to clear
    const hasDataToClear = this.#state.inputs().some((input) => {
      if (input.type === InputType.Text) {
        return !!input.value;
      }

      if (input.type === InputType.Checkbox) {
        return input.selected?.length > 0;
      }

      if (input.type === InputType.DateRange) {
        return !!input.start || !!input.stop;
      }

      if (input.type === InputType.NumberRange) {
        return input.min != null || input.max != null;
      }

      return false;
    });

    // Only proceed if there's data to clear
    if (!hasDataToClear) {
      this.#logger.debug('No filter data to clear', () => ({
        changes: false,
      }));
      return;
    }

    const inputs = this.#state.inputs().map((input) => {
      // Get the default input to preserve structural properties (like options array reference)
      const defaultInput = this.defaultState.inputs.find(
        (i) => i.key === input.key,
      );

      if (input.type === InputType.Text) {
        return { ...input, value: undefined };
      }

      if (
        input.type === InputType.Checkbox &&
        defaultInput?.type === InputType.Checkbox
      ) {
        // Preserve the options array reference from defaultState
        return { ...input, options: defaultInput.options, selected: [] };
      }

      if (input.type === InputType.DateRange) {
        return { ...input, start: undefined, stop: undefined };
      }

      if (input.type === InputType.NumberRange) {
        return { ...input, min: undefined, max: undefined };
      }

      return input;
    });

    this.#logger.debug('Clearing filter state', () => ({
      changes: true,
    }));

    patchState(this.#state, { inputs });
    if (options?.commit) {
      this.commit();
    }
  }

  /**
   * Resets the filter state to its default values based on the current settings.
   *
   * @param options - Optional parameters for the reset operation.
   * @param options.commit - If `true`, the changes will be committed after resetting the state.
   */
  reset(options?: { commit: boolean }): void {
    // Use a more lightweight approach than structuredClone
    patchState(this.#state, { ...this.defaultState });
    if (options?.commit) {
      this.commit();
    }
  }
  /**
   * Resets one or more inputs to their default state.
   *
   * @param keys - The key or array of keys of the inputs to reset.
   * @param options - Optional parameters for the reset operation.
   * @param options.commit - If `true`, commits the changes after resetting the input(s).
   *
   * @remarks
   * - If no input is found with the specified key, a warning is logged for that key.
   * - The method updates the state by replacing the input(s) with their default configuration.
   * - For checkbox inputs, keeps the original options array to maintain reference equality.
   * - For other input types, creates a shallow copy with reset values.
   *
   * @example
   * ```typescript
   * // Reset a single input
   * filterService.resetInput('exampleKey', { commit: true });
   *
   * // Reset multiple inputs
   * filterService.resetInput(['key1', 'key2'], { commit: true });
   * ```
   */
  resetInput(keys: string[], options?: { commit: boolean }): void {
    // Find all default inputs that match the provided keys
    const inputsToReset = keys
      .map((key) => {
        const inputToReset = this.defaultState.inputs.find(
          (i) => i.key === key,
        );

        if (!inputToReset) {
          this.#logger.warn(`No input found with key`, () => ({
            key,
            method: 'resetInput',
          }));
          return undefined;
        }

        return { key, defaultInput: inputToReset };
      })
      .filter(
        (item): item is { key: string; defaultInput: FilterInput } =>
          item !== undefined,
      );

    if (inputsToReset.length === 0) {
      return;
    }

    // Create a set of keys for faster lookups
    const keysToReset = new Set(inputsToReset.map((item) => item.key));

    const inputs = this.#state.inputs().map((input) => {
      if (!keysToReset.has(input.key)) {
        return input;
      }

      const defaultInput = inputsToReset.find(
        (item) => item.key === input.key,
      )?.defaultInput;

      if (!defaultInput) {
        return input;
      }

      // Reset only the mutable state while preserving structural properties (like options array reference)
      // This ensures reference equality for isEqual comparisons in isDefaultFilterInput
      switch (input.type) {
        case InputType.Checkbox:
          return defaultInput.type === InputType.Checkbox
            ? { ...input, selected: [...defaultInput.selected] }
            : input;

        case InputType.DateRange:
          return defaultInput.type === InputType.DateRange
            ? { ...input, start: defaultInput.start, stop: defaultInput.stop }
            : input;

        case InputType.NumberRange:
          return defaultInput.type === InputType.NumberRange
            ? { ...input, min: defaultInput.min, max: defaultInput.max }
            : input;

        case InputType.Text:
          return defaultInput.type === InputType.Text
            ? { ...input, value: defaultInput.value }
            : input;

        default:
          return input;
      }
    });

    patchState(this.#state, {
      inputs,
    });

    if (options?.commit) {
      this.commit();
    }
  }

  /**
   * Resets the orderBy state to its default values.
   *
   * @param options - Optional parameters for the reset operation.
   * @param options.commit - If `true`, the changes will be committed after resetting.
   */
  resetOrderBy(options?: { commit: boolean }): void {
    // Use a more lightweight approach than structuredClone
    patchState(this.#state, { orderBy: [...this.defaultState.orderBy] });

    if (options?.commit) {
      this.commit();
    }
  }

  queryParams = computed<Record<string, string>>(() => {
    const commited = this.#commitdState();
    const result: Record<string, string> = {};

    for (const input of commited.inputs) {
      switch (input.type) {
        case InputType.Text:
          if (input.value) {
            result[input.key] = input.value;
          } else {
            result[input.key] = '';
          }
          break;
        case InputType.Checkbox:
          if (input.selected) {
            result[input.key] = input.selected.join(';');
          } else {
            result[input.key] = '';
          }
          break;
        case InputType.DateRange:
          if (input.start && input.stop) {
            result[input.key] = `"${input.start}"-"${input.stop}"`;
          } else if (input.start) {
            result[input.key] = `"${input.start}"-`;
          } else if (input.stop) {
            result[input.key] = `-"${input.stop}"`;
          } else {
            result[input.key] = '';
          }
          break;
        case InputType.NumberRange: {
          const { min: minVal, max: maxVal } = input;
          if (minVal != null && maxVal != null) {
            result[input.key] = `${minVal}-${maxVal}`;
          } else if (minVal != null) {
            result[input.key] = `${minVal}-`;
          } else if (maxVal != null) {
            result[input.key] = `-${maxVal}`;
          }
          break;
        }
      }
    }

    const orderBy = commited.orderBy.find((o) => o.selected);

    if (orderBy) {
      result['order_by'] = `${orderBy.by}:${orderBy.dir}`;
    } else {
      // Ensure 'orderBy' is not included if no order is selected
      result['order_by'] = '';
    }

    return result;
  });

  /**
   * Checks if the current query parameters match the provided parameters.
   *
   * @param params - The parameters to compare with the current query parameters
   * @returns True if the parameters match, false otherwise
   */
  isQueryParamsEqual(params: Record<string, string>): boolean {
    const currentParams = this.queryParams();
    return this.queryParamKeys().every(
      (key) => params[key] === currentParams[key],
    );
  }

  queryParamKeys = computed(() => {
    const keys = this.inputs().map((i) => i.key);
    const orderBy = this.orderBy().find((o) => o.dir);

    if (orderBy) {
      keys.push('order_by');
    }

    return keys;
  });

  query = computed<Query>(() => {
    const commited = this.#commitdState();

    const orderBy = commited.orderBy.filter((o) => o.dir);

    return QuerySchema.parse({
      filter: mapFilterInputToRecord(
        commited.inputs.filter((i) => i.target === 'filter'),
      ),
      input: mapFilterInputToRecord(
        commited.inputs.filter((i) => i.target === 'input'),
      ),
      orderBy: orderBy
        .filter((o) => o.selected)
        .map((o) => {
          return {
            by: o.by,
            label: o.label,
            desc: o.dir === 'desc',
            selected: true,
          };
        }),
    });
  });

  /**
   * Parses query parameters into filter state.
   *
   * @param params - Record of query parameters to parse
   * @param options - Optional parameters
   * @param options.commit - If true, commits the changes immediately after parsing
   */ parseQueryParams(
    params: Record<string, string>,
    options?: { commit: boolean },
  ): void {
    this.reset();

    for (const key in params) {
      if (key === 'order_by') {
        const [by, dir] = params[key].split(':');
        const orderBy = this.orderBy().some(
          (o) => o.by === by && o.dir === dir,
        );

        if (orderBy) {
          this.#logger.warn(`OrderBy already exists`, () => ({
            by,
            dir,
            method: 'parseQueryParams',
          }));
          this.setOrderBy(by, OrderByDirectionSchema.parse(dir));
        }
        continue;
      }

      const input = this.inputs().find((i) => i.key === key);
      if (!input) {
        this.#logger.warn(`Input not found for key`, () => ({
          key,
          method: 'parseQueryParams',
        }));
        continue;
      }

      switch (input.type) {
        case InputType.Text:
          this.setInputTextValue(key, params[key]);
          break;
        case InputType.Checkbox:
          this.setInputCheckboxValue(key, params[key].split(';'));
          break;
        case InputType.DateRange: {
          const decoded = decodeURIComponent(params[key]);
          const [startRaw, stopRaw] = decoded.split('-"');
          const start =
            startRaw?.replace(/"-/g, '')?.replace(/"/g, '') || undefined; // Removes " and - from for example: "2023-06-05T22:00:00.000Z"-
          const stop =
            stopRaw?.replace(/-"/g, '')?.replace(/"/g, '') || undefined; // Removes " and - from for example: -"2023-06-05T22:00:00.000Z"
          this.setInputDateRangeValue(key, start, stop);
          break;
        }
        case InputType.NumberRange: {
          const decoded = decodeURIComponent(params[key]);
          const [minRaw, maxRaw] = decoded.split('-');
          const min = minRaw ? Number(minRaw.replace(/-/g, '')) : undefined;
          const max = maxRaw ? Number(maxRaw.replace(/-/g, '')) : undefined;
          this.setInputNumberRangeValue(key, min, max);
          break;
        }
        default:
          this.logUnsupportedInputType(input, 'parseQueryParams');
          break;
      }
    }

    if (options?.commit) {
      this.commit();
    }
  }
}
export { QUERY_SETTINGS };
