import { computed, ref, type Ref } from 'vue'
import type { HydraCollection } from '~/shared/utils/api'

/**
 * Composable generique de liste paginee serveur.
 *
 * Responsabilites :
 *  - centraliser l'etat tableau (page courante, items/page, tri, filtres,
 *    totalItems, items, loading, error) cote local — JAMAIS dans l'URL,
 *    conformement a la regle ABSOLUE n°6 du CLAUDE.md (« Jamais persister
 *    l'etat de tableau dans l'URL »).
 *  - dialoguer avec une ressource API Platform 4 (Hydra) en passant
 *    `page`, `itemsPerPage` et le tri/filtres en query params.
 *  - exposer une API simple a brancher sur `MalioDataTable`
 *    (props page/perPage/totalItems + events update:page / update:per-page).
 *
 * Volontairement **par-instance** (state local a chaque appel) : a la
 * difference de `useAuditLog` / `useCategoriesAdmin` qui sont des
 * singletons module-level partages, une liste paginee est propre a son
 * ecran et ne doit pas etre partagee entre pages (sinon un retour
 * arriere reprendrait la pagination d'une autre liste).
 *
 * Pas de gestion URL : si une page veut un deep link (ex : ouvrir un
 * detail), elle le fait via sa propre route, pas via la query string
 * de pagination. Derogation possible uniquement si l'utilisateur le
 * demande explicitement, cf. CLAUDE.md.
 */

/**
 * Direction de tri serveur. API Platform 4 attend `asc` ou `desc` via la
 * syntaxe `?order[field]=asc`.
 */
export type SortDirection = 'asc' | 'desc'

/**
 * Specification de tri : un seul champ trie a la fois cote front (la
 * majorite des tableaux Malio n'expose pas le multi-tri). Si null, aucun
 * `order[...]` n'est envoye et l'API applique son tri par defaut.
 */
export interface SortSpec {
    field: string
    direction: SortDirection
}

/**
 * Type des filtres : un dictionnaire de valeurs serialisables en query
 * params. Le caller decide du mapping (ex : `{ active: true }`,
 * `{ 'name[ilike]': 'a' }`). Valeurs `null` / `undefined` / chaines vides
 * sont automatiquement omises au moment de la requete.
 */
export type PaginatedListFilters = Record<string, string | number | boolean | string[] | null | undefined>

export interface UsePaginatedListOptions<F extends PaginatedListFilters = PaginatedListFilters> {
    /** URL relative au prefix `/api` (ex : `/sites`, `/categories`). */
    url: string
    /** Items par page initial. Defaut 10 (aligne avec le defaut serveur). */
    defaultItemsPerPage?: number
    /** Options proposees dans le selecteur items/page. Defaut [10, 25, 50]. */
    itemsPerPageOptions?: number[]
    /** Filtres initiaux. */
    defaultFilters?: F
    /** Tri initial. */
    defaultSort?: SortSpec | null
    /**
     * Query params additionnels propres a la ressource (ex : `includeDeleted=true`,
     * `groups[]=foo`) injectes a chaque requete. **Snapshot statique** : l'objet
     * est lu tel quel a chaque `fetch()`, ses valeurs ne sont pas deballees. Ne
     * pas y passer de `ref` / `computed` (elles seraient serialisees comme objet,
     * pas comme valeur) — pour un extra reactif, muter les filtres via
     * `setFilters` ou ouvrir un ticket pour un support `MaybeRefOrGetter`.
     */
    extraQuery?: Record<string, unknown>
}

export interface UsePaginatedListReturn<T, F extends PaginatedListFilters = PaginatedListFilters> {
    /** Items de la page courante. */
    items: Ref<T[]>
    /** Total d'items (toutes pages) renvoye par Hydra. */
    totalItems: Ref<number>
    /** Page courante (1-based). */
    currentPage: Ref<number>
    /** Taille de page courante. */
    itemsPerPage: Ref<number>
    /** Options exposees au selecteur items/page. */
    itemsPerPageOptions: Ref<number[]>
    /** Nombre total de pages (≥ 1). */
    totalPages: Ref<number>
    /** Indicateur de chargement (vrai pendant `fetch()`). */
    loading: Ref<boolean>
    /**
     * Derniere erreur de `fetch()` (null si le dernier appel a abouti).
     * Permet a la page de distinguer « liste reellement vide » d'un echec
     * reseau / 403 : sans ca, `isEmpty` confond les deux cas (la liste
     * tombe a 0 item dans les deux situations). La page decide de l'UX
     * (bandeau, bouton reessayer) — le composable ne toaste pas.
     */
    error: Ref<unknown | null>
    /** Vrai apres au moins un fetch reussi avec 0 item. */
    isEmpty: Ref<boolean>
    /** Vrai si la collection tient en une seule page (totalPages <= 1). */
    isSinglePage: Ref<boolean>
    /** Filtres courants (mutation via `setFilters`). */
    filters: Ref<F>
    /** Tri courant (mutation via `setSort`). */
    sort: Ref<SortSpec | null>
    /** Lance un fetch contre l'API et met a jour items/totalItems. */
    fetch: () => Promise<void>
    /** Va a la page demandee (bornes appliquees : 1 ≤ p ≤ totalPages). */
    goToPage: (page: number) => Promise<void>
    /** Page suivante (no-op si deja en derniere page). */
    nextPage: () => Promise<void>
    /** Page precedente (no-op si deja en premiere page). */
    prevPage: () => Promise<void>
    /** Change la taille de page et revient en page 1. */
    setItemsPerPage: (value: number) => Promise<void>
    /** Applique de nouveaux filtres et revient en page 1. */
    setFilters: (next: Partial<F>, options?: { replace?: boolean }) => Promise<void>
    /** Change le tri et revient en page 1. */
    setSort: (next: SortSpec | null) => Promise<void>
    /** Reinitialise filtres + tri + page sur les valeurs par defaut. */
    reset: () => Promise<void>
    /** Alias de `fetch()` (intention plus claire dans certains contextes). */
    refresh: () => Promise<void>
}

