import { computed, ref, watch, type ComputedRef, type Ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useDebouncedRef } from './useDebouncedRef';

export type SortDir = 'asc' | 'desc';

/** Comparateur d'une clé de tri, toujours écrit en ASC ; la direction est appliquée par le composable. */
export interface SortDef<T> {
  key: string;
  labelKey: string;
  compare: (a: T, b: T) => number;
  defaultDir?: SortDir;
}

export interface FilterOption {
  value: string;
  labelKey?: string;
  label?: string;
}

/** Un filtre activable par une ou plusieurs valeurs (OR intra-filtre, AND inter-filtres). */
export interface FilterDef<T> {
  key: string;
  labelKey: string;
  options: FilterOption[];
  predicate: (item: T, active: Set<string>) => boolean;
}

export interface ListControlsOptions<T> {
  source: () => readonly T[];
  sorts: SortDef<T>[];
  /** Tableau statique ou getter (pour des options dérivées d'un store chargé en asynchrone). */
  filters?: FilterDef<T>[] | (() => FilterDef<T>[]);
  searchAccessor?: (item: T) => string;
  defaultSortKey?: string;
  defaultPageSize?: number; // 0 = tout afficher
  /** Si fourni, l'état est synchronisé dans l'URL sous des params préfixés (liens partageables). */
  urlKey?: string;
}

/** Sous-ensemble consommé par ListToolbar (sans pagination) — implémentable aussi par un store partagé. */
export interface ListToolbarControls<T> {
  search: Ref<string>;
  sortKey: Ref<string>;
  sortDir: Ref<SortDir>;
  filters: Ref<Record<string, Set<string>>>;
  total: ComputedRef<number>;
  activeFilterCount: ComputedRef<number>;
  setSearch(v: string): void;
  setSort(key: string): void;
  toggleFilter(key: string, value: string): void;
  clearAll(): void;
  sortDefs: SortDef<T>[];
  filterDefs: ComputedRef<FilterDef<T>[]>;
}

export interface ListControls<T> extends ListToolbarControls<T> {
  page: Ref<number>;
  pageSize: Ref<number>;
  processed: ComputedRef<T[]>;
  pageCount: ComputedRef<number>;
  setPage(p: number): void;
  setPageSize(n: number): void;
}

