import { useMemo } from "react";
import { useSearchParams } from "react-router-dom";

type ParseConfig = Record<
  string,
  | { type: "string"; defaultValue?: string }
  | { type: "number"; defaultValue?: number }
  | { parse: (value: URLSearchParams) => unknown }
>;

/**
 * A utility hook to parse and type URL search params based on a configuration
 * object. This hook is useful when you want to access URL search params in a
 * typesafe way and with proper casting.
 *
 * @example
 * ```tsx
 * const { parsedParams } = useParsedSearchParams({
 *   page: { type: "number", defaultValue: 1 },
 *   search: { type: "string", defaultValue: "" },
 *   order: { type: "string", defaultValue: "asc" },
 *   sort: { type: "string" }, // You can omit default value
 *   selected: { parse: (p) => new Set(p.getAll("selected").map(Number)) },
 * });
 * ```
 */
export function useParsedSearchParams<T extends ParseConfig>(config: T) {
  const [searchParams, setSearchParams] = useSearchParams();

  return useMemo(() => {
    const parsed: Record<string, any> = {};

    for (const [key, options] of Object.entries(config)) {
      if ("parse" in options) {
        parsed[key] = options.parse(searchParams);
        continue;
      }

      const value = searchParams.get(key);
      const { type, defaultValue } = options;

      if (value !== null) {
        if (type === "number") {
          const numValue = Number(value);
          parsed[key] = isNaN(numValue) ? defaultValue : numValue;
        } else {
          parsed[key] = value;
        }
      } else {
        parsed[key] = defaultValue;
      }
    }

    // Typing this without casting is impossible...
    const parsedParams = parsed as {
      [K in keyof T]: T[K] extends {
        parse: (value: URLSearchParams) => infer P;
      }
        ? P
        : T[K] extends {
              type: infer TType extends "number" | "string";
              defaultValue?: infer TDefault;
            }
          ? // Handle the case where the `defaultValue` is `undefined`
            undefined extends TDefault
            ? TType extends "number"
              ? number | undefined
              : string | undefined
            : // Get the type based on the `defaultValue` type
              TDefault
          : never;
    };

    return {
      parsedParams,
      setSearchParams,
    };
    // The `config` object is not expected to change during the component lifecycle
  }, [searchParams]); // eslint-disable-line react-hooks/exhaustive-deps
}

// Test it out

const { parsedParams } = useParsedSearchParams({
  page: { type: "number", defaultValue: 1 },
  search: { type: "string", defaultValue: "" },
  sort: { type: "string", defaultValue: undefined },
  order: { type: "string", defaultValue: "asc" },
  selected: { parse: (p) => new Set(p.getAll("selected").map(Number)) },
});