/**
 * Force `application/ld+json` : sous `application/json`, API Platform 4
 * renvoie un tableau plat sans envelope de pagination — on ne pourrait pas
 * lire `totalItems` ni `view`. Voir aussi `useAuditLog.ts`.
 */
const JSONLD_HEADERS = { Accept: 'application/ld+json' } as const

/**
 * Filtre les entrees nulles/undefined/vides d'un objet de query : evite
 * d'envoyer `?foo=&bar=null` a l'API qui declencherait parfois des erreurs
 * de filtre cote Symfony (`FilterInterface::apply` strict).
 */
function compactQuery(raw: Record<string, unknown>): Record<string, unknown> {
    const out: Record<string, unknown> = {}
    for (const [key, value] of Object.entries(raw)) {
        if (value === null || value === undefined) continue
        if (typeof value === 'string' && value === '') continue
        if (Array.isArray(value) && value.length === 0) continue
        out[key] = value
    }
    return out
}

export function usePaginatedList<T, F extends PaginatedListFilters = PaginatedListFilters>(
    options: UsePaginatedListOptions<F>,
): UsePaginatedListReturn<T, F> {
    const api = useApi()

    const defaultItemsPerPage = options.defaultItemsPerPage ?? 10
    const initialFilters = { ...(options.defaultFilters ?? ({} as F)) } as F
    const initialSort = options.defaultSort ?? null

    const items = ref<T[]>([]) as Ref<T[]>
    const totalItems = ref(0)
    const currentPage = ref(1)
    const itemsPerPage = ref(defaultItemsPerPage)
    const itemsPerPageOptions = ref(options.itemsPerPageOptions ?? [10, 25, 50])
    const loading = ref(false)
    const error = ref<unknown | null>(null)
    // Jeton de sequence : incremente a chaque `fetch()`. Une reponse dont
    // le jeton n'est plus le dernier est ignoree (protection contre les
    // reponses periemes quand l'utilisateur enchaine page / tri / filtres
    // plus vite que le reseau ne repond — sinon la derniere reponse
    // *arrivee* gagnerait au lieu de la derniere *demandee*).
    let fetchToken = 0
    // `hasFetched` evite que `isEmpty` retourne `true` avant le premier
    // chargement (etat initial = 0 items mais on ne sait pas encore si la
    // ressource est vide ou en cours de chargement). Un appel reseau au
    // moins doit avoir abouti pour qu'on annonce une liste « vide ».
    const hasFetched = ref(false)
    const filters = ref({ ...initialFilters }) as Ref<F>
    const sort = ref<SortSpec | null>(initialSort ? { ...initialSort } : null)

    const totalPages = computed(() => {
        if (totalItems.value <= 0 || itemsPerPage.value <= 0) return 1
        return Math.max(1, Math.ceil(totalItems.value / itemsPerPage.value))
    })
    const isEmpty = computed(() => hasFetched.value && totalItems.value === 0)
    const isSinglePage = computed(() => totalPages.value <= 1)

    /**
     * Construit l'objet query envoye a l'API : extras + filtres, puis
     * pagination + tri. Les cles reservees (`page`, `itemsPerPage`,
     * `order[...]`) sont assignees **en dernier** pour qu'un filtre ou un
     * extra portant le meme nom ne puisse pas ecraser silencieusement la
     * pagination. Les filtres `null`/`undefined`/'' sont elimines pour ne
     * pas polluer l'URL.
     */
    function buildQuery(): Record<string, unknown> {
        const query: Record<string, unknown> = {}
        if (options.extraQuery) {
            Object.assign(query, options.extraQuery)
        }
        Object.assign(query, filters.value)
        // Cles reservees en dernier : priorite a la pagination/au tri.
        query.page = currentPage.value
        query.itemsPerPage = itemsPerPage.value
        if (sort.value) {
            // Format API Platform : ?order[field]=asc
            query[`order[${sort.value.field}]`] = sort.value.direction
        }
        return compactQuery(query)
    }

    /**
     * Lance un fetch et applique la borne haute si necessaire. Si la page
     * courante depasse `totalPages` apres l'application des filtres (cas
     * « j'etais en page 5, je filtre, il ne reste qu'une page »), on
     * rappelle l'API sur la derniere page valide. Un seul niveau de retry
     * pour eviter une boucle si l'API renvoie des resultats incoherents.
     */
    async function fetch(): Promise<void> {
        const token = ++fetchToken
        loading.value = true
        error.value = null
        try {
            const data = await api.get<HydraCollection<T>>(
                options.url,
                buildQuery(),
                { toast: false, headers: JSONLD_HEADERS },
            )
            // Une requete plus recente a ete lancee entre-temps : on jette
            // cette reponse pour ne pas ecraser des donnees plus fraiches.
            if (token !== fetchToken) return
            items.value = data.member ?? []
            totalItems.value = data.totalItems ?? 0

            const tp = totalItems.value > 0
                ? Math.max(1, Math.ceil(totalItems.value / itemsPerPage.value))
                : 1

            // Si on est hors borne ET qu'il y a au moins une page valide
            // a viser, on retombe sur la derniere page (cf. cas limite
            // « page hors borne apres filtre » de la spec #73). On ne
            // refetch que si la nouvelle page est differente, sinon
            // boucle infinie potentielle.
            if (currentPage.value > tp && tp >= 1 && totalItems.value > 0) {
                currentPage.value = tp
                const data2 = await api.get<HydraCollection<T>>(
                    options.url,
                    buildQuery(),
                    { toast: false, headers: JSONLD_HEADERS },
                )
                // Meme garde apres le refetch hors-borne.
                if (token !== fetchToken) return
                items.value = data2.member ?? []
                totalItems.value = data2.totalItems ?? 0
            }

            hasFetched.value = true
        } catch (e) {
            // Reponse periemee : ne pas toucher au state, une requete plus
            // recente est en cours et fera foi.
            if (token !== fetchToken) return
            // Swallow volontaire : on remet la liste a vide pour ne pas
            // afficher de donnees stale, et on expose l'erreur pour que la
            // page distingue « vide » d'« echec ». Le composant parent
            // decide de l'UX (toast / message d'erreur) — pas d'a-priori ici.
            error.value = e
            items.value = []
            totalItems.value = 0
            hasFetched.value = true
        } finally {
            // Seule la requete la plus recente eteint le spinner : une
            // reponse periemee ne doit pas le couper alors qu'un fetch plus
            // recent est encore en vol.
            if (token === fetchToken) loading.value = false
        }
    }

    async function goToPage(page: number): Promise<void> {
        const tp = totalPages.value
        const next = Math.max(1, Math.min(page, tp))
        if (next === currentPage.value) return
        currentPage.value = next
        await fetch()
    }

    async function nextPage(): Promise<void> {
        if (currentPage.value >= totalPages.value) return
        await goToPage(currentPage.value + 1)
    }

    async function prevPage(): Promise<void> {
        if (currentPage.value <= 1) return
        await goToPage(currentPage.value - 1)
    }

    async function setItemsPerPage(value: number): Promise<void> {
        if (!Number.isFinite(value) || value <= 0) return
        const rounded = Math.floor(value)
        if (rounded === itemsPerPage.value) return
        itemsPerPage.value = rounded
        currentPage.value = 1
        await fetch()
    }

    /**
     * `replace: false` (defaut) fusionne avec les filtres courants. Une
     * valeur explicitement `undefined` retire la cle (utile pour effacer
     * un filtre depuis un champ controle). `replace: true` remplace
     * integralement l'objet par `next`.
     */
    async function setFilters(next: Partial<F>, opts?: { replace?: boolean }): Promise<void> {
        if (opts?.replace) {
            filters.value = { ...(next as F) }
        } else {
            const merged = { ...filters.value, ...next } as F
            // Supprime les cles explicitement passees a undefined : sans ce
            // nettoyage, l'objet `filters` accumulerait des cles fantomes.
            for (const key of Object.keys(next)) {
                if (next[key as keyof F] === undefined) {
                    delete (merged as Record<string, unknown>)[key]
                }
            }
            filters.value = merged
        }
        currentPage.value = 1
        await fetch()
    }

    async function setSort(next: SortSpec | null): Promise<void> {
        sort.value = next ? { ...next } : null
        currentPage.value = 1
        await fetch()
    }

    async function reset(): Promise<void> {
        filters.value = { ...initialFilters }
        sort.value = initialSort ? { ...initialSort } : null
        itemsPerPage.value = defaultItemsPerPage
        currentPage.value = 1
        await fetch()
    }

    return {
        items,
        totalItems,
        currentPage,
        itemsPerPage,
        itemsPerPageOptions,
        totalPages,
        loading,
        error,
        isEmpty,
        isSinglePage,
        filters,
        sort,
        fetch,
        goToPage,
        nextPage,
        prevPage,
        setItemsPerPage,
        setFilters,
        setSort,
        reset,
        refresh: fetch,
    }
}