export function useListControls<T>(opts: ListControlsOptions<T>): ListControls<T> {
  const sortDefs = opts.sorts;
  const filterDefs = computed<FilterDef<T>[]>(() =>
    typeof opts.filters === 'function' ? opts.filters() : (opts.filters ?? []),
  );
  const defaultSortKey = opts.defaultSortKey ?? sortDefs[0]?.key ?? '';
  const defaultPageSize = opts.defaultPageSize ?? 25;

  const search = ref('');
  const sortKey = ref(defaultSortKey);
  const sortDir = ref<SortDir>(sortDefs.find((s) => s.key === defaultSortKey)?.defaultDir ?? 'asc');
  const filters = ref<Record<string, Set<string>>>({});
  const page = ref(1);
  const pageSize = ref(defaultPageSize);

  const debouncedSearch = useDebouncedRef(search, 150);

  // ---- pipeline : filtres → recherche → tri → pagination (computed purs, aucune mutation de store) ----
  const filtered = computed<T[]>(() => {
    let items = [...opts.source()];
    for (const def of filterDefs.value) {
      const active = filters.value[def.key];
      if (active && active.size > 0) items = items.filter((it) => def.predicate(it, active));
    }
    const q = debouncedSearch.value.trim().toLowerCase();
    if (q && opts.searchAccessor) {
      items = items.filter((it) => opts.searchAccessor!(it).toLowerCase().includes(q));
    }
    return items;
  });

  const sorted = computed<T[]>(() => {
    const def = sortDefs.find((s) => s.key === sortKey.value) ?? sortDefs[0];
    if (!def) return filtered.value;
    const dir = sortDir.value === 'desc' ? -1 : 1;
    return [...filtered.value].sort((a, b) => def.compare(a, b) * dir);
  });

  const total = computed(() => filtered.value.length);
  const pageCount = computed(() => (pageSize.value === 0 ? 1 : Math.max(1, Math.ceil(total.value / pageSize.value))));
  const processed = computed<T[]>(() => {
    if (pageSize.value === 0) return sorted.value;
    const start = (page.value - 1) * pageSize.value;
    return sorted.value.slice(start, start + pageSize.value);
  });
  const activeFilterCount = computed(() =>
    Object.values(filters.value).reduce((n, set) => n + set.size, 0),
  );

  // ramène la page dans les bornes quand un filtre réduit le total.
  watch(pageCount, (count) => {
    if (page.value > count) page.value = count;
  });

  // ---- setters (toute action utilisateur revient page 1) ----
  function setSort(key: string): void {
    if (sortKey.value === key) {
      sortDir.value = sortDir.value === 'asc' ? 'desc' : 'asc';
    } else {
      sortKey.value = key;
      sortDir.value = sortDefs.find((s) => s.key === key)?.defaultDir ?? 'asc';
    }
    page.value = 1;
  }
  function toggleFilter(key: string, value: string): void {
    const next = { ...filters.value };
    const set = new Set(next[key] ?? []);
    if (set.has(value)) set.delete(value);
    else set.add(value);
    if (set.size === 0) delete next[key];
    else next[key] = set;
    filters.value = next;
    page.value = 1;
  }
  function clearAll(): void {
    search.value = '';
    filters.value = {};
    page.value = 1;
  }
  function setPage(p: number): void {
    page.value = Math.min(Math.max(1, p), pageCount.value);
  }
  function setPageSize(n: number): void {
    pageSize.value = n;
    page.value = 1;
  }
  // saisie utilisateur : retour page 1 de façon synchrone (l'hydratation URL écrit search.value
  // directement, sans passer par ici → ne réinitialise pas une page fournie par l'URL).
  function setSearch(v: string): void {
    search.value = v;
    page.value = 1;
  }

  // suspend l'écriture état→URL pendant l'application des params depuis l'URL.
  let hydrating = false;

  // ---- synchro URL (optionnelle) ----
  if (opts.urlKey) {
    const k = opts.urlKey;
    const P = {
      q: `${k}_q`,
      sort: `${k}_sort`,
      dir: `${k}_dir`,
      filter: `${k}_filter`,
      page: `${k}_page`,
      size: `${k}_size`,
    };
    const route = useRoute();
    const router = useRouter();

    // sérialise les filtres de façon canonique : key:v1,v2;key2:v3 (clés/valeurs triées).
    const serializeFilters = (f: Record<string, Set<string>>): string =>
      Object.entries(f)
        .filter(([, set]) => set.size > 0)
        .sort(([a], [b]) => a.localeCompare(b))
        .map(([key, set]) => `${key}:${[...set].sort().join(',')}`)
        .join(';');

    // params possédés par cette liste (non-défauts uniquement → URLs propres).
    function ownedParams(): Record<string, string> {
      const out: Record<string, string> = {};
      if (search.value.trim()) out[P.q] = search.value.trim();
      if (sortKey.value !== defaultSortKey) out[P.sort] = sortKey.value;
      const baseDir = sortDefs.find((s) => s.key === sortKey.value)?.defaultDir ?? 'asc';
      if (sortDir.value !== baseDir) out[P.dir] = sortDir.value;
      const fser = serializeFilters(filters.value);
      if (fser) out[P.filter] = fser;
      if (page.value > 1) out[P.page] = String(page.value);
      if (pageSize.value !== defaultPageSize) out[P.size] = String(pageSize.value);
      return out;
    }

    const canonical = (o: Record<string, string>): string =>
      Object.keys(o).sort().map((key) => `${key}=${o[key]}`).join('&');

    function readOwned(query: Record<string, unknown>): Record<string, string> {
      const out: Record<string, string> = {};
      for (const key of Object.values(P)) {
        const v = query[key];
        if (typeof v === 'string' && v) out[key] = v;
      }
      return out;
    }

    function applyFromQuery(query: Record<string, unknown>): void {
      hydrating = true;
      const str = (key: string): string => (typeof query[key] === 'string' ? (query[key] as string) : '');
      search.value = str(P.q);
      sortKey.value = sortDefs.some((s) => s.key === str(P.sort)) ? str(P.sort) : defaultSortKey;
      const dir = str(P.dir);
      sortDir.value = dir === 'asc' || dir === 'desc' ? dir : sortDefs.find((s) => s.key === sortKey.value)?.defaultDir ?? 'asc';
      const parsedFilters: Record<string, Set<string>> = {};
      const fser = str(P.filter);
      if (fser) {
        for (const part of fser.split(';')) {
          const [fk, vals] = part.split(':');
          if (!fk || !vals) continue;
          const def = filterDefs.value.find((d) => d.key === fk);
          if (!def) continue;
          const allowed = new Set(def.options.map((o) => o.value));
          const set = new Set(vals.split(',').filter((v) => allowed.has(v)));
          if (set.size) parsedFilters[fk] = set;
        }
      }
      filters.value = parsedFilters;
      const sz = Number(str(P.size));
      pageSize.value = Number.isFinite(sz) && sz >= 0 ? sz : defaultPageSize;
      const pg = Number(str(P.page));
      page.value = Number.isFinite(pg) && pg >= 1 ? pg : 1;
      // libère le garde après le flush des watchers déclenchés par les assignations ci-dessus.
      void Promise.resolve().then(() => {
        hydrating = false;
      });
    }

    // URL → état (init + navigations arrière/avant) ; ignore si déjà en phase avec l'état courant.
    watch(
      () => route.query,
      (query) => {
        if (canonical(readOwned(query)) === canonical(ownedParams())) return;
        applyFromQuery(query);
      },
      { immediate: true, deep: true },
    );

    // état → URL : remplace (pas d'entrée d'historique), préserve les params non possédés.
    watch(
      [search, sortKey, sortDir, filters, page, pageSize],
      () => {
        if (hydrating) return;
        const owned = ownedParams();
        const preserved: Record<string, unknown> = {};
        for (const [key, v] of Object.entries(route.query)) {
          if (!Object.values(P).includes(key)) preserved[key] = v;
        }
        const next = { ...preserved, ...owned };
        // no-op si la query possédée est inchangée (anti-boucle / anti-spam).
        if (canonical(readOwned(route.query)) === canonical(owned)) return;
        void router.replace({ query: next as Record<string, string> });
      },
      { deep: true },
    );
  }

  return {
    search,
    sortKey,
    sortDir,
    filters,
    page,
    pageSize,
    processed,
    total,
    pageCount,
    activeFilterCount,
    setSearch,
    setSort,
    toggleFilter,
    clearAll,
    setPage,
    setPageSize,
    sortDefs,
    filterDefs,
  };
}
