internal/dashboard/frontend/src/composables/useQuerySync.ts

import { useRouteQuery } from "@vueuse/router";
import { watch } from "vue";
import { useRoute, useRouter } from "vue-router";

interface SyncOptions {
  name?: string;
  array?: boolean;
  number?: boolean;
  boolean?: boolean;
  defaultValue?: any;
  comma?: boolean;
  dynamicPrefix?: string; // e.g. "f:" for f:key=value mapping to a Record<string, string[]>
}

/**
 * Synchronizes Pinia store properties with URL query parameters.
 * @param store The Pinia store instance.
 * @param keys A map of store keys to sync options.
 */
export function useQuerySync<T extends Record<string, any>>(
  store: T,
  keys: Partial<Record<keyof T & string, SyncOptions>>,
) {
  const route = useRoute();
  const router = useRouter();

  for (const storeKey in keys) {
    const config = keys[storeKey]!;

    if (config.dynamicPrefix) {
      // Dynamic handling for Record<string, string[]>
      const prefix = config.dynamicPrefix;

      // 1. URL -> Store (Watch route query)
      watch(
        () => route.query,
        (query) => {
          const newStoreValue: Record<string, string[]> = {};

          Object.entries(query).forEach(([key, val]) => {
            if (key.startsWith(prefix) && val !== null && val !== undefined) {
              const cleanKey = key.slice(prefix.length);
              // Handle potentially multiple values for the same key (though vue-router usually invalidates duplicate keys unless array mode,
              // but standard query params can be arrays. useRouteQuery handles this, but here we access route.query directly).
              // route.query values are string | null | (string | null)[]
              const values: string[] = Array.isArray(val)
                ? (val.filter((v) => v !== null) as string[])
                : [val as string];

              newStoreValue[cleanKey] = values;
            }
          });

          // Preserve empty arrays from current store state that are missing from URL
          // This prevents the UI from removing filters immediately when the last value is cleared
          const currentStoreValue = store[storeKey] as Record<string, string[]>;
          if (currentStoreValue) {
            Object.keys(currentStoreValue).forEach((k) => {
              if (
                !newStoreValue[k] &&
                Array.isArray(currentStoreValue[k]) &&
                currentStoreValue[k].length === 0
              ) {
                newStoreValue[k] = [];
              }
            });
          }

          // Update store if different. We do a shallow comparison of keys and values.

          // Simple check: different number of keys
          if (
            Object.keys(newStoreValue).length !==
            Object.keys(currentStoreValue).length
          ) {
            (store as any)[storeKey] = newStoreValue;
            return;
          }

          // Check content
          let changed = false;
          for (const k in newStoreValue) {
            const vNew = newStoreValue[k];
            const vOld = currentStoreValue[k];
            if (
              !vOld ||
              !vNew ||
              vNew.length !== vOld.length ||
              !vNew.every((val, i) => val === vOld[i])
            ) {
              changed = true;
              break;
            }
          }
          if (changed) {
            (store as any)[storeKey] = newStoreValue;
          }
        },
        { immediate: true, deep: true },
      );

      // 2. Store -> URL (Watch store value)
      watch(
        () => store[storeKey],
        (newVal) => {
          const currentQuery = { ...route.query };
          let queryChanged = false;

          // Remove all existing keys with this prefix
          Object.keys(currentQuery).forEach((k) => {
            if (k.startsWith(prefix)) {
              delete currentQuery[k];
              queryChanged = true;
            }
          });

          // Add new keys
          const newRecord = newVal as Record<string, string[]>;
          Object.entries(newRecord).forEach(([k, values]) => {
            if (values && values.length > 0) {
              // We'll just set it. If there are multiple values, vue-router 4 supports arrays.
              (currentQuery as any)[prefix + k] = values;
              queryChanged = true;
            }
          });

          if (queryChanged) {
            router.push({ query: currentQuery });
          }
        },
        { deep: true },
      );

      continue; // Skip standard handling
    }

    // Standard handling
    const queryKey = config.name || storeKey;
    let defaultValue =
      config.defaultValue !== undefined ? config.defaultValue : store[storeKey];
    if (Array.isArray(defaultValue)) {
      defaultValue = [...defaultValue];
    }

    const q = useRouteQuery(queryKey, defaultValue, {
      mode: "push",
      transform: {
        get: (val: any) => {
          if (val === undefined || val === null) return defaultValue;
          if (config.array) {
            if (Array.isArray(val)) return val;
            if (typeof val === "string") {
              if (config.comma) {
                return val ? val.split(",") : [];
              }
              return [val];
            }
            return [val];
          }
          if (config.number) {
            const num = Number(val);
            return isNaN(num) ? defaultValue : num;
          }
          if (config.boolean) {
            return val === "true" || val === true;
          }
          return val;
        },
        set: (val: any) => {
          // Avoid cluttering the URL with default values
          if (
            val === defaultValue ||
            JSON.stringify(val) === JSON.stringify(defaultValue)
          ) {
            return undefined;
          }
          if (config.array && config.comma && Array.isArray(val)) {
            return val.join(",");
          }
          return val;
        },
      },
    });

    // 1. Initialize store from URL (immediate watch handles this via useRouteQuery internals usually, but we keep explicit sync)
    // 2. Keep store in sync
    watch(
      q,
      (newVal) => {
        if (JSON.stringify(store[storeKey]) !== JSON.stringify(newVal)) {
          (store as any)[storeKey] = newVal as any;
        }
      },
      { immediate: true },
    );

    // 3. Keep URL in sync
    watch(
      () => store[storeKey],
      (newVal) => {
        if (JSON.stringify(q.value) !== JSON.stringify(newVal)) {
          q.value = newVal as any;
        }
      },
      { deep: true },
    );
  }
}